добавление возможности создания своего цвета акцента

This commit is contained in:
Илья Братских 2026-06-28 14:59:34 +03:00 committed by KBAHTNET
parent 37220d7d17
commit 6aa5daf9ea
3 changed files with 289 additions and 6 deletions

View File

@ -173,6 +173,45 @@
data-accent="amber"
aria-label="Янтарный акцент"
></button>
<button
class="accent-swatch accent-swatch-custom"
type="button"
data-accent="custom"
aria-label="Свой градиент"
title="Свой градиент"
>
<span class="accent-swatch-label">Свой</span>
</button>
</div>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel hidden>
<p class="accent-custom-title">Свой градиент</p>
<label class="accent-custom-row">
<span>Угол</span>
<input
type="range"
min="0"
max="360"
step="1"
value="135"
data-custom-accent-angle
/>
<output data-custom-accent-angle-value>135°</output>
</label>
<label class="accent-custom-row">
<span>Цвет от</span>
<input type="color" value="#8b5cf6" data-custom-accent-from />
</label>
<label class="accent-custom-row">
<span>Цвет до</span>
<input type="color" value="#22d3ee" data-custom-accent-to />
</label>
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
</div>
</div>

115
script.js
View File

@ -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) => {

View File

@ -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;
}