From e1fd7d27135374b73dfda71dae5a5a850c3b2651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=91=D1=80=D0=B0=D1=82=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=D1=85?= Date: Mon, 29 Jun 2026 11:27:36 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B3=D0=BB=D0=BE=D0=B1=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BA=D0=B0=D1=81=D1=82=D0=BE=D0=BC=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BA=D0=B0=D0=B6=D0=B4=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D0=B8,=20=D0=B8=D0=BC=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=82-=D1=8D=D0=BA=D1=81=D0=BF=D0=BE=D1=80=D1=82-?= =?UTF-8?q?=D1=81=D0=B1=D1=80=D0=BE=D1=81=20=D0=BD=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=D0=BA,=20=D0=BF=D0=BB=D0=B0=D0=B2=D0=BD=D0=BE?= =?UTF-8?q?=D0=B5=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=81=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 283 +++++++--- examples/.vscode/settings.json | 2 +- examples/flat/index.html | 79 +-- examples/flat/settings.json | 107 ++-- examples/full/index.html | 79 +-- examples/full/settings.json | 107 ++-- examples/mixed/index.html | 79 +-- examples/mixed/settings.json | 107 ++-- examples/section-items/index.html | 79 +-- examples/section-items/settings.json | 67 ++- examples/shared/script.js | 765 +++++++++++++++++++++++++- examples/shared/styles.css | 408 ++++++++++++-- index.html | 117 ++-- script.js | 766 ++++++++++++++++++++++++++- settings-top-bar.example.json | 29 +- settings.json | 107 ++-- styles.css | 408 ++++++++++++-- 17 files changed, 3008 insertions(+), 581 deletions(-) diff --git a/README.md b/README.md index abb8a9c..181726c 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,13 @@ - [Готовые демо-страницы (`examples/`)](#готовые-демо-страницы-examples) - [Типы сервисов (`serviceMeta`)](#типы-сервисов-servicemeta) - [Рекомендуемые типы сервисов](#рекомендуемые-типы-сервисов) -- [Настройки интерфейса (⚙)](#настройки-интерфейса-) -- [Поведение интерфейса](#поведение-интерфейса) + - [Настройки интерфейса (⚙)](#настройки-интерфейса-) + - [Узоры панелей](#узоры-панелей) + - [Разделение слоёв узоров](#разделение-слоёв-узоров) + - [Свой узор (custom)](#свой-узор-custom) + - [Экспорт и импорт настроек](#экспорт-и-импорт-настроек) + - [Навигация по коду](#навигация-по-коду) + - [Поведение интерфейса](#поведение-интерфейса) - [localStorage](#localstorage) - [Разработка](#разработка) - [Ограничения](#ограничения) @@ -33,7 +38,9 @@ - **Поиск** по названию, тегам, типу сервиса и родительским разделам - **4 режима отображения**: плитки, список, компактные плитки, компактный список - **Темы**: светлая / тёмная, 4 пресета акцента + свой градиент -- **Узоры панелей** — соты, точки, сетка и др.; отдельные варианты для светлой темы; можно отключить +- **Узоры панелей** — 13 встроенных пресетов + свой URL/SVG; отдельно для nav / section / group / item; слайдеры прозрачности, высоты и перехода +- **Разделение слоёв** — подложка и blur, чтобы вложенные узоры не «наезжали» друг на друга +- **Экспорт / импорт / сброс** настроек ⚙ в JSON-файл - **Настройки** через popup (иконка ⚙) - **Изменяемая ширина** бокового меню и **высота** верхней шапки (drag, можно скрыть ручки) - **Габариты** — лимиты ширины контента, меню и шапки (⚙, для администратора) @@ -131,51 +138,62 @@ ecosystem/ "customAccentTo": "#22d3ee", "customAccentAngle": 135, "panelPatternNav": "hex", - "panelPatternNavOpacity": 70, - "panelPatternNavHeight": 45, - "panelPatternNavGradientOpacity": 80, - "panelPatternNavGradientHeight": 40, + "panelPatternNavOpacity": 58, + "panelPatternNavHeight": 38, + "panelPatternNavFade": 18, + "panelPatternNavGradientOpacity": 62, + "panelPatternNavGradientHeight": 34, + "panelPatternNavGradientFade": 16, "panelPatternSection": "hex", - "panelPatternSectionOpacity": 70, - "panelPatternSectionHeight": 45, - "panelPatternSectionGradientOpacity": 80, - "panelPatternSectionGradientHeight": 40, + "panelPatternSectionOpacity": 58, + "panelPatternSectionHeight": 38, + "panelPatternSectionFade": 18, + "panelPatternSectionGradientOpacity": 62, + "panelPatternSectionGradientHeight": 34, + "panelPatternSectionGradientFade": 16, "panelPatternGroup": "none", - "panelPatternGroupOpacity": 70, - "panelPatternGroupHeight": 45, - "panelPatternGroupGradientOpacity": 80, - "panelPatternGroupGradientHeight": 40, + "panelPatternGroupOpacity": 58, + "panelPatternGroupHeight": 38, + "panelPatternGroupFade": 18, + "panelPatternGroupGradientOpacity": 62, + "panelPatternGroupGradientHeight": 34, + "panelPatternGroupGradientFade": 16, "panelPatternItem": "none", - "panelPatternItemOpacity": 70, - "panelPatternItemHeight": 45, - "panelPatternItemGradientOpacity": 80, - "panelPatternItemGradientHeight": 40, + "panelPatternItemOpacity": 58, + "panelPatternItemHeight": 38, + "panelPatternItemFade": 18, + "panelPatternItemGradientOpacity": 62, + "panelPatternItemGradientHeight": 34, + "panelPatternItemGradientFade": 16, + "patternIsolation": 46, "showNumbers": false, "showTreeTags": true, "showTreeType": false, "treeMetaLayout": "inline", - "treeColumns": 2, + "treeColumns": 1, "autoCollapseSearch": true, - "sidebarWidth": 390, + "sidebarWidth": 360, "topBarHeight": 140, - "shellMaxWidth": 2800, - "sidebarMinWidth": 260, - "sidebarMaxWidth": 800, - "sidebarMaxWidthWide": 1400, - "treeSidebarBaseWidth": 420, - "treeSidebarColumnWidth": 120, + "shellMaxWidth": 2240, + "sidebarMinWidth": 240, + "sidebarMaxWidth": 720, + "sidebarMaxWidthWide": 1040, + "treeSidebarBaseWidth": 360, + "treeSidebarColumnWidth": 96, "topBarMinHeight": 80, "topBarMaxHeight": 560, "catalogCardsColumns": 3, "catalogListColumns": 1, - "catalogCompactCardsColumns": 5, - "catalogCompactListColumns": 3, + "catalogCompactCardsColumns": 4, + "catalogCompactListColumns": 2, + "collapseAnimation": "animated", + "collapseDuration": 260, "searchPlaceholder": "Grafana, S3, журнал, сервер...", - "settingsPanelWidth": 460 + "settingsPanelWidth": 440 }, "limits": { "shellMaxWidth": { "min": 1200, "max": 3840 }, - "sidebarMinWidth": { "min": 200, "max": 480 }, + "sidebarMinWidth": { "min": 220, "max": 480 }, "sidebarMaxWidth": { "min": 400, "max": 1200 }, "sidebarMaxWidthWide": { "min": 560, "max": 2000 }, "treeSidebarBaseWidth": { "min": 280, "max": 720 }, @@ -216,17 +234,20 @@ ecosystem/ | `customAccentTo` | `#rrggbb` | Цвет «до» для `accent: "custom"` (по умолчанию `#22d3ee`) | | `customAccentAngle` | `0`–`360` | Угол градиента в градусах (по умолчанию `135`) | | `panelPatternNav` … `panelPatternItem` | см. [узоры панелей](#узоры-панелей) | Тип узора для каждого типа блоков (`none`, `hex`, …) | -| `panelPattern{Nav\|Section\|Group\|Item}Opacity` | `0`–`100` | Прозрачность узора в % (по умолчанию `70`) | -| `panelPattern{Nav\|Section\|Group\|Item}Height` | `0`–`100` | Высота узора в % (по умолчанию `45`; `0` — скрыть) | -| `panelPattern{Nav\|Section\|Group\|Item}Fade` | `0`–`100` | Ширина зоны затухания узора в % высоты блока: `0` — резкий обрыв на линии высоты, `100` — плавное затухание от верха (по умолчанию `10`) | -| `panelPattern{Nav\|Section\|Group\|Item}GradientOpacity` | `0`–`100` | Прозрачность акцентного градиента в % (по умолчанию `80`) | -| `panelPattern{Nav\|Section\|Group\|Item}GradientHeight` | `0`–`100` | Высота градиента в % (по умолчанию `40`) | -| `panelPattern{Nav\|Section\|Group\|Item}GradientFade` | `0`–`100` | Зона затухания градиента (по умолчанию `8`) | +| `panelPattern{Nav\|Section\|Group\|Item}Opacity` | `0`–`100` | Прозрачность узора в % (по умолчанию `58`) | +| `panelPattern{Nav\|Section\|Group\|Item}Height` | `0`–`100` | Высота узора в % (по умолчанию `38`; `0` — скрыть) | +| `panelPattern{Nav\|Section\|Group\|Item}Fade` | `0`–`100` | Ширина зоны затухания узора в % высоты блока: `0` — резкий обрыв на линии высоты, `100` — плавное затухание от верха (по умолчанию `18`) | +| `panelPattern{Nav\|Section\|Group\|Item}GradientOpacity` | `0`–`100` | Прозрачность акцентного градиента в % (по умолчанию `62`) | +| `panelPattern{Nav\|Section\|Group\|Item}GradientHeight` | `0`–`100` | Высота градиента в % (по умолчанию `34`) | +| `panelPattern{Nav\|Section\|Group\|Item}GradientFade` | `0`–`100` | Зона затухания градиента (по умолчанию `16`) | +| `panelPattern{Nav\|Section\|Group\|Item}CustomImage` | строка URL | Изображение для `custom` (пусто по умолчанию) | +| `panelPattern{Nav\|Section\|Group\|Item}CustomSize` | `"WxH"` в px | Размер плитки custom (по умолчанию `32px 32px`) | +| `patternIsolation` | `0`–`100` | Разделение слоёв узоров (подложка + blur; по умолчанию `46`) | | `showNumbers` | `true`, `false` | Показывать номера разделов, подразделов и элементов | | `showTreeTags` | `true`, `false` | Теги элементов в дереве (оформление как на карточках) | | `showTreeType` | `true`, `false` | Подпись типа сервиса в дереве | | `treeMetaLayout` | `"inline"`, `"stacked"` | Расположение тегов и типа: сбоку от названия или снизу | -| `treeColumns` | `1`–`6` (по умолчанию `2`) | Число колонок элементов внутри папок дерева | +| `treeColumns` | `1`–`6` (по умолчанию `1`) | Число колонок элементов внутри папок дерева | | `autoCollapseSearch` | `true`, `false` | При поиске сворачивать ветки дерева без совпадений | | `sidebarWidth` | `sidebarMinWidth`–`sidebarMaxWidth` (или `sidebarMaxWidthWide` при `treeColumns` > 1) | Текущая ширина бокового меню (drag-resize); при `navLayout: "top"` не применяется | | `topBarHeight` | `topBarMinHeight`–`topBarMaxHeight` (до ~75% высоты окна) | Текущая высота верхней шапки (drag-resize); только при `navLayout: "top"` | @@ -242,8 +263,10 @@ ecosystem/ | `catalogListColumns` | см. `limits.catalogListColumns` | Колонок в строке в режиме «Список» | | `catalogCompactCardsColumns` | см. `limits.catalogCompactCardsColumns` | Колонок в строке в режиме «Компакт плитки» | | `catalogCompactListColumns` | см. `limits.catalogCompactListColumns` | Колонок в строке в режиме «Компакт список» | +| `collapseAnimation` | `"instant"`, `"animated"` | Режим раскрытия списков, дерева и collapsible-блоков | +| `collapseDuration` | `100`–`800` | Длительность плавного раскрытия в миллисекундах (по умолчанию `260`) | | `searchPlaceholder` | произвольная строка | Текст-подсказка в поле поиска | -| `settingsPanelWidth` | `limits.settingsPanelWidth` (по умолчанию `320`–`720`) | Ширина popup настроек в px (по умолчанию `460`) | +| `settingsPanelWidth` | `limits.settingsPanelWidth` (по умолчанию `320`–`720`) | Ширина popup настроек в px (по умолчанию `440`) | | `limits` | Формат | Описание | |----------|--------|----------| @@ -443,7 +466,8 @@ Popup сгруппирован по разделам. Доступные бло | **Габариты** | Лимиты ширины контента, меню и шапки (сворачиваемый блок; при `allowLayoutLimitsChange`) | | **Каталог** | Вид карточек, колонок в строке для каждого вида, нумерация | | **Дерево** | Колонок 1–6, теги, тип, расположение мета, авто-раскрытие (при `showTree: true`) | -| **Оформление** | Тема, акцент, узор панелей, свой градиент | +| **Оформление** | Тема, акцент, узоры по типам блоков, разделение слоёв, свой градиент | +| **(footer)** | Сброс, экспорт и импорт всех preferences в JSON | ### Режим «Сверху» @@ -464,31 +488,172 @@ Popup сгруппирован по разделам. Доступные бло ### Узоры панелей -Настраиваются в ⚙ → **Оформление** (если `allowPanelPatternChange: true`): четыре схлопывающиеся панели — **Панель навигации**, **Раздел**, **Подраздел**, **Элемент**. В каждой — свой тип узора и слайдеры. +Настраиваются в ⚙ → **Оформление** (если `allowPanelPatternChange: true`). -| Значение `panelPattern*` | Описание | -|--------------------------|----------| -| `none` | Без узора — только акцентный градиент (если применим) | -| `hex` | Соты | -| `dots` | Точечная сетка | -| `grid` | Линейная сетка | -| `diagonal` | Диагональные полосы | -| `cross` | Перекрёстная штриховка | -| `mesh` | Точки цвета акцента | +#### Четыре цели (targets) -По умолчанию для **навигации** и **разделов** — `hex`; для **подразделов** и **элементов** — `none`. +У каждого типа блока свой независимый набор: пресет, слайдеры и (опционально) свой URL. -| Панель | Preference (префикс) | DOM | -|--------|----------------------|-----| -| Панель навигации | `panelPatternNav*` | `.hero-content` | -| Раздел | `panelPatternSection*` | `.catalog-section` | -| Подраздел | `panelPatternGroup*` | `.catalog-group` | -| Элемент | `panelPatternItem*` | `.nav-card` — только узор, без акцентного градиента | +| Цель | Preference (префикс) | DOM-элемент | Градиент под узором | +|------|----------------------|-------------|---------------------| +| Панель навигации | `panelPatternNav*` | `.hero-content` | да | +| Раздел | `panelPatternSection*` | `.catalog-section` | да | +| Подраздел | `panelPatternGroup*` | `.catalog-group` | да | +| Элемент | `panelPatternItem*` | `.nav-card` | нет (только узор) | -В каждой панели (кроме «Элемент») — шесть слайдеров: прозрачность, высота и **переход** узора; то же для градиента. Для «Элемент» — три слайдера узора (без градиента). **Переход** задаёт ширину зоны затухания: `0%` — резкий обрыв на линии «высоты», большее значение — более плавное исчезновение (при значении ≥ высоты узор растворяется от самого верха блока). +#### Встроенные пресеты (`panelPattern*`) + +| id | Подпись в ⚙ | Описание | +|----|-------------|----------| +| `none` | Нет | Без повторяющейся текстуры | +| `hex` | Соты | Крупные гексагоны | +| `hex-sm` | Соты м. | Мелкие гексагоны | +| `dots` | Точки | Средняя точечная сетка | +| `dots-sm` | Точки м. | Мелкие кружки | +| `dots-lg` | Точки кр. | Крупные кружки | +| `grid` | Сетка | Линейная сетка | +| `diagonal` | Диаг. | Диагональные полосы | +| `cross` | Клетка | Перекрёстная штриховка | +| `mesh` | Mesh | Точки цвета акцента | +| `tri-sm` | △ мел. | Мелкие треугольники | +| `tri-lg` | △ кр. | Крупные треугольники | +| `diamond` | Ромбы | Ромбовидная сетка | +| `custom` | Свой | Своя текстура (см. ниже) | + +По умолчанию: **навигация** и **разделы** — `hex`; **подразделы** и **элементы** — `none`. + +#### Слайдеры (для каждой цели) + +| Сuffix preference | UI | Диапазон | По умолчанию | +|-------------------|-----|----------|--------------| +| `Opacity` | Узор: прозрачность | 0–100 % | 58 | +| `Height` | Узор: высота | 0–100 % высоты блока | 38 | +| `Fade` | Узор: переход | 0–100 % | 18 | +| `GradientOpacity` | Градиент: прозрачность | 0–100 % | 62 | +| `GradientHeight` | Градиент: высота | 0–100 % | 34 | +| `GradientFade` | Градиент: переход | 0–100 % | 16 | + +**Переход (fade):** `0%` — резкий обрыв на линии «высоты»; большее значение — плавнее затухание к низу блока. + +Для цели **Элемент** в UI только три слайдера узора (без градиента). + +#### Как узор попадает на экран + +1. `preferences` → `applyPanelPatternPreferences()` в `script.js` +2. На ``: `data-panel-pattern-nav="hex"` (и section / group / item) +3. CSS-переменные: `--panel-pattern-{target}-opacity`, `-height`, `-fade-start`, `--panel-gradient-{target}-*` +4. Встроенные пресеты: `styles.css` → `--pattern-tile-*` и селекторы `html[data-panel-pattern-*="…"]` +5. Слои на блоке: `::before` (градиент), `::after` (текстура с mask по высоте/переходу) + +Кнопки пресетов в ⚙ **не** захардкожены в HTML: контейнеры `data-pattern-swatches="nav|section|group|item"`, список — `panelPatternCatalog` в `script.js`. + +### Разделение слоёв узоров + +Слайдер **«Разделение слоёв»** в ⚙ → Оформление (`patternIsolation`, 0–100 %, по умолчанию `46`). + +Когда узоры включены на нескольких уровнях вложенности (раздел → подраздел → карточка), текстуры могут визуально накладываться. Настройка добавляет на `.catalog-section`, `.catalog-group` и `.nav-card`: + +- полупрозрачную подложку (`--panel-layer-fill`); +- `backdrop-filter: blur(...)` (`--panel-layer-blur`). + +Так каждый уровень «отрезает» узоры снизу и остаётся читаемой граница. Значение задаётся в `settings.json` → `defaults.patternIsolation` и сохраняется в `localStorage`. + +### Свой узор (custom) + +1. В панели нужной цели выберите пресет **«Свой»** +2. Появится блок с полями: + - **Изображение** — `panelPattern{Nav|Section|Group|Item}CustomImage` + - **Размер плитки** — `panelPattern*CustomSize` (например `32px 32px`) + +Допустимые форматы URL: + +- `data:image/svg+xml,…` или `data:image/png;base64,…` +- `https://…` / `http://…` +- относительный путь: `./assets/my-pattern.svg`, `assets/pattern.png` +- `javascript:` — **отклоняется** при нормализации + +Пример минимального SVG (data URI): + +``` +data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Ccircle cx='8' cy='8' r='2' fill='white' fill-opacity='.2'/%3E%3C/svg%3E +``` + +При `custom` без URL узор не рисуется (opacity = 0), пока не задано изображение. + +### Экспорт и импорт настроек + +Внизу popup ⚙ — три кнопки: + +| Кнопка | Действие | +|--------|----------| +| **Сбросить** | Подтверждение → `panelDefaults` из `settings.json`, очистка `ecosystem-panel-preferences` | +| **Экспортировать** | Скачивание `ecosystem-panel-preferences.json` | +| **Импортировать** | Выбор JSON; применение с `normalizePreferences` + `enforceFeatureLocks` | + +Формат экспорта: + +```json +{ + "version": 1, + "exportedAt": "2026-06-29T12:00:00.000Z", + "preferences": { "theme": "dark", "panelPatternNav": "hex", "patternIsolation": 46 } +} +``` + +Импорт принимает и обёртку с `preferences`, и «голый» объект настроек. + +### Раскрытие списков + +В ⚙ → **Каталог** — режим **Резко** / **Плавно** и длительность (100–800 ms). Плавное раскрытие применяется к разделам и подразделам каталога, дереву навигации и схлопывающимся блокам в настройках. Кнопки сворачивания — chevron в стеклянной рамке с акцентной подсветкой при наведении. + +| Preference | Значения | По умолчанию | +|------------|----------|--------------| +| `collapseAnimation` | `"instant"`, `"animated"` | `"animated"` | +| `collapseDuration` | `100`–`800` (ms) | `240` | Старые ключи (`panelPattern`, `panelPatternOnNav`, …) при загрузке автоматически мигрируют в per-target формат, если в `localStorage` ещё нет `panelPatternNav`. +### Навигация по коду + +Быстрый поиск: в `script.js` и `styles.css` — секции с разделителем `// =====` / `/* =====`; в `index.html` — комментарий-оглавление в начале файла. + +#### script.js + +| Секция | Содержимое | +|--------|------------| +| `Panel pattern catalog and defaults` | `panelPatternCatalog`, `availablePanelPatterns`, defaults по целям | +| `Panel pattern preferences` | normalize, apply, custom URL, UI (`renderPatternSwatches`, …) | +| `Settings import, export and reset` | `applyStoredPreferences`, export/import JSON | +| `Preferences application` | `applyPreferences`, sync контролов ⚙ | +| `Collapse state handling` | анимация раскрытия details | + +Ключевые функции узоров: `applyPanelPatternPreferences`, `applyPatternIsolationPreference`, `getPanelPatternPreferenceKey`, `setupPatternControls`. + +#### styles.css + +| Секция | Содержимое | +|--------|------------| +| `01. Theme Tokens` | `--pattern-tile-*`, mapping `html[data-panel-pattern-*]`, `--panel-layer-*` | +| `05. Sidebar Panel` | `::before` / `::after` узоров, layer isolation на catalog/group/card | +| `06. Settings Popup` | `.pattern-swatches`, `.pattern-custom-panel`, `.settings-actions` | + +#### index.html + +| Блок | Атрибуты / классы | +|------|-------------------| +| Popup ⚙ | `[data-settings-panel]`, секции `[data-settings-section="…"]` | +| Узоры | `[data-pattern-targets-panel]`, `[data-pattern-swatches="{target}"]` | +| Разделение слоёв | `[data-pattern-isolation]` | +| Экспорт | `[data-settings-reset]`, `[data-settings-export]`, `[data-settings-import]` | + +#### Добавить новый встроенный пресет + +1. Добавить id в `availablePanelPatterns` и запись в `panelPatternCatalog` (`script.js`) +2. Определить `--pattern-tile-{id}-image` и `-size` в `:root` (`styles.css`) +3. Добавить строки `html[data-panel-pattern-{nav|section|group|item}="{id}"]` для всех четырёх целей +4. Добавить превью `.pattern-swatch[data-panel-pattern="{id}"]::before` в секции настроек +5. При необходимости — override SVG для `html[data-theme="light"]` + Значения сохраняются в `localStorage` и выставляют `data-panel-pattern-*` на `` и CSS-переменные `--panel-pattern-{nav|section|group|item}-*`, `--panel-gradient-{target}-*`. ## Поведение интерфейса @@ -528,7 +693,7 @@ Popup сгруппирован по разделам. Доступные бло | Ключ | Содержимое | |------|------------| -| `ecosystem-panel-preferences` | Тема, акцент, узор, вид, колонки каталога, ширина sidebar, toggle-настройки | +| `ecosystem-panel-preferences` | Тема, акцент, узоры, вид, колонки, `patternIsolation`, custom-URL, ширина sidebar, export/import | | `ecosystem-panel-collapse-state` | Раскрытые/свернутые ветки дерева и каталога | ## Разработка diff --git a/examples/.vscode/settings.json b/examples/.vscode/settings.json index 63662ed..068fdc0 100644 --- a/examples/.vscode/settings.json +++ b/examples/.vscode/settings.json @@ -1,3 +1,3 @@ { - "liveServer.settings.port": 5502 + "liveServer.settings.port": 10001 } \ No newline at end of file diff --git a/examples/flat/index.html b/examples/flat/index.html index f1aa308..0a5759b 100644 --- a/examples/flat/index.html +++ b/examples/flat/index.html @@ -276,6 +276,18 @@ + +
+ Раскрытие списков + + +
+

Разделы каталога, дерево навигации и блоки в настройках.

+
@@ -382,15 +394,7 @@

Боковое меню и дерево

-
- - - - - - - -
+
@@ -409,15 +413,7 @@

Карточки верхнего уровня каталога

-
- - - - - - - -
+
@@ -436,15 +432,7 @@

Группы внутри раздела

-
- - - - - - - -
+
@@ -463,15 +451,7 @@

Карточки сервисов — только узор, без акцентного градиента

-
- - - - - - - -
+
@@ -481,6 +461,15 @@
+ +

+ Подложка размывает узоры нижних уровней под разделами, подразделами и карточками. +

+
+ +
+

+ Сброс, экспорт и импорт настроек интерфейса на этом устройстве. +

+
+ + + +
+ +
diff --git a/examples/flat/settings.json b/examples/flat/settings.json index 237a4fc..13617ad 100644 --- a/examples/flat/settings.json +++ b/examples/flat/settings.json @@ -2,61 +2,80 @@ "dataUrl": "./panel-data.json", "data": null, "defaults": { - "navLayout": "sidebar", - "showTree": true, - "showResizeHandles": true, + "navLayout": "top", + "showTree": false, + "showResizeHandles": false, "theme": "dark", "view": "cards", - "accent": "violet", - "customAccentFrom": "#8b5cf6", - "customAccentTo": "#22d3ee", + "accent": "custom", + "customAccentFrom": "#ff428e", + "customAccentTo": "#5bd8ec", "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, + "panelPatternNav": "diagonal", + "panelPatternNavOpacity": 20, + "panelPatternNavHeight": 100, + "panelPatternNavFade": 100, + "panelPatternNavGradientOpacity": 100, + "panelPatternNavGradientHeight": 100, + "panelPatternNavGradientFade": 100, + "panelPatternNavCustomImage": "", + "panelPatternNavCustomSize": "32px 32px", + "panelPatternSection": "diamond", + "panelPatternSectionOpacity": 30, + "panelPatternSectionHeight": 50, + "panelPatternSectionFade": 60, + "panelPatternSectionGradientOpacity": 75, + "panelPatternSectionGradientHeight": 20, + "panelPatternSectionGradientFade": 15, + "panelPatternSectionCustomImage": "", + "panelPatternSectionCustomSize": "32px 32px", + "panelPatternGroup": "custom", + "panelPatternGroupOpacity": 10, + "panelPatternGroupHeight": 100, + "panelPatternGroupFade": 100, + "panelPatternGroupGradientOpacity": 20, + "panelPatternGroupGradientHeight": 100, + "panelPatternGroupGradientFade": 100, + "panelPatternGroupCustomImage": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='s2' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23E2E8F0' /%3E%3Cstop offset='100%25' stop-color='%2364748B' /%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='20' y='20' width='24' height='24' fill='none' stroke='url(%23s2)' stroke-width='1.5' transform='rotate(45 32 32)' /%3E%3Crect x='16' y='16' width='32' height='32' fill='none' stroke='url(%23s2)' stroke-width='1' opacity='0.4' transform='rotate(45 32 32)' /%3E%3C/svg%3E", + "panelPatternGroupCustomSize": "32px 32px", + "panelPatternItem": "mesh", + "panelPatternItemOpacity": 58, + "panelPatternItemHeight": 100, + "panelPatternItemFade": 18, + "panelPatternItemGradientOpacity": 62, + "panelPatternItemGradientHeight": 34, + "panelPatternItemGradientFade": 16, + "panelPatternItemCustomImage": "", + "panelPatternItemCustomSize": "32px 32px", + "patternIsolation": 65, "showNumbers": false, - "showTreeTags": true, + "showTreeTags": false, "showTreeType": false, "treeMetaLayout": "inline", - "treeColumns": 2, + "treeColumns": 1, "autoCollapseSearch": true, - "sidebarWidth": 390, - "topBarHeight": 140, - "shellMaxWidth": 2800, - "sidebarMinWidth": 260, - "sidebarMaxWidth": 800, - "sidebarMaxWidthWide": 1400, - "treeSidebarBaseWidth": 420, - "treeSidebarColumnWidth": 120, - "topBarMinHeight": 80, - "topBarMaxHeight": 560, - "catalogCardsColumns": 3, - "catalogListColumns": 1, - "catalogCompactCardsColumns": 5, + "sidebarWidth": 360, + "topBarHeight": 72, + "shellMaxWidth": 1100, + "sidebarMinWidth": 240, + "sidebarMaxWidth": 720, + "sidebarMaxWidthWide": 1040, + "treeSidebarBaseWidth": 360, + "treeSidebarColumnWidth": 96, + "topBarMinHeight": 64, + "topBarMaxHeight": 800, + "catalogCardsColumns": 2, + "catalogListColumns": 2, + "catalogCompactCardsColumns": 3, "catalogCompactListColumns": 3, + "collapseAnimation": "animated", + "collapseDuration": 250, "searchPlaceholder": "Grafana, S3, журнал, сервер...", - "settingsPanelWidth": 460 + "settingsPanelWidth": 500 }, "limits": { - "shellMaxWidth": { "min": 1200, "max": 3840 }, - "sidebarMinWidth": { "min": 200, "max": 480 }, + "shellMaxWidth": { "min": 1100, "max": 3840 }, + "sidebarMinWidth": { "min": 220, "max": 480 }, "sidebarMaxWidth": { "min": 400, "max": 1200 }, "sidebarMaxWidthWide": { "min": 560, "max": 2000 }, "treeSidebarBaseWidth": { "min": 280, "max": 720 }, diff --git a/examples/full/index.html b/examples/full/index.html index f1aa308..0a5759b 100644 --- a/examples/full/index.html +++ b/examples/full/index.html @@ -276,6 +276,18 @@ + +
+ Раскрытие списков + + +
+

Разделы каталога, дерево навигации и блоки в настройках.

+
@@ -382,15 +394,7 @@

Боковое меню и дерево

-
- - - - - - - -
+
@@ -409,15 +413,7 @@

Карточки верхнего уровня каталога

-
- - - - - - - -
+
@@ -436,15 +432,7 @@

Группы внутри раздела

-
- - - - - - - -
+
@@ -463,15 +451,7 @@

Карточки сервисов — только узор, без акцентного градиента

-
- - - - - - - -
+
@@ -481,6 +461,15 @@
+ +

+ Подложка размывает узоры нижних уровней под разделами, подразделами и карточками. +

+
+ +
+

+ Сброс, экспорт и импорт настроек интерфейса на этом устройстве. +

+
+ + + +
+ +
diff --git a/examples/full/settings.json b/examples/full/settings.json index 237a4fc..13617ad 100644 --- a/examples/full/settings.json +++ b/examples/full/settings.json @@ -2,61 +2,80 @@ "dataUrl": "./panel-data.json", "data": null, "defaults": { - "navLayout": "sidebar", - "showTree": true, - "showResizeHandles": true, + "navLayout": "top", + "showTree": false, + "showResizeHandles": false, "theme": "dark", "view": "cards", - "accent": "violet", - "customAccentFrom": "#8b5cf6", - "customAccentTo": "#22d3ee", + "accent": "custom", + "customAccentFrom": "#ff428e", + "customAccentTo": "#5bd8ec", "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, + "panelPatternNav": "diagonal", + "panelPatternNavOpacity": 20, + "panelPatternNavHeight": 100, + "panelPatternNavFade": 100, + "panelPatternNavGradientOpacity": 100, + "panelPatternNavGradientHeight": 100, + "panelPatternNavGradientFade": 100, + "panelPatternNavCustomImage": "", + "panelPatternNavCustomSize": "32px 32px", + "panelPatternSection": "diamond", + "panelPatternSectionOpacity": 30, + "panelPatternSectionHeight": 50, + "panelPatternSectionFade": 60, + "panelPatternSectionGradientOpacity": 75, + "panelPatternSectionGradientHeight": 20, + "panelPatternSectionGradientFade": 15, + "panelPatternSectionCustomImage": "", + "panelPatternSectionCustomSize": "32px 32px", + "panelPatternGroup": "custom", + "panelPatternGroupOpacity": 10, + "panelPatternGroupHeight": 100, + "panelPatternGroupFade": 100, + "panelPatternGroupGradientOpacity": 20, + "panelPatternGroupGradientHeight": 100, + "panelPatternGroupGradientFade": 100, + "panelPatternGroupCustomImage": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='s2' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23E2E8F0' /%3E%3Cstop offset='100%25' stop-color='%2364748B' /%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='20' y='20' width='24' height='24' fill='none' stroke='url(%23s2)' stroke-width='1.5' transform='rotate(45 32 32)' /%3E%3Crect x='16' y='16' width='32' height='32' fill='none' stroke='url(%23s2)' stroke-width='1' opacity='0.4' transform='rotate(45 32 32)' /%3E%3C/svg%3E", + "panelPatternGroupCustomSize": "32px 32px", + "panelPatternItem": "mesh", + "panelPatternItemOpacity": 58, + "panelPatternItemHeight": 100, + "panelPatternItemFade": 18, + "panelPatternItemGradientOpacity": 62, + "panelPatternItemGradientHeight": 34, + "panelPatternItemGradientFade": 16, + "panelPatternItemCustomImage": "", + "panelPatternItemCustomSize": "32px 32px", + "patternIsolation": 65, "showNumbers": false, - "showTreeTags": true, + "showTreeTags": false, "showTreeType": false, "treeMetaLayout": "inline", - "treeColumns": 2, + "treeColumns": 1, "autoCollapseSearch": true, - "sidebarWidth": 390, - "topBarHeight": 140, - "shellMaxWidth": 2800, - "sidebarMinWidth": 260, - "sidebarMaxWidth": 800, - "sidebarMaxWidthWide": 1400, - "treeSidebarBaseWidth": 420, - "treeSidebarColumnWidth": 120, - "topBarMinHeight": 80, - "topBarMaxHeight": 560, - "catalogCardsColumns": 3, - "catalogListColumns": 1, - "catalogCompactCardsColumns": 5, + "sidebarWidth": 360, + "topBarHeight": 72, + "shellMaxWidth": 1100, + "sidebarMinWidth": 240, + "sidebarMaxWidth": 720, + "sidebarMaxWidthWide": 1040, + "treeSidebarBaseWidth": 360, + "treeSidebarColumnWidth": 96, + "topBarMinHeight": 64, + "topBarMaxHeight": 800, + "catalogCardsColumns": 2, + "catalogListColumns": 2, + "catalogCompactCardsColumns": 3, "catalogCompactListColumns": 3, + "collapseAnimation": "animated", + "collapseDuration": 250, "searchPlaceholder": "Grafana, S3, журнал, сервер...", - "settingsPanelWidth": 460 + "settingsPanelWidth": 500 }, "limits": { - "shellMaxWidth": { "min": 1200, "max": 3840 }, - "sidebarMinWidth": { "min": 200, "max": 480 }, + "shellMaxWidth": { "min": 1100, "max": 3840 }, + "sidebarMinWidth": { "min": 220, "max": 480 }, "sidebarMaxWidth": { "min": 400, "max": 1200 }, "sidebarMaxWidthWide": { "min": 560, "max": 2000 }, "treeSidebarBaseWidth": { "min": 280, "max": 720 }, diff --git a/examples/mixed/index.html b/examples/mixed/index.html index f1aa308..0a5759b 100644 --- a/examples/mixed/index.html +++ b/examples/mixed/index.html @@ -276,6 +276,18 @@ + +
+ Раскрытие списков + + +
+

Разделы каталога, дерево навигации и блоки в настройках.

+
@@ -382,15 +394,7 @@

Боковое меню и дерево

-
- - - - - - - -
+
@@ -409,15 +413,7 @@

Карточки верхнего уровня каталога

-
- - - - - - - -
+
@@ -436,15 +432,7 @@

Группы внутри раздела

-
- - - - - - - -
+
@@ -463,15 +451,7 @@

Карточки сервисов — только узор, без акцентного градиента

-
- - - - - - - -
+
@@ -481,6 +461,15 @@
+ +

+ Подложка размывает узоры нижних уровней под разделами, подразделами и карточками. +

+
+ +
+

+ Сброс, экспорт и импорт настроек интерфейса на этом устройстве. +

+
+ + + +
+ +
diff --git a/examples/mixed/settings.json b/examples/mixed/settings.json index 237a4fc..13617ad 100644 --- a/examples/mixed/settings.json +++ b/examples/mixed/settings.json @@ -2,61 +2,80 @@ "dataUrl": "./panel-data.json", "data": null, "defaults": { - "navLayout": "sidebar", - "showTree": true, - "showResizeHandles": true, + "navLayout": "top", + "showTree": false, + "showResizeHandles": false, "theme": "dark", "view": "cards", - "accent": "violet", - "customAccentFrom": "#8b5cf6", - "customAccentTo": "#22d3ee", + "accent": "custom", + "customAccentFrom": "#ff428e", + "customAccentTo": "#5bd8ec", "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, + "panelPatternNav": "diagonal", + "panelPatternNavOpacity": 20, + "panelPatternNavHeight": 100, + "panelPatternNavFade": 100, + "panelPatternNavGradientOpacity": 100, + "panelPatternNavGradientHeight": 100, + "panelPatternNavGradientFade": 100, + "panelPatternNavCustomImage": "", + "panelPatternNavCustomSize": "32px 32px", + "panelPatternSection": "diamond", + "panelPatternSectionOpacity": 30, + "panelPatternSectionHeight": 50, + "panelPatternSectionFade": 60, + "panelPatternSectionGradientOpacity": 75, + "panelPatternSectionGradientHeight": 20, + "panelPatternSectionGradientFade": 15, + "panelPatternSectionCustomImage": "", + "panelPatternSectionCustomSize": "32px 32px", + "panelPatternGroup": "custom", + "panelPatternGroupOpacity": 10, + "panelPatternGroupHeight": 100, + "panelPatternGroupFade": 100, + "panelPatternGroupGradientOpacity": 20, + "panelPatternGroupGradientHeight": 100, + "panelPatternGroupGradientFade": 100, + "panelPatternGroupCustomImage": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='s2' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23E2E8F0' /%3E%3Cstop offset='100%25' stop-color='%2364748B' /%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='20' y='20' width='24' height='24' fill='none' stroke='url(%23s2)' stroke-width='1.5' transform='rotate(45 32 32)' /%3E%3Crect x='16' y='16' width='32' height='32' fill='none' stroke='url(%23s2)' stroke-width='1' opacity='0.4' transform='rotate(45 32 32)' /%3E%3C/svg%3E", + "panelPatternGroupCustomSize": "32px 32px", + "panelPatternItem": "mesh", + "panelPatternItemOpacity": 58, + "panelPatternItemHeight": 100, + "panelPatternItemFade": 18, + "panelPatternItemGradientOpacity": 62, + "panelPatternItemGradientHeight": 34, + "panelPatternItemGradientFade": 16, + "panelPatternItemCustomImage": "", + "panelPatternItemCustomSize": "32px 32px", + "patternIsolation": 65, "showNumbers": false, - "showTreeTags": true, + "showTreeTags": false, "showTreeType": false, "treeMetaLayout": "inline", - "treeColumns": 2, + "treeColumns": 1, "autoCollapseSearch": true, - "sidebarWidth": 390, - "topBarHeight": 140, - "shellMaxWidth": 2800, - "sidebarMinWidth": 260, - "sidebarMaxWidth": 800, - "sidebarMaxWidthWide": 1400, - "treeSidebarBaseWidth": 420, - "treeSidebarColumnWidth": 120, - "topBarMinHeight": 80, - "topBarMaxHeight": 560, - "catalogCardsColumns": 3, - "catalogListColumns": 1, - "catalogCompactCardsColumns": 5, + "sidebarWidth": 360, + "topBarHeight": 72, + "shellMaxWidth": 1100, + "sidebarMinWidth": 240, + "sidebarMaxWidth": 720, + "sidebarMaxWidthWide": 1040, + "treeSidebarBaseWidth": 360, + "treeSidebarColumnWidth": 96, + "topBarMinHeight": 64, + "topBarMaxHeight": 800, + "catalogCardsColumns": 2, + "catalogListColumns": 2, + "catalogCompactCardsColumns": 3, "catalogCompactListColumns": 3, + "collapseAnimation": "animated", + "collapseDuration": 250, "searchPlaceholder": "Grafana, S3, журнал, сервер...", - "settingsPanelWidth": 460 + "settingsPanelWidth": 500 }, "limits": { - "shellMaxWidth": { "min": 1200, "max": 3840 }, - "sidebarMinWidth": { "min": 200, "max": 480 }, + "shellMaxWidth": { "min": 1100, "max": 3840 }, + "sidebarMinWidth": { "min": 220, "max": 480 }, "sidebarMaxWidth": { "min": 400, "max": 1200 }, "sidebarMaxWidthWide": { "min": 560, "max": 2000 }, "treeSidebarBaseWidth": { "min": 280, "max": 720 }, diff --git a/examples/section-items/index.html b/examples/section-items/index.html index f1aa308..0a5759b 100644 --- a/examples/section-items/index.html +++ b/examples/section-items/index.html @@ -276,6 +276,18 @@ + +
+ Раскрытие списков + + +
+

Разделы каталога, дерево навигации и блоки в настройках.

+
@@ -382,15 +394,7 @@

Боковое меню и дерево

-
- - - - - - - -
+
@@ -409,15 +413,7 @@

Карточки верхнего уровня каталога

-
- - - - - - - -
+
@@ -436,15 +432,7 @@

Группы внутри раздела

-
- - - - - - - -
+
@@ -463,15 +451,7 @@

Карточки сервисов — только узор, без акцентного градиента

-
- - - - - - - -
+
@@ -481,6 +461,15 @@
+ +

+ Подложка размывает узоры нижних уровней под разделами, подразделами и карточками. +

+
+ +
+

+ Сброс, экспорт и импорт настроек интерфейса на этом устройстве. +

+
+ + + +
+ +
diff --git a/examples/section-items/settings.json b/examples/section-items/settings.json index 237a4fc..11d4cf7 100644 --- a/examples/section-items/settings.json +++ b/examples/section-items/settings.json @@ -12,51 +12,62 @@ "customAccentTo": "#22d3ee", "customAccentAngle": 135, "panelPatternNav": "hex", - "panelPatternNavOpacity": 70, - "panelPatternNavHeight": 45, - "panelPatternNavGradientOpacity": 80, - "panelPatternNavGradientHeight": 40, + "panelPatternNavOpacity": 58, + "panelPatternNavHeight": 38, + "panelPatternNavFade": 18, + "panelPatternNavGradientOpacity": 62, + "panelPatternNavGradientHeight": 34, + "panelPatternNavGradientFade": 16, "panelPatternSection": "hex", - "panelPatternSectionOpacity": 70, - "panelPatternSectionHeight": 45, - "panelPatternSectionGradientOpacity": 80, - "panelPatternSectionGradientHeight": 40, + "panelPatternSectionOpacity": 58, + "panelPatternSectionHeight": 38, + "panelPatternSectionFade": 18, + "panelPatternSectionGradientOpacity": 62, + "panelPatternSectionGradientHeight": 34, + "panelPatternSectionGradientFade": 16, "panelPatternGroup": "none", - "panelPatternGroupOpacity": 70, - "panelPatternGroupHeight": 45, - "panelPatternGroupGradientOpacity": 80, - "panelPatternGroupGradientHeight": 40, + "panelPatternGroupOpacity": 58, + "panelPatternGroupHeight": 38, + "panelPatternGroupFade": 18, + "panelPatternGroupGradientOpacity": 62, + "panelPatternGroupGradientHeight": 34, + "panelPatternGroupGradientFade": 16, "panelPatternItem": "none", - "panelPatternItemOpacity": 70, - "panelPatternItemHeight": 45, - "panelPatternItemGradientOpacity": 80, - "panelPatternItemGradientHeight": 40, + "panelPatternItemOpacity": 58, + "panelPatternItemHeight": 38, + "panelPatternItemFade": 18, + "panelPatternItemGradientOpacity": 62, + "panelPatternItemGradientHeight": 34, + "panelPatternItemGradientFade": 16, + "patternIsolation": 46, "showNumbers": false, "showTreeTags": true, "showTreeType": false, "treeMetaLayout": "inline", - "treeColumns": 2, + "treeColumns": 1, "autoCollapseSearch": true, - "sidebarWidth": 390, + "sidebarWidth": 360, "topBarHeight": 140, - "shellMaxWidth": 2800, - "sidebarMinWidth": 260, - "sidebarMaxWidth": 800, - "sidebarMaxWidthWide": 1400, - "treeSidebarBaseWidth": 420, - "treeSidebarColumnWidth": 120, + "shellMaxWidth": 2240, + "sidebarMinWidth": 240, + "sidebarMaxWidth": 720, + "sidebarMaxWidthWide": 1040, + "treeSidebarBaseWidth": 360, + "treeSidebarColumnWidth": 96, "topBarMinHeight": 80, "topBarMaxHeight": 560, "catalogCardsColumns": 3, "catalogListColumns": 1, - "catalogCompactCardsColumns": 5, - "catalogCompactListColumns": 3, + "catalogCompactCardsColumns": 4, + "catalogCompactListColumns": 2, + "collapseAnimation": "animated", + "collapseDuration": 260, "searchPlaceholder": "Grafana, S3, журнал, сервер...", - "settingsPanelWidth": 460 + "settingsPanelWidth": 440 }, "limits": { "shellMaxWidth": { "min": 1200, "max": 3840 }, - "sidebarMinWidth": { "min": 200, "max": 480 }, + "sidebarMinWidth": { "min": 220, "max": 480 }, "sidebarMaxWidth": { "min": 400, "max": 1200 }, "sidebarMaxWidthWide": { "min": 560, "max": 2000 }, "treeSidebarBaseWidth": { "min": 280, "max": 720 }, diff --git a/examples/shared/script.js b/examples/shared/script.js index 170f405..68a9918 100644 --- a/examples/shared/script.js +++ b/examples/shared/script.js @@ -1,3 +1,29 @@ +// ecosystem-panel / script.js +// +// ОГЛАВЛЕНИЕ — ищите секции по строке «// =====» (Ctrl+F): +// Persistent storage keys +// Panel configuration (settings.json) +// Panel pattern catalog and defaults +// Default UI preferences +// Runtime state and DOM references +// Initial render +// Event bindings +// Panel config loading +// Data helpers +// Catalog rendering +// Search matching and highlighting +// Sidebar tree rendering +// Focusing from tree to content +// Collapse state handling +// Card pointer interaction +// Preferences application +// Generic helpers +// Panel pattern preferences (normalize, apply, custom, UI) +// localStorage helpers (read/save, import/export/reset) +// +// Связанные файлы: index.html (разметка ⚙), styles.css (узоры, темы, слои). +// Документация: README.md → «Узоры панелей», «Разделение слоёв», «Экспорт настроек». + // ============================================================================ // Persistent storage keys // ============================================================================ @@ -65,16 +91,73 @@ const defaultServiceMeta = { let serviceMeta = { ...defaultServiceMeta }; -const availablePanelPatterns = ["none", "hex", "dots", "grid", "diagonal", "cross", "mesh"]; +// ============================================================================ +// Panel pattern catalog and defaults +// ============================================================================ +// Узоры накладываются на четыре типа блоков интерфейса (см. panelPatternTargetIds). +// +// Как это работает: +// 1. panelPatternCatalog — пресеты для кнопок в ⚙ (генерируются в renderPatternSwatches). +// 2. preferences.panelPattern{Nav|Section|Group|Item} — выбранный id пресета или "custom". +// 3. applyPanelPatternPreferences() выставляет data-panel-pattern-* на и CSS-переменные. +// 4. Встроенные пресеты: styles.css → --pattern-tile-* + html[data-panel-pattern-*="hex"] … +// 5. custom: panelPattern*CustomImage + CustomSize → inline --panel-pattern-image-{target}. +// +// См. README.md → «Узоры панелей», «Свой узор», «Разделение слоёв». + +/** Допустимые id узоров (валидация в normalizePanelPatternPreferences). */ +const availablePanelPatterns = [ + "none", + "hex", + "hex-sm", + "dots", + "dots-sm", + "dots-lg", + "grid", + "diagonal", + "cross", + "mesh", + "tri-sm", + "tri-lg", + "diamond", + "custom", +]; + +/** Подписи пресетов в UI настроек. Порядок = порядок кнопок в сетке pattern-swatches. */ +const panelPatternCatalog = [ + { id: "none", label: "Нет" }, + { id: "hex", label: "Соты" }, + { id: "hex-sm", label: "Соты м." }, + { id: "dots", label: "Точки" }, + { id: "dots-sm", label: "Точки м." }, + { id: "dots-lg", label: "Точки кр." }, + { id: "grid", label: "Сетка" }, + { id: "diagonal", label: "Диаг." }, + { id: "cross", label: "Клетка" }, + { id: "mesh", label: "Mesh" }, + { id: "tri-sm", label: "△ мел." }, + { id: "tri-lg", label: "△ кр." }, + { id: "diamond", label: "Ромбы" }, + { id: "custom", label: "Свой" }, +]; + +/** Размер плитки по умолчанию для custom-узора (background-size). */ +const defaultPanelPatternCustomSize = "32px 32px"; + +/** Цели узора: nav → .hero-content, section → .catalog-section, group → .catalog-group, item → .nav-card */ const panelPatternTargetIds = ["nav", "section", "group", "item"]; + +/** Слайдеры узора/градиента по умолчанию (%, 0–100). Переопределяются settings.json → defaults. */ const panelPatternTuningDefaults = { - opacity: 70, - height: 45, - fade: 10, - gradientOpacity: 80, - gradientHeight: 40, - gradientFade: 8, + opacity: 58, + height: 38, + fade: 18, + gradientOpacity: 62, + gradientHeight: 34, + gradientFade: 16, }; + +/** Стартовый пресет узора для каждой цели, если в localStorage / settings.json нет значения. */ const panelPatternTargetMeta = { nav: { defaultPattern: "hex" }, section: { defaultPattern: "hex" }, @@ -86,6 +169,7 @@ function capitalizePanelPatternTarget(target) { return target.charAt(0).toUpperCase() + target.slice(1); } +/** Собирает ключи panelPattern* для всех целей (pattern, opacity, customImage, …). */ function buildDefaultPanelPatternPreferences() { const prefs = {}; @@ -98,6 +182,8 @@ function buildDefaultPanelPatternPreferences() { prefs[`panelPattern${cap}GradientOpacity`] = panelPatternTuningDefaults.gradientOpacity; prefs[`panelPattern${cap}GradientHeight`] = panelPatternTuningDefaults.gradientHeight; prefs[`panelPattern${cap}GradientFade`] = panelPatternTuningDefaults.gradientFade; + prefs[`panelPattern${cap}CustomImage`] = ""; + prefs[`panelPattern${cap}CustomSize`] = defaultPanelPatternCustomSize; } return prefs; @@ -115,6 +201,7 @@ const defaultPreferences = { customAccentTo: "#22d3ee", customAccentAngle: 135, ...buildDefaultPanelPatternPreferences(), + patternIsolation: 46, navLayout: "sidebar", showTree: true, showResizeHandles: true, @@ -122,24 +209,26 @@ const defaultPreferences = { showTreeTags: true, showTreeType: false, treeMetaLayout: "inline", - treeColumns: 2, + treeColumns: 1, autoCollapseSearch: true, - sidebarWidth: 390, + sidebarWidth: 360, topBarHeight: 140, - shellMaxWidth: 2800, - sidebarMinWidth: 260, - sidebarMaxWidth: 800, - sidebarMaxWidthWide: 1400, - treeSidebarBaseWidth: 420, - treeSidebarColumnWidth: 120, + shellMaxWidth: 2240, + sidebarMinWidth: 240, + sidebarMaxWidth: 720, + sidebarMaxWidthWide: 1040, + treeSidebarBaseWidth: 360, + treeSidebarColumnWidth: 96, topBarMinHeight: 80, topBarMaxHeight: 560, catalogCardsColumns: 3, catalogListColumns: 1, - catalogCompactCardsColumns: 5, - catalogCompactListColumns: 3, + catalogCompactCardsColumns: 4, + catalogCompactListColumns: 2, searchPlaceholder: "Grafana, S3, журнал, сервер...", - settingsPanelWidth: 460, + settingsPanelWidth: 440, + collapseAnimation: "animated", + collapseDuration: 260, }; const defaultPanelFeatures = { @@ -159,7 +248,7 @@ const defaultPanelFeatures = { const defaultPanelLimits = { shellMaxWidth: { min: 1200, max: 3840 }, - sidebarMinWidth: { min: 200, max: 480 }, + sidebarMinWidth: { min: 220, max: 480 }, sidebarMaxWidth: { min: 400, max: 1200 }, sidebarMaxWidthWide: { min: 560, max: 2000 }, treeSidebarBaseWidth: { min: 280, max: 720 }, @@ -199,6 +288,13 @@ const MIN_TREE_COLUMNS = 1; const MAX_TREE_COLUMNS = 6; const availableViews = ["cards", "list", "compact-cards", "compact-list"]; const availableAccents = ["violet", "cyan", "lime", "amber", "custom"]; +const availableCollapseAnimations = ["instant", "animated"]; +const MIN_COLLAPSE_DURATION = 100; +const MAX_COLLAPSE_DURATION = 800; +const MIN_PATTERN_ISOLATION = 0; +const MAX_PATTERN_ISOLATION = 100; +const COLLAPSIBLE_DETAILS_SELECTOR = + ".catalog-section, .catalog-group, .tree-section, .tree-group, .settings-section-collapsible, .pattern-target-panel"; // ============================================================================ // Runtime state and DOM references @@ -248,7 +344,16 @@ const settingsPanelWidthInput = document.querySelector("[data-settings-panel-wid const settingsPanelWidthValue = document.querySelector("[data-settings-panel-width-value]"); const layoutLimitInputs = document.querySelectorAll("[data-layout-limit]"); const catalogColumnInputs = document.querySelectorAll("[data-catalog-columns]"); +const patternIsolationInput = document.querySelector("[data-pattern-isolation]"); +const patternIsolationValue = document.querySelector("[data-pattern-isolation-value]"); +const collapseAnimationButtons = document.querySelectorAll("[data-collapse-animation]"); +const collapseDurationInput = document.querySelector("[data-collapse-duration]"); +const collapseDurationValue = document.querySelector("[data-collapse-duration-value]"); const settingsSections = document.querySelectorAll("[data-settings-section]"); +const settingsResetButton = document.querySelector("[data-settings-reset]"); +const settingsExportButton = document.querySelector("[data-settings-export]"); +const settingsImportButton = document.querySelector("[data-settings-import]"); +const settingsImportInput = document.querySelector("[data-settings-import-input]"); const emptyTemplate = document.querySelector("#empty-template"); const configErrorTemplate = document.querySelector("#config-error-template"); const pageTitleElement = document.querySelector("#page-title"); @@ -275,6 +380,7 @@ async function bootstrap() { return; } + setupPatternControls(); renderNavigationTree(); applyPreferences(preferences); renderCatalog(); @@ -309,6 +415,9 @@ navTree.addEventListener("click", (event) => { // Используем capture=true, потому что toggle не всплывает как обычный click. navTree.addEventListener("toggle", handleDetailsToggle, true); catalog.addEventListener("toggle", handleDetailsToggle, true); +settingsPanel?.addEventListener("toggle", handleDetailsToggle, true); + +document.addEventListener("click", handleAnimatedDetailsClick, true); // Открытие popup-настроек по шестеренке. settingsToggle.addEventListener("click", () => { @@ -321,6 +430,27 @@ settingsToggle.addEventListener("click", () => { settingsClose.addEventListener("click", closeSettingsPanel); settingsOverlay.addEventListener("click", closeSettingsPanel); +settingsResetButton?.addEventListener("click", resetStoredPreferences); +settingsExportButton?.addEventListener("click", exportStoredPreferences); +settingsImportButton?.addEventListener("click", () => settingsImportInput?.click()); + +settingsImportInput?.addEventListener("change", async () => { + const file = settingsImportInput.files?.[0]; + + if (!file) { + return; + } + + try { + const imported = await readImportedPreferencesFile(file); + applyStoredPreferences(imported); + } catch (error) { + window.alert(error instanceof Error ? error.message : "Не удалось импортировать настройки."); + } finally { + settingsImportInput.value = ""; + } +}); + document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeSettingsPanel(); @@ -441,23 +571,68 @@ patternTargetsPanel?.addEventListener("click", (event) => { } const target = button.dataset.panelPatternTarget; - preferences[getPanelPatternPreferenceKey(target, "pattern")] = button.dataset.panelPattern; + const pattern = button.dataset.panelPattern; + preferences[getPanelPatternPreferenceKey(target, "pattern")] = pattern; applyPreferences(preferences); savePreferences(preferences); + + if (pattern === "custom") { + const imageInput = patternTargetsPanel?.querySelector( + `[data-pattern-custom-panel="${target}"] [data-pattern-custom-field="image"]`, + ); + imageInput?.focus({ preventScroll: true }); + imageInput?.closest(".pattern-custom-panel")?.scrollIntoView({ block: "nearest", behavior: "smooth" }); + } }); patternTargetsPanel?.addEventListener("input", (event) => { - const input = event.target.closest("[data-panel-pattern-tuning]"); + const tuningInput = event.target.closest("[data-panel-pattern-tuning]"); + const customFieldInput = event.target.closest("[data-pattern-custom-field]"); - if (!input || !panelFeatures.allowPanelPatternChange) { + if (!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); + if (tuningInput) { + const target = tuningInput.dataset.panelPatternTuning; + const field = tuningInput.dataset.panelPatternField; + const key = getPanelPatternPreferenceKey(target, field); + const fallback = defaultPreferences[key] ?? panelPatternTuningDefaults[field] ?? 50; + preferences[key] = clampPanelPatternPercent(tuningInput.value, fallback); + applyPreferences(preferences); + savePreferences(preferences); + return; + } + + if (customFieldInput) { + const target = customFieldInput.dataset.panelPatternTarget; + const field = customFieldInput.dataset.patternCustomField; + const patternKey = getPanelPatternPreferenceKey(target, "pattern"); + + if (field === "image") { + preferences[getPanelPatternPreferenceKey(target, "customImage")] = normalizePatternCustomImage( + customFieldInput.value, + ); + } + + if (field === "size") { + preferences[getPanelPatternPreferenceKey(target, "customSize")] = normalizePatternCustomSize( + customFieldInput.value, + ); + } + + preferences[patternKey] = "custom"; + applyPreferences(preferences); + savePreferences(preferences); + } +}); + +patternIsolationInput?.addEventListener("input", () => { + if (!panelFeatures.allowPanelPatternChange) { + return; + } + + preferences.patternIsolation = clampPatternIsolation(patternIsolationInput.value); applyPreferences(preferences); savePreferences(preferences); }); @@ -596,6 +771,22 @@ catalogColumnInputs.forEach((input) => { }); }); +collapseAnimationButtons.forEach((button) => { + button.addEventListener("click", () => { + preferences.collapseAnimation = button.dataset.collapseAnimation; + applyPreferences(preferences); + savePreferences(preferences); + }); +}); + +function handleCollapseDurationChange() { + preferences.collapseDuration = clampCollapseDuration(collapseDurationInput?.value); + applyPreferences(preferences); + savePreferences(preferences); +} + +collapseDurationInput?.addEventListener("input", handleCollapseDurationChange); + // Drag-resize бокового меню. // Ширина сохраняется в preferences.sidebarWidth и применяется через CSS-переменную. sidebarResizer.addEventListener("pointerdown", (event) => { @@ -712,6 +903,7 @@ function normalizePreferences(raw) { migrateLegacyPanelPatternPreferences(next); normalizePanelPatternPreferences(next); + next.patternIsolation = clampPatternIsolation(next.patternIsolation); if (!availableNavLayouts.includes(next.navLayout)) { next.navLayout = defaultPreferences.navLayout; @@ -748,6 +940,10 @@ function normalizePreferences(raw) { getTopBarMaxHeight(next), ); next.settingsPanelWidth = clampLayoutPreference("settingsPanelWidth", next.settingsPanelWidth); + next.collapseAnimation = availableCollapseAnimations.includes(next.collapseAnimation) + ? next.collapseAnimation + : defaultPreferences.collapseAnimation; + next.collapseDuration = clampCollapseDuration(next.collapseDuration); return next; } @@ -823,7 +1019,10 @@ function enforceFeatureLocks(rawPreferences) { next[`panelPattern${cap}GradientOpacity`] = panelDefaults[`panelPattern${cap}GradientOpacity`]; next[`panelPattern${cap}GradientHeight`] = panelDefaults[`panelPattern${cap}GradientHeight`]; next[`panelPattern${cap}GradientFade`] = panelDefaults[`panelPattern${cap}GradientFade`]; + next[`panelPattern${cap}CustomImage`] = panelDefaults[`panelPattern${cap}CustomImage`]; + next[`panelPattern${cap}CustomSize`] = panelDefaults[`panelPattern${cap}CustomSize`]; } + next.patternIsolation = panelDefaults.patternIsolation; } if (!panelFeatures.allowShowNumbers) { @@ -1084,6 +1283,8 @@ function renderCatalog() { } else { catalog.append(emptyTemplate.content.cloneNode(true)); } + + refreshCollapseAnimBodies(); } function createItemsGridElement(items, query = "", className = "items-grid") { @@ -1317,6 +1518,7 @@ function renderNavigationTree() { }); navTree.replaceChildren(fragment); + refreshCollapseAnimBodies(); } function createTreeItemList(items, query = "", className = "tree-items") { @@ -1391,6 +1593,176 @@ function focusCatalogTarget(targetId) { // ============================================================================ // Collapse state handling // ============================================================================ +function isCollapseAnimationEnabled() { + return ( + preferences.collapseAnimation === "animated" && + !window.matchMedia("(prefers-reduced-motion: reduce)").matches + ); +} + +function isCollapsibleDetails(details) { + return details instanceof HTMLDetailsElement && details.matches(COLLAPSIBLE_DETAILS_SELECTOR); +} + +function shouldSkipAnimatedSummaryClick(event, summary) { + if (event.target.closest("a, input, textarea, select, label")) { + return true; + } + + if (summary.closest(".tree-section, .tree-group") && event.target.closest("button")) { + return true; + } + + return false; +} + +function ensureCollapseAnimBody(details) { + if (!isCollapsibleDetails(details) || details.querySelector(":scope > .collapse-anim-body")) { + return details?.querySelector(":scope > .collapse-anim-body") ?? null; + } + + const wrapper = document.createElement("div"); + wrapper.className = "collapse-anim-body"; + + [...details.children].forEach((child) => { + if (child.tagName !== "SUMMARY") { + wrapper.append(child); + } + }); + + if (!wrapper.childNodes.length) { + return null; + } + + details.append(wrapper); + return wrapper; +} + +function refreshCollapseAnimBodies() { + if (!isCollapseAnimationEnabled()) { + return; + } + + document.querySelectorAll(COLLAPSIBLE_DETAILS_SELECTOR).forEach((details) => { + ensureCollapseAnimBody(details); + }); +} + +function resetCollapseAnimBodyStyles(body) { + body.style.height = ""; + body.style.transition = ""; + body.style.overflow = ""; +} + +function getCollapseAnimDurationMs() { + return clampCollapseDuration(preferences.collapseDuration); +} + +function animateCollapseBody(body, fromHeight, toHeight) { + return new Promise((resolve) => { + const duration = getCollapseAnimDurationMs(); + let finished = false; + + const finish = (event) => { + if (finished) { + return; + } + + if (event && event.propertyName && event.propertyName !== "height") { + return; + } + + finished = true; + body.removeEventListener("transitionend", finish); + resolve(); + }; + + body.style.overflow = "hidden"; + body.style.height = `${fromHeight}px`; + body.offsetHeight; + body.style.transition = `height ${duration}ms cubic-bezier(0.4, 0, 0.2, 1)`; + body.style.height = `${toHeight}px`; + body.addEventListener("transitionend", finish); + window.setTimeout(finish, duration + 50); + }); +} + +async function openDetailsAnimated(details) { + if (details.dataset.collapseAnimating === "true") { + return; + } + + const body = ensureCollapseAnimBody(details); + if (!body) { + details.open = true; + return; + } + + details.dataset.collapseAnimating = "true"; + details.open = true; + resetCollapseAnimBodyStyles(body); + body.style.overflow = "hidden"; + body.style.height = "0px"; + body.offsetHeight; + await animateCollapseBody(body, 0, body.scrollHeight); + resetCollapseAnimBodyStyles(body); + delete details.dataset.collapseAnimating; +} + +async function closeDetailsAnimated(details) { + if (details.dataset.collapseAnimating === "true") { + return; + } + + const body = ensureCollapseAnimBody(details); + if (!body) { + details.open = false; + return; + } + + details.dataset.collapseAnimating = "true"; + const startHeight = body.scrollHeight; + await animateCollapseBody(body, startHeight, 0); + details.open = false; + resetCollapseAnimBodyStyles(body); + delete details.dataset.collapseAnimating; +} + +function handleAnimatedDetailsClick(event) { + if (!isCollapseAnimationEnabled()) { + return; + } + + const summary = event.target.closest("summary"); + if (!summary) { + return; + } + + const details = summary.closest("details"); + if (!isCollapsibleDetails(details) || shouldSkipAnimatedSummaryClick(event, summary)) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + if (details.open) { + closeDetailsAnimated(details); + } else { + openDetailsAnimated(details); + } +} + +function clearCollapseAnimInlineStyles() { + document.querySelectorAll(".collapse-anim-body").forEach((body) => { + resetCollapseAnimBodyStyles(body); + }); + + document.querySelectorAll(COLLAPSIBLE_DETAILS_SELECTOR).forEach((details) => { + delete details.dataset.collapseAnimating; + }); +} + // Сохраняет состояние каждого details, у которого есть data-collapse-key. function handleDetailsToggle(event) { const details = event.target; @@ -1473,6 +1845,7 @@ function applyPreferences(nextPreferences) { document.documentElement.dataset.theme = activePreferences.theme; document.documentElement.dataset.accent = activePreferences.accent; applyPanelPatternPreferences(activePreferences); + applyPatternIsolationPreference(activePreferences); document.documentElement.dataset.navLayout = activePreferences.navLayout === "top" ? "top" : "sidebar"; document.documentElement.dataset.showTree = String(isTreeVisible(activePreferences)); @@ -1498,6 +1871,12 @@ function applyPreferences(nextPreferences) { "--settings-panel-width", `${clampLayoutPreference("settingsPanelWidth", activePreferences.settingsPanelWidth)}px`, ); + document.documentElement.dataset.collapseAnimation = + activePreferences.collapseAnimation === "animated" ? "animated" : "instant"; + document.documentElement.style.setProperty( + "--collapse-duration", + `${clampCollapseDuration(activePreferences.collapseDuration)}ms`, + ); const treeColumns = getTreeColumns(activePreferences); const sidebarMaxWidth = getSidebarMaxWidth(activePreferences); @@ -1569,10 +1948,16 @@ function applyPreferences(nextPreferences) { setActiveButton(viewButtons, "view", activePreferences.view); setActiveButton(accentButtons, "accent", activePreferences.accent); syncPatternTargetPanels(activePreferences); + syncPatternIsolationControl(activePreferences); setActiveButton(treeMetaLayoutButtons, "treeMetaLayout", activePreferences.treeMetaLayout); syncLayoutLimitInputs(activePreferences); syncCatalogColumnInputs(activePreferences); syncSettingsPanelWidthInput(activePreferences); + syncCollapseAnimationControls(activePreferences); + refreshCollapseAnimBodies(); + if (!isCollapseAnimationEnabled()) { + clearCollapseAnimInlineStyles(); + } applyPanelFeatures(activePreferences); } @@ -1905,6 +2290,28 @@ function syncSettingsPanelWidthInput(activePreferences) { } } +function clampCollapseDuration(value) { + const numeric = Number(value); + const base = Number.isFinite(numeric) ? numeric : defaultPreferences.collapseDuration; + return clamp(Math.round(base), MIN_COLLAPSE_DURATION, MAX_COLLAPSE_DURATION); +} + +function syncCollapseAnimationControls(activePreferences) { + setActiveButton(collapseAnimationButtons, "collapseAnimation", activePreferences.collapseAnimation); + + if (collapseDurationInput) { + collapseDurationInput.min = String(MIN_COLLAPSE_DURATION); + collapseDurationInput.max = String(MAX_COLLAPSE_DURATION); + collapseDurationInput.step = "25"; + collapseDurationInput.value = String(clampCollapseDuration(activePreferences.collapseDuration)); + collapseDurationInput.disabled = activePreferences.collapseAnimation !== "animated"; + } + + if (collapseDurationValue) { + collapseDurationValue.textContent = `${clampCollapseDuration(activePreferences.collapseDuration)} ms`; + } +} + function syncCatalogColumnInputs(activePreferences) { catalogColumnInputs.forEach((input) => { const key = input.dataset.catalogColumns; @@ -1937,6 +2344,19 @@ function clampPanelPatternPercent(value, fallback = 50) { return clamp(Math.round(base), 0, 100); } +// ============================================================================ +// Panel pattern preferences +// ============================================================================ +// Нормализация, миграция legacy-ключей, применение к DOM и синхронизация UI ⚙. +// Ключи preferences: panelPatternNav, panelPatternNavOpacity, panelPatternNavCustomImage, … + +function clampPatternIsolation(value) { + const numeric = Number(value); + const fallback = panelDefaults.patternIsolation ?? defaultPreferences.patternIsolation; + const base = Number.isFinite(numeric) ? numeric : fallback; + return clamp(Math.round(base), MIN_PATTERN_ISOLATION, MAX_PATTERN_ISOLATION); +} + function getPanelPatternPreferenceKey(target, field) { const cap = capitalizePanelPatternTarget(target); @@ -1955,6 +2375,10 @@ function getPanelPatternPreferenceKey(target, field) { return `panelPattern${cap}GradientHeight`; case "gradientFade": return `panelPattern${cap}GradientFade`; + case "customImage": + return `panelPattern${cap}CustomImage`; + case "customSize": + return `panelPattern${cap}CustomSize`; default: return `panelPattern${cap}`; } @@ -2023,6 +2447,12 @@ function normalizePanelPatternPreferences(next) { next[`panelPattern${cap}GradientFade`], panelPatternTuningDefaults.gradientFade, ); + next[`panelPattern${cap}CustomImage`] = normalizePatternCustomImage( + next[`panelPattern${cap}CustomImage`], + ); + next[`panelPattern${cap}CustomSize`] = normalizePatternCustomSize( + next[`panelPattern${cap}CustomSize`], + ); } } @@ -2032,10 +2462,17 @@ function getPanelPatternFadeStart(heightPercent, fadePercent) { return `${Math.max(0, height - fade)}%`; } +/** + * Пишет узоры на : data-panel-pattern-{Nav|…}, opacity/height/fade, + * градиент и (для custom) inline --panel-pattern-image-{target}. + */ function applyPanelPatternPreferences(activePreferences) { for (const target of panelPatternTargetIds) { const cap = capitalizePanelPatternTarget(target); - const pattern = activePreferences[`panelPattern${cap}`] || "none"; + let pattern = activePreferences[`panelPattern${cap}`] || "none"; + const customImage = normalizePatternCustomImage(activePreferences[`panelPattern${cap}CustomImage`]); + const customSize = normalizePatternCustomSize(activePreferences[`panelPattern${cap}CustomSize`]); + const height = clampPanelPatternPercent( activePreferences[`panelPattern${cap}Height`], panelPatternTuningDefaults.height, @@ -2044,9 +2481,24 @@ function applyPanelPatternPreferences(activePreferences) { activePreferences[`panelPattern${cap}Opacity`], panelPatternTuningDefaults.opacity, ); - const patternOpacity = pattern === "none" || height === 0 ? 0 : opacityPercent / 100; + let patternOpacity = pattern === "none" || height === 0 ? 0 : opacityPercent / 100; + + if (pattern === "custom" && !customImage) { + patternOpacity = 0; + } document.documentElement.dataset[`panelPattern${cap}`] = pattern; + document.documentElement.style.removeProperty(`--panel-pattern-image-${target}`); + document.documentElement.style.removeProperty(`--panel-pattern-size-${target}`); + + if (pattern === "custom" && customImage) { + document.documentElement.style.setProperty( + `--panel-pattern-image-${target}`, + `url("${customImage.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}")`, + ); + document.documentElement.style.setProperty(`--panel-pattern-size-${target}`, customSize); + } + document.documentElement.style.setProperty(`--panel-pattern-${target}-opacity`, String(patternOpacity)); document.documentElement.style.setProperty(`--panel-pattern-${target}-height`, `${height}%`); document.documentElement.style.setProperty( @@ -2079,6 +2531,29 @@ function applyPanelPatternPreferences(activePreferences) { } } +function applyPatternIsolationPreference(activePreferences) { + // patternIsolation 0–100 → blur и непрозрачность подложки на section/group/card + const isolation = clampPatternIsolation(activePreferences.patternIsolation); + document.documentElement.style.setProperty("--panel-layer-blur", `${Math.round(isolation * 0.18)}px`); + document.documentElement.style.setProperty( + "--panel-layer-opacity", + String(0.04 + (isolation / 100) * 0.5), + ); +} + +function syncPatternIsolationControl(activePreferences) { + const isolation = clampPatternIsolation(activePreferences.patternIsolation); + + if (patternIsolationInput) { + patternIsolationInput.value = String(isolation); + patternIsolationInput.disabled = !panelFeatures.allowPanelPatternChange; + } + + if (patternIsolationValue) { + patternIsolationValue.textContent = `${isolation}%`; + } +} + function syncPatternTargetPanels(activePreferences) { if (!patternTargetsPanel) { return; @@ -2089,6 +2564,7 @@ function syncPatternTargetPanels(activePreferences) { for (const target of panelPatternTargetIds) { const pattern = activePreferences[getPanelPatternPreferenceKey(target, "pattern")]; + const customImage = activePreferences[getPanelPatternPreferenceKey(target, "customImage")]; patternTargetsPanel .querySelectorAll(`[data-panel-pattern-target="${target}"][data-panel-pattern]`) .forEach((button) => { @@ -2096,8 +2572,35 @@ function syncPatternTargetPanels(activePreferences) { button.classList.toggle("is-active", isActive); button.setAttribute("aria-pressed", String(isActive)); button.disabled = disabled; + + if (button.dataset.panelPattern === "custom" && customImage) { + button.style.setProperty("--pattern-swatch-preview", `url("${customImage.replace(/"/g, '\\"')}")`); + button.classList.add("has-custom-preview"); + } else { + button.style.removeProperty("--pattern-swatch-preview"); + button.classList.remove("has-custom-preview"); + } }); + const customPanel = patternTargetsPanel.querySelector(`[data-pattern-custom-panel="${target}"]`); + + if (customPanel) { + customPanel.hidden = pattern !== "custom"; + + const imageInput = customPanel.querySelector('[data-pattern-custom-field="image"]'); + const sizeInput = customPanel.querySelector('[data-pattern-custom-field="size"]'); + + if (imageInput) { + imageInput.value = activePreferences[getPanelPatternPreferenceKey(target, "customImage")] || ""; + imageInput.disabled = disabled; + } + + if (sizeInput) { + sizeInput.value = activePreferences[getPanelPatternPreferenceKey(target, "customSize")]; + sizeInput.disabled = disabled; + } + } + for (const field of tuningFields) { const key = getPanelPatternPreferenceKey(target, field); const input = patternTargetsPanel.querySelector( @@ -2119,6 +2622,124 @@ function syncPatternTargetPanels(activePreferences) { } } +// --- Custom pattern URL / tile size --- + +/** Принимает data:image/, http(s), относительные пути и *.svg|png|…; отклоняет javascript:. */ +function normalizePatternCustomImage(value) { + const trimmed = String(value ?? "").trim(); + + if (!trimmed) { + return ""; + } + + if (/^javascript:/i.test(trimmed)) { + return ""; + } + + if (/^(?:data:image\/|https?:\/\/|\.\/|\.\.\/|\/|[a-z0-9._-]+\.(?:svg|png|webp|gif|jpg|jpeg)(?:\?|$))/i.test(trimmed)) { + return trimmed; + } + + if (/^[a-z0-9._/-]+\.(?:svg|png|webp|gif|jpg|jpeg)$/i.test(trimmed)) { + return trimmed; + } + + return ""; +} + +function normalizePatternCustomSize(value) { + const trimmed = String(value ?? "").trim(); + const fallback = defaultPanelPatternCustomSize; + + if (!trimmed) { + return fallback; + } + + if (/^\d+(?:\.\d+)?px\s+\d+(?:\.\d+)?px$/.test(trimmed)) { + return trimmed; + } + + const parts = trimmed.split(/[\s,]+/).filter(Boolean); + + if (parts.length === 2 && parts.every((part) => /^\d+(?:\.\d+)?(?:px)?$/.test(part))) { + const width = parts[0].endsWith("px") ? parts[0] : `${parts[0]}px`; + const height = parts[1].endsWith("px") ? parts[1] : `${parts[1]}px`; + return `${width} ${height}`; + } + + return fallback; +} + +// --- Pattern UI in settings popup (dynamic swatches + custom panel) --- + +/** Вызывается из bootstrap(): кнопки пресетов и поля «Свой узор» для каждой цели. */ +function setupPatternControls() { + renderPatternSwatches(); + ensurePatternCustomPanels(); +} + +function renderPatternSwatches() { + // Контейнеры [data-pattern-swatches] в index.html изначально пустые — кнопки создаются здесь + if (!patternTargetsPanel) { + return; + } + + patternTargetsPanel.querySelectorAll("[data-pattern-swatches]").forEach((container) => { + const target = container.dataset.patternSwatches; + + if (!target || container.dataset.rendered === "true") { + return; + } + + container.dataset.rendered = "true"; + container.replaceChildren( + ...panelPatternCatalog.map(({ id, label }) => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "pattern-swatch"; + button.dataset.panelPatternTarget = target; + button.dataset.panelPattern = id; + button.setAttribute("aria-pressed", "false"); + button.innerHTML = `${label}`; + return button; + }), + ); + }); +} + +function ensurePatternCustomPanels() { + if (!patternTargetsPanel) { + return; + } + + for (const target of panelPatternTargetIds) { + const panel = patternTargetsPanel.querySelector(`[data-pattern-target="${target}"]`); + + if (!panel || panel.querySelector(`[data-pattern-custom-panel="${target}"]`)) { + continue; + } + + const customPanel = document.createElement("div"); + customPanel.className = "pattern-custom-panel"; + customPanel.dataset.patternCustomPanel = target; + customPanel.hidden = true; + customPanel.innerHTML = ` +

Вставьте URL или путь к SVG/PNG ниже. Пока поле пустое, узор не рисуется.

+ + + `; + + const tuning = panel.querySelector(".pattern-tuning"); + panel.querySelector(".settings-section-body")?.insertBefore(customPanel, tuning ?? null); + } +} + function clampCatalogColumn(key, value) { const limits = getPanelLimitRange(key); const fallback = panelDefaults[key] ?? defaultPreferences[key]; @@ -2201,6 +2822,92 @@ function saveCollapseState() { localStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(collapseState)); } +// ============================================================================ +// Settings import, export and reset +// ============================================================================ +// Кнопки внизу popup ⚙: data-settings-reset | export | import. +// Формат экспорта: { version, exportedAt, preferences }. Импорт принимает и «голый» объект. + +/** Применяет набор preferences, обновляет дерево/каталог и при необходимости сохраняет в localStorage. */ +function applyStoredPreferences(rawPreferences, { persist = true } = {}) { + preferences = enforceFeatureLocks(normalizePreferences({ ...panelDefaults, ...rawPreferences })); + applyPreferences(preferences); + + if (isTreeVisible(preferences)) { + renderNavigationTree(); + } else if (navTree) { + navTree.replaceChildren(); + } + + renderCatalog(); + + if (persist) { + savePreferences(preferences); + } +} + +// Сбрасывает пользовательские настройки к panelDefaults из settings.json. +function resetStoredPreferences() { + if (!window.confirm("Сбросить все настройки к значениям по умолчанию?")) { + return; + } + + localStorage.removeItem(STORAGE_KEY); + applyStoredPreferences({ ...panelDefaults }, { persist: false }); +} + +// Скачивает текущие preferences в JSON-файл. +function exportStoredPreferences() { + const payload = { + version: 1, + exportedAt: new Date().toISOString(), + preferences: { ...preferences }, + }; + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = "ecosystem-panel-preferences.json"; + anchor.click(); + URL.revokeObjectURL(url); +} + +// Читает JSON-файл и возвращает объект preferences. +function readImportedPreferencesFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = () => { + try { + const parsed = JSON.parse(String(reader.result)); + resolve(parseImportedPreferencesPayload(parsed)); + } catch (error) { + reject(error instanceof Error ? error : new Error("Не удалось прочитать файл настроек.")); + } + }; + + reader.onerror = () => { + reject(new Error("Не удалось прочитать файл настроек.")); + }; + + reader.readAsText(file); + }); +} + +// Поддерживает как обёртку { preferences }, так и «голый» объект настроек. +function parseImportedPreferencesPayload(raw) { + const source = + raw && typeof raw === "object" && !Array.isArray(raw) && raw.preferences && typeof raw.preferences === "object" + ? raw.preferences + : raw; + + if (!source || typeof source !== "object" || Array.isArray(source)) { + throw new Error("Неверный формат файла настроек."); + } + + return source; +} + // Закрывает popup-настройки и синхронизирует aria-expanded. function closeSettingsPanel() { settingsPanel.classList.remove("is-open"); diff --git a/examples/shared/styles.css b/examples/shared/styles.css index e476596..6f58d66 100644 --- a/examples/shared/styles.css +++ b/examples/shared/styles.css @@ -1,3 +1,23 @@ +/* ========================================================================== + ecosystem-panel / styles.css + -------------------------------------------------------------------------- + ОГЛАВЛЕНИЕ (поиск по «==» или номеру секции): + 01 Theme Tokens — цвета, layout, pattern tiles, pattern mapping, accents + 02 Base Reset And Page Background + 03 Application Layout — sidebar / top bar / resizers + 04 Catalog Area + 05 Sidebar Panel — hero, patterns ::before/::after, layer isolation + 06 Settings Popup — sections, pattern swatches, actions footer + 07 Search Field And Control Inputs + 08 File-Tree Navigation + 09 Catalog Sections And Groups + 10 Navigation Cards + 11 Catalog View Modes + 12 Responsive Rules + 13 Touch And Reduced Motion + 14 Print + ========================================================================== */ + /* ========================================================================== 01. Theme Tokens -------------------------------------------------------------------------- @@ -31,7 +51,9 @@ --catalog-list-columns: 1; --catalog-compact-cards-columns: 5; --catalog-compact-list-columns: 3; + /* --- Pattern: цвет штриха/точки в CSS- и SVG-узорах --- */ --pattern-color: rgba(255, 255, 255, 0.16); + /* --- Pattern tiles: повторяющаяся текстура (image + background-size) --- */ --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); @@ -70,7 +92,23 @@ transparent 0 ); --pattern-tile-mesh-size: 20px 20px; + --pattern-tile-hex-sm-image: url("data:image/svg+xml,%3Csvg width='48' height='42' viewBox='0 0 48 42' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1'%3E%3Cpath d='M12 1 23 7v12L12 26 1 19V7L12 1Z'/%3E%3Cpath d='M36 16 47 22v12L36 41 25 34V22l11-6Z'/%3E%3C/g%3E%3C/svg%3E"); + --pattern-tile-hex-sm-size: 48px 42px; + --pattern-tile-dots-sm-image: radial-gradient(circle at center, var(--pattern-color) 0.6px, transparent 0.6px); + --pattern-tile-dots-sm-size: 10px 10px; + --pattern-tile-dots-lg-image: radial-gradient(circle at center, var(--pattern-color) 1.35px, transparent 1.35px); + --pattern-tile-dots-lg-size: 28px 28px; + --pattern-tile-tri-sm-image: url("data:image/svg+xml,%3Csvg width='20' height='18' viewBox='0 0 20 18' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='white' stroke-opacity='.2' stroke-width='1' d='M10 1 19 17 H1 Z'/%3E%3C/svg%3E"); + --pattern-tile-tri-sm-size: 20px 18px; + --pattern-tile-tri-lg-image: url("data:image/svg+xml,%3Csvg width='40' height='35' viewBox='0 0 40 35' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.2' d='M20 2 38 33 H2 Z'/%3E%3C/svg%3E"); + --pattern-tile-tri-lg-size: 40px 35px; + --pattern-tile-diamond-image: url("data:image/svg+xml,%3Csvg width='22' height='22' viewBox='0 0 22 22' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='white' stroke-opacity='.18' stroke-width='1' d='M11 1 21 11 11 21 1 11 Z'/%3E%3C/svg%3E"); + --pattern-tile-diamond-size: 22px 22px; --settings-panel-width: 460px; + /* --- Pattern layer isolation (preference patternIsolation → JS) --- */ + --panel-layer-blur: 8px; + --panel-layer-opacity: 0.27; + --panel-layer-fill: rgba(15, 23, 42, var(--panel-layer-opacity)); } /* Светлая тема переопределяет только токены, структура стилей остается общей. */ @@ -87,37 +125,71 @@ html[data-theme="light"] { --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-hex-sm-image: url("data:image/svg+xml,%3Csvg width='48' height='42' viewBox='0 0 48 42' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1'%3E%3Cpath d='M12 1 23 7v12L12 26 1 19V7L12 1Z'/%3E%3Cpath d='M36 16 47 22v12L36 41 25 34V22l11-6Z'/%3E%3C/g%3E%3C/svg%3E"); + --pattern-tile-tri-sm-image: url("data:image/svg+xml,%3Csvg width='20' height='18' viewBox='0 0 20 18' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='%23101827' stroke-opacity='.16' stroke-width='1' d='M10 1 19 17 H1 Z'/%3E%3C/svg%3E"); + --pattern-tile-tri-lg-image: url("data:image/svg+xml,%3Csvg width='40' height='35' viewBox='0 0 40 35' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.2' d='M20 2 38 33 H2 Z'/%3E%3C/svg%3E"); + --pattern-tile-diamond-image: url("data:image/svg+xml,%3Csvg width='22' height='22' viewBox='0 0 22 22' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1' d='M11 1 21 11 11 21 1 11 Z'/%3E%3C/svg%3E"); --pattern-tile-mesh-image: radial-gradient( circle at 1px 1px, rgba(var(--accent-rgb), 0.16) 1px, transparent 0 ); + --panel-layer-fill: rgba(255, 255, 255, var(--panel-layer-opacity)); } +/* + * Mapping встроенных пресетов: html[data-panel-pattern-{target}="{id}"] + * → --panel-pattern-image-{target} и --panel-pattern-size-{target}. + * Цели: nav | section | group | item. Пресет custom задаётся inline из script.js. + */ 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-nav="hex-sm"] { --panel-pattern-image-nav: var(--pattern-tile-hex-sm-image); --panel-pattern-size-nav: var(--pattern-tile-hex-sm-size); } +html[data-panel-pattern-nav="dots-sm"] { --panel-pattern-image-nav: var(--pattern-tile-dots-sm-image); --panel-pattern-size-nav: var(--pattern-tile-dots-sm-size); } +html[data-panel-pattern-nav="dots-lg"] { --panel-pattern-image-nav: var(--pattern-tile-dots-lg-image); --panel-pattern-size-nav: var(--pattern-tile-dots-lg-size); } +html[data-panel-pattern-nav="tri-sm"] { --panel-pattern-image-nav: var(--pattern-tile-tri-sm-image); --panel-pattern-size-nav: var(--pattern-tile-tri-sm-size); } +html[data-panel-pattern-nav="tri-lg"] { --panel-pattern-image-nav: var(--pattern-tile-tri-lg-image); --panel-pattern-size-nav: var(--pattern-tile-tri-lg-size); } +html[data-panel-pattern-nav="diamond"] { --panel-pattern-image-nav: var(--pattern-tile-diamond-image); --panel-pattern-size-nav: var(--pattern-tile-diamond-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-section="hex-sm"] { --panel-pattern-image-section: var(--pattern-tile-hex-sm-image); --panel-pattern-size-section: var(--pattern-tile-hex-sm-size); } +html[data-panel-pattern-section="dots-sm"] { --panel-pattern-image-section: var(--pattern-tile-dots-sm-image); --panel-pattern-size-section: var(--pattern-tile-dots-sm-size); } +html[data-panel-pattern-section="dots-lg"] { --panel-pattern-image-section: var(--pattern-tile-dots-lg-image); --panel-pattern-size-section: var(--pattern-tile-dots-lg-size); } +html[data-panel-pattern-section="tri-sm"] { --panel-pattern-image-section: var(--pattern-tile-tri-sm-image); --panel-pattern-size-section: var(--pattern-tile-tri-sm-size); } +html[data-panel-pattern-section="tri-lg"] { --panel-pattern-image-section: var(--pattern-tile-tri-lg-image); --panel-pattern-size-section: var(--pattern-tile-tri-lg-size); } +html[data-panel-pattern-section="diamond"] { --panel-pattern-image-section: var(--pattern-tile-diamond-image); --panel-pattern-size-section: var(--pattern-tile-diamond-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-group="hex-sm"] { --panel-pattern-image-group: var(--pattern-tile-hex-sm-image); --panel-pattern-size-group: var(--pattern-tile-hex-sm-size); } +html[data-panel-pattern-group="dots-sm"] { --panel-pattern-image-group: var(--pattern-tile-dots-sm-image); --panel-pattern-size-group: var(--pattern-tile-dots-sm-size); } +html[data-panel-pattern-group="dots-lg"] { --panel-pattern-image-group: var(--pattern-tile-dots-lg-image); --panel-pattern-size-group: var(--pattern-tile-dots-lg-size); } +html[data-panel-pattern-group="tri-sm"] { --panel-pattern-image-group: var(--pattern-tile-tri-sm-image); --panel-pattern-size-group: var(--pattern-tile-tri-sm-size); } +html[data-panel-pattern-group="tri-lg"] { --panel-pattern-image-group: var(--pattern-tile-tri-lg-image); --panel-pattern-size-group: var(--pattern-tile-tri-lg-size); } +html[data-panel-pattern-group="diamond"] { --panel-pattern-image-group: var(--pattern-tile-diamond-image); --panel-pattern-size-group: var(--pattern-tile-diamond-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); } +html[data-panel-pattern-item="hex-sm"] { --panel-pattern-image-item: var(--pattern-tile-hex-sm-image); --panel-pattern-size-item: var(--pattern-tile-hex-sm-size); } +html[data-panel-pattern-item="dots-sm"] { --panel-pattern-image-item: var(--pattern-tile-dots-sm-image); --panel-pattern-size-item: var(--pattern-tile-dots-sm-size); } +html[data-panel-pattern-item="dots-lg"] { --panel-pattern-image-item: var(--pattern-tile-dots-lg-image); --panel-pattern-size-item: var(--pattern-tile-dots-lg-size); } +html[data-panel-pattern-item="tri-sm"] { --panel-pattern-image-item: var(--pattern-tile-tri-sm-image); --panel-pattern-size-item: var(--pattern-tile-tri-sm-size); } +html[data-panel-pattern-item="tri-lg"] { --panel-pattern-image-item: var(--pattern-tile-tri-lg-image); --panel-pattern-size-item: var(--pattern-tile-tri-lg-size); } +html[data-panel-pattern-item="diamond"] { --panel-pattern-image-item: var(--pattern-tile-diamond-image); --panel-pattern-size-item: var(--pattern-tile-diamond-size); } /* Наборы акцентов. --accent-rgb нужен для прозрачных градиентов через rgba(). */ html[data-accent="cyan"] { @@ -400,6 +472,30 @@ body.is-resizing-top-bar { overflow: hidden; } +/* + * Разделение слоёв узоров: полупрозрачная подложка + backdrop-filter. + * JS: applyPatternIsolationPreference() → --panel-layer-blur / --panel-layer-opacity. + * Сильнее всего влияет на .catalog-section (доп. blur), затем group и .nav-card. + */ +.catalog-section, +.catalog-group, +.nav-card { + background: + linear-gradient(var(--panel-layer-fill), var(--panel-layer-fill)), + var(--panel); +} + +.catalog-section { + backdrop-filter: blur(calc(14px + var(--panel-layer-blur))); + -webkit-backdrop-filter: blur(calc(14px + var(--panel-layer-blur))); +} + +.catalog-group, +.nav-card { + backdrop-filter: blur(var(--panel-layer-blur)); + -webkit-backdrop-filter: blur(var(--panel-layer-blur)); +} + .hero-content { max-height: calc(100vh - 40px); min-height: 0; @@ -417,7 +513,13 @@ body.is-resizing-top-bar { overflow: auto; } -/* Акцентный градиент под узором — отдельно для каждого типа блоков. */ +/* + * Декоративные слои узора на блоках каталога и навигации. + * ::before — акцентный градиент (кроме .nav-card). + * ::after — повторяющаяся текстура; маска по высоте/переходу из CSS-переменных JS. + * Контент поверх узора: z-index: 1 у summary / .collapse-anim-body / детей .hero-content. + */ +/* Акcentный градиент под узором — отдельно для каждого типа блоков. */ html:not([data-panel-pattern-nav="none"]) .hero-content::before { content: ""; position: absolute; @@ -519,8 +621,10 @@ html:not([data-panel-pattern-item="none"]) .nav-card::after { .catalog-section > summary, .catalog-section > .section-groups, +.catalog-section > .collapse-anim-body, .catalog-group > summary, -.catalog-group > .items-grid { +.catalog-group > .items-grid, +.catalog-group > .collapse-anim-body { position: relative; z-index: 1; } @@ -732,6 +836,51 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta { font-size: 0.78rem; } +/* Footer popup ⚙: сброс / экспорт / импорт preferences (script.js → settings-actions). */ +.settings-actions { + margin-top: 4px; + padding-top: 12px; + border-top: 1px solid var(--border); + display: grid; + gap: 10px; +} + +.settings-actions-buttons { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.settings-action-button { + flex: 1 1 calc(33.333% - 6px); + min-width: 0; + min-height: 36px; + padding: 0 10px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--surface); + color: var(--text); + font-size: 0.84rem; + font-weight: 800; + cursor: pointer; + transition: transform 160ms ease, background 160ms ease, border-color 160ms ease; +} + +.settings-action-button:hover { + transform: translateY(-1px); + border-color: rgba(var(--accent-rgb), 0.5); + background: rgba(var(--accent-rgb), 0.12); +} + +.settings-action-button[data-settings-reset] { + color: #ef4444; +} + +.settings-action-button[data-settings-reset]:hover { + border-color: rgba(239, 68, 68, 0.45); + background: rgba(239, 68, 68, 0.12); +} + .catalog-columns-group { display: grid; gap: 8px; @@ -762,8 +911,21 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta { margin: 0; } -.settings-section-collapsible:not([open]) > .settings-section-summary .collapse-indicator { - transform: rotate(-90deg); +.settings-section-summary:hover .collapse-indicator { + border-color: rgba(var(--accent-rgb), 0.42); + background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.22), rgba(255, 255, 255, 0.05)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 8px 20px rgba(var(--accent-rgb), 0.12); +} + +html[data-collapse-animation="instant"] [data-collapse-duration-row] { + display: none; +} + +html[data-collapse-animation="animated"] .collapse-anim-body { + min-height: 0; + overflow: hidden; } .settings-section-body { @@ -1095,6 +1257,12 @@ html[data-theme="light"] .accent-swatch-custom { align-items: stretch; } +/* + * Блок узоров в ⚙ → Оформление. + * .pattern-swatches — контейнер; кнопки генерирует renderPatternSwatches() в script.js. + * .pattern-custom-panel — поля «Свой» (URL + размер плитки), создаёт ensurePatternCustomPanels(). + * .pattern-isolation-row — слайдер «Разделение слоёв». + */ .pattern-targets-panel { display: grid; gap: 10px; @@ -1109,13 +1277,13 @@ html[data-theme="light"] .accent-swatch-custom { .pattern-swatches { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; width: 100%; } .pattern-swatch { - min-height: 54px; + min-height: 48px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); @@ -1151,6 +1319,53 @@ 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="hex-sm"]::before { + background-image: var(--pattern-tile-hex-sm-image); + background-size: 24px 21px; +} + +.pattern-swatch[data-panel-pattern="dots-sm"]::before { + background-image: var(--pattern-tile-dots-sm-image); + background-size: 8px 8px; +} + +.pattern-swatch[data-panel-pattern="dots-lg"]::before { + background-image: var(--pattern-tile-dots-lg-image); + background-size: 18px 18px; +} + +.pattern-swatch[data-panel-pattern="tri-sm"]::before { + background-image: var(--pattern-tile-tri-sm-image); + background-size: 16px 14px; +} + +.pattern-swatch[data-panel-pattern="tri-lg"]::before { + background-image: var(--pattern-tile-tri-lg-image); + background-size: 28px 24px; +} + +.pattern-swatch[data-panel-pattern="diamond"]::before { + background-image: var(--pattern-tile-diamond-image); + background-size: 16px 16px; +} + +.pattern-swatch[data-panel-pattern="custom"]::before { + background: + linear-gradient(135deg, rgba(var(--accent-rgb), 0.18), transparent 62%), + repeating-linear-gradient( + -45deg, + transparent, + transparent 5px, + rgba(var(--accent-rgb), 0.12) 5px, + rgba(var(--accent-rgb), 0.12) 6px + ); +} + +.pattern-swatch[data-panel-pattern="custom"].has-custom-preview::before { + background-image: var(--pattern-swatch-preview); + background-size: 18px 18px; +} + .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; @@ -1251,6 +1466,53 @@ html[data-theme="light"] .pattern-swatch-label { grid-row: 2; } +.pattern-isolation-row { + grid-template-columns: minmax(120px, 1fr) minmax(0, 1.4fr) auto; +} + +.pattern-custom-panel { + display: grid; + gap: 8px; + padding: 10px; + border: 1px solid var(--border); + border-radius: 18px; + background: var(--surface); +} + +.pattern-custom-panel[hidden] { + display: none; +} + +.pattern-custom-panel input[type="url"], +.pattern-custom-panel input[type="text"] { + width: 100%; + min-height: 34px; + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--panel); + color: var(--text); + font: inherit; + font-size: 0.82rem; +} + +.pattern-custom-image-row { + grid-template-columns: minmax(72px, 0.8fr) minmax(0, 1.6fr); +} + +.pattern-custom-image-row input[type="url"], +.pattern-custom-image-row input[type="text"] { + width: 100%; + min-height: 34px; + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--panel); + color: var(--text); + font: inherit; + font-size: 0.82rem; +} + /* Панель настройки своего градиента появляется при data-accent="custom". */ .accent-custom-panel { display: grid; @@ -1354,8 +1616,8 @@ html[data-theme="light"] .pattern-swatch-label { .tree-section > summary, .tree-group > summary { display: grid; - grid-template-columns: 16px minmax(0, 1fr); - gap: 3px; + grid-template-columns: 22px minmax(0, 1fr); + gap: 6px; align-items: center; list-style: none; cursor: pointer; @@ -1372,17 +1634,38 @@ html[data-theme="light"] .pattern-swatch-label { /* Caret поворачивается через состояние [open] у details. */ .tree-caret { - width: 16px; - height: 24px; + width: 22px; + height: 22px; display: grid; place-items: center; + border-radius: 8px; color: var(--muted); + background: rgba(var(--accent-rgb), 0.06); + border: 1px solid transparent; + transition: + background 140ms ease, + border-color 140ms ease, + color 140ms ease; +} + +.tree-section > summary:hover .tree-caret, +.tree-group > summary:hover .tree-caret { + color: var(--accent); + border-color: rgba(var(--accent-rgb), 0.18); + background: rgba(var(--accent-rgb), 0.12); } .tree-caret::before { - content: "▾"; - font-size: 0.72rem; - transition: transform 140ms ease; + content: ""; + display: block; + width: 13px; + height: 13px; + background: currentColor; + mask: url("data:image/svg+xml,%3Csvg width='13' height='13' viewBox='0 0 13 13' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.5 5 6.5 8 9.5 5' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; + -webkit-mask: url("data:image/svg+xml,%3Csvg width='13' height='13' viewBox='0 0 13 13' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.5 5 6.5 8 9.5 5' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; + transform: rotate(0deg); + transform-origin: center; + transition: transform var(--collapse-duration, 240ms) cubic-bezier(0.4, 0, 0.2, 1); } .tree-section:not([open]) > summary .tree-caret::before, @@ -1635,23 +1918,63 @@ html:not([data-tree-columns="1"]) .tree-node-icon { font-weight: 800; } -/* Стрелка сворачивания поворачивается через [open]. */ +/* Стрелка сворачивания — аккуратный chevron в стеклянной кнопке. */ .collapse-indicator { - width: 34px; - height: 34px; + position: relative; + flex-shrink: 0; + width: 36px; + height: 36px; display: grid; place-items: center; - border: 1px solid var(--border); - border-radius: 999px; - background: var(--surface); + border-radius: 12px; + border: 1px solid rgba(var(--accent-rgb), 0.22); + background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.14), rgba(255, 255, 255, 0.03)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); color: var(--accent); - font-weight: 900; - transition: transform 160ms ease; + font-size: 0; + transition: + border-color 160ms ease, + background 160ms ease, + box-shadow 160ms ease, + transform 160ms ease; +} + +.collapse-indicator::before { + content: ""; + display: block; + width: 16px; + height: 16px; + background: currentColor; + mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.25 6.25 8 10l3.75-3.75' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; + -webkit-mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.25 6.25 8 10l3.75-3.75' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; + transform: rotate(0deg); + transform-origin: center; + transition: transform var(--collapse-duration, 240ms) cubic-bezier(0.4, 0, 0.2, 1); +} + +.catalog-section:not([open]) > .section-head .collapse-indicator::before, +.catalog-group:not([open]) > .group-head .collapse-indicator::before, +.settings-section-collapsible:not([open]) > .settings-section-summary .collapse-indicator::before { + transform: rotate(-90deg); +} + +.section-head:hover .collapse-indicator, +.group-head:hover .collapse-indicator { + border-color: rgba(var(--accent-rgb), 0.42); + background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.22), rgba(255, 255, 255, 0.05)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 8px 20px rgba(var(--accent-rgb), 0.12); +} + +html[data-theme="light"] .collapse-indicator { + background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.1), rgba(255, 255, 255, 0.72)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85); } .catalog-section:not([open]) > .section-head .collapse-indicator, .catalog-group:not([open]) > .group-head .collapse-indicator { - transform: rotate(-90deg); + transform: none; } /* Подразделы внутри секции. */ @@ -1682,7 +2005,9 @@ html:not([data-tree-columns="1"]) .tree-node-icon { position: relative; border: 1px solid var(--border); border-radius: 18px; - background: rgba(255, 255, 255, 0.035); + background: + linear-gradient(var(--panel-layer-fill), var(--panel-layer-fill)), + rgba(255, 255, 255, 0.035); overflow: hidden; scroll-margin-top: 18px; } @@ -1741,7 +2066,9 @@ html:not([data-tree-columns="1"]) .tree-node-icon { padding: 14px; border: 1px solid var(--border); border-radius: 18px; - background: var(--panel); + background: + linear-gradient(var(--panel-layer-fill), var(--panel-layer-fill)), + var(--panel); color: var(--text); text-decoration: none; overflow: hidden; @@ -1785,7 +2112,9 @@ html:not([data-tree-columns="1"]) .tree-node-icon { .nav-card.is-hovered, .nav-card:focus-visible { border-color: rgba(var(--accent-rgb), 0.52); - background: var(--panel-strong); + background: + linear-gradient(var(--panel-layer-fill), var(--panel-layer-fill)), + var(--panel-strong); box-shadow: 0 22px 70px rgba(var(--accent-rgb), 0.16); outline: none; } @@ -1853,18 +2182,37 @@ html:not([data-tree-columns="1"]) .tree-node-icon { height: 36px; display: grid; place-items: center; - border-radius: 999px; - background: rgba(var(--accent-rgb), 0.14); + border-radius: 12px; + border: 1px solid rgba(var(--accent-rgb), 0.18); + background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.12), rgba(255, 255, 255, 0.03)); color: var(--accent); - font-size: 1.15rem; - transition: transform 180ms ease, background 180ms ease, color 180ms ease; + font-size: 0; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); + transition: + transform 180ms ease, + background 180ms ease, + color 180ms ease, + border-color 180ms ease, + box-shadow 180ms ease; +} + +.arrow::before { + content: ""; + display: block; + width: 16px; + height: 16px; + background: currentColor; + mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 4 10 8 6 12' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; + -webkit-mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 4 10 8 6 12' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; } .nav-card.is-hovered .arrow, .nav-card:focus-visible .arrow { - transform: translateX(4px); + transform: translateX(3px); + border-color: rgba(var(--accent-rgb), 0.35); background: linear-gradient(var(--accent-angle), var(--accent), var(--accent-2)); color: white; + box-shadow: 0 10px 24px rgba(var(--accent-rgb), 0.24); } /* Декоративный shine слой, положение которого обновляется pointermove. */ diff --git a/index.html b/index.html index 58c8651..6cafb8c 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,19 @@ + @@ -82,13 +97,14 @@
@@ -277,6 +293,18 @@ + +
+ Раскрытие списков + + +
+

Разделы каталога, дерево навигации и блоки в настройках.

+
@@ -323,6 +351,7 @@
+

Оформление

@@ -374,6 +403,11 @@
+

Узор и градиент настраиваются отдельно для каждого типа блоков.

@@ -383,15 +417,7 @@

Боковое меню и дерево

-
- - - - - - - -
+
@@ -410,15 +436,7 @@

Карточки верхнего уровня каталога

-
- - - - - - - -
+
@@ -437,15 +455,7 @@

Группы внутри раздела

-
- - - - - - - -
+
@@ -464,15 +474,7 @@

Карточки сервисов — только узор, без акцентного градиента

-
- - - - - - - -
+
@@ -482,6 +484,16 @@
+ + +

+ Подложка размывает узоры нижних уровней под разделами, подразделами и карточками. +

+ + + +
+

+ Сброс, экспорт и импорт настроек интерфейса на этом устройстве. +

+
+ + + +
+ +
diff --git a/script.js b/script.js index 170f405..41eab2c 100644 --- a/script.js +++ b/script.js @@ -1,3 +1,29 @@ +// ecosystem-panel / script.js +// +// ОГЛАВЛЕНИЕ — ищите секции по строке «// =====» (Ctrl+F): +// Persistent storage keys +// Panel configuration (settings.json) +// Panel pattern catalog and defaults +// Default UI preferences +// Runtime state and DOM references +// Initial render +// Event bindings +// Panel config loading +// Data helpers +// Catalog rendering +// Search matching and highlighting +// Sidebar tree rendering +// Focusing from tree to content +// Collapse state handling +// Card pointer interaction +// Preferences application +// Generic helpers +// Panel pattern preferences (normalize, apply, custom, UI) +// localStorage helpers (read/save, import/export/reset) +// +// Связанные файлы: index.html (разметка ⚙), styles.css (узоры, темы, слои). +// Документация: README.md → «Узоры панелей», «Разделение слоёв», «Экспорт настроек». + // ============================================================================ // Persistent storage keys // ============================================================================ @@ -65,16 +91,73 @@ const defaultServiceMeta = { let serviceMeta = { ...defaultServiceMeta }; -const availablePanelPatterns = ["none", "hex", "dots", "grid", "diagonal", "cross", "mesh"]; +// ============================================================================ +// Panel pattern catalog and defaults +// ============================================================================ +// Узоры накладываются на четыре типа блоков интерфейса (см. panelPatternTargetIds). +// +// Как это работает: +// 1. panelPatternCatalog — пресеты для кнопок в ⚙ (генерируются в renderPatternSwatches). +// 2. preferences.panelPattern{Nav|Section|Group|Item} — выбранный id пресета или "custom". +// 3. applyPanelPatternPreferences() выставляет data-panel-pattern-* на и CSS-переменные. +// 4. Встроенные пресеты: styles.css → --pattern-tile-* + html[data-panel-pattern-*="hex"] … +// 5. custom: panelPattern*CustomImage + CustomSize → inline --panel-pattern-image-{target}. +// +// См. README.md → «Узоры панелей», «Свой узор», «Разделение слоёв». + +/** Допустимые id узоров (валидация в normalizePanelPatternPreferences). */ +const availablePanelPatterns = [ + "none", + "hex", + "hex-sm", + "dots", + "dots-sm", + "dots-lg", + "grid", + "diagonal", + "cross", + "mesh", + "tri-sm", + "tri-lg", + "diamond", + "custom", +]; + +/** Подписи пресетов в UI настроек. Порядок = порядок кнопок в сетке pattern-swatches. */ +const panelPatternCatalog = [ + { id: "none", label: "Нет" }, + { id: "hex", label: "Соты" }, + { id: "hex-sm", label: "Соты м." }, + { id: "dots", label: "Точки" }, + { id: "dots-sm", label: "Точки м." }, + { id: "dots-lg", label: "Точки кр." }, + { id: "grid", label: "Сетка" }, + { id: "diagonal", label: "Диаг." }, + { id: "cross", label: "Клетка" }, + { id: "mesh", label: "Mesh" }, + { id: "tri-sm", label: "△ мел." }, + { id: "tri-lg", label: "△ кр." }, + { id: "diamond", label: "Ромбы" }, + { id: "custom", label: "Свой" }, +]; + +/** Размер плитки по умолчанию для custom-узора (background-size). */ +const defaultPanelPatternCustomSize = "32px 32px"; + +/** Цели узора: nav → .hero-content, section → .catalog-section, group → .catalog-group, item → .nav-card */ const panelPatternTargetIds = ["nav", "section", "group", "item"]; + +/** Слайдеры узора/градиента по умолчанию (%, 0–100). Переопределяются settings.json → defaults. */ const panelPatternTuningDefaults = { - opacity: 70, - height: 45, - fade: 10, - gradientOpacity: 80, - gradientHeight: 40, - gradientFade: 8, + opacity: 58, + height: 38, + fade: 18, + gradientOpacity: 62, + gradientHeight: 34, + gradientFade: 16, }; + +/** Стартовый пресет узора для каждой цели, если в localStorage / settings.json нет значения. */ const panelPatternTargetMeta = { nav: { defaultPattern: "hex" }, section: { defaultPattern: "hex" }, @@ -86,6 +169,7 @@ function capitalizePanelPatternTarget(target) { return target.charAt(0).toUpperCase() + target.slice(1); } +/** Собирает ключи panelPattern* для всех целей (pattern, opacity, customImage, …). */ function buildDefaultPanelPatternPreferences() { const prefs = {}; @@ -98,6 +182,8 @@ function buildDefaultPanelPatternPreferences() { prefs[`panelPattern${cap}GradientOpacity`] = panelPatternTuningDefaults.gradientOpacity; prefs[`panelPattern${cap}GradientHeight`] = panelPatternTuningDefaults.gradientHeight; prefs[`panelPattern${cap}GradientFade`] = panelPatternTuningDefaults.gradientFade; + prefs[`panelPattern${cap}CustomImage`] = ""; + prefs[`panelPattern${cap}CustomSize`] = defaultPanelPatternCustomSize; } return prefs; @@ -115,6 +201,7 @@ const defaultPreferences = { customAccentTo: "#22d3ee", customAccentAngle: 135, ...buildDefaultPanelPatternPreferences(), + patternIsolation: 46, navLayout: "sidebar", showTree: true, showResizeHandles: true, @@ -122,24 +209,26 @@ const defaultPreferences = { showTreeTags: true, showTreeType: false, treeMetaLayout: "inline", - treeColumns: 2, + treeColumns: 1, autoCollapseSearch: true, - sidebarWidth: 390, + sidebarWidth: 360, topBarHeight: 140, - shellMaxWidth: 2800, - sidebarMinWidth: 260, - sidebarMaxWidth: 800, - sidebarMaxWidthWide: 1400, - treeSidebarBaseWidth: 420, - treeSidebarColumnWidth: 120, + shellMaxWidth: 2240, + sidebarMinWidth: 240, + sidebarMaxWidth: 720, + sidebarMaxWidthWide: 1040, + treeSidebarBaseWidth: 360, + treeSidebarColumnWidth: 96, topBarMinHeight: 80, topBarMaxHeight: 560, catalogCardsColumns: 3, catalogListColumns: 1, - catalogCompactCardsColumns: 5, - catalogCompactListColumns: 3, + catalogCompactCardsColumns: 4, + catalogCompactListColumns: 2, searchPlaceholder: "Grafana, S3, журнал, сервер...", - settingsPanelWidth: 460, + settingsPanelWidth: 440, + collapseAnimation: "animated", + collapseDuration: 260, }; const defaultPanelFeatures = { @@ -159,7 +248,7 @@ const defaultPanelFeatures = { const defaultPanelLimits = { shellMaxWidth: { min: 1200, max: 3840 }, - sidebarMinWidth: { min: 200, max: 480 }, + sidebarMinWidth: { min: 220, max: 480 }, sidebarMaxWidth: { min: 400, max: 1200 }, sidebarMaxWidthWide: { min: 560, max: 2000 }, treeSidebarBaseWidth: { min: 280, max: 720 }, @@ -199,6 +288,13 @@ const MIN_TREE_COLUMNS = 1; const MAX_TREE_COLUMNS = 6; const availableViews = ["cards", "list", "compact-cards", "compact-list"]; const availableAccents = ["violet", "cyan", "lime", "amber", "custom"]; +const availableCollapseAnimations = ["instant", "animated"]; +const MIN_COLLAPSE_DURATION = 100; +const MAX_COLLAPSE_DURATION = 800; +const MIN_PATTERN_ISOLATION = 0; +const MAX_PATTERN_ISOLATION = 100; +const COLLAPSIBLE_DETAILS_SELECTOR = + ".catalog-section, .catalog-group, .tree-section, .tree-group, .settings-section-collapsible, .pattern-target-panel"; // ============================================================================ // Runtime state and DOM references @@ -248,7 +344,16 @@ const settingsPanelWidthInput = document.querySelector("[data-settings-panel-wid const settingsPanelWidthValue = document.querySelector("[data-settings-panel-width-value]"); const layoutLimitInputs = document.querySelectorAll("[data-layout-limit]"); const catalogColumnInputs = document.querySelectorAll("[data-catalog-columns]"); +const patternIsolationInput = document.querySelector("[data-pattern-isolation]"); +const patternIsolationValue = document.querySelector("[data-pattern-isolation-value]"); +const collapseAnimationButtons = document.querySelectorAll("[data-collapse-animation]"); +const collapseDurationInput = document.querySelector("[data-collapse-duration]"); +const collapseDurationValue = document.querySelector("[data-collapse-duration-value]"); const settingsSections = document.querySelectorAll("[data-settings-section]"); +const settingsResetButton = document.querySelector("[data-settings-reset]"); +const settingsExportButton = document.querySelector("[data-settings-export]"); +const settingsImportButton = document.querySelector("[data-settings-import]"); +const settingsImportInput = document.querySelector("[data-settings-import-input]"); const emptyTemplate = document.querySelector("#empty-template"); const configErrorTemplate = document.querySelector("#config-error-template"); const pageTitleElement = document.querySelector("#page-title"); @@ -275,6 +380,7 @@ async function bootstrap() { return; } + setupPatternControls(); renderNavigationTree(); applyPreferences(preferences); renderCatalog(); @@ -309,6 +415,9 @@ navTree.addEventListener("click", (event) => { // Используем capture=true, потому что toggle не всплывает как обычный click. navTree.addEventListener("toggle", handleDetailsToggle, true); catalog.addEventListener("toggle", handleDetailsToggle, true); +settingsPanel?.addEventListener("toggle", handleDetailsToggle, true); + +document.addEventListener("click", handleAnimatedDetailsClick, true); // Открытие popup-настроек по шестеренке. settingsToggle.addEventListener("click", () => { @@ -321,6 +430,27 @@ settingsToggle.addEventListener("click", () => { settingsClose.addEventListener("click", closeSettingsPanel); settingsOverlay.addEventListener("click", closeSettingsPanel); +settingsResetButton?.addEventListener("click", resetStoredPreferences); +settingsExportButton?.addEventListener("click", exportStoredPreferences); +settingsImportButton?.addEventListener("click", () => settingsImportInput?.click()); + +settingsImportInput?.addEventListener("change", async () => { + const file = settingsImportInput.files?.[0]; + + if (!file) { + return; + } + + try { + const imported = await readImportedPreferencesFile(file); + applyStoredPreferences(imported); + } catch (error) { + window.alert(error instanceof Error ? error.message : "Не удалось импортировать настройки."); + } finally { + settingsImportInput.value = ""; + } +}); + document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeSettingsPanel(); @@ -441,23 +571,68 @@ patternTargetsPanel?.addEventListener("click", (event) => { } const target = button.dataset.panelPatternTarget; - preferences[getPanelPatternPreferenceKey(target, "pattern")] = button.dataset.panelPattern; + const pattern = button.dataset.panelPattern; + preferences[getPanelPatternPreferenceKey(target, "pattern")] = pattern; applyPreferences(preferences); savePreferences(preferences); + + if (pattern === "custom") { + const imageInput = patternTargetsPanel?.querySelector( + `[data-pattern-custom-panel="${target}"] [data-pattern-custom-field="image"]`, + ); + imageInput?.focus({ preventScroll: true }); + imageInput?.closest(".pattern-custom-panel")?.scrollIntoView({ block: "nearest", behavior: "smooth" }); + } }); patternTargetsPanel?.addEventListener("input", (event) => { - const input = event.target.closest("[data-panel-pattern-tuning]"); + const tuningInput = event.target.closest("[data-panel-pattern-tuning]"); + const customFieldInput = event.target.closest("[data-pattern-custom-field]"); - if (!input || !panelFeatures.allowPanelPatternChange) { + if (!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); + if (tuningInput) { + const target = tuningInput.dataset.panelPatternTuning; + const field = tuningInput.dataset.panelPatternField; + const key = getPanelPatternPreferenceKey(target, field); + const fallback = defaultPreferences[key] ?? panelPatternTuningDefaults[field] ?? 50; + preferences[key] = clampPanelPatternPercent(tuningInput.value, fallback); + applyPreferences(preferences); + savePreferences(preferences); + return; + } + + if (customFieldInput) { + const target = customFieldInput.dataset.panelPatternTarget; + const field = customFieldInput.dataset.patternCustomField; + const patternKey = getPanelPatternPreferenceKey(target, "pattern"); + + if (field === "image") { + preferences[getPanelPatternPreferenceKey(target, "customImage")] = normalizePatternCustomImage( + customFieldInput.value, + ); + } + + if (field === "size") { + preferences[getPanelPatternPreferenceKey(target, "customSize")] = normalizePatternCustomSize( + customFieldInput.value, + ); + } + + preferences[patternKey] = "custom"; + applyPreferences(preferences); + savePreferences(preferences); + } +}); + +patternIsolationInput?.addEventListener("input", () => { + if (!panelFeatures.allowPanelPatternChange) { + return; + } + + preferences.patternIsolation = clampPatternIsolation(patternIsolationInput.value); applyPreferences(preferences); savePreferences(preferences); }); @@ -596,6 +771,22 @@ catalogColumnInputs.forEach((input) => { }); }); +collapseAnimationButtons.forEach((button) => { + button.addEventListener("click", () => { + preferences.collapseAnimation = button.dataset.collapseAnimation; + applyPreferences(preferences); + savePreferences(preferences); + }); +}); + +function handleCollapseDurationChange() { + preferences.collapseDuration = clampCollapseDuration(collapseDurationInput?.value); + applyPreferences(preferences); + savePreferences(preferences); +} + +collapseDurationInput?.addEventListener("input", handleCollapseDurationChange); + // Drag-resize бокового меню. // Ширина сохраняется в preferences.sidebarWidth и применяется через CSS-переменную. sidebarResizer.addEventListener("pointerdown", (event) => { @@ -712,6 +903,7 @@ function normalizePreferences(raw) { migrateLegacyPanelPatternPreferences(next); normalizePanelPatternPreferences(next); + next.patternIsolation = clampPatternIsolation(next.patternIsolation); if (!availableNavLayouts.includes(next.navLayout)) { next.navLayout = defaultPreferences.navLayout; @@ -748,6 +940,10 @@ function normalizePreferences(raw) { getTopBarMaxHeight(next), ); next.settingsPanelWidth = clampLayoutPreference("settingsPanelWidth", next.settingsPanelWidth); + next.collapseAnimation = availableCollapseAnimations.includes(next.collapseAnimation) + ? next.collapseAnimation + : defaultPreferences.collapseAnimation; + next.collapseDuration = clampCollapseDuration(next.collapseDuration); return next; } @@ -823,7 +1019,10 @@ function enforceFeatureLocks(rawPreferences) { next[`panelPattern${cap}GradientOpacity`] = panelDefaults[`panelPattern${cap}GradientOpacity`]; next[`panelPattern${cap}GradientHeight`] = panelDefaults[`panelPattern${cap}GradientHeight`]; next[`panelPattern${cap}GradientFade`] = panelDefaults[`panelPattern${cap}GradientFade`]; + next[`panelPattern${cap}CustomImage`] = panelDefaults[`panelPattern${cap}CustomImage`]; + next[`panelPattern${cap}CustomSize`] = panelDefaults[`panelPattern${cap}CustomSize`]; } + next.patternIsolation = panelDefaults.patternIsolation; } if (!panelFeatures.allowShowNumbers) { @@ -1084,6 +1283,8 @@ function renderCatalog() { } else { catalog.append(emptyTemplate.content.cloneNode(true)); } + + refreshCollapseAnimBodies(); } function createItemsGridElement(items, query = "", className = "items-grid") { @@ -1317,6 +1518,7 @@ function renderNavigationTree() { }); navTree.replaceChildren(fragment); + refreshCollapseAnimBodies(); } function createTreeItemList(items, query = "", className = "tree-items") { @@ -1391,6 +1593,176 @@ function focusCatalogTarget(targetId) { // ============================================================================ // Collapse state handling // ============================================================================ +function isCollapseAnimationEnabled() { + return ( + preferences.collapseAnimation === "animated" && + !window.matchMedia("(prefers-reduced-motion: reduce)").matches + ); +} + +function isCollapsibleDetails(details) { + return details instanceof HTMLDetailsElement && details.matches(COLLAPSIBLE_DETAILS_SELECTOR); +} + +function shouldSkipAnimatedSummaryClick(event, summary) { + if (event.target.closest("a, input, textarea, select, label")) { + return true; + } + + if (summary.closest(".tree-section, .tree-group") && event.target.closest("button")) { + return true; + } + + return false; +} + +function ensureCollapseAnimBody(details) { + if (!isCollapsibleDetails(details) || details.querySelector(":scope > .collapse-anim-body")) { + return details?.querySelector(":scope > .collapse-anim-body") ?? null; + } + + const wrapper = document.createElement("div"); + wrapper.className = "collapse-anim-body"; + + [...details.children].forEach((child) => { + if (child.tagName !== "SUMMARY") { + wrapper.append(child); + } + }); + + if (!wrapper.childNodes.length) { + return null; + } + + details.append(wrapper); + return wrapper; +} + +function refreshCollapseAnimBodies() { + if (!isCollapseAnimationEnabled()) { + return; + } + + document.querySelectorAll(COLLAPSIBLE_DETAILS_SELECTOR).forEach((details) => { + ensureCollapseAnimBody(details); + }); +} + +function resetCollapseAnimBodyStyles(body) { + body.style.height = ""; + body.style.transition = ""; + body.style.overflow = ""; +} + +function getCollapseAnimDurationMs() { + return clampCollapseDuration(preferences.collapseDuration); +} + +function animateCollapseBody(body, fromHeight, toHeight) { + return new Promise((resolve) => { + const duration = getCollapseAnimDurationMs(); + let finished = false; + + const finish = (event) => { + if (finished) { + return; + } + + if (event && event.propertyName && event.propertyName !== "height") { + return; + } + + finished = true; + body.removeEventListener("transitionend", finish); + resolve(); + }; + + body.style.overflow = "hidden"; + body.style.height = `${fromHeight}px`; + body.offsetHeight; + body.style.transition = `height ${duration}ms cubic-bezier(0.4, 0, 0.2, 1)`; + body.style.height = `${toHeight}px`; + body.addEventListener("transitionend", finish); + window.setTimeout(finish, duration + 50); + }); +} + +async function openDetailsAnimated(details) { + if (details.dataset.collapseAnimating === "true") { + return; + } + + const body = ensureCollapseAnimBody(details); + if (!body) { + details.open = true; + return; + } + + details.dataset.collapseAnimating = "true"; + details.open = true; + resetCollapseAnimBodyStyles(body); + body.style.overflow = "hidden"; + body.style.height = "0px"; + body.offsetHeight; + await animateCollapseBody(body, 0, body.scrollHeight); + resetCollapseAnimBodyStyles(body); + delete details.dataset.collapseAnimating; +} + +async function closeDetailsAnimated(details) { + if (details.dataset.collapseAnimating === "true") { + return; + } + + const body = ensureCollapseAnimBody(details); + if (!body) { + details.open = false; + return; + } + + details.dataset.collapseAnimating = "true"; + const startHeight = body.scrollHeight; + await animateCollapseBody(body, startHeight, 0); + details.open = false; + resetCollapseAnimBodyStyles(body); + delete details.dataset.collapseAnimating; +} + +function handleAnimatedDetailsClick(event) { + if (!isCollapseAnimationEnabled()) { + return; + } + + const summary = event.target.closest("summary"); + if (!summary) { + return; + } + + const details = summary.closest("details"); + if (!isCollapsibleDetails(details) || shouldSkipAnimatedSummaryClick(event, summary)) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + if (details.open) { + closeDetailsAnimated(details); + } else { + openDetailsAnimated(details); + } +} + +function clearCollapseAnimInlineStyles() { + document.querySelectorAll(".collapse-anim-body").forEach((body) => { + resetCollapseAnimBodyStyles(body); + }); + + document.querySelectorAll(COLLAPSIBLE_DETAILS_SELECTOR).forEach((details) => { + delete details.dataset.collapseAnimating; + }); +} + // Сохраняет состояние каждого details, у которого есть data-collapse-key. function handleDetailsToggle(event) { const details = event.target; @@ -1473,6 +1845,7 @@ function applyPreferences(nextPreferences) { document.documentElement.dataset.theme = activePreferences.theme; document.documentElement.dataset.accent = activePreferences.accent; applyPanelPatternPreferences(activePreferences); + applyPatternIsolationPreference(activePreferences); document.documentElement.dataset.navLayout = activePreferences.navLayout === "top" ? "top" : "sidebar"; document.documentElement.dataset.showTree = String(isTreeVisible(activePreferences)); @@ -1498,6 +1871,12 @@ function applyPreferences(nextPreferences) { "--settings-panel-width", `${clampLayoutPreference("settingsPanelWidth", activePreferences.settingsPanelWidth)}px`, ); + document.documentElement.dataset.collapseAnimation = + activePreferences.collapseAnimation === "animated" ? "animated" : "instant"; + document.documentElement.style.setProperty( + "--collapse-duration", + `${clampCollapseDuration(activePreferences.collapseDuration)}ms`, + ); const treeColumns = getTreeColumns(activePreferences); const sidebarMaxWidth = getSidebarMaxWidth(activePreferences); @@ -1569,10 +1948,16 @@ function applyPreferences(nextPreferences) { setActiveButton(viewButtons, "view", activePreferences.view); setActiveButton(accentButtons, "accent", activePreferences.accent); syncPatternTargetPanels(activePreferences); + syncPatternIsolationControl(activePreferences); setActiveButton(treeMetaLayoutButtons, "treeMetaLayout", activePreferences.treeMetaLayout); syncLayoutLimitInputs(activePreferences); syncCatalogColumnInputs(activePreferences); syncSettingsPanelWidthInput(activePreferences); + syncCollapseAnimationControls(activePreferences); + refreshCollapseAnimBodies(); + if (!isCollapseAnimationEnabled()) { + clearCollapseAnimInlineStyles(); + } applyPanelFeatures(activePreferences); } @@ -1905,6 +2290,28 @@ function syncSettingsPanelWidthInput(activePreferences) { } } +function clampCollapseDuration(value) { + const numeric = Number(value); + const base = Number.isFinite(numeric) ? numeric : defaultPreferences.collapseDuration; + return clamp(Math.round(base), MIN_COLLAPSE_DURATION, MAX_COLLAPSE_DURATION); +} + +function syncCollapseAnimationControls(activePreferences) { + setActiveButton(collapseAnimationButtons, "collapseAnimation", activePreferences.collapseAnimation); + + if (collapseDurationInput) { + collapseDurationInput.min = String(MIN_COLLAPSE_DURATION); + collapseDurationInput.max = String(MAX_COLLAPSE_DURATION); + collapseDurationInput.step = "25"; + collapseDurationInput.value = String(clampCollapseDuration(activePreferences.collapseDuration)); + collapseDurationInput.disabled = activePreferences.collapseAnimation !== "animated"; + } + + if (collapseDurationValue) { + collapseDurationValue.textContent = `${clampCollapseDuration(activePreferences.collapseDuration)} ms`; + } +} + function syncCatalogColumnInputs(activePreferences) { catalogColumnInputs.forEach((input) => { const key = input.dataset.catalogColumns; @@ -1937,6 +2344,19 @@ function clampPanelPatternPercent(value, fallback = 50) { return clamp(Math.round(base), 0, 100); } +// ============================================================================ +// Panel pattern preferences +// ============================================================================ +// Нормализация, миграция legacy-ключей, применение к DOM и синхронизация UI ⚙. +// Ключи preferences: panelPatternNav, panelPatternNavOpacity, panelPatternNavCustomImage, … + +function clampPatternIsolation(value) { + const numeric = Number(value); + const fallback = panelDefaults.patternIsolation ?? defaultPreferences.patternIsolation; + const base = Number.isFinite(numeric) ? numeric : fallback; + return clamp(Math.round(base), MIN_PATTERN_ISOLATION, MAX_PATTERN_ISOLATION); +} + function getPanelPatternPreferenceKey(target, field) { const cap = capitalizePanelPatternTarget(target); @@ -1955,6 +2375,10 @@ function getPanelPatternPreferenceKey(target, field) { return `panelPattern${cap}GradientHeight`; case "gradientFade": return `panelPattern${cap}GradientFade`; + case "customImage": + return `panelPattern${cap}CustomImage`; + case "customSize": + return `panelPattern${cap}CustomSize`; default: return `panelPattern${cap}`; } @@ -2023,6 +2447,12 @@ function normalizePanelPatternPreferences(next) { next[`panelPattern${cap}GradientFade`], panelPatternTuningDefaults.gradientFade, ); + next[`panelPattern${cap}CustomImage`] = normalizePatternCustomImage( + next[`panelPattern${cap}CustomImage`], + ); + next[`panelPattern${cap}CustomSize`] = normalizePatternCustomSize( + next[`panelPattern${cap}CustomSize`], + ); } } @@ -2032,10 +2462,17 @@ function getPanelPatternFadeStart(heightPercent, fadePercent) { return `${Math.max(0, height - fade)}%`; } +/** + * Пишет узоры на : data-panel-pattern-{Nav|…}, opacity/height/fade, + * градиент и (для custom) inline --panel-pattern-image-{target}. + */ function applyPanelPatternPreferences(activePreferences) { for (const target of panelPatternTargetIds) { const cap = capitalizePanelPatternTarget(target); - const pattern = activePreferences[`panelPattern${cap}`] || "none"; + let pattern = activePreferences[`panelPattern${cap}`] || "none"; + const customImage = normalizePatternCustomImage(activePreferences[`panelPattern${cap}CustomImage`]); + const customSize = normalizePatternCustomSize(activePreferences[`panelPattern${cap}CustomSize`]); + const height = clampPanelPatternPercent( activePreferences[`panelPattern${cap}Height`], panelPatternTuningDefaults.height, @@ -2044,9 +2481,24 @@ function applyPanelPatternPreferences(activePreferences) { activePreferences[`panelPattern${cap}Opacity`], panelPatternTuningDefaults.opacity, ); - const patternOpacity = pattern === "none" || height === 0 ? 0 : opacityPercent / 100; + let patternOpacity = pattern === "none" || height === 0 ? 0 : opacityPercent / 100; + + if (pattern === "custom" && !customImage) { + patternOpacity = 0; + } document.documentElement.dataset[`panelPattern${cap}`] = pattern; + document.documentElement.style.removeProperty(`--panel-pattern-image-${target}`); + document.documentElement.style.removeProperty(`--panel-pattern-size-${target}`); + + if (pattern === "custom" && customImage) { + document.documentElement.style.setProperty( + `--panel-pattern-image-${target}`, + `url("${customImage.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}")`, + ); + document.documentElement.style.setProperty(`--panel-pattern-size-${target}`, customSize); + } + document.documentElement.style.setProperty(`--panel-pattern-${target}-opacity`, String(patternOpacity)); document.documentElement.style.setProperty(`--panel-pattern-${target}-height`, `${height}%`); document.documentElement.style.setProperty( @@ -2079,6 +2531,29 @@ function applyPanelPatternPreferences(activePreferences) { } } +function applyPatternIsolationPreference(activePreferences) { + // patternIsolation 0–100 → blur и непрозрачность подложки на section/group/card + const isolation = clampPatternIsolation(activePreferences.patternIsolation); + document.documentElement.style.setProperty("--panel-layer-blur", `${Math.round(isolation * 0.18)}px`); + document.documentElement.style.setProperty( + "--panel-layer-opacity", + String(0.04 + (isolation / 100) * 0.5), + ); +} + +function syncPatternIsolationControl(activePreferences) { + const isolation = clampPatternIsolation(activePreferences.patternIsolation); + + if (patternIsolationInput) { + patternIsolationInput.value = String(isolation); + patternIsolationInput.disabled = !panelFeatures.allowPanelPatternChange; + } + + if (patternIsolationValue) { + patternIsolationValue.textContent = `${isolation}%`; + } +} + function syncPatternTargetPanels(activePreferences) { if (!patternTargetsPanel) { return; @@ -2089,6 +2564,7 @@ function syncPatternTargetPanels(activePreferences) { for (const target of panelPatternTargetIds) { const pattern = activePreferences[getPanelPatternPreferenceKey(target, "pattern")]; + const customImage = activePreferences[getPanelPatternPreferenceKey(target, "customImage")]; patternTargetsPanel .querySelectorAll(`[data-panel-pattern-target="${target}"][data-panel-pattern]`) .forEach((button) => { @@ -2096,8 +2572,35 @@ function syncPatternTargetPanels(activePreferences) { button.classList.toggle("is-active", isActive); button.setAttribute("aria-pressed", String(isActive)); button.disabled = disabled; + + if (button.dataset.panelPattern === "custom" && customImage) { + button.style.setProperty("--pattern-swatch-preview", `url("${customImage.replace(/"/g, '\\"')}")`); + button.classList.add("has-custom-preview"); + } else { + button.style.removeProperty("--pattern-swatch-preview"); + button.classList.remove("has-custom-preview"); + } }); + const customPanel = patternTargetsPanel.querySelector(`[data-pattern-custom-panel="${target}"]`); + + if (customPanel) { + customPanel.hidden = pattern !== "custom"; + + const imageInput = customPanel.querySelector('[data-pattern-custom-field="image"]'); + const sizeInput = customPanel.querySelector('[data-pattern-custom-field="size"]'); + + if (imageInput) { + imageInput.value = activePreferences[getPanelPatternPreferenceKey(target, "customImage")] || ""; + imageInput.disabled = disabled; + } + + if (sizeInput) { + sizeInput.value = activePreferences[getPanelPatternPreferenceKey(target, "customSize")]; + sizeInput.disabled = disabled; + } + } + for (const field of tuningFields) { const key = getPanelPatternPreferenceKey(target, field); const input = patternTargetsPanel.querySelector( @@ -2119,6 +2622,125 @@ function syncPatternTargetPanels(activePreferences) { } } +// --- Custom pattern URL / tile size --- + +/** Принимает data:image/, http(s), относительные пути и *.svg|png|…; отклоняет javascript:. */ +function normalizePatternCustomImage(value) { + const trimmed = String(value ?? "").trim(); + + if (!trimmed) { + return ""; + } + + if (/^javascript:/i.test(trimmed)) { + return ""; + } + + if (/^(?:data:image\/|https?:\/\/|\.\/|\.\.\/|\/|[a-z0-9._-]+\.(?:svg|png|webp|gif|jpg|jpeg)(?:\?|$))/i.test(trimmed)) { + return trimmed; + } + + if (/^[a-z0-9._/-]+\.(?:svg|png|webp|gif|jpg|jpeg)$/i.test(trimmed)) { + return trimmed; + } + + return ""; +} + +function normalizePatternCustomSize(value) { + const trimmed = String(value ?? "").trim(); + const fallback = defaultPanelPatternCustomSize; + + if (!trimmed) { + return fallback; + } + + if (/^\d+(?:\.\d+)?px\s+\d+(?:\.\d+)?px$/.test(trimmed)) { + return trimmed; + } + + const parts = trimmed.split(/[\s,]+/).filter(Boolean); + + if (parts.length === 2 && parts.every((part) => /^\d+(?:\.\d+)?(?:px)?$/.test(part))) { + const width = parts[0].endsWith("px") ? parts[0] : `${parts[0]}px`; + const height = parts[1].endsWith("px") ? parts[1] : `${parts[1]}px`; + return `${width} ${height}`; + } + + return fallback; +} + +// --- Pattern UI in settings popup (dynamic swatches + custom panel) --- + +/** Вызывается из bootstrap(): кнопки пресетов и поля «Свой узор» для каждой цели. */ +function setupPatternControls() { + renderPatternSwatches(); + ensurePatternCustomPanels(); +} + +function renderPatternSwatches() { + // Контейнеры [data-pattern-swatches] в index.html изначально пустые — кнопки создаются здесь + if (!patternTargetsPanel) { + return; + } + + patternTargetsPanel.querySelectorAll("[data-pattern-swatches]").forEach((container) => { + const target = container.dataset.patternSwatches; + + if (!target || container.dataset.rendered === "true") { + return; + } + + container.dataset.rendered = "true"; + container.replaceChildren( + ...panelPatternCatalog.map(({ id, label }) => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "pattern-swatch"; + button.dataset.panelPatternTarget = target; + button.dataset.panelPattern = id; + button.setAttribute("aria-pressed", "false"); + button.innerHTML = `${label}`; + return button; + }), + ); + }); +} + +function ensurePatternCustomPanels() { + // Вставляет .pattern-custom-panel перед .pattern-tuning в каждой data-pattern-target-panel + if (!patternTargetsPanel) { + return; + } + + for (const target of panelPatternTargetIds) { + const panel = patternTargetsPanel.querySelector(`[data-pattern-target="${target}"]`); + + if (!panel || panel.querySelector(`[data-pattern-custom-panel="${target}"]`)) { + continue; + } + + const customPanel = document.createElement("div"); + customPanel.className = "pattern-custom-panel"; + customPanel.dataset.patternCustomPanel = target; + customPanel.hidden = true; + customPanel.innerHTML = ` +

Вставьте URL или путь к SVG/PNG ниже. Пока поле пустое, узор не рисуется.

+ + + `; + + const tuning = panel.querySelector(".pattern-tuning"); + panel.querySelector(".settings-section-body")?.insertBefore(customPanel, tuning ?? null); + } +} + function clampCatalogColumn(key, value) { const limits = getPanelLimitRange(key); const fallback = panelDefaults[key] ?? defaultPreferences[key]; @@ -2201,6 +2823,92 @@ function saveCollapseState() { localStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(collapseState)); } +// ============================================================================ +// Settings import, export and reset +// ============================================================================ +// Кнопки внизу popup ⚙: data-settings-reset | export | import. +// Формат экспорта: { version, exportedAt, preferences }. Импорт принимает и «голый» объект. + +/** Применяет набор preferences, обновляет дерево/каталог и при необходимости сохраняет в localStorage. */ +function applyStoredPreferences(rawPreferences, { persist = true } = {}) { + preferences = enforceFeatureLocks(normalizePreferences({ ...panelDefaults, ...rawPreferences })); + applyPreferences(preferences); + + if (isTreeVisible(preferences)) { + renderNavigationTree(); + } else if (navTree) { + navTree.replaceChildren(); + } + + renderCatalog(); + + if (persist) { + savePreferences(preferences); + } +} + +// Сбрасывает пользовательские настройки к panelDefaults из settings.json. +function resetStoredPreferences() { + if (!window.confirm("Сбросить все настройки к значениям по умолчанию?")) { + return; + } + + localStorage.removeItem(STORAGE_KEY); + applyStoredPreferences({ ...panelDefaults }, { persist: false }); +} + +// Скачивает текущие preferences в JSON-файл. +function exportStoredPreferences() { + const payload = { + version: 1, + exportedAt: new Date().toISOString(), + preferences: { ...preferences }, + }; + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = "ecosystem-panel-preferences.json"; + anchor.click(); + URL.revokeObjectURL(url); +} + +// Читает JSON-файл и возвращает объект preferences. +function readImportedPreferencesFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = () => { + try { + const parsed = JSON.parse(String(reader.result)); + resolve(parseImportedPreferencesPayload(parsed)); + } catch (error) { + reject(error instanceof Error ? error : new Error("Не удалось прочитать файл настроек.")); + } + }; + + reader.onerror = () => { + reject(new Error("Не удалось прочитать файл настроек.")); + }; + + reader.readAsText(file); + }); +} + +// Поддерживает как обёртку { preferences }, так и «голый» объект настроек. +function parseImportedPreferencesPayload(raw) { + const source = + raw && typeof raw === "object" && !Array.isArray(raw) && raw.preferences && typeof raw.preferences === "object" + ? raw.preferences + : raw; + + if (!source || typeof source !== "object" || Array.isArray(source)) { + throw new Error("Неверный формат файла настроек."); + } + + return source; +} + // Закрывает popup-настройки и синхронизирует aria-expanded. function closeSettingsPanel() { settingsPanel.classList.remove("is-open"); diff --git a/settings-top-bar.example.json b/settings-top-bar.example.json index 46271cf..e68a269 100644 --- a/settings-top-bar.example.json +++ b/settings-top-bar.example.json @@ -7,33 +7,38 @@ "theme": "dark", "view": "cards", "accent": "cyan", + "patternIsolation": 46, "showNumbers": false, "showTreeTags": true, "showTreeType": false, "treeMetaLayout": "inline", - "treeColumns": 2, + "treeColumns": 1, "autoCollapseSearch": true, - "sidebarWidth": 390, - "topBarHeight": 180, - "shellMaxWidth": 2800, - "sidebarMinWidth": 260, - "sidebarMaxWidth": 800, - "sidebarMaxWidthWide": 1400, - "treeSidebarBaseWidth": 420, - "treeSidebarColumnWidth": 120, + "sidebarWidth": 360, + "topBarHeight": 160, + "shellMaxWidth": 2240, + "sidebarMinWidth": 240, + "sidebarMaxWidth": 720, + "sidebarMaxWidthWide": 1040, + "treeSidebarBaseWidth": 360, + "treeSidebarColumnWidth": 96, "topBarMinHeight": 80, "topBarMaxHeight": 560, - "searchPlaceholder": "Поиск сервиса..." + "collapseAnimation": "animated", + "collapseDuration": 260, + "searchPlaceholder": "Поиск сервиса...", + "settingsPanelWidth": 440 }, "limits": { "shellMaxWidth": { "min": 1200, "max": 3840 }, - "sidebarMinWidth": { "min": 200, "max": 480 }, + "sidebarMinWidth": { "min": 220, "max": 480 }, "sidebarMaxWidth": { "min": 400, "max": 1200 }, "sidebarMaxWidthWide": { "min": 560, "max": 2000 }, "treeSidebarBaseWidth": { "min": 280, "max": 720 }, "treeSidebarColumnWidth": { "min": 60, "max": 200 }, "topBarMinHeight": { "min": 64, "max": 240 }, - "topBarMaxHeight": { "min": 160, "max": 800 } + "topBarMaxHeight": { "min": 160, "max": 800 }, + "settingsPanelWidth": { "min": 320, "max": 720 } }, "features": { "allowNavLayoutChange": true, diff --git a/settings.json b/settings.json index 237a4fc..13617ad 100644 --- a/settings.json +++ b/settings.json @@ -2,61 +2,80 @@ "dataUrl": "./panel-data.json", "data": null, "defaults": { - "navLayout": "sidebar", - "showTree": true, - "showResizeHandles": true, + "navLayout": "top", + "showTree": false, + "showResizeHandles": false, "theme": "dark", "view": "cards", - "accent": "violet", - "customAccentFrom": "#8b5cf6", - "customAccentTo": "#22d3ee", + "accent": "custom", + "customAccentFrom": "#ff428e", + "customAccentTo": "#5bd8ec", "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, + "panelPatternNav": "diagonal", + "panelPatternNavOpacity": 20, + "panelPatternNavHeight": 100, + "panelPatternNavFade": 100, + "panelPatternNavGradientOpacity": 100, + "panelPatternNavGradientHeight": 100, + "panelPatternNavGradientFade": 100, + "panelPatternNavCustomImage": "", + "panelPatternNavCustomSize": "32px 32px", + "panelPatternSection": "diamond", + "panelPatternSectionOpacity": 30, + "panelPatternSectionHeight": 50, + "panelPatternSectionFade": 60, + "panelPatternSectionGradientOpacity": 75, + "panelPatternSectionGradientHeight": 20, + "panelPatternSectionGradientFade": 15, + "panelPatternSectionCustomImage": "", + "panelPatternSectionCustomSize": "32px 32px", + "panelPatternGroup": "custom", + "panelPatternGroupOpacity": 10, + "panelPatternGroupHeight": 100, + "panelPatternGroupFade": 100, + "panelPatternGroupGradientOpacity": 20, + "panelPatternGroupGradientHeight": 100, + "panelPatternGroupGradientFade": 100, + "panelPatternGroupCustomImage": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='s2' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23E2E8F0' /%3E%3Cstop offset='100%25' stop-color='%2364748B' /%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='20' y='20' width='24' height='24' fill='none' stroke='url(%23s2)' stroke-width='1.5' transform='rotate(45 32 32)' /%3E%3Crect x='16' y='16' width='32' height='32' fill='none' stroke='url(%23s2)' stroke-width='1' opacity='0.4' transform='rotate(45 32 32)' /%3E%3C/svg%3E", + "panelPatternGroupCustomSize": "32px 32px", + "panelPatternItem": "mesh", + "panelPatternItemOpacity": 58, + "panelPatternItemHeight": 100, + "panelPatternItemFade": 18, + "panelPatternItemGradientOpacity": 62, + "panelPatternItemGradientHeight": 34, + "panelPatternItemGradientFade": 16, + "panelPatternItemCustomImage": "", + "panelPatternItemCustomSize": "32px 32px", + "patternIsolation": 65, "showNumbers": false, - "showTreeTags": true, + "showTreeTags": false, "showTreeType": false, "treeMetaLayout": "inline", - "treeColumns": 2, + "treeColumns": 1, "autoCollapseSearch": true, - "sidebarWidth": 390, - "topBarHeight": 140, - "shellMaxWidth": 2800, - "sidebarMinWidth": 260, - "sidebarMaxWidth": 800, - "sidebarMaxWidthWide": 1400, - "treeSidebarBaseWidth": 420, - "treeSidebarColumnWidth": 120, - "topBarMinHeight": 80, - "topBarMaxHeight": 560, - "catalogCardsColumns": 3, - "catalogListColumns": 1, - "catalogCompactCardsColumns": 5, + "sidebarWidth": 360, + "topBarHeight": 72, + "shellMaxWidth": 1100, + "sidebarMinWidth": 240, + "sidebarMaxWidth": 720, + "sidebarMaxWidthWide": 1040, + "treeSidebarBaseWidth": 360, + "treeSidebarColumnWidth": 96, + "topBarMinHeight": 64, + "topBarMaxHeight": 800, + "catalogCardsColumns": 2, + "catalogListColumns": 2, + "catalogCompactCardsColumns": 3, "catalogCompactListColumns": 3, + "collapseAnimation": "animated", + "collapseDuration": 250, "searchPlaceholder": "Grafana, S3, журнал, сервер...", - "settingsPanelWidth": 460 + "settingsPanelWidth": 500 }, "limits": { - "shellMaxWidth": { "min": 1200, "max": 3840 }, - "sidebarMinWidth": { "min": 200, "max": 480 }, + "shellMaxWidth": { "min": 1100, "max": 3840 }, + "sidebarMinWidth": { "min": 220, "max": 480 }, "sidebarMaxWidth": { "min": 400, "max": 1200 }, "sidebarMaxWidthWide": { "min": 560, "max": 2000 }, "treeSidebarBaseWidth": { "min": 280, "max": 720 }, diff --git a/styles.css b/styles.css index e476596..6f58d66 100644 --- a/styles.css +++ b/styles.css @@ -1,3 +1,23 @@ +/* ========================================================================== + ecosystem-panel / styles.css + -------------------------------------------------------------------------- + ОГЛАВЛЕНИЕ (поиск по «==» или номеру секции): + 01 Theme Tokens — цвета, layout, pattern tiles, pattern mapping, accents + 02 Base Reset And Page Background + 03 Application Layout — sidebar / top bar / resizers + 04 Catalog Area + 05 Sidebar Panel — hero, patterns ::before/::after, layer isolation + 06 Settings Popup — sections, pattern swatches, actions footer + 07 Search Field And Control Inputs + 08 File-Tree Navigation + 09 Catalog Sections And Groups + 10 Navigation Cards + 11 Catalog View Modes + 12 Responsive Rules + 13 Touch And Reduced Motion + 14 Print + ========================================================================== */ + /* ========================================================================== 01. Theme Tokens -------------------------------------------------------------------------- @@ -31,7 +51,9 @@ --catalog-list-columns: 1; --catalog-compact-cards-columns: 5; --catalog-compact-list-columns: 3; + /* --- Pattern: цвет штриха/точки в CSS- и SVG-узорах --- */ --pattern-color: rgba(255, 255, 255, 0.16); + /* --- Pattern tiles: повторяющаяся текстура (image + background-size) --- */ --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); @@ -70,7 +92,23 @@ transparent 0 ); --pattern-tile-mesh-size: 20px 20px; + --pattern-tile-hex-sm-image: url("data:image/svg+xml,%3Csvg width='48' height='42' viewBox='0 0 48 42' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1'%3E%3Cpath d='M12 1 23 7v12L12 26 1 19V7L12 1Z'/%3E%3Cpath d='M36 16 47 22v12L36 41 25 34V22l11-6Z'/%3E%3C/g%3E%3C/svg%3E"); + --pattern-tile-hex-sm-size: 48px 42px; + --pattern-tile-dots-sm-image: radial-gradient(circle at center, var(--pattern-color) 0.6px, transparent 0.6px); + --pattern-tile-dots-sm-size: 10px 10px; + --pattern-tile-dots-lg-image: radial-gradient(circle at center, var(--pattern-color) 1.35px, transparent 1.35px); + --pattern-tile-dots-lg-size: 28px 28px; + --pattern-tile-tri-sm-image: url("data:image/svg+xml,%3Csvg width='20' height='18' viewBox='0 0 20 18' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='white' stroke-opacity='.2' stroke-width='1' d='M10 1 19 17 H1 Z'/%3E%3C/svg%3E"); + --pattern-tile-tri-sm-size: 20px 18px; + --pattern-tile-tri-lg-image: url("data:image/svg+xml,%3Csvg width='40' height='35' viewBox='0 0 40 35' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.2' d='M20 2 38 33 H2 Z'/%3E%3C/svg%3E"); + --pattern-tile-tri-lg-size: 40px 35px; + --pattern-tile-diamond-image: url("data:image/svg+xml,%3Csvg width='22' height='22' viewBox='0 0 22 22' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='white' stroke-opacity='.18' stroke-width='1' d='M11 1 21 11 11 21 1 11 Z'/%3E%3C/svg%3E"); + --pattern-tile-diamond-size: 22px 22px; --settings-panel-width: 460px; + /* --- Pattern layer isolation (preference patternIsolation → JS) --- */ + --panel-layer-blur: 8px; + --panel-layer-opacity: 0.27; + --panel-layer-fill: rgba(15, 23, 42, var(--panel-layer-opacity)); } /* Светлая тема переопределяет только токены, структура стилей остается общей. */ @@ -87,37 +125,71 @@ html[data-theme="light"] { --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-hex-sm-image: url("data:image/svg+xml,%3Csvg width='48' height='42' viewBox='0 0 48 42' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1'%3E%3Cpath d='M12 1 23 7v12L12 26 1 19V7L12 1Z'/%3E%3Cpath d='M36 16 47 22v12L36 41 25 34V22l11-6Z'/%3E%3C/g%3E%3C/svg%3E"); + --pattern-tile-tri-sm-image: url("data:image/svg+xml,%3Csvg width='20' height='18' viewBox='0 0 20 18' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='%23101827' stroke-opacity='.16' stroke-width='1' d='M10 1 19 17 H1 Z'/%3E%3C/svg%3E"); + --pattern-tile-tri-lg-image: url("data:image/svg+xml,%3Csvg width='40' height='35' viewBox='0 0 40 35' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.2' d='M20 2 38 33 H2 Z'/%3E%3C/svg%3E"); + --pattern-tile-diamond-image: url("data:image/svg+xml,%3Csvg width='22' height='22' viewBox='0 0 22 22' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1' d='M11 1 21 11 11 21 1 11 Z'/%3E%3C/svg%3E"); --pattern-tile-mesh-image: radial-gradient( circle at 1px 1px, rgba(var(--accent-rgb), 0.16) 1px, transparent 0 ); + --panel-layer-fill: rgba(255, 255, 255, var(--panel-layer-opacity)); } +/* + * Mapping встроенных пресетов: html[data-panel-pattern-{target}="{id}"] + * → --panel-pattern-image-{target} и --panel-pattern-size-{target}. + * Цели: nav | section | group | item. Пресет custom задаётся inline из script.js. + */ 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-nav="hex-sm"] { --panel-pattern-image-nav: var(--pattern-tile-hex-sm-image); --panel-pattern-size-nav: var(--pattern-tile-hex-sm-size); } +html[data-panel-pattern-nav="dots-sm"] { --panel-pattern-image-nav: var(--pattern-tile-dots-sm-image); --panel-pattern-size-nav: var(--pattern-tile-dots-sm-size); } +html[data-panel-pattern-nav="dots-lg"] { --panel-pattern-image-nav: var(--pattern-tile-dots-lg-image); --panel-pattern-size-nav: var(--pattern-tile-dots-lg-size); } +html[data-panel-pattern-nav="tri-sm"] { --panel-pattern-image-nav: var(--pattern-tile-tri-sm-image); --panel-pattern-size-nav: var(--pattern-tile-tri-sm-size); } +html[data-panel-pattern-nav="tri-lg"] { --panel-pattern-image-nav: var(--pattern-tile-tri-lg-image); --panel-pattern-size-nav: var(--pattern-tile-tri-lg-size); } +html[data-panel-pattern-nav="diamond"] { --panel-pattern-image-nav: var(--pattern-tile-diamond-image); --panel-pattern-size-nav: var(--pattern-tile-diamond-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-section="hex-sm"] { --panel-pattern-image-section: var(--pattern-tile-hex-sm-image); --panel-pattern-size-section: var(--pattern-tile-hex-sm-size); } +html[data-panel-pattern-section="dots-sm"] { --panel-pattern-image-section: var(--pattern-tile-dots-sm-image); --panel-pattern-size-section: var(--pattern-tile-dots-sm-size); } +html[data-panel-pattern-section="dots-lg"] { --panel-pattern-image-section: var(--pattern-tile-dots-lg-image); --panel-pattern-size-section: var(--pattern-tile-dots-lg-size); } +html[data-panel-pattern-section="tri-sm"] { --panel-pattern-image-section: var(--pattern-tile-tri-sm-image); --panel-pattern-size-section: var(--pattern-tile-tri-sm-size); } +html[data-panel-pattern-section="tri-lg"] { --panel-pattern-image-section: var(--pattern-tile-tri-lg-image); --panel-pattern-size-section: var(--pattern-tile-tri-lg-size); } +html[data-panel-pattern-section="diamond"] { --panel-pattern-image-section: var(--pattern-tile-diamond-image); --panel-pattern-size-section: var(--pattern-tile-diamond-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-group="hex-sm"] { --panel-pattern-image-group: var(--pattern-tile-hex-sm-image); --panel-pattern-size-group: var(--pattern-tile-hex-sm-size); } +html[data-panel-pattern-group="dots-sm"] { --panel-pattern-image-group: var(--pattern-tile-dots-sm-image); --panel-pattern-size-group: var(--pattern-tile-dots-sm-size); } +html[data-panel-pattern-group="dots-lg"] { --panel-pattern-image-group: var(--pattern-tile-dots-lg-image); --panel-pattern-size-group: var(--pattern-tile-dots-lg-size); } +html[data-panel-pattern-group="tri-sm"] { --panel-pattern-image-group: var(--pattern-tile-tri-sm-image); --panel-pattern-size-group: var(--pattern-tile-tri-sm-size); } +html[data-panel-pattern-group="tri-lg"] { --panel-pattern-image-group: var(--pattern-tile-tri-lg-image); --panel-pattern-size-group: var(--pattern-tile-tri-lg-size); } +html[data-panel-pattern-group="diamond"] { --panel-pattern-image-group: var(--pattern-tile-diamond-image); --panel-pattern-size-group: var(--pattern-tile-diamond-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); } +html[data-panel-pattern-item="hex-sm"] { --panel-pattern-image-item: var(--pattern-tile-hex-sm-image); --panel-pattern-size-item: var(--pattern-tile-hex-sm-size); } +html[data-panel-pattern-item="dots-sm"] { --panel-pattern-image-item: var(--pattern-tile-dots-sm-image); --panel-pattern-size-item: var(--pattern-tile-dots-sm-size); } +html[data-panel-pattern-item="dots-lg"] { --panel-pattern-image-item: var(--pattern-tile-dots-lg-image); --panel-pattern-size-item: var(--pattern-tile-dots-lg-size); } +html[data-panel-pattern-item="tri-sm"] { --panel-pattern-image-item: var(--pattern-tile-tri-sm-image); --panel-pattern-size-item: var(--pattern-tile-tri-sm-size); } +html[data-panel-pattern-item="tri-lg"] { --panel-pattern-image-item: var(--pattern-tile-tri-lg-image); --panel-pattern-size-item: var(--pattern-tile-tri-lg-size); } +html[data-panel-pattern-item="diamond"] { --panel-pattern-image-item: var(--pattern-tile-diamond-image); --panel-pattern-size-item: var(--pattern-tile-diamond-size); } /* Наборы акцентов. --accent-rgb нужен для прозрачных градиентов через rgba(). */ html[data-accent="cyan"] { @@ -400,6 +472,30 @@ body.is-resizing-top-bar { overflow: hidden; } +/* + * Разделение слоёв узоров: полупрозрачная подложка + backdrop-filter. + * JS: applyPatternIsolationPreference() → --panel-layer-blur / --panel-layer-opacity. + * Сильнее всего влияет на .catalog-section (доп. blur), затем group и .nav-card. + */ +.catalog-section, +.catalog-group, +.nav-card { + background: + linear-gradient(var(--panel-layer-fill), var(--panel-layer-fill)), + var(--panel); +} + +.catalog-section { + backdrop-filter: blur(calc(14px + var(--panel-layer-blur))); + -webkit-backdrop-filter: blur(calc(14px + var(--panel-layer-blur))); +} + +.catalog-group, +.nav-card { + backdrop-filter: blur(var(--panel-layer-blur)); + -webkit-backdrop-filter: blur(var(--panel-layer-blur)); +} + .hero-content { max-height: calc(100vh - 40px); min-height: 0; @@ -417,7 +513,13 @@ body.is-resizing-top-bar { overflow: auto; } -/* Акцентный градиент под узором — отдельно для каждого типа блоков. */ +/* + * Декоративные слои узора на блоках каталога и навигации. + * ::before — акцентный градиент (кроме .nav-card). + * ::after — повторяющаяся текстура; маска по высоте/переходу из CSS-переменных JS. + * Контент поверх узора: z-index: 1 у summary / .collapse-anim-body / детей .hero-content. + */ +/* Акcentный градиент под узором — отдельно для каждого типа блоков. */ html:not([data-panel-pattern-nav="none"]) .hero-content::before { content: ""; position: absolute; @@ -519,8 +621,10 @@ html:not([data-panel-pattern-item="none"]) .nav-card::after { .catalog-section > summary, .catalog-section > .section-groups, +.catalog-section > .collapse-anim-body, .catalog-group > summary, -.catalog-group > .items-grid { +.catalog-group > .items-grid, +.catalog-group > .collapse-anim-body { position: relative; z-index: 1; } @@ -732,6 +836,51 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta { font-size: 0.78rem; } +/* Footer popup ⚙: сброс / экспорт / импорт preferences (script.js → settings-actions). */ +.settings-actions { + margin-top: 4px; + padding-top: 12px; + border-top: 1px solid var(--border); + display: grid; + gap: 10px; +} + +.settings-actions-buttons { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.settings-action-button { + flex: 1 1 calc(33.333% - 6px); + min-width: 0; + min-height: 36px; + padding: 0 10px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--surface); + color: var(--text); + font-size: 0.84rem; + font-weight: 800; + cursor: pointer; + transition: transform 160ms ease, background 160ms ease, border-color 160ms ease; +} + +.settings-action-button:hover { + transform: translateY(-1px); + border-color: rgba(var(--accent-rgb), 0.5); + background: rgba(var(--accent-rgb), 0.12); +} + +.settings-action-button[data-settings-reset] { + color: #ef4444; +} + +.settings-action-button[data-settings-reset]:hover { + border-color: rgba(239, 68, 68, 0.45); + background: rgba(239, 68, 68, 0.12); +} + .catalog-columns-group { display: grid; gap: 8px; @@ -762,8 +911,21 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta { margin: 0; } -.settings-section-collapsible:not([open]) > .settings-section-summary .collapse-indicator { - transform: rotate(-90deg); +.settings-section-summary:hover .collapse-indicator { + border-color: rgba(var(--accent-rgb), 0.42); + background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.22), rgba(255, 255, 255, 0.05)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 8px 20px rgba(var(--accent-rgb), 0.12); +} + +html[data-collapse-animation="instant"] [data-collapse-duration-row] { + display: none; +} + +html[data-collapse-animation="animated"] .collapse-anim-body { + min-height: 0; + overflow: hidden; } .settings-section-body { @@ -1095,6 +1257,12 @@ html[data-theme="light"] .accent-swatch-custom { align-items: stretch; } +/* + * Блок узоров в ⚙ → Оформление. + * .pattern-swatches — контейнер; кнопки генерирует renderPatternSwatches() в script.js. + * .pattern-custom-panel — поля «Свой» (URL + размер плитки), создаёт ensurePatternCustomPanels(). + * .pattern-isolation-row — слайдер «Разделение слоёв». + */ .pattern-targets-panel { display: grid; gap: 10px; @@ -1109,13 +1277,13 @@ html[data-theme="light"] .accent-swatch-custom { .pattern-swatches { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; width: 100%; } .pattern-swatch { - min-height: 54px; + min-height: 48px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); @@ -1151,6 +1319,53 @@ 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="hex-sm"]::before { + background-image: var(--pattern-tile-hex-sm-image); + background-size: 24px 21px; +} + +.pattern-swatch[data-panel-pattern="dots-sm"]::before { + background-image: var(--pattern-tile-dots-sm-image); + background-size: 8px 8px; +} + +.pattern-swatch[data-panel-pattern="dots-lg"]::before { + background-image: var(--pattern-tile-dots-lg-image); + background-size: 18px 18px; +} + +.pattern-swatch[data-panel-pattern="tri-sm"]::before { + background-image: var(--pattern-tile-tri-sm-image); + background-size: 16px 14px; +} + +.pattern-swatch[data-panel-pattern="tri-lg"]::before { + background-image: var(--pattern-tile-tri-lg-image); + background-size: 28px 24px; +} + +.pattern-swatch[data-panel-pattern="diamond"]::before { + background-image: var(--pattern-tile-diamond-image); + background-size: 16px 16px; +} + +.pattern-swatch[data-panel-pattern="custom"]::before { + background: + linear-gradient(135deg, rgba(var(--accent-rgb), 0.18), transparent 62%), + repeating-linear-gradient( + -45deg, + transparent, + transparent 5px, + rgba(var(--accent-rgb), 0.12) 5px, + rgba(var(--accent-rgb), 0.12) 6px + ); +} + +.pattern-swatch[data-panel-pattern="custom"].has-custom-preview::before { + background-image: var(--pattern-swatch-preview); + background-size: 18px 18px; +} + .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; @@ -1251,6 +1466,53 @@ html[data-theme="light"] .pattern-swatch-label { grid-row: 2; } +.pattern-isolation-row { + grid-template-columns: minmax(120px, 1fr) minmax(0, 1.4fr) auto; +} + +.pattern-custom-panel { + display: grid; + gap: 8px; + padding: 10px; + border: 1px solid var(--border); + border-radius: 18px; + background: var(--surface); +} + +.pattern-custom-panel[hidden] { + display: none; +} + +.pattern-custom-panel input[type="url"], +.pattern-custom-panel input[type="text"] { + width: 100%; + min-height: 34px; + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--panel); + color: var(--text); + font: inherit; + font-size: 0.82rem; +} + +.pattern-custom-image-row { + grid-template-columns: minmax(72px, 0.8fr) minmax(0, 1.6fr); +} + +.pattern-custom-image-row input[type="url"], +.pattern-custom-image-row input[type="text"] { + width: 100%; + min-height: 34px; + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--panel); + color: var(--text); + font: inherit; + font-size: 0.82rem; +} + /* Панель настройки своего градиента появляется при data-accent="custom". */ .accent-custom-panel { display: grid; @@ -1354,8 +1616,8 @@ html[data-theme="light"] .pattern-swatch-label { .tree-section > summary, .tree-group > summary { display: grid; - grid-template-columns: 16px minmax(0, 1fr); - gap: 3px; + grid-template-columns: 22px minmax(0, 1fr); + gap: 6px; align-items: center; list-style: none; cursor: pointer; @@ -1372,17 +1634,38 @@ html[data-theme="light"] .pattern-swatch-label { /* Caret поворачивается через состояние [open] у details. */ .tree-caret { - width: 16px; - height: 24px; + width: 22px; + height: 22px; display: grid; place-items: center; + border-radius: 8px; color: var(--muted); + background: rgba(var(--accent-rgb), 0.06); + border: 1px solid transparent; + transition: + background 140ms ease, + border-color 140ms ease, + color 140ms ease; +} + +.tree-section > summary:hover .tree-caret, +.tree-group > summary:hover .tree-caret { + color: var(--accent); + border-color: rgba(var(--accent-rgb), 0.18); + background: rgba(var(--accent-rgb), 0.12); } .tree-caret::before { - content: "▾"; - font-size: 0.72rem; - transition: transform 140ms ease; + content: ""; + display: block; + width: 13px; + height: 13px; + background: currentColor; + mask: url("data:image/svg+xml,%3Csvg width='13' height='13' viewBox='0 0 13 13' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.5 5 6.5 8 9.5 5' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; + -webkit-mask: url("data:image/svg+xml,%3Csvg width='13' height='13' viewBox='0 0 13 13' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.5 5 6.5 8 9.5 5' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; + transform: rotate(0deg); + transform-origin: center; + transition: transform var(--collapse-duration, 240ms) cubic-bezier(0.4, 0, 0.2, 1); } .tree-section:not([open]) > summary .tree-caret::before, @@ -1635,23 +1918,63 @@ html:not([data-tree-columns="1"]) .tree-node-icon { font-weight: 800; } -/* Стрелка сворачивания поворачивается через [open]. */ +/* Стрелка сворачивания — аккуратный chevron в стеклянной кнопке. */ .collapse-indicator { - width: 34px; - height: 34px; + position: relative; + flex-shrink: 0; + width: 36px; + height: 36px; display: grid; place-items: center; - border: 1px solid var(--border); - border-radius: 999px; - background: var(--surface); + border-radius: 12px; + border: 1px solid rgba(var(--accent-rgb), 0.22); + background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.14), rgba(255, 255, 255, 0.03)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); color: var(--accent); - font-weight: 900; - transition: transform 160ms ease; + font-size: 0; + transition: + border-color 160ms ease, + background 160ms ease, + box-shadow 160ms ease, + transform 160ms ease; +} + +.collapse-indicator::before { + content: ""; + display: block; + width: 16px; + height: 16px; + background: currentColor; + mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.25 6.25 8 10l3.75-3.75' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; + -webkit-mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.25 6.25 8 10l3.75-3.75' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; + transform: rotate(0deg); + transform-origin: center; + transition: transform var(--collapse-duration, 240ms) cubic-bezier(0.4, 0, 0.2, 1); +} + +.catalog-section:not([open]) > .section-head .collapse-indicator::before, +.catalog-group:not([open]) > .group-head .collapse-indicator::before, +.settings-section-collapsible:not([open]) > .settings-section-summary .collapse-indicator::before { + transform: rotate(-90deg); +} + +.section-head:hover .collapse-indicator, +.group-head:hover .collapse-indicator { + border-color: rgba(var(--accent-rgb), 0.42); + background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.22), rgba(255, 255, 255, 0.05)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 8px 20px rgba(var(--accent-rgb), 0.12); +} + +html[data-theme="light"] .collapse-indicator { + background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.1), rgba(255, 255, 255, 0.72)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85); } .catalog-section:not([open]) > .section-head .collapse-indicator, .catalog-group:not([open]) > .group-head .collapse-indicator { - transform: rotate(-90deg); + transform: none; } /* Подразделы внутри секции. */ @@ -1682,7 +2005,9 @@ html:not([data-tree-columns="1"]) .tree-node-icon { position: relative; border: 1px solid var(--border); border-radius: 18px; - background: rgba(255, 255, 255, 0.035); + background: + linear-gradient(var(--panel-layer-fill), var(--panel-layer-fill)), + rgba(255, 255, 255, 0.035); overflow: hidden; scroll-margin-top: 18px; } @@ -1741,7 +2066,9 @@ html:not([data-tree-columns="1"]) .tree-node-icon { padding: 14px; border: 1px solid var(--border); border-radius: 18px; - background: var(--panel); + background: + linear-gradient(var(--panel-layer-fill), var(--panel-layer-fill)), + var(--panel); color: var(--text); text-decoration: none; overflow: hidden; @@ -1785,7 +2112,9 @@ html:not([data-tree-columns="1"]) .tree-node-icon { .nav-card.is-hovered, .nav-card:focus-visible { border-color: rgba(var(--accent-rgb), 0.52); - background: var(--panel-strong); + background: + linear-gradient(var(--panel-layer-fill), var(--panel-layer-fill)), + var(--panel-strong); box-shadow: 0 22px 70px rgba(var(--accent-rgb), 0.16); outline: none; } @@ -1853,18 +2182,37 @@ html:not([data-tree-columns="1"]) .tree-node-icon { height: 36px; display: grid; place-items: center; - border-radius: 999px; - background: rgba(var(--accent-rgb), 0.14); + border-radius: 12px; + border: 1px solid rgba(var(--accent-rgb), 0.18); + background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.12), rgba(255, 255, 255, 0.03)); color: var(--accent); - font-size: 1.15rem; - transition: transform 180ms ease, background 180ms ease, color 180ms ease; + font-size: 0; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); + transition: + transform 180ms ease, + background 180ms ease, + color 180ms ease, + border-color 180ms ease, + box-shadow 180ms ease; +} + +.arrow::before { + content: ""; + display: block; + width: 16px; + height: 16px; + background: currentColor; + mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 4 10 8 6 12' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; + -webkit-mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 4 10 8 6 12' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; } .nav-card.is-hovered .arrow, .nav-card:focus-visible .arrow { - transform: translateX(4px); + transform: translateX(3px); + border-color: rgba(var(--accent-rgb), 0.35); background: linear-gradient(var(--accent-angle), var(--accent), var(--accent-2)); color: white; + box-shadow: 0 10px 24px rgba(var(--accent-rgb), 0.24); } /* Декоративный shine слой, положение которого обновляется pointermove. */