глобальные изменения кастомизации каждой панели, импорт-экспорт-сброс настроек, плавное закрытие скрытие

This commit is contained in:
Илья Братских 2026-06-29 11:27:36 +03:00
parent 26dc3bac2b
commit e1fd7d2713
17 changed files with 3008 additions and 581 deletions

283
README.md
View File

@ -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. На `<html>`: `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-*` на `<html>` и 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` | Раскрытые/свернутые ветки дерева и каталога |
## Разработка

View File

@ -1,3 +1,3 @@
{
"liveServer.settings.port": 5502
"liveServer.settings.port": 10001
}

View File

@ -276,6 +276,18 @@
</span>
<input type="checkbox" data-show-numbers />
</label>
<div class="control-group collapse-animation-group">
<span>Раскрытие списков</span>
<button class="segmented" type="button" data-collapse-animation="instant">Резко</button>
<button class="segmented is-active" type="button" data-collapse-animation="animated">Плавно</button>
</div>
<p class="settings-section-hint">Разделы каталога, дерево навигации и блоки в настройках.</p>
<label class="accent-custom-row" data-collapse-duration-row>
<span>Длительность анимации</span>
<input type="range" min="100" max="800" step="25" value="240" data-collapse-duration />
<output data-collapse-duration-value>240 ms</output>
</label>
</section>
<section class="settings-section" data-settings-section="tree">
@ -382,15 +394,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Боковое меню и дерево</p>
<div class="pattern-swatches">
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="none" aria-pressed="false"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="nav" data-panel-pattern="hex" aria-pressed="true"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="nav"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="nav" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="nav" data-panel-pattern-field="height" /><output data-panel-pattern-value="nav" data-panel-pattern-field="height">45%</output></label>
@ -409,15 +413,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Карточки верхнего уровня каталога</p>
<div class="pattern-swatches">
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="none" aria-pressed="false"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="section" data-panel-pattern="hex" aria-pressed="true"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="section"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="section" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="section" data-panel-pattern-field="height" /><output data-panel-pattern-value="section" data-panel-pattern-field="height">45%</output></label>
@ -436,15 +432,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Группы внутри раздела</p>
<div class="pattern-swatches">
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="group" data-panel-pattern="none" aria-pressed="true"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="hex" aria-pressed="false"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="group"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="group" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="group" data-panel-pattern-field="height" /><output data-panel-pattern-value="group" data-panel-pattern-field="height">45%</output></label>
@ -463,15 +451,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Карточки сервисов — только узор, без акцентного градиента</p>
<div class="pattern-swatches">
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="item" data-panel-pattern="none" aria-pressed="true"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="hex" aria-pressed="false"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="item"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="item" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="item" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="item" data-panel-pattern-field="height" /><output data-panel-pattern-value="item" data-panel-pattern-field="height">45%</output></label>
@ -481,6 +461,15 @@
</details>
</div>
<label class="accent-custom-row pattern-isolation-row" data-feature="allowPanelPatternChange">
<span>Разделение слоёв</span>
<input type="range" min="0" max="100" step="1" value="46" data-pattern-isolation />
<output data-pattern-isolation-value>46%</output>
</label>
<p class="settings-section-hint" data-feature="allowPanelPatternChange">
Подложка размывает узоры нижних уровней под разделами, подразделами и карточками.
</p>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>
@ -511,6 +500,24 @@
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
</div>
</section>
<div class="settings-actions">
<p class="settings-section-hint">
Сброс, экспорт и импорт настроек интерфейса на этом устройстве.
</p>
<div class="settings-actions-buttons">
<button class="settings-action-button" type="button" data-settings-reset>
Сбросить
</button>
<button class="settings-action-button" type="button" data-settings-export>
Экспортировать
</button>
<button class="settings-action-button" type="button" data-settings-import>
Импортировать
</button>
</div>
<input type="file" accept="application/json,.json" data-settings-import-input hidden />
</div>
</div>
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->

View File

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

View File

@ -276,6 +276,18 @@
</span>
<input type="checkbox" data-show-numbers />
</label>
<div class="control-group collapse-animation-group">
<span>Раскрытие списков</span>
<button class="segmented" type="button" data-collapse-animation="instant">Резко</button>
<button class="segmented is-active" type="button" data-collapse-animation="animated">Плавно</button>
</div>
<p class="settings-section-hint">Разделы каталога, дерево навигации и блоки в настройках.</p>
<label class="accent-custom-row" data-collapse-duration-row>
<span>Длительность анимации</span>
<input type="range" min="100" max="800" step="25" value="240" data-collapse-duration />
<output data-collapse-duration-value>240 ms</output>
</label>
</section>
<section class="settings-section" data-settings-section="tree">
@ -382,15 +394,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Боковое меню и дерево</p>
<div class="pattern-swatches">
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="none" aria-pressed="false"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="nav" data-panel-pattern="hex" aria-pressed="true"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="nav"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="nav" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="nav" data-panel-pattern-field="height" /><output data-panel-pattern-value="nav" data-panel-pattern-field="height">45%</output></label>
@ -409,15 +413,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Карточки верхнего уровня каталога</p>
<div class="pattern-swatches">
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="none" aria-pressed="false"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="section" data-panel-pattern="hex" aria-pressed="true"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="section"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="section" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="section" data-panel-pattern-field="height" /><output data-panel-pattern-value="section" data-panel-pattern-field="height">45%</output></label>
@ -436,15 +432,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Группы внутри раздела</p>
<div class="pattern-swatches">
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="group" data-panel-pattern="none" aria-pressed="true"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="hex" aria-pressed="false"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="group"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="group" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="group" data-panel-pattern-field="height" /><output data-panel-pattern-value="group" data-panel-pattern-field="height">45%</output></label>
@ -463,15 +451,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Карточки сервисов — только узор, без акцентного градиента</p>
<div class="pattern-swatches">
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="item" data-panel-pattern="none" aria-pressed="true"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="hex" aria-pressed="false"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="item"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="item" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="item" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="item" data-panel-pattern-field="height" /><output data-panel-pattern-value="item" data-panel-pattern-field="height">45%</output></label>
@ -481,6 +461,15 @@
</details>
</div>
<label class="accent-custom-row pattern-isolation-row" data-feature="allowPanelPatternChange">
<span>Разделение слоёв</span>
<input type="range" min="0" max="100" step="1" value="46" data-pattern-isolation />
<output data-pattern-isolation-value>46%</output>
</label>
<p class="settings-section-hint" data-feature="allowPanelPatternChange">
Подложка размывает узоры нижних уровней под разделами, подразделами и карточками.
</p>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>
@ -511,6 +500,24 @@
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
</div>
</section>
<div class="settings-actions">
<p class="settings-section-hint">
Сброс, экспорт и импорт настроек интерфейса на этом устройстве.
</p>
<div class="settings-actions-buttons">
<button class="settings-action-button" type="button" data-settings-reset>
Сбросить
</button>
<button class="settings-action-button" type="button" data-settings-export>
Экспортировать
</button>
<button class="settings-action-button" type="button" data-settings-import>
Импортировать
</button>
</div>
<input type="file" accept="application/json,.json" data-settings-import-input hidden />
</div>
</div>
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->

View File

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

View File

@ -276,6 +276,18 @@
</span>
<input type="checkbox" data-show-numbers />
</label>
<div class="control-group collapse-animation-group">
<span>Раскрытие списков</span>
<button class="segmented" type="button" data-collapse-animation="instant">Резко</button>
<button class="segmented is-active" type="button" data-collapse-animation="animated">Плавно</button>
</div>
<p class="settings-section-hint">Разделы каталога, дерево навигации и блоки в настройках.</p>
<label class="accent-custom-row" data-collapse-duration-row>
<span>Длительность анимации</span>
<input type="range" min="100" max="800" step="25" value="240" data-collapse-duration />
<output data-collapse-duration-value>240 ms</output>
</label>
</section>
<section class="settings-section" data-settings-section="tree">
@ -382,15 +394,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Боковое меню и дерево</p>
<div class="pattern-swatches">
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="none" aria-pressed="false"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="nav" data-panel-pattern="hex" aria-pressed="true"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="nav"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="nav" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="nav" data-panel-pattern-field="height" /><output data-panel-pattern-value="nav" data-panel-pattern-field="height">45%</output></label>
@ -409,15 +413,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Карточки верхнего уровня каталога</p>
<div class="pattern-swatches">
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="none" aria-pressed="false"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="section" data-panel-pattern="hex" aria-pressed="true"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="section"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="section" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="section" data-panel-pattern-field="height" /><output data-panel-pattern-value="section" data-panel-pattern-field="height">45%</output></label>
@ -436,15 +432,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Группы внутри раздела</p>
<div class="pattern-swatches">
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="group" data-panel-pattern="none" aria-pressed="true"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="hex" aria-pressed="false"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="group"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="group" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="group" data-panel-pattern-field="height" /><output data-panel-pattern-value="group" data-panel-pattern-field="height">45%</output></label>
@ -463,15 +451,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Карточки сервисов — только узор, без акцентного градиента</p>
<div class="pattern-swatches">
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="item" data-panel-pattern="none" aria-pressed="true"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="hex" aria-pressed="false"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="item"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="item" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="item" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="item" data-panel-pattern-field="height" /><output data-panel-pattern-value="item" data-panel-pattern-field="height">45%</output></label>
@ -481,6 +461,15 @@
</details>
</div>
<label class="accent-custom-row pattern-isolation-row" data-feature="allowPanelPatternChange">
<span>Разделение слоёв</span>
<input type="range" min="0" max="100" step="1" value="46" data-pattern-isolation />
<output data-pattern-isolation-value>46%</output>
</label>
<p class="settings-section-hint" data-feature="allowPanelPatternChange">
Подложка размывает узоры нижних уровней под разделами, подразделами и карточками.
</p>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>
@ -511,6 +500,24 @@
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
</div>
</section>
<div class="settings-actions">
<p class="settings-section-hint">
Сброс, экспорт и импорт настроек интерфейса на этом устройстве.
</p>
<div class="settings-actions-buttons">
<button class="settings-action-button" type="button" data-settings-reset>
Сбросить
</button>
<button class="settings-action-button" type="button" data-settings-export>
Экспортировать
</button>
<button class="settings-action-button" type="button" data-settings-import>
Импортировать
</button>
</div>
<input type="file" accept="application/json,.json" data-settings-import-input hidden />
</div>
</div>
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->

View File

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

View File

@ -276,6 +276,18 @@
</span>
<input type="checkbox" data-show-numbers />
</label>
<div class="control-group collapse-animation-group">
<span>Раскрытие списков</span>
<button class="segmented" type="button" data-collapse-animation="instant">Резко</button>
<button class="segmented is-active" type="button" data-collapse-animation="animated">Плавно</button>
</div>
<p class="settings-section-hint">Разделы каталога, дерево навигации и блоки в настройках.</p>
<label class="accent-custom-row" data-collapse-duration-row>
<span>Длительность анимации</span>
<input type="range" min="100" max="800" step="25" value="240" data-collapse-duration />
<output data-collapse-duration-value>240 ms</output>
</label>
</section>
<section class="settings-section" data-settings-section="tree">
@ -382,15 +394,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Боковое меню и дерево</p>
<div class="pattern-swatches">
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="none" aria-pressed="false"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="nav" data-panel-pattern="hex" aria-pressed="true"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="nav"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="nav" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="nav" data-panel-pattern-field="height" /><output data-panel-pattern-value="nav" data-panel-pattern-field="height">45%</output></label>
@ -409,15 +413,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Карточки верхнего уровня каталога</p>
<div class="pattern-swatches">
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="none" aria-pressed="false"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="section" data-panel-pattern="hex" aria-pressed="true"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="section"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="section" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="section" data-panel-pattern-field="height" /><output data-panel-pattern-value="section" data-panel-pattern-field="height">45%</output></label>
@ -436,15 +432,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Группы внутри раздела</p>
<div class="pattern-swatches">
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="group" data-panel-pattern="none" aria-pressed="true"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="hex" aria-pressed="false"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="group"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="group" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="group" data-panel-pattern-field="height" /><output data-panel-pattern-value="group" data-panel-pattern-field="height">45%</output></label>
@ -463,15 +451,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Карточки сервисов — только узор, без акцентного градиента</p>
<div class="pattern-swatches">
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="item" data-panel-pattern="none" aria-pressed="true"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="hex" aria-pressed="false"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="item"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="item" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="item" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="item" data-panel-pattern-field="height" /><output data-panel-pattern-value="item" data-panel-pattern-field="height">45%</output></label>
@ -481,6 +461,15 @@
</details>
</div>
<label class="accent-custom-row pattern-isolation-row" data-feature="allowPanelPatternChange">
<span>Разделение слоёв</span>
<input type="range" min="0" max="100" step="1" value="46" data-pattern-isolation />
<output data-pattern-isolation-value>46%</output>
</label>
<p class="settings-section-hint" data-feature="allowPanelPatternChange">
Подложка размывает узоры нижних уровней под разделами, подразделами и карточками.
</p>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>
@ -511,6 +500,24 @@
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
</div>
</section>
<div class="settings-actions">
<p class="settings-section-hint">
Сброс, экспорт и импорт настроек интерфейса на этом устройстве.
</p>
<div class="settings-actions-buttons">
<button class="settings-action-button" type="button" data-settings-reset>
Сбросить
</button>
<button class="settings-action-button" type="button" data-settings-export>
Экспортировать
</button>
<button class="settings-action-button" type="button" data-settings-import>
Импортировать
</button>
</div>
<input type="file" accept="application/json,.json" data-settings-import-input hidden />
</div>
</div>
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->

View File

@ -12,51 +12,62 @@
"customAccentTo": "#22d3ee",
"customAccentAngle": 135,
"panelPatternNav": "hex",
"panelPatternNavOpacity": 70,
"panelPatternNavHeight": 45,
"panelPatternNavGradientOpacity": 80,
"panelPatternNavGradientHeight": 40,
"panelPatternNavOpacity": 58,
"panelPatternNavHeight": 38,
"panelPatternNavFade": 18,
"panelPatternNavGradientOpacity": 62,
"panelPatternNavGradientHeight": 34,
"panelPatternNavGradientFade": 16,
"panelPatternSection": "hex",
"panelPatternSectionOpacity": 70,
"panelPatternSectionHeight": 45,
"panelPatternSectionGradientOpacity": 80,
"panelPatternSectionGradientHeight": 40,
"panelPatternSectionOpacity": 58,
"panelPatternSectionHeight": 38,
"panelPatternSectionFade": 18,
"panelPatternSectionGradientOpacity": 62,
"panelPatternSectionGradientHeight": 34,
"panelPatternSectionGradientFade": 16,
"panelPatternGroup": "none",
"panelPatternGroupOpacity": 70,
"panelPatternGroupHeight": 45,
"panelPatternGroupGradientOpacity": 80,
"panelPatternGroupGradientHeight": 40,
"panelPatternGroupOpacity": 58,
"panelPatternGroupHeight": 38,
"panelPatternGroupFade": 18,
"panelPatternGroupGradientOpacity": 62,
"panelPatternGroupGradientHeight": 34,
"panelPatternGroupGradientFade": 16,
"panelPatternItem": "none",
"panelPatternItemOpacity": 70,
"panelPatternItemHeight": 45,
"panelPatternItemGradientOpacity": 80,
"panelPatternItemGradientHeight": 40,
"panelPatternItemOpacity": 58,
"panelPatternItemHeight": 38,
"panelPatternItemFade": 18,
"panelPatternItemGradientOpacity": 62,
"panelPatternItemGradientHeight": 34,
"panelPatternItemGradientFade": 16,
"patternIsolation": 46,
"showNumbers": false,
"showTreeTags": true,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 2,
"treeColumns": 1,
"autoCollapseSearch": true,
"sidebarWidth": 390,
"sidebarWidth": 360,
"topBarHeight": 140,
"shellMaxWidth": 2800,
"sidebarMinWidth": 260,
"sidebarMaxWidth": 800,
"sidebarMaxWidthWide": 1400,
"treeSidebarBaseWidth": 420,
"treeSidebarColumnWidth": 120,
"shellMaxWidth": 2240,
"sidebarMinWidth": 240,
"sidebarMaxWidth": 720,
"sidebarMaxWidthWide": 1040,
"treeSidebarBaseWidth": 360,
"treeSidebarColumnWidth": 96,
"topBarMinHeight": 80,
"topBarMaxHeight": 560,
"catalogCardsColumns": 3,
"catalogListColumns": 1,
"catalogCompactCardsColumns": 5,
"catalogCompactListColumns": 3,
"catalogCompactCardsColumns": 4,
"catalogCompactListColumns": 2,
"collapseAnimation": "animated",
"collapseDuration": 260,
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
"settingsPanelWidth": 460
"settingsPanelWidth": 440
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
"sidebarMinWidth": { "min": 200, "max": 480 },
"sidebarMinWidth": { "min": 220, "max": 480 },
"sidebarMaxWidth": { "min": 400, "max": 1200 },
"sidebarMaxWidthWide": { "min": 560, "max": 2000 },
"treeSidebarBaseWidth": { "min": 280, "max": 720 },

View File

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

View File

@ -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. */

View File

@ -1,4 +1,19 @@
<!doctype html>
<!--
ecosystem-panel / index.html
ОГЛАВЛЕНИЕ РАЗМЕТКИ:
- .ambient-* — декоративный фон
- .side-panel — навигация: заголовок, поиск, дерево (.hero-content)
- .sidebar-resizer — drag ширины меню
- .top-bar-resizer — drag высоты шапки (режим «Сверху»)
- .control-panel — popup настроек ⚙ (секции data-settings-section)
- .catalog — каталог карточек (рендер в script.js)
- script.js — data-ecosystem-panel, settings.json по умолчанию
Настройки ⚙ (сверху вниз): layout → dimensions → catalog → tree → appearance → actions.
Узоры: data-pattern-targets-panel, контейнеры data-pattern-swatches="{nav|section|group|item}".
-->
<html lang="ru">
<head>
<meta charset="UTF-8" />
@ -82,13 +97,14 @@
<div class="settings-overlay" data-settings-overlay></div>
<!--
Popup настроек:
- тема;
- режим отображения карточек;
- видимость нумерации;
- видимость тегов в дереве;
- авто-раскрытие дерева при поиске;
- цветовой акцент.
Popup настроек (⚙). Секции data-settings-section:
- layout — макет, дерево, ручки resize, ширина popup
- dimensions — лимиты shell/sidebar/topBar (collapsible)
- catalog — вид карточек, колонки, нумерация, анимация раскрытия
- tree — колонки дерева, теги, тип, авто-раскрытие поиска
- appearance — тема, акцент, узоры, разделение слоёв
- (footer) — settings-actions: сброс / экспорт / импорт preferences
Видимость блоков: features в settings.json + data-feature на элементах.
-->
<div class="control-panel" aria-label="Настройки панели" data-settings-panel>
<div class="settings-panel-head">
@ -277,6 +293,18 @@
</span>
<input type="checkbox" data-show-numbers />
</label>
<div class="control-group collapse-animation-group">
<span>Раскрытие списков</span>
<button class="segmented" type="button" data-collapse-animation="instant">Резко</button>
<button class="segmented is-active" type="button" data-collapse-animation="animated">Плавно</button>
</div>
<p class="settings-section-hint">Разделы каталога, дерево навигации и блоки в настройках.</p>
<label class="accent-custom-row" data-collapse-duration-row>
<span>Длительность анимации</span>
<input type="range" min="100" max="800" step="25" value="240" data-collapse-duration />
<output data-collapse-duration-value>240 ms</output>
</label>
</section>
<section class="settings-section" data-settings-section="tree">
@ -323,6 +351,7 @@
</label>
</section>
<!-- === Оформление: тема, акцент, узоры, разделение слоёв === -->
<section class="settings-section" data-settings-section="appearance">
<p class="settings-section-title">Оформление</p>
@ -374,6 +403,11 @@
</button>
</div>
<!--
Узоры по типам блоков. Кнопки пресетов — пустые data-pattern-swatches;
script.js → renderPatternSwatches(). Панель «Свой» — ensurePatternCustomPanels().
Слайдеры data-panel-pattern-tuning синхронизируются в syncPatternTargetPanels().
-->
<div class="pattern-targets-panel" data-pattern-targets-panel data-feature="allowPanelPatternChange">
<p class="settings-section-hint">Узор и градиент настраиваются отдельно для каждого типа блоков.</p>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="nav">
@ -383,15 +417,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Боковое меню и дерево</p>
<div class="pattern-swatches">
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="none" aria-pressed="false"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="nav" data-panel-pattern="hex" aria-pressed="true"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="nav" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="nav"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="nav" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="nav" data-panel-pattern-field="height" /><output data-panel-pattern-value="nav" data-panel-pattern-field="height">45%</output></label>
@ -410,15 +436,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Карточки верхнего уровня каталога</p>
<div class="pattern-swatches">
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="none" aria-pressed="false"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="section" data-panel-pattern="hex" aria-pressed="true"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="section" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="section"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="section" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="section" data-panel-pattern-field="height" /><output data-panel-pattern-value="section" data-panel-pattern-field="height">45%</output></label>
@ -437,15 +455,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Группы внутри раздела</p>
<div class="pattern-swatches">
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="group" data-panel-pattern="none" aria-pressed="true"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="hex" aria-pressed="false"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="group" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="group"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="group" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="group" data-panel-pattern-field="height" /><output data-panel-pattern-value="group" data-panel-pattern-field="height">45%</output></label>
@ -464,15 +474,7 @@
</summary>
<div class="settings-section-body">
<p class="settings-section-hint">Карточки сервисов — только узор, без акцентного градиента</p>
<div class="pattern-swatches">
<button class="pattern-swatch is-active" type="button" data-panel-pattern-target="item" data-panel-pattern="none" aria-pressed="true"><span class="pattern-swatch-label">Нет</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="hex" aria-pressed="false"><span class="pattern-swatch-label">Соты</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="dots" aria-pressed="false"><span class="pattern-swatch-label">Точки</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="grid" aria-pressed="false"><span class="pattern-swatch-label">Сетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="diagonal" aria-pressed="false"><span class="pattern-swatch-label">Диаг.</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="cross" aria-pressed="false"><span class="pattern-swatch-label">Клетка</span></button>
<button class="pattern-swatch" type="button" data-panel-pattern-target="item" data-panel-pattern="mesh" aria-pressed="false"><span class="pattern-swatch-label">Mesh</span></button>
</div>
<div class="pattern-swatches" data-pattern-swatches="item"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="item" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="item" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="item" data-panel-pattern-field="height" /><output data-panel-pattern-value="item" data-panel-pattern-field="height">45%</output></label>
@ -482,6 +484,16 @@
</details>
</div>
<!-- patternIsolation → --panel-layer-* (размывает узоры нижних уровней) -->
<label class="accent-custom-row pattern-isolation-row" data-feature="allowPanelPatternChange">
<span>Разделение слоёв</span>
<input type="range" min="0" max="100" step="1" value="46" data-pattern-isolation />
<output data-pattern-isolation-value>46%</output>
</label>
<p class="settings-section-hint" data-feature="allowPanelPatternChange">
Подложка размывает узоры нижних уровней под разделами, подразделами и карточками.
</p>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>
@ -512,6 +524,25 @@
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
</div>
</section>
<!-- Экспорт/импорт/сброс: ecosystem-panel-preferences JSON (script.js) -->
<div class="settings-actions">
<p class="settings-section-hint">
Сброс, экспорт и импорт настроек интерфейса на этом устройстве.
</p>
<div class="settings-actions-buttons">
<button class="settings-action-button" type="button" data-settings-reset>
Сбросить
</button>
<button class="settings-action-button" type="button" data-settings-export>
Экспортировать
</button>
<button class="settings-action-button" type="button" data-settings-import>
Импортировать
</button>
</div>
<input type="file" accept="application/json,.json" data-settings-import-input hidden />
</div>
</div>
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->

766
script.js
View File

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

View File

@ -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,

View File

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

View File

@ -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. */