diff --git a/index.html b/index.html
index 88efaf5..cfeddbb 100644
--- a/index.html
+++ b/index.html
@@ -173,6 +173,45 @@
data-accent="amber"
aria-label="Янтарный акцент"
>
+
+
+
+
+
diff --git a/script.js b/script.js
index 0bd275b..309e8ea 100644
--- a/script.js
+++ b/script.js
@@ -166,12 +166,16 @@ const defaultPreferences = {
theme: window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark",
view: "cards",
accent: "violet",
+ customAccentFrom: "#8b5cf6",
+ customAccentTo: "#22d3ee",
+ customAccentAngle: 135,
showNumbers: false,
showTreeTags: true,
autoCollapseSearch: true,
sidebarWidth: 390,
};
const availableViews = ["cards", "list", "compact-cards", "compact-list"];
+const availableAccents = ["violet", "cyan", "lime", "amber", "custom"];
// ============================================================================
// Runtime state and DOM references
@@ -194,6 +198,12 @@ const settingsClose = document.querySelector("[data-settings-close]");
const themeButtons = document.querySelectorAll("[data-theme]");
const viewButtons = document.querySelectorAll("[data-view]");
const accentButtons = document.querySelectorAll("[data-accent]");
+const accentCustomPanel = document.querySelector("[data-accent-custom-panel]");
+const customAccentFromInput = document.querySelector("[data-custom-accent-from]");
+const customAccentToInput = document.querySelector("[data-custom-accent-to]");
+const customAccentAngleInput = document.querySelector("[data-custom-accent-angle]");
+const customAccentAngleValue = document.querySelector("[data-custom-accent-angle-value]");
+const customAccentPreview = document.querySelector("[data-custom-accent-preview]");
const showNumbersInput = document.querySelector("[data-show-numbers]");
const showTreeTagsInput = document.querySelector("[data-show-tree-tags]");
const autoCollapseSearchInput = document.querySelector("[data-auto-collapse-search]");
@@ -285,7 +295,7 @@ viewButtons.forEach((button) => {
});
});
-// Выбор цветового акцента.
+// Выбор цветового акцента из пресетов или пользовательского градиента.
accentButtons.forEach((button) => {
button.addEventListener("click", () => {
preferences.accent = button.dataset.accent;
@@ -294,6 +304,20 @@ accentButtons.forEach((button) => {
});
});
+// Изменение параметров своего градиента сразу переключает accent на custom.
+function handleCustomAccentChange() {
+ preferences.customAccentFrom = customAccentFromInput.value;
+ preferences.customAccentTo = customAccentToInput.value;
+ preferences.customAccentAngle = Number(customAccentAngleInput.value);
+ preferences.accent = "custom";
+ applyPreferences(preferences);
+ savePreferences(preferences);
+}
+
+customAccentFromInput.addEventListener("input", handleCustomAccentChange);
+customAccentToInput.addEventListener("input", handleCustomAccentChange);
+customAccentAngleInput.addEventListener("input", handleCustomAccentChange);
+
// Показывает/скрывает номера в секциях, группах и карточках.
showNumbersInput.addEventListener("change", () => {
preferences.showNumbers = showNumbersInput.checked;
@@ -718,8 +742,21 @@ function applyPreferences(nextPreferences) {
nextPreferences.view = defaultPreferences.view;
}
+ if (!availableAccents.includes(nextPreferences.accent)) {
+ nextPreferences.accent = defaultPreferences.accent;
+ }
+
+ nextPreferences.customAccentFrom = normalizeHexColor(nextPreferences.customAccentFrom) || defaultPreferences.customAccentFrom;
+ nextPreferences.customAccentTo = normalizeHexColor(nextPreferences.customAccentTo) || defaultPreferences.customAccentTo;
+ nextPreferences.customAccentAngle = clamp(
+ Number(nextPreferences.customAccentAngle) || defaultPreferences.customAccentAngle,
+ 0,
+ 360,
+ );
+
document.documentElement.dataset.theme = nextPreferences.theme;
document.documentElement.dataset.accent = nextPreferences.accent;
+ applyAccentVariables(nextPreferences);
document.documentElement.style.setProperty(
"--sidebar-width",
`${clamp(Number(nextPreferences.sidebarWidth) || defaultPreferences.sidebarWidth, 260, 560)}px`,
@@ -731,11 +768,87 @@ function applyPreferences(nextPreferences) {
showNumbersInput.checked = Boolean(nextPreferences.showNumbers);
showTreeTagsInput.checked = Boolean(nextPreferences.showTreeTags);
autoCollapseSearchInput.checked = Boolean(nextPreferences.autoCollapseSearch);
+ customAccentFromInput.value = nextPreferences.customAccentFrom;
+ customAccentToInput.value = nextPreferences.customAccentTo;
+ customAccentAngleInput.value = String(nextPreferences.customAccentAngle);
+ customAccentAngleValue.textContent = `${nextPreferences.customAccentAngle}°`;
+ accentCustomPanel.hidden = nextPreferences.accent !== "custom";
+ updateCustomAccentPreview(nextPreferences);
setActiveButton(themeButtons, "theme", nextPreferences.theme);
setActiveButton(viewButtons, "view", nextPreferences.view);
setActiveButton(accentButtons, "accent", nextPreferences.accent);
}
+// Применяет CSS-переменные акцента для пресетов или пользовательского градиента.
+function applyAccentVariables(nextPreferences) {
+ const root = document.documentElement;
+
+ if (nextPreferences.accent === "custom") {
+ root.style.setProperty("--accent", nextPreferences.customAccentFrom);
+ root.style.setProperty("--accent-2", nextPreferences.customAccentTo);
+ root.style.setProperty("--accent-angle", `${nextPreferences.customAccentAngle}deg`);
+
+ const rgb = hexToRgb(nextPreferences.customAccentFrom);
+ if (rgb) {
+ root.style.setProperty("--accent-rgb", `${rgb.r}, ${rgb.g}, ${rgb.b}`);
+ }
+
+ return;
+ }
+
+ root.style.removeProperty("--accent");
+ root.style.removeProperty("--accent-2");
+ root.style.removeProperty("--accent-rgb");
+ root.style.removeProperty("--accent-angle");
+}
+
+// Обновляет превью пользовательского градиента в настройках и на кнопке custom.
+function updateCustomAccentPreview(nextPreferences) {
+ const gradient = `linear-gradient(${nextPreferences.customAccentAngle}deg, ${nextPreferences.customAccentFrom}, ${nextPreferences.customAccentTo})`;
+ customAccentPreview.style.background = gradient;
+
+ const customSwatch = document.querySelector('[data-accent="custom"]');
+ if (customSwatch) {
+ customSwatch.style.setProperty("--custom-accent-from", nextPreferences.customAccentFrom);
+ customSwatch.style.setProperty("--custom-accent-to", nextPreferences.customAccentTo);
+ customSwatch.style.setProperty("--custom-accent-angle", `${nextPreferences.customAccentAngle}deg`);
+ }
+}
+
+// Приводит hex-цвет к формату #rrggbb для input[type="color"].
+function normalizeHexColor(value) {
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ const normalized = value.trim().toLowerCase();
+
+ if (/^#[0-9a-f]{6}$/.test(normalized)) {
+ return normalized;
+ }
+
+ if (/^#[0-9a-f]{3}$/.test(normalized)) {
+ return `#${normalized[1]}${normalized[1]}${normalized[2]}${normalized[2]}${normalized[3]}${normalized[3]}`;
+ }
+
+ return null;
+}
+
+// Конвертирует hex в RGB для CSS-переменной --accent-rgb.
+function hexToRgb(value) {
+ const hex = normalizeHexColor(value);
+
+ if (!hex) {
+ return null;
+ }
+
+ return {
+ r: Number.parseInt(hex.slice(1, 3), 16),
+ g: Number.parseInt(hex.slice(3, 5), 16),
+ b: Number.parseInt(hex.slice(5, 7), 16),
+ };
+}
+
// Обновляет визуальное состояние группы кнопок.
function setActiveButton(buttons, key, value) {
buttons.forEach((button) => {
diff --git a/styles.css b/styles.css
index cc1618e..50cc569 100644
--- a/styles.css
+++ b/styles.css
@@ -20,6 +20,7 @@
--accent: #8b5cf6;
--accent-2: #22d3ee;
--accent-rgb: 139, 92, 246;
+ --accent-angle: 135deg;
--card-x: 50%;
--card-y: 50%;
--sidebar-width: 390px;
@@ -273,7 +274,7 @@ html[data-show-tree-tags="false"] .tree-node small {
border-radius: 4px;
padding: 0 2px;
color: white;
- background: linear-gradient(135deg, var(--accent), var(--accent-2));
+ background: linear-gradient(var(--accent-angle), var(--accent), var(--accent-2));
}
/* ==========================================================================
@@ -494,7 +495,7 @@ html[data-show-tree-tags="false"] .tree-node small {
.toggle-row input:checked {
border-color: rgba(var(--accent-rgb), 0.6);
- background: linear-gradient(135deg, var(--accent), var(--accent-2));
+ background: linear-gradient(var(--accent-angle), var(--accent), var(--accent-2));
}
.toggle-row input:checked::before {
@@ -556,12 +557,18 @@ html[data-show-tree-tags="false"] .tree-node small {
}
.segmented.is-active {
- background: linear-gradient(135deg, var(--accent), var(--accent-2));
+ background: linear-gradient(var(--accent-angle), var(--accent), var(--accent-2));
color: white;
}
.accent-group {
- justify-content: space-between;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ row-gap: 8px;
+}
+
+.accent-group > span {
+ width: 100%;
}
.accent-swatch {
@@ -594,6 +601,130 @@ html[data-show-tree-tags="false"] .tree-node small {
transform: scale(1.05);
}
+/* Кнопка своего градиента: pill, пунктир, подпись и превию через CSS-переменные. */
+.accent-swatch-custom {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ width: auto;
+ min-width: 58px;
+ height: 32px;
+ padding: 0 10px;
+ border: 2px dashed rgba(255, 255, 255, 0.42);
+ box-shadow: none;
+ background:
+ linear-gradient(45deg, rgba(255, 255, 255, 0.08) 25%, transparent 25%, transparent 75%, rgba(255, 255, 255, 0.08) 75%),
+ linear-gradient(45deg, rgba(255, 255, 255, 0.08) 25%, transparent 25%, transparent 75%, rgba(255, 255, 255, 0.08) 75%);
+ background-size: 8px 8px;
+ background-position: 0 0, 4px 4px;
+}
+
+html[data-theme="light"] .accent-swatch-custom {
+ border-color: rgba(15, 23, 42, 0.28);
+ background:
+ linear-gradient(45deg, rgba(15, 23, 42, 0.06) 25%, transparent 25%, transparent 75%, rgba(15, 23, 42, 0.06) 75%),
+ linear-gradient(45deg, rgba(15, 23, 42, 0.06) 25%, transparent 25%, transparent 75%, rgba(15, 23, 42, 0.06) 75%);
+ background-size: 8px 8px;
+ background-position: 0 0, 4px 4px;
+}
+
+.accent-swatch-custom::before {
+ content: "";
+ position: absolute;
+ inset: 2px;
+ border-radius: inherit;
+ background: linear-gradient(
+ var(--custom-accent-angle, 135deg),
+ var(--custom-accent-from, #8b5cf6),
+ var(--custom-accent-to, #22d3ee)
+ );
+}
+
+.accent-swatch-label {
+ position: relative;
+ z-index: 1;
+ color: white;
+ font-size: 0.72rem;
+ font-weight: 900;
+ letter-spacing: 0.04em;
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
+}
+
+.accent-swatch-custom.is-active {
+ border-style: solid;
+ border-color: var(--text);
+ transform: scale(1.05);
+}
+
+/* Панель настройки своего градиента появляется при data-accent="custom". */
+.accent-custom-panel {
+ display: grid;
+ gap: 8px;
+ padding: 10px;
+ border: 1px solid var(--border);
+ border-radius: 18px;
+ background: var(--surface);
+}
+
+.accent-custom-panel[hidden] {
+ display: none;
+}
+
+.accent-custom-title {
+ margin: 0;
+ color: var(--muted);
+ font-size: 0.78rem;
+ font-weight: 800;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.accent-custom-row {
+ display: grid;
+ grid-template-columns: 72px minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 8px;
+ color: var(--muted);
+ font-size: 0.84rem;
+ font-weight: 700;
+}
+
+.accent-custom-row input[type="range"] {
+ width: 100%;
+ accent-color: var(--accent);
+}
+
+.accent-custom-row input[type="color"] {
+ width: 100%;
+ height: 34px;
+ padding: 2px;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ background: var(--panel);
+ cursor: pointer;
+}
+
+.accent-custom-row:has(input[type="color"]) {
+ grid-template-columns: 72px 1fr;
+}
+
+.accent-custom-row output {
+ min-width: 3.2rem;
+ color: var(--accent);
+ font-size: 0.82rem;
+ font-weight: 800;
+ text-align: right;
+}
+
+.accent-custom-preview {
+ height: 36px;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ background: linear-gradient(var(--accent-angle), var(--accent), var(--accent-2));
+}
+
/* ==========================================================================
08. File-Tree Navigation
--------------------------------------------------------------------------
@@ -1038,7 +1169,7 @@ html[data-show-tree-tags="false"] .tree-node small {
.nav-card.is-hovered .arrow,
.nav-card:focus-visible .arrow {
transform: translateX(4px);
- background: linear-gradient(135deg, var(--accent), var(--accent-2));
+ background: linear-gradient(var(--accent-angle), var(--accent), var(--accent-2));
color: white;
}