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 @@ + +
Разделы каталога, дерево навигации и блоки в настройках.
+Боковое меню и дерево
-Карточки верхнего уровня каталога
-Группы внутри раздела
-Карточки сервисов — только узор, без акцентного градиента
-+ Подложка размывает узоры нижних уровней под разделами, подразделами и карточками. +
+Свой градиент
@@ -511,6 +500,24 @@+ Сброс, экспорт и импорт настроек интерфейса на этом устройстве. +
+ + +Разделы каталога, дерево навигации и блоки в настройках.
+Боковое меню и дерево
-Карточки верхнего уровня каталога
-Группы внутри раздела
-Карточки сервисов — только узор, без акцентного градиента
-+ Подложка размывает узоры нижних уровней под разделами, подразделами и карточками. +
+Свой градиент
@@ -511,6 +500,24 @@+ Сброс, экспорт и импорт настроек интерфейса на этом устройстве. +
+ + +Разделы каталога, дерево навигации и блоки в настройках.
+Боковое меню и дерево
-Карточки верхнего уровня каталога
-Группы внутри раздела
-Карточки сервисов — только узор, без акцентного градиента
-+ Подложка размывает узоры нижних уровней под разделами, подразделами и карточками. +
+Свой градиент
@@ -511,6 +500,24 @@+ Сброс, экспорт и импорт настроек интерфейса на этом устройстве. +
+ + +Разделы каталога, дерево навигации и блоки в настройках.
+Боковое меню и дерево
-Карточки верхнего уровня каталога
-Группы внутри раздела
-Карточки сервисов — только узор, без акцентного градиента
-+ Подложка размывает узоры нижних уровней под разделами, подразделами и карточками. +
+Свой градиент
@@ -511,6 +500,24 @@+ Сброс, экспорт и импорт настроек интерфейса на этом устройстве. +
+ + +Вставьте 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 @@Разделы каталога, дерево навигации и блоки в настройках.
+Оформление
@@ -374,6 +403,11 @@Узор и градиент настраиваются отдельно для каждого типа блоков.
Боковое меню и дерево
-Карточки верхнего уровня каталога
-Группы внутри раздела
-Карточки сервисов — только узор, без акцентного градиента
-+ Подложка размывает узоры нижних уровней под разделами, подразделами и карточками. +
+Свой градиент
@@ -512,6 +524,25 @@+ Сброс, экспорт и импорт настроек интерфейса на этом устройстве. +
+ + +Вставьте 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. */