Узор и градиент настраиваются отдельно для каждого типа блоков.
+Боковое меню и дерево
+Карточки верхнего уровня каталога
+Группы внутри раздела
+Карточки сервисов — только узор, без акцентного градиента
+Свой градиент
diff --git a/examples/flat/settings.json b/examples/flat/settings.json index b961c14..237a4fc 100644 --- a/examples/flat/settings.json +++ b/examples/flat/settings.json @@ -11,6 +11,26 @@ "customAccentFrom": "#8b5cf6", "customAccentTo": "#22d3ee", "customAccentAngle": 135, + "panelPatternNav": "hex", + "panelPatternNavOpacity": 70, + "panelPatternNavHeight": 45, + "panelPatternNavGradientOpacity": 80, + "panelPatternNavGradientHeight": 40, + "panelPatternSection": "hex", + "panelPatternSectionOpacity": 70, + "panelPatternSectionHeight": 45, + "panelPatternSectionGradientOpacity": 80, + "panelPatternSectionGradientHeight": 40, + "panelPatternGroup": "none", + "panelPatternGroupOpacity": 70, + "panelPatternGroupHeight": 45, + "panelPatternGroupGradientOpacity": 80, + "panelPatternGroupGradientHeight": 40, + "panelPatternItem": "none", + "panelPatternItemOpacity": 70, + "panelPatternItemHeight": 45, + "panelPatternItemGradientOpacity": 80, + "panelPatternItemGradientHeight": 40, "showNumbers": false, "showTreeTags": true, "showTreeType": false, @@ -31,7 +51,8 @@ "catalogListColumns": 1, "catalogCompactCardsColumns": 5, "catalogCompactListColumns": 3, - "searchPlaceholder": "Grafana, S3, журнал, сервер..." + "searchPlaceholder": "Grafana, S3, журнал, сервер...", + "settingsPanelWidth": 460 }, "limits": { "shellMaxWidth": { "min": 1200, "max": 3840 }, @@ -45,7 +66,8 @@ "catalogCardsColumns": { "min": 1, "max": 6 }, "catalogListColumns": { "min": 1, "max": 4 }, "catalogCompactCardsColumns": { "min": 2, "max": 8 }, - "catalogCompactListColumns": { "min": 1, "max": 6 } + "catalogCompactListColumns": { "min": 1, "max": 6 }, + "settingsPanelWidth": { "min": 320, "max": 720 } }, "features": { "allowNavLayoutChange": true, @@ -58,6 +80,7 @@ "allowSidebarResize": true, "allowLayoutLimitsChange": true, "allowThemeChange": true, - "allowAccentChange": true + "allowAccentChange": true, + "allowPanelPatternChange": true } } diff --git a/examples/full/index.html b/examples/full/index.html index 4d80fc9..f1aa308 100644 --- a/examples/full/index.html +++ b/examples/full/index.html @@ -126,6 +126,11 @@ +Узор и градиент настраиваются отдельно для каждого типа блоков.
+Боковое меню и дерево
+Карточки верхнего уровня каталога
+Группы внутри раздела
+Карточки сервисов — только узор, без акцентного градиента
+Свой градиент
diff --git a/examples/full/settings.json b/examples/full/settings.json index b961c14..237a4fc 100644 --- a/examples/full/settings.json +++ b/examples/full/settings.json @@ -11,6 +11,26 @@ "customAccentFrom": "#8b5cf6", "customAccentTo": "#22d3ee", "customAccentAngle": 135, + "panelPatternNav": "hex", + "panelPatternNavOpacity": 70, + "panelPatternNavHeight": 45, + "panelPatternNavGradientOpacity": 80, + "panelPatternNavGradientHeight": 40, + "panelPatternSection": "hex", + "panelPatternSectionOpacity": 70, + "panelPatternSectionHeight": 45, + "panelPatternSectionGradientOpacity": 80, + "panelPatternSectionGradientHeight": 40, + "panelPatternGroup": "none", + "panelPatternGroupOpacity": 70, + "panelPatternGroupHeight": 45, + "panelPatternGroupGradientOpacity": 80, + "panelPatternGroupGradientHeight": 40, + "panelPatternItem": "none", + "panelPatternItemOpacity": 70, + "panelPatternItemHeight": 45, + "panelPatternItemGradientOpacity": 80, + "panelPatternItemGradientHeight": 40, "showNumbers": false, "showTreeTags": true, "showTreeType": false, @@ -31,7 +51,8 @@ "catalogListColumns": 1, "catalogCompactCardsColumns": 5, "catalogCompactListColumns": 3, - "searchPlaceholder": "Grafana, S3, журнал, сервер..." + "searchPlaceholder": "Grafana, S3, журнал, сервер...", + "settingsPanelWidth": 460 }, "limits": { "shellMaxWidth": { "min": 1200, "max": 3840 }, @@ -45,7 +66,8 @@ "catalogCardsColumns": { "min": 1, "max": 6 }, "catalogListColumns": { "min": 1, "max": 4 }, "catalogCompactCardsColumns": { "min": 2, "max": 8 }, - "catalogCompactListColumns": { "min": 1, "max": 6 } + "catalogCompactListColumns": { "min": 1, "max": 6 }, + "settingsPanelWidth": { "min": 320, "max": 720 } }, "features": { "allowNavLayoutChange": true, @@ -58,6 +80,7 @@ "allowSidebarResize": true, "allowLayoutLimitsChange": true, "allowThemeChange": true, - "allowAccentChange": true + "allowAccentChange": true, + "allowPanelPatternChange": true } } diff --git a/examples/mixed/index.html b/examples/mixed/index.html index 4d80fc9..f1aa308 100644 --- a/examples/mixed/index.html +++ b/examples/mixed/index.html @@ -126,6 +126,11 @@ +Узор и градиент настраиваются отдельно для каждого типа блоков.
+Боковое меню и дерево
+Карточки верхнего уровня каталога
+Группы внутри раздела
+Карточки сервисов — только узор, без акцентного градиента
+Свой градиент
diff --git a/examples/mixed/settings.json b/examples/mixed/settings.json index b961c14..237a4fc 100644 --- a/examples/mixed/settings.json +++ b/examples/mixed/settings.json @@ -11,6 +11,26 @@ "customAccentFrom": "#8b5cf6", "customAccentTo": "#22d3ee", "customAccentAngle": 135, + "panelPatternNav": "hex", + "panelPatternNavOpacity": 70, + "panelPatternNavHeight": 45, + "panelPatternNavGradientOpacity": 80, + "panelPatternNavGradientHeight": 40, + "panelPatternSection": "hex", + "panelPatternSectionOpacity": 70, + "panelPatternSectionHeight": 45, + "panelPatternSectionGradientOpacity": 80, + "panelPatternSectionGradientHeight": 40, + "panelPatternGroup": "none", + "panelPatternGroupOpacity": 70, + "panelPatternGroupHeight": 45, + "panelPatternGroupGradientOpacity": 80, + "panelPatternGroupGradientHeight": 40, + "panelPatternItem": "none", + "panelPatternItemOpacity": 70, + "panelPatternItemHeight": 45, + "panelPatternItemGradientOpacity": 80, + "panelPatternItemGradientHeight": 40, "showNumbers": false, "showTreeTags": true, "showTreeType": false, @@ -31,7 +51,8 @@ "catalogListColumns": 1, "catalogCompactCardsColumns": 5, "catalogCompactListColumns": 3, - "searchPlaceholder": "Grafana, S3, журнал, сервер..." + "searchPlaceholder": "Grafana, S3, журнал, сервер...", + "settingsPanelWidth": 460 }, "limits": { "shellMaxWidth": { "min": 1200, "max": 3840 }, @@ -45,7 +66,8 @@ "catalogCardsColumns": { "min": 1, "max": 6 }, "catalogListColumns": { "min": 1, "max": 4 }, "catalogCompactCardsColumns": { "min": 2, "max": 8 }, - "catalogCompactListColumns": { "min": 1, "max": 6 } + "catalogCompactListColumns": { "min": 1, "max": 6 }, + "settingsPanelWidth": { "min": 320, "max": 720 } }, "features": { "allowNavLayoutChange": true, @@ -58,6 +80,7 @@ "allowSidebarResize": true, "allowLayoutLimitsChange": true, "allowThemeChange": true, - "allowAccentChange": true + "allowAccentChange": true, + "allowPanelPatternChange": true } } diff --git a/examples/section-items/index.html b/examples/section-items/index.html index 4d80fc9..f1aa308 100644 --- a/examples/section-items/index.html +++ b/examples/section-items/index.html @@ -126,6 +126,11 @@ +Узор и градиент настраиваются отдельно для каждого типа блоков.
+Боковое меню и дерево
+Карточки верхнего уровня каталога
+Группы внутри раздела
+Карточки сервисов — только узор, без акцентного градиента
+Свой градиент
diff --git a/examples/section-items/settings.json b/examples/section-items/settings.json index b961c14..237a4fc 100644 --- a/examples/section-items/settings.json +++ b/examples/section-items/settings.json @@ -11,6 +11,26 @@ "customAccentFrom": "#8b5cf6", "customAccentTo": "#22d3ee", "customAccentAngle": 135, + "panelPatternNav": "hex", + "panelPatternNavOpacity": 70, + "panelPatternNavHeight": 45, + "panelPatternNavGradientOpacity": 80, + "panelPatternNavGradientHeight": 40, + "panelPatternSection": "hex", + "panelPatternSectionOpacity": 70, + "panelPatternSectionHeight": 45, + "panelPatternSectionGradientOpacity": 80, + "panelPatternSectionGradientHeight": 40, + "panelPatternGroup": "none", + "panelPatternGroupOpacity": 70, + "panelPatternGroupHeight": 45, + "panelPatternGroupGradientOpacity": 80, + "panelPatternGroupGradientHeight": 40, + "panelPatternItem": "none", + "panelPatternItemOpacity": 70, + "panelPatternItemHeight": 45, + "panelPatternItemGradientOpacity": 80, + "panelPatternItemGradientHeight": 40, "showNumbers": false, "showTreeTags": true, "showTreeType": false, @@ -31,7 +51,8 @@ "catalogListColumns": 1, "catalogCompactCardsColumns": 5, "catalogCompactListColumns": 3, - "searchPlaceholder": "Grafana, S3, журнал, сервер..." + "searchPlaceholder": "Grafana, S3, журнал, сервер...", + "settingsPanelWidth": 460 }, "limits": { "shellMaxWidth": { "min": 1200, "max": 3840 }, @@ -45,7 +66,8 @@ "catalogCardsColumns": { "min": 1, "max": 6 }, "catalogListColumns": { "min": 1, "max": 4 }, "catalogCompactCardsColumns": { "min": 2, "max": 8 }, - "catalogCompactListColumns": { "min": 1, "max": 6 } + "catalogCompactListColumns": { "min": 1, "max": 6 }, + "settingsPanelWidth": { "min": 320, "max": 720 } }, "features": { "allowNavLayoutChange": true, @@ -58,6 +80,7 @@ "allowSidebarResize": true, "allowLayoutLimitsChange": true, "allowThemeChange": true, - "allowAccentChange": true + "allowAccentChange": true, + "allowPanelPatternChange": true } } diff --git a/examples/shared/script.js b/examples/shared/script.js index 9c45aa7..170f405 100644 --- a/examples/shared/script.js +++ b/examples/shared/script.js @@ -65,6 +65,44 @@ const defaultServiceMeta = { let serviceMeta = { ...defaultServiceMeta }; +const availablePanelPatterns = ["none", "hex", "dots", "grid", "diagonal", "cross", "mesh"]; +const panelPatternTargetIds = ["nav", "section", "group", "item"]; +const panelPatternTuningDefaults = { + opacity: 70, + height: 45, + fade: 10, + gradientOpacity: 80, + gradientHeight: 40, + gradientFade: 8, +}; +const panelPatternTargetMeta = { + nav: { defaultPattern: "hex" }, + section: { defaultPattern: "hex" }, + group: { defaultPattern: "none" }, + item: { defaultPattern: "none" }, +}; + +function capitalizePanelPatternTarget(target) { + return target.charAt(0).toUpperCase() + target.slice(1); +} + +function buildDefaultPanelPatternPreferences() { + const prefs = {}; + + for (const target of panelPatternTargetIds) { + const cap = capitalizePanelPatternTarget(target); + prefs[`panelPattern${cap}`] = panelPatternTargetMeta[target].defaultPattern; + prefs[`panelPattern${cap}Opacity`] = panelPatternTuningDefaults.opacity; + prefs[`panelPattern${cap}Height`] = panelPatternTuningDefaults.height; + prefs[`panelPattern${cap}Fade`] = panelPatternTuningDefaults.fade; + prefs[`panelPattern${cap}GradientOpacity`] = panelPatternTuningDefaults.gradientOpacity; + prefs[`panelPattern${cap}GradientHeight`] = panelPatternTuningDefaults.gradientHeight; + prefs[`panelPattern${cap}GradientFade`] = panelPatternTuningDefaults.gradientFade; + } + + return prefs; +} + // ============================================================================ // Default UI preferences // ============================================================================ @@ -76,6 +114,7 @@ const defaultPreferences = { customAccentFrom: "#8b5cf6", customAccentTo: "#22d3ee", customAccentAngle: 135, + ...buildDefaultPanelPatternPreferences(), navLayout: "sidebar", showTree: true, showResizeHandles: true, @@ -100,6 +139,7 @@ const defaultPreferences = { catalogCompactCardsColumns: 5, catalogCompactListColumns: 3, searchPlaceholder: "Grafana, S3, журнал, сервер...", + settingsPanelWidth: 460, }; const defaultPanelFeatures = { @@ -114,6 +154,7 @@ const defaultPanelFeatures = { allowLayoutLimitsChange: true, allowThemeChange: true, allowAccentChange: true, + allowPanelPatternChange: true, }; const defaultPanelLimits = { @@ -129,6 +170,7 @@ const defaultPanelLimits = { catalogListColumns: { min: 1, max: 4 }, catalogCompactCardsColumns: { min: 2, max: 8 }, catalogCompactListColumns: { min: 1, max: 6 }, + settingsPanelWidth: { min: 320, max: 720 }, }; const layoutLimitKeys = [ @@ -149,7 +191,7 @@ const catalogColumnKeys = [ "catalogCompactListColumns", ]; -const panelLimitKeys = [...layoutLimitKeys, ...catalogColumnKeys]; +const panelLimitKeys = [...layoutLimitKeys, ...catalogColumnKeys, "settingsPanelWidth"]; const availableNavLayouts = ["sidebar", "top"]; const availableTreeMetaLayouts = ["inline", "stacked"]; @@ -186,6 +228,7 @@ const themeButtons = document.querySelectorAll("[data-theme]"); const navLayoutButtons = document.querySelectorAll("[data-nav-layout]"); const viewButtons = document.querySelectorAll("[data-view]"); const accentButtons = document.querySelectorAll("[data-accent]"); +const patternTargetsPanel = document.querySelector("[data-pattern-targets-panel]"); const accentCustomPanel = document.querySelector("[data-accent-custom-panel]"); const customAccentFromInput = document.querySelector("[data-custom-accent-from]"); const customAccentToInput = document.querySelector("[data-custom-accent-to]"); @@ -201,6 +244,8 @@ const treeColumnsInput = document.querySelector("[data-tree-columns]"); const treeColumnsValue = document.querySelector("[data-tree-columns-value]"); const treeMetaLayoutButtons = document.querySelectorAll("[data-tree-meta-layout]"); const autoCollapseSearchInput = document.querySelector("[data-auto-collapse-search]"); +const settingsPanelWidthInput = document.querySelector("[data-settings-panel-width]"); +const settingsPanelWidthValue = document.querySelector("[data-settings-panel-width-value]"); const layoutLimitInputs = document.querySelectorAll("[data-layout-limit]"); const catalogColumnInputs = document.querySelectorAll("[data-catalog-columns]"); const settingsSections = document.querySelectorAll("[data-settings-section]"); @@ -388,6 +433,35 @@ accentButtons.forEach((button) => { }); }); +patternTargetsPanel?.addEventListener("click", (event) => { + const button = event.target.closest("[data-panel-pattern-target][data-panel-pattern]"); + + if (!button || !panelFeatures.allowPanelPatternChange) { + return; + } + + const target = button.dataset.panelPatternTarget; + preferences[getPanelPatternPreferenceKey(target, "pattern")] = button.dataset.panelPattern; + applyPreferences(preferences); + savePreferences(preferences); +}); + +patternTargetsPanel?.addEventListener("input", (event) => { + const input = event.target.closest("[data-panel-pattern-tuning]"); + + if (!input || !panelFeatures.allowPanelPatternChange) { + return; + } + + const target = input.dataset.panelPatternTuning; + const field = input.dataset.panelPatternField; + const key = getPanelPatternPreferenceKey(target, field); + const fallback = defaultPreferences[key] ?? panelPatternTuningDefaults[field] ?? 50; + preferences[key] = clampPanelPatternPercent(input.value, fallback); + applyPreferences(preferences); + savePreferences(preferences); +}); + // Изменение параметров своего градиента сразу переключает accent на custom. function handleCustomAccentChange() { if (!panelFeatures.allowAccentChange) { @@ -459,6 +533,17 @@ if (treeColumnsInput) { treeColumnsInput.addEventListener("input", handleTreeColumnsChange); } +function handleSettingsPanelWidthChange() { + preferences.settingsPanelWidth = clampLayoutPreference( + "settingsPanelWidth", + settingsPanelWidthInput?.value, + ); + applyPreferences(preferences); + savePreferences(preferences); +} + +settingsPanelWidthInput?.addEventListener("input", handleSettingsPanelWidthChange); + treeMetaLayoutButtons.forEach((button) => { button.addEventListener("click", () => { if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) { @@ -625,6 +710,9 @@ function normalizePreferences(raw) { next.accent = defaultPreferences.accent; } + migrateLegacyPanelPatternPreferences(next); + normalizePanelPatternPreferences(next); + if (!availableNavLayouts.includes(next.navLayout)) { next.navLayout = defaultPreferences.navLayout; } @@ -659,6 +747,7 @@ function normalizePreferences(raw) { getTopBarMinHeight(next), getTopBarMaxHeight(next), ); + next.settingsPanelWidth = clampLayoutPreference("settingsPanelWidth", next.settingsPanelWidth); return next; } @@ -724,6 +813,19 @@ function enforceFeatureLocks(rawPreferences) { next.customAccentAngle = panelDefaults.customAccentAngle; } + if (!panelFeatures.allowPanelPatternChange) { + for (const target of panelPatternTargetIds) { + const cap = capitalizePanelPatternTarget(target); + next[`panelPattern${cap}`] = panelDefaults[`panelPattern${cap}`]; + next[`panelPattern${cap}Opacity`] = panelDefaults[`panelPattern${cap}Opacity`]; + next[`panelPattern${cap}Height`] = panelDefaults[`panelPattern${cap}Height`]; + next[`panelPattern${cap}Fade`] = panelDefaults[`panelPattern${cap}Fade`]; + next[`panelPattern${cap}GradientOpacity`] = panelDefaults[`panelPattern${cap}GradientOpacity`]; + next[`panelPattern${cap}GradientHeight`] = panelDefaults[`panelPattern${cap}GradientHeight`]; + next[`panelPattern${cap}GradientFade`] = panelDefaults[`panelPattern${cap}GradientFade`]; + } + } + if (!panelFeatures.allowShowNumbers) { next.showNumbers = panelDefaults.showNumbers; } @@ -1370,6 +1472,7 @@ function applyPreferences(nextPreferences) { document.documentElement.dataset.theme = activePreferences.theme; document.documentElement.dataset.accent = activePreferences.accent; + applyPanelPatternPreferences(activePreferences); document.documentElement.dataset.navLayout = activePreferences.navLayout === "top" ? "top" : "sidebar"; document.documentElement.dataset.showTree = String(isTreeVisible(activePreferences)); @@ -1391,6 +1494,10 @@ function applyPreferences(nextPreferences) { getTopBarMaxHeight(activePreferences), )}px`, ); + document.documentElement.style.setProperty( + "--settings-panel-width", + `${clampLayoutPreference("settingsPanelWidth", activePreferences.settingsPanelWidth)}px`, + ); const treeColumns = getTreeColumns(activePreferences); const sidebarMaxWidth = getSidebarMaxWidth(activePreferences); @@ -1461,9 +1568,11 @@ function applyPreferences(nextPreferences) { setActiveButton(navLayoutButtons, "navLayout", activePreferences.navLayout); setActiveButton(viewButtons, "view", activePreferences.view); setActiveButton(accentButtons, "accent", activePreferences.accent); + syncPatternTargetPanels(activePreferences); setActiveButton(treeMetaLayoutButtons, "treeMetaLayout", activePreferences.treeMetaLayout); syncLayoutLimitInputs(activePreferences); syncCatalogColumnInputs(activePreferences); + syncSettingsPanelWidthInput(activePreferences); applyPanelFeatures(activePreferences); } @@ -1524,7 +1633,11 @@ function isSettingsSectionVisible(sectionName, activePreferences) { case "tree": return panelFeatures.allowTreeOptions && activePreferences.showTree; case "appearance": - return panelFeatures.allowThemeChange || panelFeatures.allowAccentChange; + return ( + panelFeatures.allowThemeChange || + panelFeatures.allowAccentChange || + panelFeatures.allowPanelPatternChange + ); default: return true; } @@ -1776,6 +1889,22 @@ function syncLayoutLimitInputs(activePreferences) { }); } +function syncSettingsPanelWidthInput(activePreferences) { + if (!settingsPanelWidthInput) { + return; + } + + const limits = getPanelLimitRange("settingsPanelWidth"); + settingsPanelWidthInput.min = String(limits.min); + settingsPanelWidthInput.max = String(limits.max); + settingsPanelWidthInput.step = "10"; + settingsPanelWidthInput.value = String(activePreferences.settingsPanelWidth); + + if (settingsPanelWidthValue) { + settingsPanelWidthValue.textContent = `${activePreferences.settingsPanelWidth}px`; + } +} + function syncCatalogColumnInputs(activePreferences) { catalogColumnInputs.forEach((input) => { const key = input.dataset.catalogColumns; @@ -1802,6 +1931,194 @@ function getPanelLimitRange(key) { return getLayoutLimitRange(key); } +function clampPanelPatternPercent(value, fallback = 50) { + const numeric = Number(value); + const base = Number.isFinite(numeric) ? numeric : fallback; + return clamp(Math.round(base), 0, 100); +} + +function getPanelPatternPreferenceKey(target, field) { + const cap = capitalizePanelPatternTarget(target); + + switch (field) { + case "pattern": + return `panelPattern${cap}`; + case "opacity": + return `panelPattern${cap}Opacity`; + case "height": + return `panelPattern${cap}Height`; + case "fade": + return `panelPattern${cap}Fade`; + case "gradientOpacity": + return `panelPattern${cap}GradientOpacity`; + case "gradientHeight": + return `panelPattern${cap}GradientHeight`; + case "gradientFade": + return `panelPattern${cap}GradientFade`; + default: + return `panelPattern${cap}`; + } +} + +function migrateLegacyPanelPatternPreferences(next) { + if (next.panelPatternNav !== undefined) { + return; + } + + const legacyPattern = availablePanelPatterns.includes(next.panelPattern) + ? next.panelPattern + : panelPatternTargetMeta.nav.defaultPattern; + const legacyOpacity = next.panelPatternOpacity ?? panelPatternTuningDefaults.opacity; + const legacyHeight = next.panelPatternHeight ?? panelPatternTuningDefaults.height; + const legacyGradientOpacity = next.panelGradientOpacity ?? panelPatternTuningDefaults.gradientOpacity; + const legacyGradientHeight = next.panelGradientHeight ?? panelPatternTuningDefaults.gradientHeight; + const legacyTargets = [ + ["Nav", next.panelPatternOnNav !== false], + ["Section", next.panelPatternOnSection !== false], + ["Group", Boolean(next.panelPatternOnGroup)], + ["Item", Boolean(next.panelPatternOnItem)], + ]; + + for (const [suffix, enabled] of legacyTargets) { + next[`panelPattern${suffix}`] = enabled && legacyPattern !== "none" ? legacyPattern : "none"; + next[`panelPattern${suffix}Opacity`] = legacyOpacity; + next[`panelPattern${suffix}Height`] = legacyHeight; + next[`panelPattern${suffix}Fade`] = panelPatternTuningDefaults.fade; + next[`panelPattern${suffix}GradientOpacity`] = legacyGradientOpacity; + next[`panelPattern${suffix}GradientHeight`] = legacyGradientHeight; + next[`panelPattern${suffix}GradientFade`] = panelPatternTuningDefaults.gradientFade; + } +} + +function normalizePanelPatternPreferences(next) { + for (const target of panelPatternTargetIds) { + const cap = capitalizePanelPatternTarget(target); + const patternKey = `panelPattern${cap}`; + + if (!availablePanelPatterns.includes(next[patternKey])) { + next[patternKey] = panelPatternTargetMeta[target].defaultPattern; + } + + next[`panelPattern${cap}Opacity`] = clampPanelPatternPercent( + next[`panelPattern${cap}Opacity`], + panelPatternTuningDefaults.opacity, + ); + next[`panelPattern${cap}Height`] = clampPanelPatternPercent( + next[`panelPattern${cap}Height`], + panelPatternTuningDefaults.height, + ); + next[`panelPattern${cap}Fade`] = clampPanelPatternPercent( + next[`panelPattern${cap}Fade`], + panelPatternTuningDefaults.fade, + ); + next[`panelPattern${cap}GradientOpacity`] = clampPanelPatternPercent( + next[`panelPattern${cap}GradientOpacity`], + panelPatternTuningDefaults.gradientOpacity, + ); + next[`panelPattern${cap}GradientHeight`] = clampPanelPatternPercent( + next[`panelPattern${cap}GradientHeight`], + panelPatternTuningDefaults.gradientHeight, + ); + next[`panelPattern${cap}GradientFade`] = clampPanelPatternPercent( + next[`panelPattern${cap}GradientFade`], + panelPatternTuningDefaults.gradientFade, + ); + } +} + +function getPanelPatternFadeStart(heightPercent, fadePercent) { + const height = clampPanelPatternPercent(heightPercent, panelPatternTuningDefaults.height); + const fade = clampPanelPatternPercent(fadePercent, panelPatternTuningDefaults.fade); + return `${Math.max(0, height - fade)}%`; +} + +function applyPanelPatternPreferences(activePreferences) { + for (const target of panelPatternTargetIds) { + const cap = capitalizePanelPatternTarget(target); + const pattern = activePreferences[`panelPattern${cap}`] || "none"; + const height = clampPanelPatternPercent( + activePreferences[`panelPattern${cap}Height`], + panelPatternTuningDefaults.height, + ); + const opacityPercent = clampPanelPatternPercent( + activePreferences[`panelPattern${cap}Opacity`], + panelPatternTuningDefaults.opacity, + ); + const patternOpacity = pattern === "none" || height === 0 ? 0 : opacityPercent / 100; + + document.documentElement.dataset[`panelPattern${cap}`] = pattern; + document.documentElement.style.setProperty(`--panel-pattern-${target}-opacity`, String(patternOpacity)); + document.documentElement.style.setProperty(`--panel-pattern-${target}-height`, `${height}%`); + document.documentElement.style.setProperty( + `--panel-pattern-${target}-fade-start`, + getPanelPatternFadeStart(height, activePreferences[`panelPattern${cap}Fade`]), + ); + document.documentElement.style.setProperty( + `--panel-gradient-${target}-strength`, + String( + (clampPanelPatternPercent( + activePreferences[`panelPattern${cap}GradientOpacity`], + panelPatternTuningDefaults.gradientOpacity, + ) / + 100) * + 0.28, + ), + ); + const gradientHeight = clampPanelPatternPercent( + activePreferences[`panelPattern${cap}GradientHeight`], + panelPatternTuningDefaults.gradientHeight, + ); + document.documentElement.style.setProperty( + `--panel-gradient-${target}-height`, + `${gradientHeight}%`, + ); + document.documentElement.style.setProperty( + `--panel-gradient-${target}-fade-start`, + getPanelPatternFadeStart(gradientHeight, activePreferences[`panelPattern${cap}GradientFade`]), + ); + } +} + +function syncPatternTargetPanels(activePreferences) { + if (!patternTargetsPanel) { + return; + } + + const disabled = !panelFeatures.allowPanelPatternChange; + const tuningFields = ["opacity", "height", "fade", "gradientOpacity", "gradientHeight", "gradientFade"]; + + for (const target of panelPatternTargetIds) { + const pattern = activePreferences[getPanelPatternPreferenceKey(target, "pattern")]; + patternTargetsPanel + .querySelectorAll(`[data-panel-pattern-target="${target}"][data-panel-pattern]`) + .forEach((button) => { + const isActive = button.dataset.panelPattern === pattern; + button.classList.toggle("is-active", isActive); + button.setAttribute("aria-pressed", String(isActive)); + button.disabled = disabled; + }); + + for (const field of tuningFields) { + const key = getPanelPatternPreferenceKey(target, field); + const input = patternTargetsPanel.querySelector( + `[data-panel-pattern-tuning="${target}"][data-panel-pattern-field="${field}"]`, + ); + const output = patternTargetsPanel.querySelector( + `[data-panel-pattern-value="${target}"][data-panel-pattern-field="${field}"]`, + ); + + if (input) { + input.value = String(activePreferences[key]); + input.disabled = disabled; + } + + if (output) { + output.textContent = `${activePreferences[key]}%`; + } + } + } +} + function clampCatalogColumn(key, value) { const limits = getPanelLimitRange(key); const fallback = panelDefaults[key] ?? defaultPreferences[key]; diff --git a/examples/shared/styles.css b/examples/shared/styles.css index befc0ff..e476596 100644 --- a/examples/shared/styles.css +++ b/examples/shared/styles.css @@ -31,6 +31,46 @@ --catalog-list-columns: 1; --catalog-compact-cards-columns: 5; --catalog-compact-list-columns: 3; + --pattern-color: rgba(255, 255, 255, 0.16); + --pattern-tile-hex-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E"); + --pattern-tile-hex-size: 96px 84px; + --pattern-tile-dots-image: radial-gradient(circle at center, var(--pattern-color) 0.95px, transparent 1px); + --pattern-tile-dots-size: 18px 18px; + --pattern-tile-grid-image: + linear-gradient(var(--pattern-color) 1px, transparent 1px), + linear-gradient(90deg, var(--pattern-color) 1px, transparent 1px); + --pattern-tile-grid-size: 26px 26px; + --pattern-tile-diagonal-image: repeating-linear-gradient( + -45deg, + transparent, + transparent 10px, + var(--pattern-color) 10px, + var(--pattern-color) 11px + ); + --pattern-tile-diagonal-size: auto; + --pattern-tile-cross-image: + repeating-linear-gradient( + 0deg, + transparent, + transparent 14px, + var(--pattern-color) 14px, + var(--pattern-color) 15px + ), + repeating-linear-gradient( + 90deg, + transparent, + transparent 14px, + var(--pattern-color) 14px, + var(--pattern-color) 15px + ); + --pattern-tile-cross-size: auto; + --pattern-tile-mesh-image: radial-gradient( + circle at 1px 1px, + rgba(var(--accent-rgb), 0.22) 1px, + transparent 0 + ); + --pattern-tile-mesh-size: 20px 20px; + --settings-panel-width: 460px; } /* Светлая тема переопределяет только токены, структура стилей остается общей. */ @@ -45,8 +85,40 @@ html[data-theme="light"] { --muted: #5f6b7a; --border: rgba(15, 23, 42, 0.12); --shadow: 0 24px 70px rgba(56, 65, 91, 0.18); + --pattern-color: rgba(15, 23, 42, 0.13); + --pattern-tile-hex-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E"); + --pattern-tile-mesh-image: radial-gradient( + circle at 1px 1px, + rgba(var(--accent-rgb), 0.16) 1px, + transparent 0 + ); } +html[data-panel-pattern-nav="hex"] { --panel-pattern-image-nav: var(--pattern-tile-hex-image); --panel-pattern-size-nav: var(--pattern-tile-hex-size); } +html[data-panel-pattern-nav="dots"] { --panel-pattern-image-nav: var(--pattern-tile-dots-image); --panel-pattern-size-nav: var(--pattern-tile-dots-size); } +html[data-panel-pattern-nav="grid"] { --panel-pattern-image-nav: var(--pattern-tile-grid-image); --panel-pattern-size-nav: var(--pattern-tile-grid-size); } +html[data-panel-pattern-nav="diagonal"] { --panel-pattern-image-nav: var(--pattern-tile-diagonal-image); --panel-pattern-size-nav: var(--pattern-tile-diagonal-size); } +html[data-panel-pattern-nav="cross"] { --panel-pattern-image-nav: var(--pattern-tile-cross-image); --panel-pattern-size-nav: var(--pattern-tile-cross-size); } +html[data-panel-pattern-nav="mesh"] { --panel-pattern-image-nav: var(--pattern-tile-mesh-image); --panel-pattern-size-nav: var(--pattern-tile-mesh-size); } +html[data-panel-pattern-section="hex"] { --panel-pattern-image-section: var(--pattern-tile-hex-image); --panel-pattern-size-section: var(--pattern-tile-hex-size); } +html[data-panel-pattern-section="dots"] { --panel-pattern-image-section: var(--pattern-tile-dots-image); --panel-pattern-size-section: var(--pattern-tile-dots-size); } +html[data-panel-pattern-section="grid"] { --panel-pattern-image-section: var(--pattern-tile-grid-image); --panel-pattern-size-section: var(--pattern-tile-grid-size); } +html[data-panel-pattern-section="diagonal"] { --panel-pattern-image-section: var(--pattern-tile-diagonal-image); --panel-pattern-size-section: var(--pattern-tile-diagonal-size); } +html[data-panel-pattern-section="cross"] { --panel-pattern-image-section: var(--pattern-tile-cross-image); --panel-pattern-size-section: var(--pattern-tile-cross-size); } +html[data-panel-pattern-section="mesh"] { --panel-pattern-image-section: var(--pattern-tile-mesh-image); --panel-pattern-size-section: var(--pattern-tile-mesh-size); } +html[data-panel-pattern-group="hex"] { --panel-pattern-image-group: var(--pattern-tile-hex-image); --panel-pattern-size-group: var(--pattern-tile-hex-size); } +html[data-panel-pattern-group="dots"] { --panel-pattern-image-group: var(--pattern-tile-dots-image); --panel-pattern-size-group: var(--pattern-tile-dots-size); } +html[data-panel-pattern-group="grid"] { --panel-pattern-image-group: var(--pattern-tile-grid-image); --panel-pattern-size-group: var(--pattern-tile-grid-size); } +html[data-panel-pattern-group="diagonal"] { --panel-pattern-image-group: var(--pattern-tile-diagonal-image); --panel-pattern-size-group: var(--pattern-tile-diagonal-size); } +html[data-panel-pattern-group="cross"] { --panel-pattern-image-group: var(--pattern-tile-cross-image); --panel-pattern-size-group: var(--pattern-tile-cross-size); } +html[data-panel-pattern-group="mesh"] { --panel-pattern-image-group: var(--pattern-tile-mesh-image); --panel-pattern-size-group: var(--pattern-tile-mesh-size); } +html[data-panel-pattern-item="hex"] { --panel-pattern-image-item: var(--pattern-tile-hex-image); --panel-pattern-size-item: var(--pattern-tile-hex-size); } +html[data-panel-pattern-item="dots"] { --panel-pattern-image-item: var(--pattern-tile-dots-image); --panel-pattern-size-item: var(--pattern-tile-dots-size); } +html[data-panel-pattern-item="grid"] { --panel-pattern-image-item: var(--pattern-tile-grid-image); --panel-pattern-size-item: var(--pattern-tile-grid-size); } +html[data-panel-pattern-item="diagonal"] { --panel-pattern-image-item: var(--pattern-tile-diagonal-image); --panel-pattern-size-item: var(--pattern-tile-diagonal-size); } +html[data-panel-pattern-item="cross"] { --panel-pattern-image-item: var(--pattern-tile-cross-image); --panel-pattern-size-item: var(--pattern-tile-cross-size); } +html[data-panel-pattern-item="mesh"] { --panel-pattern-image-item: var(--pattern-tile-mesh-image); --panel-pattern-size-item: var(--pattern-tile-mesh-size); } + /* Наборы акцентов. --accent-rgb нужен для прозрачных градиентов через rgba(). */ html[data-accent="cyan"] { --accent: #06b6d4; @@ -324,6 +396,8 @@ body.is-resizing-top-bar { background: var(--panel); box-shadow: var(--shadow); backdrop-filter: blur(22px); + position: relative; + overflow: hidden; } .hero-content { @@ -335,7 +409,6 @@ body.is-resizing-top-bar { gap: 8px; padding: clamp(16px, 2.6vw, 24px); border-radius: 22px; - position: relative; overflow: visible; } @@ -344,21 +417,117 @@ body.is-resizing-top-bar { overflow: auto; } -/* Внутренний фон sidebar: градиент + hexagon pattern. */ -.hero-content::before { +/* Акцентный градиент под узором — отдельно для каждого типа блоков. */ +html:not([data-panel-pattern-nav="none"]) .hero-content::before { content: ""; position: absolute; inset: 0; border-radius: inherit; - background: - linear-gradient(120deg, rgba(var(--accent-rgb), 0.25), transparent 34%), - url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.10' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E"); - mask-image: linear-gradient(to bottom, black, transparent 88%); + background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%); + opacity: var(--panel-gradient-nav-strength); + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-nav-fade-start), transparent var(--panel-gradient-nav-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-nav-fade-start), transparent var(--panel-gradient-nav-height)); pointer-events: none; + z-index: 0; +} + +html:not([data-panel-pattern-section="none"]) .catalog-section::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%); + opacity: var(--panel-gradient-section-strength); + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-section-fade-start), transparent var(--panel-gradient-section-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-section-fade-start), transparent var(--panel-gradient-section-height)); + pointer-events: none; + z-index: 0; +} + +html:not([data-panel-pattern-group="none"]) .catalog-group::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%); + opacity: var(--panel-gradient-group-strength); + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-group-fade-start), transparent var(--panel-gradient-group-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-group-fade-start), transparent var(--panel-gradient-group-height)); + pointer-events: none; + z-index: 0; +} + +html:not([data-panel-pattern-nav="none"]) .hero-content::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + z-index: 0; + opacity: var(--panel-pattern-nav-opacity); + background-image: var(--panel-pattern-image-nav); + background-size: var(--panel-pattern-size-nav); + background-repeat: repeat; + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-nav-fade-start), transparent var(--panel-pattern-nav-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-nav-fade-start), transparent var(--panel-pattern-nav-height)); +} + +html:not([data-panel-pattern-section="none"]) .catalog-section::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + z-index: 0; + opacity: var(--panel-pattern-section-opacity); + background-image: var(--panel-pattern-image-section); + background-size: var(--panel-pattern-size-section); + background-repeat: repeat; + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-section-fade-start), transparent var(--panel-pattern-section-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-section-fade-start), transparent var(--panel-pattern-section-height)); +} + +html:not([data-panel-pattern-group="none"]) .catalog-group::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + z-index: 0; + opacity: var(--panel-pattern-group-opacity); + background-image: var(--panel-pattern-image-group); + background-size: var(--panel-pattern-size-group); + background-repeat: repeat; + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-group-fade-start), transparent var(--panel-pattern-group-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-group-fade-start), transparent var(--panel-pattern-group-height)); +} + +html:not([data-panel-pattern-item="none"]) .nav-card::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + z-index: 0; + opacity: var(--panel-pattern-item-opacity); + background-image: var(--panel-pattern-image-item); + background-size: var(--panel-pattern-size-item); + background-repeat: repeat; + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-item-fade-start), transparent var(--panel-pattern-item-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-item-fade-start), transparent var(--panel-pattern-item-height)); +} + +.catalog-section > summary, +.catalog-section > .section-groups, +.catalog-group > summary, +.catalog-group > .items-grid { + position: relative; + z-index: 1; } .hero-content > * { position: relative; + z-index: 1; } /* Общий стиль маленьких uppercase-меток и номеров. */ @@ -414,7 +583,7 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta { z-index: 1001; top: 24px; right: 24px; - width: min(460px, calc(100% - 48px)); + width: min(var(--settings-panel-width), calc(100% - 48px)); max-height: calc(100vh - 48px); overflow: auto; display: grid; @@ -512,6 +681,27 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta { line-height: 1; } +.settings-width-row { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto auto; + gap: 6px 10px; +} + +.settings-width-row span { + grid-column: 1; + grid-row: 1; +} + +.settings-width-row output { + grid-column: 2; + grid-row: 1; +} + +.settings-width-row input[type="range"] { + grid-column: 1 / -1; + grid-row: 2; +} + .settings-section { display: grid; gap: 10px; @@ -901,6 +1091,166 @@ html[data-theme="light"] .accent-swatch-custom { transform: scale(1.05); } +.pattern-group { + align-items: stretch; +} + +.pattern-targets-panel { + display: grid; + gap: 10px; + width: 100%; +} + +.pattern-target-panel .settings-section-body { + display: grid; + gap: 10px; + padding-top: 10px; +} + +.pattern-swatches { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + width: 100%; +} + +.pattern-swatch { + min-height: 54px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--surface); + color: var(--muted); + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; + cursor: pointer; + position: relative; + overflow: hidden; + padding: 0; +} + +.pattern-swatch::before { + content: ""; + position: absolute; + inset: 0; + opacity: 0.95; + pointer-events: none; +} + +.pattern-swatch[data-panel-pattern="none"]::before { + background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.12), transparent 70%); +} + +.pattern-swatch[data-panel-pattern="hex"]::before { + background-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E"); + background-size: 48px 42px; +} + +html[data-theme="light"] .pattern-swatch[data-panel-pattern="hex"]::before { + background-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E"); +} + +.pattern-swatch[data-panel-pattern="dots"]::before { + background-image: radial-gradient(circle at center, var(--pattern-color) 0.95px, transparent 1px); + background-size: 12px 12px; +} + +.pattern-swatch[data-panel-pattern="grid"]::before { + background-image: + linear-gradient(var(--pattern-color) 1px, transparent 1px), + linear-gradient(90deg, var(--pattern-color) 1px, transparent 1px); + background-size: 16px 16px; +} + +.pattern-swatch[data-panel-pattern="diagonal"]::before { + background-image: repeating-linear-gradient( + -45deg, + transparent, + transparent 7px, + var(--pattern-color) 7px, + var(--pattern-color) 8px + ); +} + +.pattern-swatch[data-panel-pattern="cross"]::before { + background-image: + repeating-linear-gradient( + 0deg, + transparent, + transparent 9px, + var(--pattern-color) 9px, + var(--pattern-color) 10px + ), + repeating-linear-gradient( + 90deg, + transparent, + transparent 9px, + var(--pattern-color) 9px, + var(--pattern-color) 10px + ); +} + +.pattern-swatch[data-panel-pattern="mesh"]::before { + background-image: radial-gradient( + circle at 1px 1px, + rgba(var(--accent-rgb), 0.24) 1px, + transparent 0 + ); + background-size: 12px 12px; +} + +.pattern-swatch-label { + position: relative; + z-index: 1; + display: grid; + place-items: end start; + width: 100%; + height: 100%; + padding: 6px 8px; + background: linear-gradient(to top, rgba(0, 0, 0, 0.28), transparent 58%); + color: #f8fafc; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35); +} + +html[data-theme="light"] .pattern-swatch-label { + background: linear-gradient(to top, rgba(255, 255, 255, 0.82), transparent 58%); + color: #101827; + text-shadow: none; +} + +.pattern-swatch.is-active { + border-color: rgba(var(--accent-rgb), 0.72); + box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.18); +} + +.pattern-tuning { + display: grid; + gap: 12px; + width: 100%; +} + +.pattern-tuning .accent-custom-row { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto auto; + gap: 6px 10px; +} + +.pattern-tuning .accent-custom-row span { + grid-column: 1; + grid-row: 1; +} + +.pattern-tuning .accent-custom-row output { + grid-column: 2; + grid-row: 1; +} + +.pattern-tuning .accent-custom-row input[type="range"] { + grid-column: 1 / -1; + grid-row: 2; +} + /* Панель настройки своего градиента появляется при data-accent="custom". */ .accent-custom-panel { display: grid; @@ -1329,6 +1679,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon { } .catalog-group { + position: relative; border: 1px solid var(--border); border-radius: 18px; background: rgba(255, 255, 255, 0.035); @@ -1427,6 +1778,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon { opacity: 0; transition: opacity 180ms ease; pointer-events: none; + z-index: 1; } /* Hover задается классом .is-hovered из JS, чтобы убрать скачки на границах. */ @@ -1445,6 +1797,8 @@ html:not([data-tree-columns="1"]) .tree-node-icon { /* Иконка сервиса берется из serviceMeta. */ .nav-card img { + position: relative; + z-index: 1; width: 50px; height: 50px; object-fit: contain; @@ -1453,6 +1807,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon { .card-content { position: relative; + z-index: 1; display: grid; gap: 5px; } @@ -1493,6 +1848,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon { position: absolute; right: 16px; bottom: 14px; + z-index: 1; width: 36px; height: 36px; display: grid; @@ -1515,6 +1871,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon { .shine { position: absolute; inset: 0; + z-index: 2; background: linear-gradient( 110deg, transparent 0%, diff --git a/index.html b/index.html index d471259..58c8651 100644 --- a/index.html +++ b/index.html @@ -127,6 +127,11 @@ +Узор и градиент настраиваются отдельно для каждого типа блоков.
+Боковое меню и дерево
+Карточки верхнего уровня каталога
+Группы внутри раздела
+Карточки сервисов — только узор, без акцентного градиента
+Свой градиент
diff --git a/script.js b/script.js index 9c45aa7..170f405 100644 --- a/script.js +++ b/script.js @@ -65,6 +65,44 @@ const defaultServiceMeta = { let serviceMeta = { ...defaultServiceMeta }; +const availablePanelPatterns = ["none", "hex", "dots", "grid", "diagonal", "cross", "mesh"]; +const panelPatternTargetIds = ["nav", "section", "group", "item"]; +const panelPatternTuningDefaults = { + opacity: 70, + height: 45, + fade: 10, + gradientOpacity: 80, + gradientHeight: 40, + gradientFade: 8, +}; +const panelPatternTargetMeta = { + nav: { defaultPattern: "hex" }, + section: { defaultPattern: "hex" }, + group: { defaultPattern: "none" }, + item: { defaultPattern: "none" }, +}; + +function capitalizePanelPatternTarget(target) { + return target.charAt(0).toUpperCase() + target.slice(1); +} + +function buildDefaultPanelPatternPreferences() { + const prefs = {}; + + for (const target of panelPatternTargetIds) { + const cap = capitalizePanelPatternTarget(target); + prefs[`panelPattern${cap}`] = panelPatternTargetMeta[target].defaultPattern; + prefs[`panelPattern${cap}Opacity`] = panelPatternTuningDefaults.opacity; + prefs[`panelPattern${cap}Height`] = panelPatternTuningDefaults.height; + prefs[`panelPattern${cap}Fade`] = panelPatternTuningDefaults.fade; + prefs[`panelPattern${cap}GradientOpacity`] = panelPatternTuningDefaults.gradientOpacity; + prefs[`panelPattern${cap}GradientHeight`] = panelPatternTuningDefaults.gradientHeight; + prefs[`panelPattern${cap}GradientFade`] = panelPatternTuningDefaults.gradientFade; + } + + return prefs; +} + // ============================================================================ // Default UI preferences // ============================================================================ @@ -76,6 +114,7 @@ const defaultPreferences = { customAccentFrom: "#8b5cf6", customAccentTo: "#22d3ee", customAccentAngle: 135, + ...buildDefaultPanelPatternPreferences(), navLayout: "sidebar", showTree: true, showResizeHandles: true, @@ -100,6 +139,7 @@ const defaultPreferences = { catalogCompactCardsColumns: 5, catalogCompactListColumns: 3, searchPlaceholder: "Grafana, S3, журнал, сервер...", + settingsPanelWidth: 460, }; const defaultPanelFeatures = { @@ -114,6 +154,7 @@ const defaultPanelFeatures = { allowLayoutLimitsChange: true, allowThemeChange: true, allowAccentChange: true, + allowPanelPatternChange: true, }; const defaultPanelLimits = { @@ -129,6 +170,7 @@ const defaultPanelLimits = { catalogListColumns: { min: 1, max: 4 }, catalogCompactCardsColumns: { min: 2, max: 8 }, catalogCompactListColumns: { min: 1, max: 6 }, + settingsPanelWidth: { min: 320, max: 720 }, }; const layoutLimitKeys = [ @@ -149,7 +191,7 @@ const catalogColumnKeys = [ "catalogCompactListColumns", ]; -const panelLimitKeys = [...layoutLimitKeys, ...catalogColumnKeys]; +const panelLimitKeys = [...layoutLimitKeys, ...catalogColumnKeys, "settingsPanelWidth"]; const availableNavLayouts = ["sidebar", "top"]; const availableTreeMetaLayouts = ["inline", "stacked"]; @@ -186,6 +228,7 @@ const themeButtons = document.querySelectorAll("[data-theme]"); const navLayoutButtons = document.querySelectorAll("[data-nav-layout]"); const viewButtons = document.querySelectorAll("[data-view]"); const accentButtons = document.querySelectorAll("[data-accent]"); +const patternTargetsPanel = document.querySelector("[data-pattern-targets-panel]"); const accentCustomPanel = document.querySelector("[data-accent-custom-panel]"); const customAccentFromInput = document.querySelector("[data-custom-accent-from]"); const customAccentToInput = document.querySelector("[data-custom-accent-to]"); @@ -201,6 +244,8 @@ const treeColumnsInput = document.querySelector("[data-tree-columns]"); const treeColumnsValue = document.querySelector("[data-tree-columns-value]"); const treeMetaLayoutButtons = document.querySelectorAll("[data-tree-meta-layout]"); const autoCollapseSearchInput = document.querySelector("[data-auto-collapse-search]"); +const settingsPanelWidthInput = document.querySelector("[data-settings-panel-width]"); +const settingsPanelWidthValue = document.querySelector("[data-settings-panel-width-value]"); const layoutLimitInputs = document.querySelectorAll("[data-layout-limit]"); const catalogColumnInputs = document.querySelectorAll("[data-catalog-columns]"); const settingsSections = document.querySelectorAll("[data-settings-section]"); @@ -388,6 +433,35 @@ accentButtons.forEach((button) => { }); }); +patternTargetsPanel?.addEventListener("click", (event) => { + const button = event.target.closest("[data-panel-pattern-target][data-panel-pattern]"); + + if (!button || !panelFeatures.allowPanelPatternChange) { + return; + } + + const target = button.dataset.panelPatternTarget; + preferences[getPanelPatternPreferenceKey(target, "pattern")] = button.dataset.panelPattern; + applyPreferences(preferences); + savePreferences(preferences); +}); + +patternTargetsPanel?.addEventListener("input", (event) => { + const input = event.target.closest("[data-panel-pattern-tuning]"); + + if (!input || !panelFeatures.allowPanelPatternChange) { + return; + } + + const target = input.dataset.panelPatternTuning; + const field = input.dataset.panelPatternField; + const key = getPanelPatternPreferenceKey(target, field); + const fallback = defaultPreferences[key] ?? panelPatternTuningDefaults[field] ?? 50; + preferences[key] = clampPanelPatternPercent(input.value, fallback); + applyPreferences(preferences); + savePreferences(preferences); +}); + // Изменение параметров своего градиента сразу переключает accent на custom. function handleCustomAccentChange() { if (!panelFeatures.allowAccentChange) { @@ -459,6 +533,17 @@ if (treeColumnsInput) { treeColumnsInput.addEventListener("input", handleTreeColumnsChange); } +function handleSettingsPanelWidthChange() { + preferences.settingsPanelWidth = clampLayoutPreference( + "settingsPanelWidth", + settingsPanelWidthInput?.value, + ); + applyPreferences(preferences); + savePreferences(preferences); +} + +settingsPanelWidthInput?.addEventListener("input", handleSettingsPanelWidthChange); + treeMetaLayoutButtons.forEach((button) => { button.addEventListener("click", () => { if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) { @@ -625,6 +710,9 @@ function normalizePreferences(raw) { next.accent = defaultPreferences.accent; } + migrateLegacyPanelPatternPreferences(next); + normalizePanelPatternPreferences(next); + if (!availableNavLayouts.includes(next.navLayout)) { next.navLayout = defaultPreferences.navLayout; } @@ -659,6 +747,7 @@ function normalizePreferences(raw) { getTopBarMinHeight(next), getTopBarMaxHeight(next), ); + next.settingsPanelWidth = clampLayoutPreference("settingsPanelWidth", next.settingsPanelWidth); return next; } @@ -724,6 +813,19 @@ function enforceFeatureLocks(rawPreferences) { next.customAccentAngle = panelDefaults.customAccentAngle; } + if (!panelFeatures.allowPanelPatternChange) { + for (const target of panelPatternTargetIds) { + const cap = capitalizePanelPatternTarget(target); + next[`panelPattern${cap}`] = panelDefaults[`panelPattern${cap}`]; + next[`panelPattern${cap}Opacity`] = panelDefaults[`panelPattern${cap}Opacity`]; + next[`panelPattern${cap}Height`] = panelDefaults[`panelPattern${cap}Height`]; + next[`panelPattern${cap}Fade`] = panelDefaults[`panelPattern${cap}Fade`]; + next[`panelPattern${cap}GradientOpacity`] = panelDefaults[`panelPattern${cap}GradientOpacity`]; + next[`panelPattern${cap}GradientHeight`] = panelDefaults[`panelPattern${cap}GradientHeight`]; + next[`panelPattern${cap}GradientFade`] = panelDefaults[`panelPattern${cap}GradientFade`]; + } + } + if (!panelFeatures.allowShowNumbers) { next.showNumbers = panelDefaults.showNumbers; } @@ -1370,6 +1472,7 @@ function applyPreferences(nextPreferences) { document.documentElement.dataset.theme = activePreferences.theme; document.documentElement.dataset.accent = activePreferences.accent; + applyPanelPatternPreferences(activePreferences); document.documentElement.dataset.navLayout = activePreferences.navLayout === "top" ? "top" : "sidebar"; document.documentElement.dataset.showTree = String(isTreeVisible(activePreferences)); @@ -1391,6 +1494,10 @@ function applyPreferences(nextPreferences) { getTopBarMaxHeight(activePreferences), )}px`, ); + document.documentElement.style.setProperty( + "--settings-panel-width", + `${clampLayoutPreference("settingsPanelWidth", activePreferences.settingsPanelWidth)}px`, + ); const treeColumns = getTreeColumns(activePreferences); const sidebarMaxWidth = getSidebarMaxWidth(activePreferences); @@ -1461,9 +1568,11 @@ function applyPreferences(nextPreferences) { setActiveButton(navLayoutButtons, "navLayout", activePreferences.navLayout); setActiveButton(viewButtons, "view", activePreferences.view); setActiveButton(accentButtons, "accent", activePreferences.accent); + syncPatternTargetPanels(activePreferences); setActiveButton(treeMetaLayoutButtons, "treeMetaLayout", activePreferences.treeMetaLayout); syncLayoutLimitInputs(activePreferences); syncCatalogColumnInputs(activePreferences); + syncSettingsPanelWidthInput(activePreferences); applyPanelFeatures(activePreferences); } @@ -1524,7 +1633,11 @@ function isSettingsSectionVisible(sectionName, activePreferences) { case "tree": return panelFeatures.allowTreeOptions && activePreferences.showTree; case "appearance": - return panelFeatures.allowThemeChange || panelFeatures.allowAccentChange; + return ( + panelFeatures.allowThemeChange || + panelFeatures.allowAccentChange || + panelFeatures.allowPanelPatternChange + ); default: return true; } @@ -1776,6 +1889,22 @@ function syncLayoutLimitInputs(activePreferences) { }); } +function syncSettingsPanelWidthInput(activePreferences) { + if (!settingsPanelWidthInput) { + return; + } + + const limits = getPanelLimitRange("settingsPanelWidth"); + settingsPanelWidthInput.min = String(limits.min); + settingsPanelWidthInput.max = String(limits.max); + settingsPanelWidthInput.step = "10"; + settingsPanelWidthInput.value = String(activePreferences.settingsPanelWidth); + + if (settingsPanelWidthValue) { + settingsPanelWidthValue.textContent = `${activePreferences.settingsPanelWidth}px`; + } +} + function syncCatalogColumnInputs(activePreferences) { catalogColumnInputs.forEach((input) => { const key = input.dataset.catalogColumns; @@ -1802,6 +1931,194 @@ function getPanelLimitRange(key) { return getLayoutLimitRange(key); } +function clampPanelPatternPercent(value, fallback = 50) { + const numeric = Number(value); + const base = Number.isFinite(numeric) ? numeric : fallback; + return clamp(Math.round(base), 0, 100); +} + +function getPanelPatternPreferenceKey(target, field) { + const cap = capitalizePanelPatternTarget(target); + + switch (field) { + case "pattern": + return `panelPattern${cap}`; + case "opacity": + return `panelPattern${cap}Opacity`; + case "height": + return `panelPattern${cap}Height`; + case "fade": + return `panelPattern${cap}Fade`; + case "gradientOpacity": + return `panelPattern${cap}GradientOpacity`; + case "gradientHeight": + return `panelPattern${cap}GradientHeight`; + case "gradientFade": + return `panelPattern${cap}GradientFade`; + default: + return `panelPattern${cap}`; + } +} + +function migrateLegacyPanelPatternPreferences(next) { + if (next.panelPatternNav !== undefined) { + return; + } + + const legacyPattern = availablePanelPatterns.includes(next.panelPattern) + ? next.panelPattern + : panelPatternTargetMeta.nav.defaultPattern; + const legacyOpacity = next.panelPatternOpacity ?? panelPatternTuningDefaults.opacity; + const legacyHeight = next.panelPatternHeight ?? panelPatternTuningDefaults.height; + const legacyGradientOpacity = next.panelGradientOpacity ?? panelPatternTuningDefaults.gradientOpacity; + const legacyGradientHeight = next.panelGradientHeight ?? panelPatternTuningDefaults.gradientHeight; + const legacyTargets = [ + ["Nav", next.panelPatternOnNav !== false], + ["Section", next.panelPatternOnSection !== false], + ["Group", Boolean(next.panelPatternOnGroup)], + ["Item", Boolean(next.panelPatternOnItem)], + ]; + + for (const [suffix, enabled] of legacyTargets) { + next[`panelPattern${suffix}`] = enabled && legacyPattern !== "none" ? legacyPattern : "none"; + next[`panelPattern${suffix}Opacity`] = legacyOpacity; + next[`panelPattern${suffix}Height`] = legacyHeight; + next[`panelPattern${suffix}Fade`] = panelPatternTuningDefaults.fade; + next[`panelPattern${suffix}GradientOpacity`] = legacyGradientOpacity; + next[`panelPattern${suffix}GradientHeight`] = legacyGradientHeight; + next[`panelPattern${suffix}GradientFade`] = panelPatternTuningDefaults.gradientFade; + } +} + +function normalizePanelPatternPreferences(next) { + for (const target of panelPatternTargetIds) { + const cap = capitalizePanelPatternTarget(target); + const patternKey = `panelPattern${cap}`; + + if (!availablePanelPatterns.includes(next[patternKey])) { + next[patternKey] = panelPatternTargetMeta[target].defaultPattern; + } + + next[`panelPattern${cap}Opacity`] = clampPanelPatternPercent( + next[`panelPattern${cap}Opacity`], + panelPatternTuningDefaults.opacity, + ); + next[`panelPattern${cap}Height`] = clampPanelPatternPercent( + next[`panelPattern${cap}Height`], + panelPatternTuningDefaults.height, + ); + next[`panelPattern${cap}Fade`] = clampPanelPatternPercent( + next[`panelPattern${cap}Fade`], + panelPatternTuningDefaults.fade, + ); + next[`panelPattern${cap}GradientOpacity`] = clampPanelPatternPercent( + next[`panelPattern${cap}GradientOpacity`], + panelPatternTuningDefaults.gradientOpacity, + ); + next[`panelPattern${cap}GradientHeight`] = clampPanelPatternPercent( + next[`panelPattern${cap}GradientHeight`], + panelPatternTuningDefaults.gradientHeight, + ); + next[`panelPattern${cap}GradientFade`] = clampPanelPatternPercent( + next[`panelPattern${cap}GradientFade`], + panelPatternTuningDefaults.gradientFade, + ); + } +} + +function getPanelPatternFadeStart(heightPercent, fadePercent) { + const height = clampPanelPatternPercent(heightPercent, panelPatternTuningDefaults.height); + const fade = clampPanelPatternPercent(fadePercent, panelPatternTuningDefaults.fade); + return `${Math.max(0, height - fade)}%`; +} + +function applyPanelPatternPreferences(activePreferences) { + for (const target of panelPatternTargetIds) { + const cap = capitalizePanelPatternTarget(target); + const pattern = activePreferences[`panelPattern${cap}`] || "none"; + const height = clampPanelPatternPercent( + activePreferences[`panelPattern${cap}Height`], + panelPatternTuningDefaults.height, + ); + const opacityPercent = clampPanelPatternPercent( + activePreferences[`panelPattern${cap}Opacity`], + panelPatternTuningDefaults.opacity, + ); + const patternOpacity = pattern === "none" || height === 0 ? 0 : opacityPercent / 100; + + document.documentElement.dataset[`panelPattern${cap}`] = pattern; + document.documentElement.style.setProperty(`--panel-pattern-${target}-opacity`, String(patternOpacity)); + document.documentElement.style.setProperty(`--panel-pattern-${target}-height`, `${height}%`); + document.documentElement.style.setProperty( + `--panel-pattern-${target}-fade-start`, + getPanelPatternFadeStart(height, activePreferences[`panelPattern${cap}Fade`]), + ); + document.documentElement.style.setProperty( + `--panel-gradient-${target}-strength`, + String( + (clampPanelPatternPercent( + activePreferences[`panelPattern${cap}GradientOpacity`], + panelPatternTuningDefaults.gradientOpacity, + ) / + 100) * + 0.28, + ), + ); + const gradientHeight = clampPanelPatternPercent( + activePreferences[`panelPattern${cap}GradientHeight`], + panelPatternTuningDefaults.gradientHeight, + ); + document.documentElement.style.setProperty( + `--panel-gradient-${target}-height`, + `${gradientHeight}%`, + ); + document.documentElement.style.setProperty( + `--panel-gradient-${target}-fade-start`, + getPanelPatternFadeStart(gradientHeight, activePreferences[`panelPattern${cap}GradientFade`]), + ); + } +} + +function syncPatternTargetPanels(activePreferences) { + if (!patternTargetsPanel) { + return; + } + + const disabled = !panelFeatures.allowPanelPatternChange; + const tuningFields = ["opacity", "height", "fade", "gradientOpacity", "gradientHeight", "gradientFade"]; + + for (const target of panelPatternTargetIds) { + const pattern = activePreferences[getPanelPatternPreferenceKey(target, "pattern")]; + patternTargetsPanel + .querySelectorAll(`[data-panel-pattern-target="${target}"][data-panel-pattern]`) + .forEach((button) => { + const isActive = button.dataset.panelPattern === pattern; + button.classList.toggle("is-active", isActive); + button.setAttribute("aria-pressed", String(isActive)); + button.disabled = disabled; + }); + + for (const field of tuningFields) { + const key = getPanelPatternPreferenceKey(target, field); + const input = patternTargetsPanel.querySelector( + `[data-panel-pattern-tuning="${target}"][data-panel-pattern-field="${field}"]`, + ); + const output = patternTargetsPanel.querySelector( + `[data-panel-pattern-value="${target}"][data-panel-pattern-field="${field}"]`, + ); + + if (input) { + input.value = String(activePreferences[key]); + input.disabled = disabled; + } + + if (output) { + output.textContent = `${activePreferences[key]}%`; + } + } + } +} + function clampCatalogColumn(key, value) { const limits = getPanelLimitRange(key); const fallback = panelDefaults[key] ?? defaultPreferences[key]; diff --git a/settings.json b/settings.json index b961c14..237a4fc 100644 --- a/settings.json +++ b/settings.json @@ -11,6 +11,26 @@ "customAccentFrom": "#8b5cf6", "customAccentTo": "#22d3ee", "customAccentAngle": 135, + "panelPatternNav": "hex", + "panelPatternNavOpacity": 70, + "panelPatternNavHeight": 45, + "panelPatternNavGradientOpacity": 80, + "panelPatternNavGradientHeight": 40, + "panelPatternSection": "hex", + "panelPatternSectionOpacity": 70, + "panelPatternSectionHeight": 45, + "panelPatternSectionGradientOpacity": 80, + "panelPatternSectionGradientHeight": 40, + "panelPatternGroup": "none", + "panelPatternGroupOpacity": 70, + "panelPatternGroupHeight": 45, + "panelPatternGroupGradientOpacity": 80, + "panelPatternGroupGradientHeight": 40, + "panelPatternItem": "none", + "panelPatternItemOpacity": 70, + "panelPatternItemHeight": 45, + "panelPatternItemGradientOpacity": 80, + "panelPatternItemGradientHeight": 40, "showNumbers": false, "showTreeTags": true, "showTreeType": false, @@ -31,7 +51,8 @@ "catalogListColumns": 1, "catalogCompactCardsColumns": 5, "catalogCompactListColumns": 3, - "searchPlaceholder": "Grafana, S3, журнал, сервер..." + "searchPlaceholder": "Grafana, S3, журнал, сервер...", + "settingsPanelWidth": 460 }, "limits": { "shellMaxWidth": { "min": 1200, "max": 3840 }, @@ -45,7 +66,8 @@ "catalogCardsColumns": { "min": 1, "max": 6 }, "catalogListColumns": { "min": 1, "max": 4 }, "catalogCompactCardsColumns": { "min": 2, "max": 8 }, - "catalogCompactListColumns": { "min": 1, "max": 6 } + "catalogCompactListColumns": { "min": 1, "max": 6 }, + "settingsPanelWidth": { "min": 320, "max": 720 } }, "features": { "allowNavLayoutChange": true, @@ -58,6 +80,7 @@ "allowSidebarResize": true, "allowLayoutLimitsChange": true, "allowThemeChange": true, - "allowAccentChange": true + "allowAccentChange": true, + "allowPanelPatternChange": true } } diff --git a/styles.css b/styles.css index befc0ff..e476596 100644 --- a/styles.css +++ b/styles.css @@ -31,6 +31,46 @@ --catalog-list-columns: 1; --catalog-compact-cards-columns: 5; --catalog-compact-list-columns: 3; + --pattern-color: rgba(255, 255, 255, 0.16); + --pattern-tile-hex-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E"); + --pattern-tile-hex-size: 96px 84px; + --pattern-tile-dots-image: radial-gradient(circle at center, var(--pattern-color) 0.95px, transparent 1px); + --pattern-tile-dots-size: 18px 18px; + --pattern-tile-grid-image: + linear-gradient(var(--pattern-color) 1px, transparent 1px), + linear-gradient(90deg, var(--pattern-color) 1px, transparent 1px); + --pattern-tile-grid-size: 26px 26px; + --pattern-tile-diagonal-image: repeating-linear-gradient( + -45deg, + transparent, + transparent 10px, + var(--pattern-color) 10px, + var(--pattern-color) 11px + ); + --pattern-tile-diagonal-size: auto; + --pattern-tile-cross-image: + repeating-linear-gradient( + 0deg, + transparent, + transparent 14px, + var(--pattern-color) 14px, + var(--pattern-color) 15px + ), + repeating-linear-gradient( + 90deg, + transparent, + transparent 14px, + var(--pattern-color) 14px, + var(--pattern-color) 15px + ); + --pattern-tile-cross-size: auto; + --pattern-tile-mesh-image: radial-gradient( + circle at 1px 1px, + rgba(var(--accent-rgb), 0.22) 1px, + transparent 0 + ); + --pattern-tile-mesh-size: 20px 20px; + --settings-panel-width: 460px; } /* Светлая тема переопределяет только токены, структура стилей остается общей. */ @@ -45,8 +85,40 @@ html[data-theme="light"] { --muted: #5f6b7a; --border: rgba(15, 23, 42, 0.12); --shadow: 0 24px 70px rgba(56, 65, 91, 0.18); + --pattern-color: rgba(15, 23, 42, 0.13); + --pattern-tile-hex-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E"); + --pattern-tile-mesh-image: radial-gradient( + circle at 1px 1px, + rgba(var(--accent-rgb), 0.16) 1px, + transparent 0 + ); } +html[data-panel-pattern-nav="hex"] { --panel-pattern-image-nav: var(--pattern-tile-hex-image); --panel-pattern-size-nav: var(--pattern-tile-hex-size); } +html[data-panel-pattern-nav="dots"] { --panel-pattern-image-nav: var(--pattern-tile-dots-image); --panel-pattern-size-nav: var(--pattern-tile-dots-size); } +html[data-panel-pattern-nav="grid"] { --panel-pattern-image-nav: var(--pattern-tile-grid-image); --panel-pattern-size-nav: var(--pattern-tile-grid-size); } +html[data-panel-pattern-nav="diagonal"] { --panel-pattern-image-nav: var(--pattern-tile-diagonal-image); --panel-pattern-size-nav: var(--pattern-tile-diagonal-size); } +html[data-panel-pattern-nav="cross"] { --panel-pattern-image-nav: var(--pattern-tile-cross-image); --panel-pattern-size-nav: var(--pattern-tile-cross-size); } +html[data-panel-pattern-nav="mesh"] { --panel-pattern-image-nav: var(--pattern-tile-mesh-image); --panel-pattern-size-nav: var(--pattern-tile-mesh-size); } +html[data-panel-pattern-section="hex"] { --panel-pattern-image-section: var(--pattern-tile-hex-image); --panel-pattern-size-section: var(--pattern-tile-hex-size); } +html[data-panel-pattern-section="dots"] { --panel-pattern-image-section: var(--pattern-tile-dots-image); --panel-pattern-size-section: var(--pattern-tile-dots-size); } +html[data-panel-pattern-section="grid"] { --panel-pattern-image-section: var(--pattern-tile-grid-image); --panel-pattern-size-section: var(--pattern-tile-grid-size); } +html[data-panel-pattern-section="diagonal"] { --panel-pattern-image-section: var(--pattern-tile-diagonal-image); --panel-pattern-size-section: var(--pattern-tile-diagonal-size); } +html[data-panel-pattern-section="cross"] { --panel-pattern-image-section: var(--pattern-tile-cross-image); --panel-pattern-size-section: var(--pattern-tile-cross-size); } +html[data-panel-pattern-section="mesh"] { --panel-pattern-image-section: var(--pattern-tile-mesh-image); --panel-pattern-size-section: var(--pattern-tile-mesh-size); } +html[data-panel-pattern-group="hex"] { --panel-pattern-image-group: var(--pattern-tile-hex-image); --panel-pattern-size-group: var(--pattern-tile-hex-size); } +html[data-panel-pattern-group="dots"] { --panel-pattern-image-group: var(--pattern-tile-dots-image); --panel-pattern-size-group: var(--pattern-tile-dots-size); } +html[data-panel-pattern-group="grid"] { --panel-pattern-image-group: var(--pattern-tile-grid-image); --panel-pattern-size-group: var(--pattern-tile-grid-size); } +html[data-panel-pattern-group="diagonal"] { --panel-pattern-image-group: var(--pattern-tile-diagonal-image); --panel-pattern-size-group: var(--pattern-tile-diagonal-size); } +html[data-panel-pattern-group="cross"] { --panel-pattern-image-group: var(--pattern-tile-cross-image); --panel-pattern-size-group: var(--pattern-tile-cross-size); } +html[data-panel-pattern-group="mesh"] { --panel-pattern-image-group: var(--pattern-tile-mesh-image); --panel-pattern-size-group: var(--pattern-tile-mesh-size); } +html[data-panel-pattern-item="hex"] { --panel-pattern-image-item: var(--pattern-tile-hex-image); --panel-pattern-size-item: var(--pattern-tile-hex-size); } +html[data-panel-pattern-item="dots"] { --panel-pattern-image-item: var(--pattern-tile-dots-image); --panel-pattern-size-item: var(--pattern-tile-dots-size); } +html[data-panel-pattern-item="grid"] { --panel-pattern-image-item: var(--pattern-tile-grid-image); --panel-pattern-size-item: var(--pattern-tile-grid-size); } +html[data-panel-pattern-item="diagonal"] { --panel-pattern-image-item: var(--pattern-tile-diagonal-image); --panel-pattern-size-item: var(--pattern-tile-diagonal-size); } +html[data-panel-pattern-item="cross"] { --panel-pattern-image-item: var(--pattern-tile-cross-image); --panel-pattern-size-item: var(--pattern-tile-cross-size); } +html[data-panel-pattern-item="mesh"] { --panel-pattern-image-item: var(--pattern-tile-mesh-image); --panel-pattern-size-item: var(--pattern-tile-mesh-size); } + /* Наборы акцентов. --accent-rgb нужен для прозрачных градиентов через rgba(). */ html[data-accent="cyan"] { --accent: #06b6d4; @@ -324,6 +396,8 @@ body.is-resizing-top-bar { background: var(--panel); box-shadow: var(--shadow); backdrop-filter: blur(22px); + position: relative; + overflow: hidden; } .hero-content { @@ -335,7 +409,6 @@ body.is-resizing-top-bar { gap: 8px; padding: clamp(16px, 2.6vw, 24px); border-radius: 22px; - position: relative; overflow: visible; } @@ -344,21 +417,117 @@ body.is-resizing-top-bar { overflow: auto; } -/* Внутренний фон sidebar: градиент + hexagon pattern. */ -.hero-content::before { +/* Акцентный градиент под узором — отдельно для каждого типа блоков. */ +html:not([data-panel-pattern-nav="none"]) .hero-content::before { content: ""; position: absolute; inset: 0; border-radius: inherit; - background: - linear-gradient(120deg, rgba(var(--accent-rgb), 0.25), transparent 34%), - url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.10' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E"); - mask-image: linear-gradient(to bottom, black, transparent 88%); + background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%); + opacity: var(--panel-gradient-nav-strength); + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-nav-fade-start), transparent var(--panel-gradient-nav-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-nav-fade-start), transparent var(--panel-gradient-nav-height)); pointer-events: none; + z-index: 0; +} + +html:not([data-panel-pattern-section="none"]) .catalog-section::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%); + opacity: var(--panel-gradient-section-strength); + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-section-fade-start), transparent var(--panel-gradient-section-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-section-fade-start), transparent var(--panel-gradient-section-height)); + pointer-events: none; + z-index: 0; +} + +html:not([data-panel-pattern-group="none"]) .catalog-group::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%); + opacity: var(--panel-gradient-group-strength); + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-group-fade-start), transparent var(--panel-gradient-group-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-group-fade-start), transparent var(--panel-gradient-group-height)); + pointer-events: none; + z-index: 0; +} + +html:not([data-panel-pattern-nav="none"]) .hero-content::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + z-index: 0; + opacity: var(--panel-pattern-nav-opacity); + background-image: var(--panel-pattern-image-nav); + background-size: var(--panel-pattern-size-nav); + background-repeat: repeat; + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-nav-fade-start), transparent var(--panel-pattern-nav-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-nav-fade-start), transparent var(--panel-pattern-nav-height)); +} + +html:not([data-panel-pattern-section="none"]) .catalog-section::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + z-index: 0; + opacity: var(--panel-pattern-section-opacity); + background-image: var(--panel-pattern-image-section); + background-size: var(--panel-pattern-size-section); + background-repeat: repeat; + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-section-fade-start), transparent var(--panel-pattern-section-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-section-fade-start), transparent var(--panel-pattern-section-height)); +} + +html:not([data-panel-pattern-group="none"]) .catalog-group::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + z-index: 0; + opacity: var(--panel-pattern-group-opacity); + background-image: var(--panel-pattern-image-group); + background-size: var(--panel-pattern-size-group); + background-repeat: repeat; + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-group-fade-start), transparent var(--panel-pattern-group-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-group-fade-start), transparent var(--panel-pattern-group-height)); +} + +html:not([data-panel-pattern-item="none"]) .nav-card::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + z-index: 0; + opacity: var(--panel-pattern-item-opacity); + background-image: var(--panel-pattern-image-item); + background-size: var(--panel-pattern-size-item); + background-repeat: repeat; + mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-item-fade-start), transparent var(--panel-pattern-item-height)); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-item-fade-start), transparent var(--panel-pattern-item-height)); +} + +.catalog-section > summary, +.catalog-section > .section-groups, +.catalog-group > summary, +.catalog-group > .items-grid { + position: relative; + z-index: 1; } .hero-content > * { position: relative; + z-index: 1; } /* Общий стиль маленьких uppercase-меток и номеров. */ @@ -414,7 +583,7 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta { z-index: 1001; top: 24px; right: 24px; - width: min(460px, calc(100% - 48px)); + width: min(var(--settings-panel-width), calc(100% - 48px)); max-height: calc(100vh - 48px); overflow: auto; display: grid; @@ -512,6 +681,27 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta { line-height: 1; } +.settings-width-row { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto auto; + gap: 6px 10px; +} + +.settings-width-row span { + grid-column: 1; + grid-row: 1; +} + +.settings-width-row output { + grid-column: 2; + grid-row: 1; +} + +.settings-width-row input[type="range"] { + grid-column: 1 / -1; + grid-row: 2; +} + .settings-section { display: grid; gap: 10px; @@ -901,6 +1091,166 @@ html[data-theme="light"] .accent-swatch-custom { transform: scale(1.05); } +.pattern-group { + align-items: stretch; +} + +.pattern-targets-panel { + display: grid; + gap: 10px; + width: 100%; +} + +.pattern-target-panel .settings-section-body { + display: grid; + gap: 10px; + padding-top: 10px; +} + +.pattern-swatches { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + width: 100%; +} + +.pattern-swatch { + min-height: 54px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--surface); + color: var(--muted); + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; + cursor: pointer; + position: relative; + overflow: hidden; + padding: 0; +} + +.pattern-swatch::before { + content: ""; + position: absolute; + inset: 0; + opacity: 0.95; + pointer-events: none; +} + +.pattern-swatch[data-panel-pattern="none"]::before { + background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.12), transparent 70%); +} + +.pattern-swatch[data-panel-pattern="hex"]::before { + background-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E"); + background-size: 48px 42px; +} + +html[data-theme="light"] .pattern-swatch[data-panel-pattern="hex"]::before { + background-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E"); +} + +.pattern-swatch[data-panel-pattern="dots"]::before { + background-image: radial-gradient(circle at center, var(--pattern-color) 0.95px, transparent 1px); + background-size: 12px 12px; +} + +.pattern-swatch[data-panel-pattern="grid"]::before { + background-image: + linear-gradient(var(--pattern-color) 1px, transparent 1px), + linear-gradient(90deg, var(--pattern-color) 1px, transparent 1px); + background-size: 16px 16px; +} + +.pattern-swatch[data-panel-pattern="diagonal"]::before { + background-image: repeating-linear-gradient( + -45deg, + transparent, + transparent 7px, + var(--pattern-color) 7px, + var(--pattern-color) 8px + ); +} + +.pattern-swatch[data-panel-pattern="cross"]::before { + background-image: + repeating-linear-gradient( + 0deg, + transparent, + transparent 9px, + var(--pattern-color) 9px, + var(--pattern-color) 10px + ), + repeating-linear-gradient( + 90deg, + transparent, + transparent 9px, + var(--pattern-color) 9px, + var(--pattern-color) 10px + ); +} + +.pattern-swatch[data-panel-pattern="mesh"]::before { + background-image: radial-gradient( + circle at 1px 1px, + rgba(var(--accent-rgb), 0.24) 1px, + transparent 0 + ); + background-size: 12px 12px; +} + +.pattern-swatch-label { + position: relative; + z-index: 1; + display: grid; + place-items: end start; + width: 100%; + height: 100%; + padding: 6px 8px; + background: linear-gradient(to top, rgba(0, 0, 0, 0.28), transparent 58%); + color: #f8fafc; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35); +} + +html[data-theme="light"] .pattern-swatch-label { + background: linear-gradient(to top, rgba(255, 255, 255, 0.82), transparent 58%); + color: #101827; + text-shadow: none; +} + +.pattern-swatch.is-active { + border-color: rgba(var(--accent-rgb), 0.72); + box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.18); +} + +.pattern-tuning { + display: grid; + gap: 12px; + width: 100%; +} + +.pattern-tuning .accent-custom-row { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto auto; + gap: 6px 10px; +} + +.pattern-tuning .accent-custom-row span { + grid-column: 1; + grid-row: 1; +} + +.pattern-tuning .accent-custom-row output { + grid-column: 2; + grid-row: 1; +} + +.pattern-tuning .accent-custom-row input[type="range"] { + grid-column: 1 / -1; + grid-row: 2; +} + /* Панель настройки своего градиента появляется при data-accent="custom". */ .accent-custom-panel { display: grid; @@ -1329,6 +1679,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon { } .catalog-group { + position: relative; border: 1px solid var(--border); border-radius: 18px; background: rgba(255, 255, 255, 0.035); @@ -1427,6 +1778,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon { opacity: 0; transition: opacity 180ms ease; pointer-events: none; + z-index: 1; } /* Hover задается классом .is-hovered из JS, чтобы убрать скачки на границах. */ @@ -1445,6 +1797,8 @@ html:not([data-tree-columns="1"]) .tree-node-icon { /* Иконка сервиса берется из serviceMeta. */ .nav-card img { + position: relative; + z-index: 1; width: 50px; height: 50px; object-fit: contain; @@ -1453,6 +1807,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon { .card-content { position: relative; + z-index: 1; display: grid; gap: 5px; } @@ -1493,6 +1848,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon { position: absolute; right: 16px; bottom: 14px; + z-index: 1; width: 36px; height: 36px; display: grid; @@ -1515,6 +1871,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon { .shine { position: absolute; inset: 0; + z-index: 2; background: linear-gradient( 110deg, transparent 0%,