глобальные изменения кастомизации каждой панели, импорт-экспорт-сброс настроек, плавное закрытие скрытие
This commit is contained in:
parent
26dc3bac2b
commit
e1fd7d2713
283
README.md
283
README.md
@ -19,8 +19,13 @@
|
||||
- [Готовые демо-страницы (`examples/`)](#готовые-демо-страницы-examples)
|
||||
- [Типы сервисов (`serviceMeta`)](#типы-сервисов-servicemeta)
|
||||
- [Рекомендуемые типы сервисов](#рекомендуемые-типы-сервисов)
|
||||
- [Настройки интерфейса (⚙)](#настройки-интерфейса-)
|
||||
- [Поведение интерфейса](#поведение-интерфейса)
|
||||
- [Настройки интерфейса (⚙)](#настройки-интерфейса-)
|
||||
- [Узоры панелей](#узоры-панелей)
|
||||
- [Разделение слоёв узоров](#разделение-слоёв-узоров)
|
||||
- [Свой узор (custom)](#свой-узор-custom)
|
||||
- [Экспорт и импорт настроек](#экспорт-и-импорт-настроек)
|
||||
- [Навигация по коду](#навигация-по-коду)
|
||||
- [Поведение интерфейса](#поведение-интерфейса)
|
||||
- [localStorage](#localstorage)
|
||||
- [Разработка](#разработка)
|
||||
- [Ограничения](#ограничения)
|
||||
@ -33,7 +38,9 @@
|
||||
- **Поиск** по названию, тегам, типу сервиса и родительским разделам
|
||||
- **4 режима отображения**: плитки, список, компактные плитки, компактный список
|
||||
- **Темы**: светлая / тёмная, 4 пресета акцента + свой градиент
|
||||
- **Узоры панелей** — соты, точки, сетка и др.; отдельные варианты для светлой темы; можно отключить
|
||||
- **Узоры панелей** — 13 встроенных пресетов + свой URL/SVG; отдельно для nav / section / group / item; слайдеры прозрачности, высоты и перехода
|
||||
- **Разделение слоёв** — подложка и blur, чтобы вложенные узоры не «наезжали» друг на друга
|
||||
- **Экспорт / импорт / сброс** настроек ⚙ в JSON-файл
|
||||
- **Настройки** через popup (иконка ⚙)
|
||||
- **Изменяемая ширина** бокового меню и **высота** верхней шапки (drag, можно скрыть ручки)
|
||||
- **Габариты** — лимиты ширины контента, меню и шапки (⚙, для администратора)
|
||||
@ -131,51 +138,62 @@ ecosystem/
|
||||
"customAccentTo": "#22d3ee",
|
||||
"customAccentAngle": 135,
|
||||
"panelPatternNav": "hex",
|
||||
"panelPatternNavOpacity": 70,
|
||||
"panelPatternNavHeight": 45,
|
||||
"panelPatternNavGradientOpacity": 80,
|
||||
"panelPatternNavGradientHeight": 40,
|
||||
"panelPatternNavOpacity": 58,
|
||||
"panelPatternNavHeight": 38,
|
||||
"panelPatternNavFade": 18,
|
||||
"panelPatternNavGradientOpacity": 62,
|
||||
"panelPatternNavGradientHeight": 34,
|
||||
"panelPatternNavGradientFade": 16,
|
||||
"panelPatternSection": "hex",
|
||||
"panelPatternSectionOpacity": 70,
|
||||
"panelPatternSectionHeight": 45,
|
||||
"panelPatternSectionGradientOpacity": 80,
|
||||
"panelPatternSectionGradientHeight": 40,
|
||||
"panelPatternSectionOpacity": 58,
|
||||
"panelPatternSectionHeight": 38,
|
||||
"panelPatternSectionFade": 18,
|
||||
"panelPatternSectionGradientOpacity": 62,
|
||||
"panelPatternSectionGradientHeight": 34,
|
||||
"panelPatternSectionGradientFade": 16,
|
||||
"panelPatternGroup": "none",
|
||||
"panelPatternGroupOpacity": 70,
|
||||
"panelPatternGroupHeight": 45,
|
||||
"panelPatternGroupGradientOpacity": 80,
|
||||
"panelPatternGroupGradientHeight": 40,
|
||||
"panelPatternGroupOpacity": 58,
|
||||
"panelPatternGroupHeight": 38,
|
||||
"panelPatternGroupFade": 18,
|
||||
"panelPatternGroupGradientOpacity": 62,
|
||||
"panelPatternGroupGradientHeight": 34,
|
||||
"panelPatternGroupGradientFade": 16,
|
||||
"panelPatternItem": "none",
|
||||
"panelPatternItemOpacity": 70,
|
||||
"panelPatternItemHeight": 45,
|
||||
"panelPatternItemGradientOpacity": 80,
|
||||
"panelPatternItemGradientHeight": 40,
|
||||
"panelPatternItemOpacity": 58,
|
||||
"panelPatternItemHeight": 38,
|
||||
"panelPatternItemFade": 18,
|
||||
"panelPatternItemGradientOpacity": 62,
|
||||
"panelPatternItemGradientHeight": 34,
|
||||
"panelPatternItemGradientFade": 16,
|
||||
"patternIsolation": 46,
|
||||
"showNumbers": false,
|
||||
"showTreeTags": true,
|
||||
"showTreeType": false,
|
||||
"treeMetaLayout": "inline",
|
||||
"treeColumns": 2,
|
||||
"treeColumns": 1,
|
||||
"autoCollapseSearch": true,
|
||||
"sidebarWidth": 390,
|
||||
"sidebarWidth": 360,
|
||||
"topBarHeight": 140,
|
||||
"shellMaxWidth": 2800,
|
||||
"sidebarMinWidth": 260,
|
||||
"sidebarMaxWidth": 800,
|
||||
"sidebarMaxWidthWide": 1400,
|
||||
"treeSidebarBaseWidth": 420,
|
||||
"treeSidebarColumnWidth": 120,
|
||||
"shellMaxWidth": 2240,
|
||||
"sidebarMinWidth": 240,
|
||||
"sidebarMaxWidth": 720,
|
||||
"sidebarMaxWidthWide": 1040,
|
||||
"treeSidebarBaseWidth": 360,
|
||||
"treeSidebarColumnWidth": 96,
|
||||
"topBarMinHeight": 80,
|
||||
"topBarMaxHeight": 560,
|
||||
"catalogCardsColumns": 3,
|
||||
"catalogListColumns": 1,
|
||||
"catalogCompactCardsColumns": 5,
|
||||
"catalogCompactListColumns": 3,
|
||||
"catalogCompactCardsColumns": 4,
|
||||
"catalogCompactListColumns": 2,
|
||||
"collapseAnimation": "animated",
|
||||
"collapseDuration": 260,
|
||||
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
|
||||
"settingsPanelWidth": 460
|
||||
"settingsPanelWidth": 440
|
||||
},
|
||||
"limits": {
|
||||
"shellMaxWidth": { "min": 1200, "max": 3840 },
|
||||
"sidebarMinWidth": { "min": 200, "max": 480 },
|
||||
"sidebarMinWidth": { "min": 220, "max": 480 },
|
||||
"sidebarMaxWidth": { "min": 400, "max": 1200 },
|
||||
"sidebarMaxWidthWide": { "min": 560, "max": 2000 },
|
||||
"treeSidebarBaseWidth": { "min": 280, "max": 720 },
|
||||
@ -216,17 +234,20 @@ ecosystem/
|
||||
| `customAccentTo` | `#rrggbb` | Цвет «до» для `accent: "custom"` (по умолчанию `#22d3ee`) |
|
||||
| `customAccentAngle` | `0`–`360` | Угол градиента в градусах (по умолчанию `135`) |
|
||||
| `panelPatternNav` … `panelPatternItem` | см. [узоры панелей](#узоры-панелей) | Тип узора для каждого типа блоков (`none`, `hex`, …) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}Opacity` | `0`–`100` | Прозрачность узора в % (по умолчанию `70`) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}Height` | `0`–`100` | Высота узора в % (по умолчанию `45`; `0` — скрыть) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}Fade` | `0`–`100` | Ширина зоны затухания узора в % высоты блока: `0` — резкий обрыв на линии высоты, `100` — плавное затухание от верха (по умолчанию `10`) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}GradientOpacity` | `0`–`100` | Прозрачность акцентного градиента в % (по умолчанию `80`) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}GradientHeight` | `0`–`100` | Высота градиента в % (по умолчанию `40`) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}GradientFade` | `0`–`100` | Зона затухания градиента (по умолчанию `8`) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}Opacity` | `0`–`100` | Прозрачность узора в % (по умолчанию `58`) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}Height` | `0`–`100` | Высота узора в % (по умолчанию `38`; `0` — скрыть) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}Fade` | `0`–`100` | Ширина зоны затухания узора в % высоты блока: `0` — резкий обрыв на линии высоты, `100` — плавное затухание от верха (по умолчанию `18`) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}GradientOpacity` | `0`–`100` | Прозрачность акцентного градиента в % (по умолчанию `62`) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}GradientHeight` | `0`–`100` | Высота градиента в % (по умолчанию `34`) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}GradientFade` | `0`–`100` | Зона затухания градиента (по умолчанию `16`) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}CustomImage` | строка URL | Изображение для `custom` (пусто по умолчанию) |
|
||||
| `panelPattern{Nav\|Section\|Group\|Item}CustomSize` | `"WxH"` в px | Размер плитки custom (по умолчанию `32px 32px`) |
|
||||
| `patternIsolation` | `0`–`100` | Разделение слоёв узоров (подложка + blur; по умолчанию `46`) |
|
||||
| `showNumbers` | `true`, `false` | Показывать номера разделов, подразделов и элементов |
|
||||
| `showTreeTags` | `true`, `false` | Теги элементов в дереве (оформление как на карточках) |
|
||||
| `showTreeType` | `true`, `false` | Подпись типа сервиса в дереве |
|
||||
| `treeMetaLayout` | `"inline"`, `"stacked"` | Расположение тегов и типа: сбоку от названия или снизу |
|
||||
| `treeColumns` | `1`–`6` (по умолчанию `2`) | Число колонок элементов внутри папок дерева |
|
||||
| `treeColumns` | `1`–`6` (по умолчанию `1`) | Число колонок элементов внутри папок дерева |
|
||||
| `autoCollapseSearch` | `true`, `false` | При поиске сворачивать ветки дерева без совпадений |
|
||||
| `sidebarWidth` | `sidebarMinWidth`–`sidebarMaxWidth` (или `sidebarMaxWidthWide` при `treeColumns` > 1) | Текущая ширина бокового меню (drag-resize); при `navLayout: "top"` не применяется |
|
||||
| `topBarHeight` | `topBarMinHeight`–`topBarMaxHeight` (до ~75% высоты окна) | Текущая высота верхней шапки (drag-resize); только при `navLayout: "top"` |
|
||||
@ -242,8 +263,10 @@ ecosystem/
|
||||
| `catalogListColumns` | см. `limits.catalogListColumns` | Колонок в строке в режиме «Список» |
|
||||
| `catalogCompactCardsColumns` | см. `limits.catalogCompactCardsColumns` | Колонок в строке в режиме «Компакт плитки» |
|
||||
| `catalogCompactListColumns` | см. `limits.catalogCompactListColumns` | Колонок в строке в режиме «Компакт список» |
|
||||
| `collapseAnimation` | `"instant"`, `"animated"` | Режим раскрытия списков, дерева и collapsible-блоков |
|
||||
| `collapseDuration` | `100`–`800` | Длительность плавного раскрытия в миллисекундах (по умолчанию `260`) |
|
||||
| `searchPlaceholder` | произвольная строка | Текст-подсказка в поле поиска |
|
||||
| `settingsPanelWidth` | `limits.settingsPanelWidth` (по умолчанию `320`–`720`) | Ширина popup настроек в px (по умолчанию `460`) |
|
||||
| `settingsPanelWidth` | `limits.settingsPanelWidth` (по умолчанию `320`–`720`) | Ширина popup настроек в px (по умолчанию `440`) |
|
||||
|
||||
| `limits` | Формат | Описание |
|
||||
|----------|--------|----------|
|
||||
@ -443,7 +466,8 @@ Popup сгруппирован по разделам. Доступные бло
|
||||
| **Габариты** | Лимиты ширины контента, меню и шапки (сворачиваемый блок; при `allowLayoutLimitsChange`) |
|
||||
| **Каталог** | Вид карточек, колонок в строке для каждого вида, нумерация |
|
||||
| **Дерево** | Колонок 1–6, теги, тип, расположение мета, авто-раскрытие (при `showTree: true`) |
|
||||
| **Оформление** | Тема, акцент, узор панелей, свой градиент |
|
||||
| **Оформление** | Тема, акцент, узоры по типам блоков, разделение слоёв, свой градиент |
|
||||
| **(footer)** | Сброс, экспорт и импорт всех preferences в JSON |
|
||||
|
||||
### Режим «Сверху»
|
||||
|
||||
@ -464,31 +488,172 @@ Popup сгруппирован по разделам. Доступные бло
|
||||
|
||||
### Узоры панелей
|
||||
|
||||
Настраиваются в ⚙ → **Оформление** (если `allowPanelPatternChange: true`): четыре схлопывающиеся панели — **Панель навигации**, **Раздел**, **Подраздел**, **Элемент**. В каждой — свой тип узора и слайдеры.
|
||||
Настраиваются в ⚙ → **Оформление** (если `allowPanelPatternChange: true`).
|
||||
|
||||
| Значение `panelPattern*` | Описание |
|
||||
|--------------------------|----------|
|
||||
| `none` | Без узора — только акцентный градиент (если применим) |
|
||||
| `hex` | Соты |
|
||||
| `dots` | Точечная сетка |
|
||||
| `grid` | Линейная сетка |
|
||||
| `diagonal` | Диагональные полосы |
|
||||
| `cross` | Перекрёстная штриховка |
|
||||
| `mesh` | Точки цвета акцента |
|
||||
#### Четыре цели (targets)
|
||||
|
||||
По умолчанию для **навигации** и **разделов** — `hex`; для **подразделов** и **элементов** — `none`.
|
||||
У каждого типа блока свой независимый набор: пресет, слайдеры и (опционально) свой URL.
|
||||
|
||||
| Панель | Preference (префикс) | DOM |
|
||||
|--------|----------------------|-----|
|
||||
| Панель навигации | `panelPatternNav*` | `.hero-content` |
|
||||
| Раздел | `panelPatternSection*` | `.catalog-section` |
|
||||
| Подраздел | `panelPatternGroup*` | `.catalog-group` |
|
||||
| Элемент | `panelPatternItem*` | `.nav-card` — только узор, без акцентного градиента |
|
||||
| Цель | Preference (префикс) | DOM-элемент | Градиент под узором |
|
||||
|------|----------------------|-------------|---------------------|
|
||||
| Панель навигации | `panelPatternNav*` | `.hero-content` | да |
|
||||
| Раздел | `panelPatternSection*` | `.catalog-section` | да |
|
||||
| Подраздел | `panelPatternGroup*` | `.catalog-group` | да |
|
||||
| Элемент | `panelPatternItem*` | `.nav-card` | нет (только узор) |
|
||||
|
||||
В каждой панели (кроме «Элемент») — шесть слайдеров: прозрачность, высота и **переход** узора; то же для градиента. Для «Элемент» — три слайдера узора (без градиента). **Переход** задаёт ширину зоны затухания: `0%` — резкий обрыв на линии «высоты», большее значение — более плавное исчезновение (при значении ≥ высоты узор растворяется от самого верха блока).
|
||||
#### Встроенные пресеты (`panelPattern*`)
|
||||
|
||||
| id | Подпись в ⚙ | Описание |
|
||||
|----|-------------|----------|
|
||||
| `none` | Нет | Без повторяющейся текстуры |
|
||||
| `hex` | Соты | Крупные гексагоны |
|
||||
| `hex-sm` | Соты м. | Мелкие гексагоны |
|
||||
| `dots` | Точки | Средняя точечная сетка |
|
||||
| `dots-sm` | Точки м. | Мелкие кружки |
|
||||
| `dots-lg` | Точки кр. | Крупные кружки |
|
||||
| `grid` | Сетка | Линейная сетка |
|
||||
| `diagonal` | Диаг. | Диагональные полосы |
|
||||
| `cross` | Клетка | Перекрёстная штриховка |
|
||||
| `mesh` | Mesh | Точки цвета акцента |
|
||||
| `tri-sm` | △ мел. | Мелкие треугольники |
|
||||
| `tri-lg` | △ кр. | Крупные треугольники |
|
||||
| `diamond` | Ромбы | Ромбовидная сетка |
|
||||
| `custom` | Свой | Своя текстура (см. ниже) |
|
||||
|
||||
По умолчанию: **навигация** и **разделы** — `hex`; **подразделы** и **элементы** — `none`.
|
||||
|
||||
#### Слайдеры (для каждой цели)
|
||||
|
||||
| Сuffix preference | UI | Диапазон | По умолчанию |
|
||||
|-------------------|-----|----------|--------------|
|
||||
| `Opacity` | Узор: прозрачность | 0–100 % | 58 |
|
||||
| `Height` | Узор: высота | 0–100 % высоты блока | 38 |
|
||||
| `Fade` | Узор: переход | 0–100 % | 18 |
|
||||
| `GradientOpacity` | Градиент: прозрачность | 0–100 % | 62 |
|
||||
| `GradientHeight` | Градиент: высота | 0–100 % | 34 |
|
||||
| `GradientFade` | Градиент: переход | 0–100 % | 16 |
|
||||
|
||||
**Переход (fade):** `0%` — резкий обрыв на линии «высоты»; большее значение — плавнее затухание к низу блока.
|
||||
|
||||
Для цели **Элемент** в UI только три слайдера узора (без градиента).
|
||||
|
||||
#### Как узор попадает на экран
|
||||
|
||||
1. `preferences` → `applyPanelPatternPreferences()` в `script.js`
|
||||
2. На `<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` | Раскрытые/свернутые ветки дерева и каталога |
|
||||
|
||||
## Разработка
|
||||
|
||||
2
examples/.vscode/settings.json
vendored
2
examples/.vscode/settings.json
vendored
@ -1,3 +1,3 @@
|
||||
{
|
||||
"liveServer.settings.port": 5502
|
||||
"liveServer.settings.port": 10001
|
||||
}
|
||||
@ -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. -->
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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. -->
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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. -->
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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. -->
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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. */
|
||||
|
||||
117
index.html
117
index.html
@ -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
766
script.js
@ -1,3 +1,29 @@
|
||||
// ecosystem-panel / script.js
|
||||
//
|
||||
// ОГЛАВЛЕНИЕ — ищите секции по строке «// =====» (Ctrl+F):
|
||||
// Persistent storage keys
|
||||
// Panel configuration (settings.json)
|
||||
// Panel pattern catalog and defaults
|
||||
// Default UI preferences
|
||||
// Runtime state and DOM references
|
||||
// Initial render
|
||||
// Event bindings
|
||||
// Panel config loading
|
||||
// Data helpers
|
||||
// Catalog rendering
|
||||
// Search matching and highlighting
|
||||
// Sidebar tree rendering
|
||||
// Focusing from tree to content
|
||||
// Collapse state handling
|
||||
// Card pointer interaction
|
||||
// Preferences application
|
||||
// Generic helpers
|
||||
// Panel pattern preferences (normalize, apply, custom, UI)
|
||||
// localStorage helpers (read/save, import/export/reset)
|
||||
//
|
||||
// Связанные файлы: index.html (разметка ⚙), styles.css (узоры, темы, слои).
|
||||
// Документация: README.md → «Узоры панелей», «Разделение слоёв», «Экспорт настроек».
|
||||
|
||||
// ============================================================================
|
||||
// Persistent storage keys
|
||||
// ============================================================================
|
||||
@ -65,16 +91,73 @@ const defaultServiceMeta = {
|
||||
|
||||
let serviceMeta = { ...defaultServiceMeta };
|
||||
|
||||
const availablePanelPatterns = ["none", "hex", "dots", "grid", "diagonal", "cross", "mesh"];
|
||||
// ============================================================================
|
||||
// Panel pattern catalog and defaults
|
||||
// ============================================================================
|
||||
// Узоры накладываются на четыре типа блоков интерфейса (см. panelPatternTargetIds).
|
||||
//
|
||||
// Как это работает:
|
||||
// 1. panelPatternCatalog — пресеты для кнопок в ⚙ (генерируются в renderPatternSwatches).
|
||||
// 2. preferences.panelPattern{Nav|Section|Group|Item} — выбранный id пресета или "custom".
|
||||
// 3. applyPanelPatternPreferences() выставляет data-panel-pattern-* на <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");
|
||||
|
||||
@ -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,
|
||||
|
||||
107
settings.json
107
settings.json
@ -2,61 +2,80 @@
|
||||
"dataUrl": "./panel-data.json",
|
||||
"data": null,
|
||||
"defaults": {
|
||||
"navLayout": "sidebar",
|
||||
"showTree": true,
|
||||
"showResizeHandles": true,
|
||||
"navLayout": "top",
|
||||
"showTree": false,
|
||||
"showResizeHandles": false,
|
||||
"theme": "dark",
|
||||
"view": "cards",
|
||||
"accent": "violet",
|
||||
"customAccentFrom": "#8b5cf6",
|
||||
"customAccentTo": "#22d3ee",
|
||||
"accent": "custom",
|
||||
"customAccentFrom": "#ff428e",
|
||||
"customAccentTo": "#5bd8ec",
|
||||
"customAccentAngle": 135,
|
||||
"panelPatternNav": "hex",
|
||||
"panelPatternNavOpacity": 70,
|
||||
"panelPatternNavHeight": 45,
|
||||
"panelPatternNavGradientOpacity": 80,
|
||||
"panelPatternNavGradientHeight": 40,
|
||||
"panelPatternSection": "hex",
|
||||
"panelPatternSectionOpacity": 70,
|
||||
"panelPatternSectionHeight": 45,
|
||||
"panelPatternSectionGradientOpacity": 80,
|
||||
"panelPatternSectionGradientHeight": 40,
|
||||
"panelPatternGroup": "none",
|
||||
"panelPatternGroupOpacity": 70,
|
||||
"panelPatternGroupHeight": 45,
|
||||
"panelPatternGroupGradientOpacity": 80,
|
||||
"panelPatternGroupGradientHeight": 40,
|
||||
"panelPatternItem": "none",
|
||||
"panelPatternItemOpacity": 70,
|
||||
"panelPatternItemHeight": 45,
|
||||
"panelPatternItemGradientOpacity": 80,
|
||||
"panelPatternItemGradientHeight": 40,
|
||||
"panelPatternNav": "diagonal",
|
||||
"panelPatternNavOpacity": 20,
|
||||
"panelPatternNavHeight": 100,
|
||||
"panelPatternNavFade": 100,
|
||||
"panelPatternNavGradientOpacity": 100,
|
||||
"panelPatternNavGradientHeight": 100,
|
||||
"panelPatternNavGradientFade": 100,
|
||||
"panelPatternNavCustomImage": "",
|
||||
"panelPatternNavCustomSize": "32px 32px",
|
||||
"panelPatternSection": "diamond",
|
||||
"panelPatternSectionOpacity": 30,
|
||||
"panelPatternSectionHeight": 50,
|
||||
"panelPatternSectionFade": 60,
|
||||
"panelPatternSectionGradientOpacity": 75,
|
||||
"panelPatternSectionGradientHeight": 20,
|
||||
"panelPatternSectionGradientFade": 15,
|
||||
"panelPatternSectionCustomImage": "",
|
||||
"panelPatternSectionCustomSize": "32px 32px",
|
||||
"panelPatternGroup": "custom",
|
||||
"panelPatternGroupOpacity": 10,
|
||||
"panelPatternGroupHeight": 100,
|
||||
"panelPatternGroupFade": 100,
|
||||
"panelPatternGroupGradientOpacity": 20,
|
||||
"panelPatternGroupGradientHeight": 100,
|
||||
"panelPatternGroupGradientFade": 100,
|
||||
"panelPatternGroupCustomImage": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='s2' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23E2E8F0' /%3E%3Cstop offset='100%25' stop-color='%2364748B' /%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='20' y='20' width='24' height='24' fill='none' stroke='url(%23s2)' stroke-width='1.5' transform='rotate(45 32 32)' /%3E%3Crect x='16' y='16' width='32' height='32' fill='none' stroke='url(%23s2)' stroke-width='1' opacity='0.4' transform='rotate(45 32 32)' /%3E%3C/svg%3E",
|
||||
"panelPatternGroupCustomSize": "32px 32px",
|
||||
"panelPatternItem": "mesh",
|
||||
"panelPatternItemOpacity": 58,
|
||||
"panelPatternItemHeight": 100,
|
||||
"panelPatternItemFade": 18,
|
||||
"panelPatternItemGradientOpacity": 62,
|
||||
"panelPatternItemGradientHeight": 34,
|
||||
"panelPatternItemGradientFade": 16,
|
||||
"panelPatternItemCustomImage": "",
|
||||
"panelPatternItemCustomSize": "32px 32px",
|
||||
"patternIsolation": 65,
|
||||
"showNumbers": false,
|
||||
"showTreeTags": true,
|
||||
"showTreeTags": false,
|
||||
"showTreeType": false,
|
||||
"treeMetaLayout": "inline",
|
||||
"treeColumns": 2,
|
||||
"treeColumns": 1,
|
||||
"autoCollapseSearch": true,
|
||||
"sidebarWidth": 390,
|
||||
"topBarHeight": 140,
|
||||
"shellMaxWidth": 2800,
|
||||
"sidebarMinWidth": 260,
|
||||
"sidebarMaxWidth": 800,
|
||||
"sidebarMaxWidthWide": 1400,
|
||||
"treeSidebarBaseWidth": 420,
|
||||
"treeSidebarColumnWidth": 120,
|
||||
"topBarMinHeight": 80,
|
||||
"topBarMaxHeight": 560,
|
||||
"catalogCardsColumns": 3,
|
||||
"catalogListColumns": 1,
|
||||
"catalogCompactCardsColumns": 5,
|
||||
"sidebarWidth": 360,
|
||||
"topBarHeight": 72,
|
||||
"shellMaxWidth": 1100,
|
||||
"sidebarMinWidth": 240,
|
||||
"sidebarMaxWidth": 720,
|
||||
"sidebarMaxWidthWide": 1040,
|
||||
"treeSidebarBaseWidth": 360,
|
||||
"treeSidebarColumnWidth": 96,
|
||||
"topBarMinHeight": 64,
|
||||
"topBarMaxHeight": 800,
|
||||
"catalogCardsColumns": 2,
|
||||
"catalogListColumns": 2,
|
||||
"catalogCompactCardsColumns": 3,
|
||||
"catalogCompactListColumns": 3,
|
||||
"collapseAnimation": "animated",
|
||||
"collapseDuration": 250,
|
||||
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
|
||||
"settingsPanelWidth": 460
|
||||
"settingsPanelWidth": 500
|
||||
},
|
||||
"limits": {
|
||||
"shellMaxWidth": { "min": 1200, "max": 3840 },
|
||||
"sidebarMinWidth": { "min": 200, "max": 480 },
|
||||
"shellMaxWidth": { "min": 1100, "max": 3840 },
|
||||
"sidebarMinWidth": { "min": 220, "max": 480 },
|
||||
"sidebarMaxWidth": { "min": 400, "max": 1200 },
|
||||
"sidebarMaxWidthWide": { "min": 560, "max": 2000 },
|
||||
"treeSidebarBaseWidth": { "min": 280, "max": 720 },
|
||||
|
||||
408
styles.css
408
styles.css
@ -1,3 +1,23 @@
|
||||
/* ==========================================================================
|
||||
ecosystem-panel / styles.css
|
||||
--------------------------------------------------------------------------
|
||||
ОГЛАВЛЕНИЕ (поиск по «==» или номеру секции):
|
||||
01 Theme Tokens — цвета, layout, pattern tiles, pattern mapping, accents
|
||||
02 Base Reset And Page Background
|
||||
03 Application Layout — sidebar / top bar / resizers
|
||||
04 Catalog Area
|
||||
05 Sidebar Panel — hero, patterns ::before/::after, layer isolation
|
||||
06 Settings Popup — sections, pattern swatches, actions footer
|
||||
07 Search Field And Control Inputs
|
||||
08 File-Tree Navigation
|
||||
09 Catalog Sections And Groups
|
||||
10 Navigation Cards
|
||||
11 Catalog View Modes
|
||||
12 Responsive Rules
|
||||
13 Touch And Reduced Motion
|
||||
14 Print
|
||||
========================================================================== */
|
||||
|
||||
/* ==========================================================================
|
||||
01. Theme Tokens
|
||||
--------------------------------------------------------------------------
|
||||
@ -31,7 +51,9 @@
|
||||
--catalog-list-columns: 1;
|
||||
--catalog-compact-cards-columns: 5;
|
||||
--catalog-compact-list-columns: 3;
|
||||
/* --- Pattern: цвет штриха/точки в CSS- и SVG-узорах --- */
|
||||
--pattern-color: rgba(255, 255, 255, 0.16);
|
||||
/* --- Pattern tiles: повторяющаяся текстура (image + background-size) --- */
|
||||
--pattern-tile-hex-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
--pattern-tile-hex-size: 96px 84px;
|
||||
--pattern-tile-dots-image: radial-gradient(circle at center, var(--pattern-color) 0.95px, transparent 1px);
|
||||
@ -70,7 +92,23 @@
|
||||
transparent 0
|
||||
);
|
||||
--pattern-tile-mesh-size: 20px 20px;
|
||||
--pattern-tile-hex-sm-image: url("data:image/svg+xml,%3Csvg width='48' height='42' viewBox='0 0 48 42' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1'%3E%3Cpath d='M12 1 23 7v12L12 26 1 19V7L12 1Z'/%3E%3Cpath d='M36 16 47 22v12L36 41 25 34V22l11-6Z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
--pattern-tile-hex-sm-size: 48px 42px;
|
||||
--pattern-tile-dots-sm-image: radial-gradient(circle at center, var(--pattern-color) 0.6px, transparent 0.6px);
|
||||
--pattern-tile-dots-sm-size: 10px 10px;
|
||||
--pattern-tile-dots-lg-image: radial-gradient(circle at center, var(--pattern-color) 1.35px, transparent 1.35px);
|
||||
--pattern-tile-dots-lg-size: 28px 28px;
|
||||
--pattern-tile-tri-sm-image: url("data:image/svg+xml,%3Csvg width='20' height='18' viewBox='0 0 20 18' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='white' stroke-opacity='.2' stroke-width='1' d='M10 1 19 17 H1 Z'/%3E%3C/svg%3E");
|
||||
--pattern-tile-tri-sm-size: 20px 18px;
|
||||
--pattern-tile-tri-lg-image: url("data:image/svg+xml,%3Csvg width='40' height='35' viewBox='0 0 40 35' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.2' d='M20 2 38 33 H2 Z'/%3E%3C/svg%3E");
|
||||
--pattern-tile-tri-lg-size: 40px 35px;
|
||||
--pattern-tile-diamond-image: url("data:image/svg+xml,%3Csvg width='22' height='22' viewBox='0 0 22 22' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='white' stroke-opacity='.18' stroke-width='1' d='M11 1 21 11 11 21 1 11 Z'/%3E%3C/svg%3E");
|
||||
--pattern-tile-diamond-size: 22px 22px;
|
||||
--settings-panel-width: 460px;
|
||||
/* --- Pattern layer isolation (preference patternIsolation → JS) --- */
|
||||
--panel-layer-blur: 8px;
|
||||
--panel-layer-opacity: 0.27;
|
||||
--panel-layer-fill: rgba(15, 23, 42, var(--panel-layer-opacity));
|
||||
}
|
||||
|
||||
/* Светлая тема переопределяет только токены, структура стилей остается общей. */
|
||||
@ -87,37 +125,71 @@ html[data-theme="light"] {
|
||||
--shadow: 0 24px 70px rgba(56, 65, 91, 0.18);
|
||||
--pattern-color: rgba(15, 23, 42, 0.13);
|
||||
--pattern-tile-hex-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
--pattern-tile-hex-sm-image: url("data:image/svg+xml,%3Csvg width='48' height='42' viewBox='0 0 48 42' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1'%3E%3Cpath d='M12 1 23 7v12L12 26 1 19V7L12 1Z'/%3E%3Cpath d='M36 16 47 22v12L36 41 25 34V22l11-6Z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
--pattern-tile-tri-sm-image: url("data:image/svg+xml,%3Csvg width='20' height='18' viewBox='0 0 20 18' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='%23101827' stroke-opacity='.16' stroke-width='1' d='M10 1 19 17 H1 Z'/%3E%3C/svg%3E");
|
||||
--pattern-tile-tri-lg-image: url("data:image/svg+xml,%3Csvg width='40' height='35' viewBox='0 0 40 35' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.2' d='M20 2 38 33 H2 Z'/%3E%3C/svg%3E");
|
||||
--pattern-tile-diamond-image: url("data:image/svg+xml,%3Csvg width='22' height='22' viewBox='0 0 22 22' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1' d='M11 1 21 11 11 21 1 11 Z'/%3E%3C/svg%3E");
|
||||
--pattern-tile-mesh-image: radial-gradient(
|
||||
circle at 1px 1px,
|
||||
rgba(var(--accent-rgb), 0.16) 1px,
|
||||
transparent 0
|
||||
);
|
||||
--panel-layer-fill: rgba(255, 255, 255, var(--panel-layer-opacity));
|
||||
}
|
||||
|
||||
/*
|
||||
* Mapping встроенных пресетов: html[data-panel-pattern-{target}="{id}"]
|
||||
* → --panel-pattern-image-{target} и --panel-pattern-size-{target}.
|
||||
* Цели: nav | section | group | item. Пресет custom задаётся inline из script.js.
|
||||
*/
|
||||
html[data-panel-pattern-nav="hex"] { --panel-pattern-image-nav: var(--pattern-tile-hex-image); --panel-pattern-size-nav: var(--pattern-tile-hex-size); }
|
||||
html[data-panel-pattern-nav="dots"] { --panel-pattern-image-nav: var(--pattern-tile-dots-image); --panel-pattern-size-nav: var(--pattern-tile-dots-size); }
|
||||
html[data-panel-pattern-nav="grid"] { --panel-pattern-image-nav: var(--pattern-tile-grid-image); --panel-pattern-size-nav: var(--pattern-tile-grid-size); }
|
||||
html[data-panel-pattern-nav="diagonal"] { --panel-pattern-image-nav: var(--pattern-tile-diagonal-image); --panel-pattern-size-nav: var(--pattern-tile-diagonal-size); }
|
||||
html[data-panel-pattern-nav="cross"] { --panel-pattern-image-nav: var(--pattern-tile-cross-image); --panel-pattern-size-nav: var(--pattern-tile-cross-size); }
|
||||
html[data-panel-pattern-nav="mesh"] { --panel-pattern-image-nav: var(--pattern-tile-mesh-image); --panel-pattern-size-nav: var(--pattern-tile-mesh-size); }
|
||||
html[data-panel-pattern-nav="hex-sm"] { --panel-pattern-image-nav: var(--pattern-tile-hex-sm-image); --panel-pattern-size-nav: var(--pattern-tile-hex-sm-size); }
|
||||
html[data-panel-pattern-nav="dots-sm"] { --panel-pattern-image-nav: var(--pattern-tile-dots-sm-image); --panel-pattern-size-nav: var(--pattern-tile-dots-sm-size); }
|
||||
html[data-panel-pattern-nav="dots-lg"] { --panel-pattern-image-nav: var(--pattern-tile-dots-lg-image); --panel-pattern-size-nav: var(--pattern-tile-dots-lg-size); }
|
||||
html[data-panel-pattern-nav="tri-sm"] { --panel-pattern-image-nav: var(--pattern-tile-tri-sm-image); --panel-pattern-size-nav: var(--pattern-tile-tri-sm-size); }
|
||||
html[data-panel-pattern-nav="tri-lg"] { --panel-pattern-image-nav: var(--pattern-tile-tri-lg-image); --panel-pattern-size-nav: var(--pattern-tile-tri-lg-size); }
|
||||
html[data-panel-pattern-nav="diamond"] { --panel-pattern-image-nav: var(--pattern-tile-diamond-image); --panel-pattern-size-nav: var(--pattern-tile-diamond-size); }
|
||||
html[data-panel-pattern-section="hex"] { --panel-pattern-image-section: var(--pattern-tile-hex-image); --panel-pattern-size-section: var(--pattern-tile-hex-size); }
|
||||
html[data-panel-pattern-section="dots"] { --panel-pattern-image-section: var(--pattern-tile-dots-image); --panel-pattern-size-section: var(--pattern-tile-dots-size); }
|
||||
html[data-panel-pattern-section="grid"] { --panel-pattern-image-section: var(--pattern-tile-grid-image); --panel-pattern-size-section: var(--pattern-tile-grid-size); }
|
||||
html[data-panel-pattern-section="diagonal"] { --panel-pattern-image-section: var(--pattern-tile-diagonal-image); --panel-pattern-size-section: var(--pattern-tile-diagonal-size); }
|
||||
html[data-panel-pattern-section="cross"] { --panel-pattern-image-section: var(--pattern-tile-cross-image); --panel-pattern-size-section: var(--pattern-tile-cross-size); }
|
||||
html[data-panel-pattern-section="mesh"] { --panel-pattern-image-section: var(--pattern-tile-mesh-image); --panel-pattern-size-section: var(--pattern-tile-mesh-size); }
|
||||
html[data-panel-pattern-section="hex-sm"] { --panel-pattern-image-section: var(--pattern-tile-hex-sm-image); --panel-pattern-size-section: var(--pattern-tile-hex-sm-size); }
|
||||
html[data-panel-pattern-section="dots-sm"] { --panel-pattern-image-section: var(--pattern-tile-dots-sm-image); --panel-pattern-size-section: var(--pattern-tile-dots-sm-size); }
|
||||
html[data-panel-pattern-section="dots-lg"] { --panel-pattern-image-section: var(--pattern-tile-dots-lg-image); --panel-pattern-size-section: var(--pattern-tile-dots-lg-size); }
|
||||
html[data-panel-pattern-section="tri-sm"] { --panel-pattern-image-section: var(--pattern-tile-tri-sm-image); --panel-pattern-size-section: var(--pattern-tile-tri-sm-size); }
|
||||
html[data-panel-pattern-section="tri-lg"] { --panel-pattern-image-section: var(--pattern-tile-tri-lg-image); --panel-pattern-size-section: var(--pattern-tile-tri-lg-size); }
|
||||
html[data-panel-pattern-section="diamond"] { --panel-pattern-image-section: var(--pattern-tile-diamond-image); --panel-pattern-size-section: var(--pattern-tile-diamond-size); }
|
||||
html[data-panel-pattern-group="hex"] { --panel-pattern-image-group: var(--pattern-tile-hex-image); --panel-pattern-size-group: var(--pattern-tile-hex-size); }
|
||||
html[data-panel-pattern-group="dots"] { --panel-pattern-image-group: var(--pattern-tile-dots-image); --panel-pattern-size-group: var(--pattern-tile-dots-size); }
|
||||
html[data-panel-pattern-group="grid"] { --panel-pattern-image-group: var(--pattern-tile-grid-image); --panel-pattern-size-group: var(--pattern-tile-grid-size); }
|
||||
html[data-panel-pattern-group="diagonal"] { --panel-pattern-image-group: var(--pattern-tile-diagonal-image); --panel-pattern-size-group: var(--pattern-tile-diagonal-size); }
|
||||
html[data-panel-pattern-group="cross"] { --panel-pattern-image-group: var(--pattern-tile-cross-image); --panel-pattern-size-group: var(--pattern-tile-cross-size); }
|
||||
html[data-panel-pattern-group="mesh"] { --panel-pattern-image-group: var(--pattern-tile-mesh-image); --panel-pattern-size-group: var(--pattern-tile-mesh-size); }
|
||||
html[data-panel-pattern-group="hex-sm"] { --panel-pattern-image-group: var(--pattern-tile-hex-sm-image); --panel-pattern-size-group: var(--pattern-tile-hex-sm-size); }
|
||||
html[data-panel-pattern-group="dots-sm"] { --panel-pattern-image-group: var(--pattern-tile-dots-sm-image); --panel-pattern-size-group: var(--pattern-tile-dots-sm-size); }
|
||||
html[data-panel-pattern-group="dots-lg"] { --panel-pattern-image-group: var(--pattern-tile-dots-lg-image); --panel-pattern-size-group: var(--pattern-tile-dots-lg-size); }
|
||||
html[data-panel-pattern-group="tri-sm"] { --panel-pattern-image-group: var(--pattern-tile-tri-sm-image); --panel-pattern-size-group: var(--pattern-tile-tri-sm-size); }
|
||||
html[data-panel-pattern-group="tri-lg"] { --panel-pattern-image-group: var(--pattern-tile-tri-lg-image); --panel-pattern-size-group: var(--pattern-tile-tri-lg-size); }
|
||||
html[data-panel-pattern-group="diamond"] { --panel-pattern-image-group: var(--pattern-tile-diamond-image); --panel-pattern-size-group: var(--pattern-tile-diamond-size); }
|
||||
html[data-panel-pattern-item="hex"] { --panel-pattern-image-item: var(--pattern-tile-hex-image); --panel-pattern-size-item: var(--pattern-tile-hex-size); }
|
||||
html[data-panel-pattern-item="dots"] { --panel-pattern-image-item: var(--pattern-tile-dots-image); --panel-pattern-size-item: var(--pattern-tile-dots-size); }
|
||||
html[data-panel-pattern-item="grid"] { --panel-pattern-image-item: var(--pattern-tile-grid-image); --panel-pattern-size-item: var(--pattern-tile-grid-size); }
|
||||
html[data-panel-pattern-item="diagonal"] { --panel-pattern-image-item: var(--pattern-tile-diagonal-image); --panel-pattern-size-item: var(--pattern-tile-diagonal-size); }
|
||||
html[data-panel-pattern-item="cross"] { --panel-pattern-image-item: var(--pattern-tile-cross-image); --panel-pattern-size-item: var(--pattern-tile-cross-size); }
|
||||
html[data-panel-pattern-item="mesh"] { --panel-pattern-image-item: var(--pattern-tile-mesh-image); --panel-pattern-size-item: var(--pattern-tile-mesh-size); }
|
||||
html[data-panel-pattern-item="hex-sm"] { --panel-pattern-image-item: var(--pattern-tile-hex-sm-image); --panel-pattern-size-item: var(--pattern-tile-hex-sm-size); }
|
||||
html[data-panel-pattern-item="dots-sm"] { --panel-pattern-image-item: var(--pattern-tile-dots-sm-image); --panel-pattern-size-item: var(--pattern-tile-dots-sm-size); }
|
||||
html[data-panel-pattern-item="dots-lg"] { --panel-pattern-image-item: var(--pattern-tile-dots-lg-image); --panel-pattern-size-item: var(--pattern-tile-dots-lg-size); }
|
||||
html[data-panel-pattern-item="tri-sm"] { --panel-pattern-image-item: var(--pattern-tile-tri-sm-image); --panel-pattern-size-item: var(--pattern-tile-tri-sm-size); }
|
||||
html[data-panel-pattern-item="tri-lg"] { --panel-pattern-image-item: var(--pattern-tile-tri-lg-image); --panel-pattern-size-item: var(--pattern-tile-tri-lg-size); }
|
||||
html[data-panel-pattern-item="diamond"] { --panel-pattern-image-item: var(--pattern-tile-diamond-image); --panel-pattern-size-item: var(--pattern-tile-diamond-size); }
|
||||
|
||||
/* Наборы акцентов. --accent-rgb нужен для прозрачных градиентов через rgba(). */
|
||||
html[data-accent="cyan"] {
|
||||
@ -400,6 +472,30 @@ body.is-resizing-top-bar {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/*
|
||||
* Разделение слоёв узоров: полупрозрачная подложка + backdrop-filter.
|
||||
* JS: applyPatternIsolationPreference() → --panel-layer-blur / --panel-layer-opacity.
|
||||
* Сильнее всего влияет на .catalog-section (доп. blur), затем group и .nav-card.
|
||||
*/
|
||||
.catalog-section,
|
||||
.catalog-group,
|
||||
.nav-card {
|
||||
background:
|
||||
linear-gradient(var(--panel-layer-fill), var(--panel-layer-fill)),
|
||||
var(--panel);
|
||||
}
|
||||
|
||||
.catalog-section {
|
||||
backdrop-filter: blur(calc(14px + var(--panel-layer-blur)));
|
||||
-webkit-backdrop-filter: blur(calc(14px + var(--panel-layer-blur)));
|
||||
}
|
||||
|
||||
.catalog-group,
|
||||
.nav-card {
|
||||
backdrop-filter: blur(var(--panel-layer-blur));
|
||||
-webkit-backdrop-filter: blur(var(--panel-layer-blur));
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
max-height: calc(100vh - 40px);
|
||||
min-height: 0;
|
||||
@ -417,7 +513,13 @@ body.is-resizing-top-bar {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Акцентный градиент под узором — отдельно для каждого типа блоков. */
|
||||
/*
|
||||
* Декоративные слои узора на блоках каталога и навигации.
|
||||
* ::before — акцентный градиент (кроме .nav-card).
|
||||
* ::after — повторяющаяся текстура; маска по высоте/переходу из CSS-переменных JS.
|
||||
* Контент поверх узора: z-index: 1 у summary / .collapse-anim-body / детей .hero-content.
|
||||
*/
|
||||
/* Акcentный градиент под узором — отдельно для каждого типа блоков. */
|
||||
html:not([data-panel-pattern-nav="none"]) .hero-content::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@ -519,8 +621,10 @@ html:not([data-panel-pattern-item="none"]) .nav-card::after {
|
||||
|
||||
.catalog-section > summary,
|
||||
.catalog-section > .section-groups,
|
||||
.catalog-section > .collapse-anim-body,
|
||||
.catalog-group > summary,
|
||||
.catalog-group > .items-grid {
|
||||
.catalog-group > .items-grid,
|
||||
.catalog-group > .collapse-anim-body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@ -732,6 +836,51 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
/* Footer popup ⚙: сброс / экспорт / импорт preferences (script.js → settings-actions). */
|
||||
.settings-actions {
|
||||
margin-top: 4px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.settings-actions-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.settings-action-button {
|
||||
flex: 1 1 calc(33.333% - 6px);
|
||||
min-width: 0;
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
font-size: 0.84rem;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
transition: transform 160ms ease, background 160ms ease, border-color 160ms ease;
|
||||
}
|
||||
|
||||
.settings-action-button:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(var(--accent-rgb), 0.5);
|
||||
background: rgba(var(--accent-rgb), 0.12);
|
||||
}
|
||||
|
||||
.settings-action-button[data-settings-reset] {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.settings-action-button[data-settings-reset]:hover {
|
||||
border-color: rgba(239, 68, 68, 0.45);
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.catalog-columns-group {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@ -762,8 +911,21 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-section-collapsible:not([open]) > .settings-section-summary .collapse-indicator {
|
||||
transform: rotate(-90deg);
|
||||
.settings-section-summary:hover .collapse-indicator {
|
||||
border-color: rgba(var(--accent-rgb), 0.42);
|
||||
background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.22), rgba(255, 255, 255, 0.05));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
||||
0 8px 20px rgba(var(--accent-rgb), 0.12);
|
||||
}
|
||||
|
||||
html[data-collapse-animation="instant"] [data-collapse-duration-row] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-collapse-animation="animated"] .collapse-anim-body {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-section-body {
|
||||
@ -1095,6 +1257,12 @@ html[data-theme="light"] .accent-swatch-custom {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/*
|
||||
* Блок узоров в ⚙ → Оформление.
|
||||
* .pattern-swatches — контейнер; кнопки генерирует renderPatternSwatches() в script.js.
|
||||
* .pattern-custom-panel — поля «Свой» (URL + размер плитки), создаёт ensurePatternCustomPanels().
|
||||
* .pattern-isolation-row — слайдер «Разделение слоёв».
|
||||
*/
|
||||
.pattern-targets-panel {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@ -1109,13 +1277,13 @@ html[data-theme="light"] .accent-swatch-custom {
|
||||
|
||||
.pattern-swatches {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pattern-swatch {
|
||||
min-height: 54px;
|
||||
min-height: 48px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
background: var(--surface);
|
||||
@ -1151,6 +1319,53 @@ html[data-theme="light"] .pattern-swatch[data-panel-pattern="hex"]::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.pattern-swatch[data-panel-pattern="hex-sm"]::before {
|
||||
background-image: var(--pattern-tile-hex-sm-image);
|
||||
background-size: 24px 21px;
|
||||
}
|
||||
|
||||
.pattern-swatch[data-panel-pattern="dots-sm"]::before {
|
||||
background-image: var(--pattern-tile-dots-sm-image);
|
||||
background-size: 8px 8px;
|
||||
}
|
||||
|
||||
.pattern-swatch[data-panel-pattern="dots-lg"]::before {
|
||||
background-image: var(--pattern-tile-dots-lg-image);
|
||||
background-size: 18px 18px;
|
||||
}
|
||||
|
||||
.pattern-swatch[data-panel-pattern="tri-sm"]::before {
|
||||
background-image: var(--pattern-tile-tri-sm-image);
|
||||
background-size: 16px 14px;
|
||||
}
|
||||
|
||||
.pattern-swatch[data-panel-pattern="tri-lg"]::before {
|
||||
background-image: var(--pattern-tile-tri-lg-image);
|
||||
background-size: 28px 24px;
|
||||
}
|
||||
|
||||
.pattern-swatch[data-panel-pattern="diamond"]::before {
|
||||
background-image: var(--pattern-tile-diamond-image);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
.pattern-swatch[data-panel-pattern="custom"]::before {
|
||||
background:
|
||||
linear-gradient(135deg, rgba(var(--accent-rgb), 0.18), transparent 62%),
|
||||
repeating-linear-gradient(
|
||||
-45deg,
|
||||
transparent,
|
||||
transparent 5px,
|
||||
rgba(var(--accent-rgb), 0.12) 5px,
|
||||
rgba(var(--accent-rgb), 0.12) 6px
|
||||
);
|
||||
}
|
||||
|
||||
.pattern-swatch[data-panel-pattern="custom"].has-custom-preview::before {
|
||||
background-image: var(--pattern-swatch-preview);
|
||||
background-size: 18px 18px;
|
||||
}
|
||||
|
||||
.pattern-swatch[data-panel-pattern="dots"]::before {
|
||||
background-image: radial-gradient(circle at center, var(--pattern-color) 0.95px, transparent 1px);
|
||||
background-size: 12px 12px;
|
||||
@ -1251,6 +1466,53 @@ html[data-theme="light"] .pattern-swatch-label {
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.pattern-isolation-row {
|
||||
grid-template-columns: minmax(120px, 1fr) minmax(0, 1.4fr) auto;
|
||||
}
|
||||
|
||||
.pattern-custom-panel {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.pattern-custom-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pattern-custom-panel input[type="url"],
|
||||
.pattern-custom-panel input[type="text"] {
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.pattern-custom-image-row {
|
||||
grid-template-columns: minmax(72px, 0.8fr) minmax(0, 1.6fr);
|
||||
}
|
||||
|
||||
.pattern-custom-image-row input[type="url"],
|
||||
.pattern-custom-image-row input[type="text"] {
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
/* Панель настройки своего градиента появляется при data-accent="custom". */
|
||||
.accent-custom-panel {
|
||||
display: grid;
|
||||
@ -1354,8 +1616,8 @@ html[data-theme="light"] .pattern-swatch-label {
|
||||
.tree-section > summary,
|
||||
.tree-group > summary {
|
||||
display: grid;
|
||||
grid-template-columns: 16px minmax(0, 1fr);
|
||||
gap: 3px;
|
||||
grid-template-columns: 22px minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
@ -1372,17 +1634,38 @@ html[data-theme="light"] .pattern-swatch-label {
|
||||
|
||||
/* Caret поворачивается через состояние [open] у details. */
|
||||
.tree-caret {
|
||||
width: 16px;
|
||||
height: 24px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
color: var(--muted);
|
||||
background: rgba(var(--accent-rgb), 0.06);
|
||||
border: 1px solid transparent;
|
||||
transition:
|
||||
background 140ms ease,
|
||||
border-color 140ms ease,
|
||||
color 140ms ease;
|
||||
}
|
||||
|
||||
.tree-section > summary:hover .tree-caret,
|
||||
.tree-group > summary:hover .tree-caret {
|
||||
color: var(--accent);
|
||||
border-color: rgba(var(--accent-rgb), 0.18);
|
||||
background: rgba(var(--accent-rgb), 0.12);
|
||||
}
|
||||
|
||||
.tree-caret::before {
|
||||
content: "▾";
|
||||
font-size: 0.72rem;
|
||||
transition: transform 140ms ease;
|
||||
content: "";
|
||||
display: block;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
background: currentColor;
|
||||
mask: url("data:image/svg+xml,%3Csvg width='13' height='13' viewBox='0 0 13 13' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.5 5 6.5 8 9.5 5' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg width='13' height='13' viewBox='0 0 13 13' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.5 5 6.5 8 9.5 5' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
transform: rotate(0deg);
|
||||
transform-origin: center;
|
||||
transition: transform var(--collapse-duration, 240ms) cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.tree-section:not([open]) > summary .tree-caret::before,
|
||||
@ -1635,23 +1918,63 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
/* Стрелка сворачивания поворачивается через [open]. */
|
||||
/* Стрелка сворачивания — аккуратный chevron в стеклянной кнопке. */
|
||||
.collapse-indicator {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
background: var(--surface);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.22);
|
||||
background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.14), rgba(255, 255, 255, 0.03));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
color: var(--accent);
|
||||
font-weight: 900;
|
||||
transition: transform 160ms ease;
|
||||
font-size: 0;
|
||||
transition:
|
||||
border-color 160ms ease,
|
||||
background 160ms ease,
|
||||
box-shadow 160ms ease,
|
||||
transform 160ms ease;
|
||||
}
|
||||
|
||||
.collapse-indicator::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: currentColor;
|
||||
mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.25 6.25 8 10l3.75-3.75' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.25 6.25 8 10l3.75-3.75' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
transform: rotate(0deg);
|
||||
transform-origin: center;
|
||||
transition: transform var(--collapse-duration, 240ms) cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.catalog-section:not([open]) > .section-head .collapse-indicator::before,
|
||||
.catalog-group:not([open]) > .group-head .collapse-indicator::before,
|
||||
.settings-section-collapsible:not([open]) > .settings-section-summary .collapse-indicator::before {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.section-head:hover .collapse-indicator,
|
||||
.group-head:hover .collapse-indicator {
|
||||
border-color: rgba(var(--accent-rgb), 0.42);
|
||||
background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.22), rgba(255, 255, 255, 0.05));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
||||
0 8px 20px rgba(var(--accent-rgb), 0.12);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .collapse-indicator {
|
||||
background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.1), rgba(255, 255, 255, 0.72));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.catalog-section:not([open]) > .section-head .collapse-indicator,
|
||||
.catalog-group:not([open]) > .group-head .collapse-indicator {
|
||||
transform: rotate(-90deg);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Подразделы внутри секции. */
|
||||
@ -1682,7 +2005,9 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
|
||||
position: relative;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
background:
|
||||
linear-gradient(var(--panel-layer-fill), var(--panel-layer-fill)),
|
||||
rgba(255, 255, 255, 0.035);
|
||||
overflow: hidden;
|
||||
scroll-margin-top: 18px;
|
||||
}
|
||||
@ -1741,7 +2066,9 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
background: var(--panel);
|
||||
background:
|
||||
linear-gradient(var(--panel-layer-fill), var(--panel-layer-fill)),
|
||||
var(--panel);
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
@ -1785,7 +2112,9 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
|
||||
.nav-card.is-hovered,
|
||||
.nav-card:focus-visible {
|
||||
border-color: rgba(var(--accent-rgb), 0.52);
|
||||
background: var(--panel-strong);
|
||||
background:
|
||||
linear-gradient(var(--panel-layer-fill), var(--panel-layer-fill)),
|
||||
var(--panel-strong);
|
||||
box-shadow: 0 22px 70px rgba(var(--accent-rgb), 0.16);
|
||||
outline: none;
|
||||
}
|
||||
@ -1853,18 +2182,37 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
|
||||
height: 36px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--accent-rgb), 0.14);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.18);
|
||||
background: linear-gradient(160deg, rgba(var(--accent-rgb), 0.12), rgba(255, 255, 255, 0.03));
|
||||
color: var(--accent);
|
||||
font-size: 1.15rem;
|
||||
transition: transform 180ms ease, background 180ms ease, color 180ms ease;
|
||||
font-size: 0;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
transition:
|
||||
transform 180ms ease,
|
||||
background 180ms ease,
|
||||
color 180ms ease,
|
||||
border-color 180ms ease,
|
||||
box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.arrow::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: currentColor;
|
||||
mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 4 10 8 6 12' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 4 10 8 6 12' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||
}
|
||||
|
||||
.nav-card.is-hovered .arrow,
|
||||
.nav-card:focus-visible .arrow {
|
||||
transform: translateX(4px);
|
||||
transform: translateX(3px);
|
||||
border-color: rgba(var(--accent-rgb), 0.35);
|
||||
background: linear-gradient(var(--accent-angle), var(--accent), var(--accent-2));
|
||||
color: white;
|
||||
box-shadow: 0 10px 24px rgba(var(--accent-rgb), 0.24);
|
||||
}
|
||||
|
||||
/* Декоративный shine слой, положение которого обновляется pointermove. */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user