добавлена возможность настраивать каждый элемент (узоры)

This commit is contained in:
Илья Братских 2026-06-28 19:12:47 +03:00
parent 50a04e3856
commit 26dc3bac2b
15 changed files with 2132 additions and 40 deletions

View File

@ -33,6 +33,7 @@
- **Поиск** по названию, тегам, типу сервиса и родительским разделам
- **4 режима отображения**: плитки, список, компактные плитки, компактный список
- **Темы**: светлая / тёмная, 4 пресета акцента + свой градиент
- **Узоры панелей** — соты, точки, сетка и др.; отдельные варианты для светлой темы; можно отключить
- **Настройки** через popup (иконка ⚙)
- **Изменяемая ширина** бокового меню и **высота** верхней шапки (drag, можно скрыть ручки)
- **Габариты** — лимиты ширины контента, меню и шапки (⚙, для администратора)
@ -126,6 +127,29 @@ ecosystem/
"theme": "dark",
"view": "cards",
"accent": "violet",
"customAccentFrom": "#8b5cf6",
"customAccentTo": "#22d3ee",
"customAccentAngle": 135,
"panelPatternNav": "hex",
"panelPatternNavOpacity": 70,
"panelPatternNavHeight": 45,
"panelPatternNavGradientOpacity": 80,
"panelPatternNavGradientHeight": 40,
"panelPatternSection": "hex",
"panelPatternSectionOpacity": 70,
"panelPatternSectionHeight": 45,
"panelPatternSectionGradientOpacity": 80,
"panelPatternSectionGradientHeight": 40,
"panelPatternGroup": "none",
"panelPatternGroupOpacity": 70,
"panelPatternGroupHeight": 45,
"panelPatternGroupGradientOpacity": 80,
"panelPatternGroupGradientHeight": 40,
"panelPatternItem": "none",
"panelPatternItemOpacity": 70,
"panelPatternItemHeight": 45,
"panelPatternItemGradientOpacity": 80,
"panelPatternItemGradientHeight": 40,
"showNumbers": false,
"showTreeTags": true,
"showTreeType": false,
@ -146,7 +170,8 @@ ecosystem/
"catalogListColumns": 1,
"catalogCompactCardsColumns": 5,
"catalogCompactListColumns": 3,
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
"settingsPanelWidth": 460
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
@ -173,7 +198,8 @@ ecosystem/
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true
"allowAccentChange": true,
"allowPanelPatternChange": true
}
}
```
@ -189,6 +215,13 @@ ecosystem/
| `customAccentFrom` | `#rrggbb` | Цвет «от» для `accent: "custom"` (по умолчанию `#8b5cf6`) |
| `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`) |
| `showNumbers` | `true`, `false` | Показывать номера разделов, подразделов и элементов |
| `showTreeTags` | `true`, `false` | Теги элементов в дереве (оформление как на карточках) |
| `showTreeType` | `true`, `false` | Подпись типа сервиса в дереве |
@ -210,6 +243,7 @@ ecosystem/
| `catalogCompactCardsColumns` | см. `limits.catalogCompactCardsColumns` | Колонок в строке в режиме «Компакт плитки» |
| `catalogCompactListColumns` | см. `limits.catalogCompactListColumns` | Колонок в строке в режиме «Компакт список» |
| `searchPlaceholder` | произвольная строка | Текст-подсказка в поле поиска |
| `settingsPanelWidth` | `limits.settingsPanelWidth` (по умолчанию `320`–`720`) | Ширина popup настроек в px (по умолчанию `460`) |
| `limits` | Формат | Описание |
|----------|--------|----------|
@ -243,6 +277,7 @@ ecosystem/
| `allowLayoutLimitsChange` | `true`, `false` | Раздел ⚙ «Габариты»: лимиты ширины контента и навигации |
| `allowThemeChange` | `true`, `false` | Светлая / тёмная тема |
| `allowAccentChange` | `true`, `false` | Пресеты и свой градиент |
| `allowPanelPatternChange` | `true`, `false` | Выбор узора панелей в ⚙ → Оформление |
Пользовательские изменения сохраняются в `localStorage`, но заблокированные `features` всегда возвращаются к `defaults`.
@ -404,11 +439,11 @@ Popup сгруппирован по разделам. Доступные бло
| Раздел | Настройки |
|--------|-----------|
| **Макет** | **Сбоку** / **Сверху**; toggle «Показывать дерево»; toggle «Ручки изменения размера» |
| **Макет** | **Сбоку** / **Сверху**; toggle «Показывать дерево»; toggle «Ручки изменения размера»; слайдер «Ширина настроек» |
| **Габариты** | Лимиты ширины контента, меню и шапки (сворачиваемый блок; при `allowLayoutLimitsChange`) |
| **Каталог** | Вид карточек, колонок в строке для каждого вида, нумерация |
| **Дерево** | Колонок 1–6, теги, тип, расположение мета, авто-раскрытие (при `showTree: true`) |
| **Оформление** | Тема, акцент, свой градиент |
| **Оформление** | Тема, акцент, узор панелей, свой градиент |
### Режим «Сверху»
@ -427,6 +462,35 @@ Popup сгруппирован по разделам. Доступные бло
Значения сохраняются в `localStorage`.
### Узоры панелей
Настраиваются в ⚙ → **Оформление** (если `allowPanelPatternChange: true`): четыре схлопывающиеся панели — **Панель навигации**, **Раздел**, **Подраздел**, **Элемент**. В каждой — свой тип узора и слайдеры.
| Значение `panelPattern*` | Описание |
|--------------------------|----------|
| `none` | Без узора — только акцентный градиент (если применим) |
| `hex` | Соты |
| `dots` | Точечная сетка |
| `grid` | Линейная сетка |
| `diagonal` | Диагональные полосы |
| `cross` | Перекрёстная штриховка |
| `mesh` | Точки цвета акцента |
По умолчанию для **навигации** и **разделов** — `hex`; для **подразделов** и **элементов** — `none`.
| Панель | Preference (префикс) | DOM |
|--------|----------------------|-----|
| Панель навигации | `panelPatternNav*` | `.hero-content` |
| Раздел | `panelPatternSection*` | `.catalog-section` |
| Подраздел | `panelPatternGroup*` | `.catalog-group` |
| Элемент | `panelPatternItem*` | `.nav-card` — только узор, без акцентного градиента |
В каждой панели (кроме «Элемент») — шесть слайдеров: прозрачность, высота и **переход** узора; то же для градиента. Для «Элемент» — три слайдера узора (без градиента). **Переход** задаёт ширину зоны затухания: `0%` — резкий обрыв на линии «высоты», большее значение — более плавное исчезновение (при значении ≥ высоты узор растворяется от самого верха блока).
Старые ключи (`panelPattern`, `panelPatternOnNav`, …) при загрузке автоматически мигрируют в per-target формат, если в `localStorage` ещё нет `panelPatternNav`.
Значения сохраняются в `localStorage` и выставляют `data-panel-pattern-*` на `<html>` и CSS-переменные `--panel-pattern-{nav|section|group|item}-*`, `--panel-gradient-{target}-*`.
## Поведение интерфейса
### Поиск
@ -464,7 +528,7 @@ Popup сгруппирован по разделам. Доступные бло
| Ключ | Содержимое |
|------|------------|
| `ecosystem-panel-preferences` | Тема, акцент, вид, ширина sidebar, toggle-настройки |
| `ecosystem-panel-preferences` | Тема, акцент, узор, вид, колонки каталога, ширина sidebar, toggle-настройки |
| `ecosystem-panel-collapse-state` | Раскрытые/свернутые ветки дерева и каталога |
## Разработка

View File

@ -126,6 +126,11 @@
</span>
<input type="checkbox" data-show-resize-handles checked />
</label>
<label class="accent-custom-row settings-width-row">
<span>Ширина настроек</span>
<input type="range" data-settings-panel-width />
<output data-settings-panel-width-value>460px</output>
</label>
</section>
<details class="settings-section settings-section-collapsible" data-settings-section="dimensions">
@ -368,6 +373,114 @@
</button>
</div>
<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">
<summary class="settings-section-summary">
<span class="settings-section-title">Панель навигации</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="nav" data-panel-pattern-field="fade" /><output data-panel-pattern-value="nav" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="section">
<summary class="settings-section-summary">
<span class="settings-section-title">Раздел</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="section" data-panel-pattern-field="fade" /><output data-panel-pattern-value="section" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="group">
<summary class="settings-section-summary">
<span class="settings-section-title">Подраздел</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="group" data-panel-pattern-field="fade" /><output data-panel-pattern-value="group" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="item">
<summary class="settings-section-summary">
<span class="settings-section-title">Элемент</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="item" data-panel-pattern-field="fade" /><output data-panel-pattern-value="item" data-panel-pattern-field="fade">10%</output></label>
</div>
</div>
</details>
</div>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>

View File

@ -11,6 +11,26 @@
"customAccentFrom": "#8b5cf6",
"customAccentTo": "#22d3ee",
"customAccentAngle": 135,
"panelPatternNav": "hex",
"panelPatternNavOpacity": 70,
"panelPatternNavHeight": 45,
"panelPatternNavGradientOpacity": 80,
"panelPatternNavGradientHeight": 40,
"panelPatternSection": "hex",
"panelPatternSectionOpacity": 70,
"panelPatternSectionHeight": 45,
"panelPatternSectionGradientOpacity": 80,
"panelPatternSectionGradientHeight": 40,
"panelPatternGroup": "none",
"panelPatternGroupOpacity": 70,
"panelPatternGroupHeight": 45,
"panelPatternGroupGradientOpacity": 80,
"panelPatternGroupGradientHeight": 40,
"panelPatternItem": "none",
"panelPatternItemOpacity": 70,
"panelPatternItemHeight": 45,
"panelPatternItemGradientOpacity": 80,
"panelPatternItemGradientHeight": 40,
"showNumbers": false,
"showTreeTags": true,
"showTreeType": false,
@ -31,7 +51,8 @@
"catalogListColumns": 1,
"catalogCompactCardsColumns": 5,
"catalogCompactListColumns": 3,
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
"settingsPanelWidth": 460
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
@ -45,7 +66,8 @@
"catalogCardsColumns": { "min": 1, "max": 6 },
"catalogListColumns": { "min": 1, "max": 4 },
"catalogCompactCardsColumns": { "min": 2, "max": 8 },
"catalogCompactListColumns": { "min": 1, "max": 6 }
"catalogCompactListColumns": { "min": 1, "max": 6 },
"settingsPanelWidth": { "min": 320, "max": 720 }
},
"features": {
"allowNavLayoutChange": true,
@ -58,6 +80,7 @@
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true
"allowAccentChange": true,
"allowPanelPatternChange": true
}
}

View File

@ -126,6 +126,11 @@
</span>
<input type="checkbox" data-show-resize-handles checked />
</label>
<label class="accent-custom-row settings-width-row">
<span>Ширина настроек</span>
<input type="range" data-settings-panel-width />
<output data-settings-panel-width-value>460px</output>
</label>
</section>
<details class="settings-section settings-section-collapsible" data-settings-section="dimensions">
@ -368,6 +373,114 @@
</button>
</div>
<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">
<summary class="settings-section-summary">
<span class="settings-section-title">Панель навигации</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="nav" data-panel-pattern-field="fade" /><output data-panel-pattern-value="nav" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="section">
<summary class="settings-section-summary">
<span class="settings-section-title">Раздел</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="section" data-panel-pattern-field="fade" /><output data-panel-pattern-value="section" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="group">
<summary class="settings-section-summary">
<span class="settings-section-title">Подраздел</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="group" data-panel-pattern-field="fade" /><output data-panel-pattern-value="group" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="item">
<summary class="settings-section-summary">
<span class="settings-section-title">Элемент</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="item" data-panel-pattern-field="fade" /><output data-panel-pattern-value="item" data-panel-pattern-field="fade">10%</output></label>
</div>
</div>
</details>
</div>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>

View File

@ -11,6 +11,26 @@
"customAccentFrom": "#8b5cf6",
"customAccentTo": "#22d3ee",
"customAccentAngle": 135,
"panelPatternNav": "hex",
"panelPatternNavOpacity": 70,
"panelPatternNavHeight": 45,
"panelPatternNavGradientOpacity": 80,
"panelPatternNavGradientHeight": 40,
"panelPatternSection": "hex",
"panelPatternSectionOpacity": 70,
"panelPatternSectionHeight": 45,
"panelPatternSectionGradientOpacity": 80,
"panelPatternSectionGradientHeight": 40,
"panelPatternGroup": "none",
"panelPatternGroupOpacity": 70,
"panelPatternGroupHeight": 45,
"panelPatternGroupGradientOpacity": 80,
"panelPatternGroupGradientHeight": 40,
"panelPatternItem": "none",
"panelPatternItemOpacity": 70,
"panelPatternItemHeight": 45,
"panelPatternItemGradientOpacity": 80,
"panelPatternItemGradientHeight": 40,
"showNumbers": false,
"showTreeTags": true,
"showTreeType": false,
@ -31,7 +51,8 @@
"catalogListColumns": 1,
"catalogCompactCardsColumns": 5,
"catalogCompactListColumns": 3,
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
"settingsPanelWidth": 460
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
@ -45,7 +66,8 @@
"catalogCardsColumns": { "min": 1, "max": 6 },
"catalogListColumns": { "min": 1, "max": 4 },
"catalogCompactCardsColumns": { "min": 2, "max": 8 },
"catalogCompactListColumns": { "min": 1, "max": 6 }
"catalogCompactListColumns": { "min": 1, "max": 6 },
"settingsPanelWidth": { "min": 320, "max": 720 }
},
"features": {
"allowNavLayoutChange": true,
@ -58,6 +80,7 @@
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true
"allowAccentChange": true,
"allowPanelPatternChange": true
}
}

View File

@ -126,6 +126,11 @@
</span>
<input type="checkbox" data-show-resize-handles checked />
</label>
<label class="accent-custom-row settings-width-row">
<span>Ширина настроек</span>
<input type="range" data-settings-panel-width />
<output data-settings-panel-width-value>460px</output>
</label>
</section>
<details class="settings-section settings-section-collapsible" data-settings-section="dimensions">
@ -368,6 +373,114 @@
</button>
</div>
<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">
<summary class="settings-section-summary">
<span class="settings-section-title">Панель навигации</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="nav" data-panel-pattern-field="fade" /><output data-panel-pattern-value="nav" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="section">
<summary class="settings-section-summary">
<span class="settings-section-title">Раздел</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="section" data-panel-pattern-field="fade" /><output data-panel-pattern-value="section" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="group">
<summary class="settings-section-summary">
<span class="settings-section-title">Подраздел</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="group" data-panel-pattern-field="fade" /><output data-panel-pattern-value="group" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="item">
<summary class="settings-section-summary">
<span class="settings-section-title">Элемент</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="item" data-panel-pattern-field="fade" /><output data-panel-pattern-value="item" data-panel-pattern-field="fade">10%</output></label>
</div>
</div>
</details>
</div>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>

View File

@ -11,6 +11,26 @@
"customAccentFrom": "#8b5cf6",
"customAccentTo": "#22d3ee",
"customAccentAngle": 135,
"panelPatternNav": "hex",
"panelPatternNavOpacity": 70,
"panelPatternNavHeight": 45,
"panelPatternNavGradientOpacity": 80,
"panelPatternNavGradientHeight": 40,
"panelPatternSection": "hex",
"panelPatternSectionOpacity": 70,
"panelPatternSectionHeight": 45,
"panelPatternSectionGradientOpacity": 80,
"panelPatternSectionGradientHeight": 40,
"panelPatternGroup": "none",
"panelPatternGroupOpacity": 70,
"panelPatternGroupHeight": 45,
"panelPatternGroupGradientOpacity": 80,
"panelPatternGroupGradientHeight": 40,
"panelPatternItem": "none",
"panelPatternItemOpacity": 70,
"panelPatternItemHeight": 45,
"panelPatternItemGradientOpacity": 80,
"panelPatternItemGradientHeight": 40,
"showNumbers": false,
"showTreeTags": true,
"showTreeType": false,
@ -31,7 +51,8 @@
"catalogListColumns": 1,
"catalogCompactCardsColumns": 5,
"catalogCompactListColumns": 3,
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
"settingsPanelWidth": 460
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
@ -45,7 +66,8 @@
"catalogCardsColumns": { "min": 1, "max": 6 },
"catalogListColumns": { "min": 1, "max": 4 },
"catalogCompactCardsColumns": { "min": 2, "max": 8 },
"catalogCompactListColumns": { "min": 1, "max": 6 }
"catalogCompactListColumns": { "min": 1, "max": 6 },
"settingsPanelWidth": { "min": 320, "max": 720 }
},
"features": {
"allowNavLayoutChange": true,
@ -58,6 +80,7 @@
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true
"allowAccentChange": true,
"allowPanelPatternChange": true
}
}

View File

@ -126,6 +126,11 @@
</span>
<input type="checkbox" data-show-resize-handles checked />
</label>
<label class="accent-custom-row settings-width-row">
<span>Ширина настроек</span>
<input type="range" data-settings-panel-width />
<output data-settings-panel-width-value>460px</output>
</label>
</section>
<details class="settings-section settings-section-collapsible" data-settings-section="dimensions">
@ -368,6 +373,114 @@
</button>
</div>
<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">
<summary class="settings-section-summary">
<span class="settings-section-title">Панель навигации</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="nav" data-panel-pattern-field="fade" /><output data-panel-pattern-value="nav" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="section">
<summary class="settings-section-summary">
<span class="settings-section-title">Раздел</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="section" data-panel-pattern-field="fade" /><output data-panel-pattern-value="section" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="group">
<summary class="settings-section-summary">
<span class="settings-section-title">Подраздел</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="group" data-panel-pattern-field="fade" /><output data-panel-pattern-value="group" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="item">
<summary class="settings-section-summary">
<span class="settings-section-title">Элемент</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="item" data-panel-pattern-field="fade" /><output data-panel-pattern-value="item" data-panel-pattern-field="fade">10%</output></label>
</div>
</div>
</details>
</div>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>

View File

@ -11,6 +11,26 @@
"customAccentFrom": "#8b5cf6",
"customAccentTo": "#22d3ee",
"customAccentAngle": 135,
"panelPatternNav": "hex",
"panelPatternNavOpacity": 70,
"panelPatternNavHeight": 45,
"panelPatternNavGradientOpacity": 80,
"panelPatternNavGradientHeight": 40,
"panelPatternSection": "hex",
"panelPatternSectionOpacity": 70,
"panelPatternSectionHeight": 45,
"panelPatternSectionGradientOpacity": 80,
"panelPatternSectionGradientHeight": 40,
"panelPatternGroup": "none",
"panelPatternGroupOpacity": 70,
"panelPatternGroupHeight": 45,
"panelPatternGroupGradientOpacity": 80,
"panelPatternGroupGradientHeight": 40,
"panelPatternItem": "none",
"panelPatternItemOpacity": 70,
"panelPatternItemHeight": 45,
"panelPatternItemGradientOpacity": 80,
"panelPatternItemGradientHeight": 40,
"showNumbers": false,
"showTreeTags": true,
"showTreeType": false,
@ -31,7 +51,8 @@
"catalogListColumns": 1,
"catalogCompactCardsColumns": 5,
"catalogCompactListColumns": 3,
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
"settingsPanelWidth": 460
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
@ -45,7 +66,8 @@
"catalogCardsColumns": { "min": 1, "max": 6 },
"catalogListColumns": { "min": 1, "max": 4 },
"catalogCompactCardsColumns": { "min": 2, "max": 8 },
"catalogCompactListColumns": { "min": 1, "max": 6 }
"catalogCompactListColumns": { "min": 1, "max": 6 },
"settingsPanelWidth": { "min": 320, "max": 720 }
},
"features": {
"allowNavLayoutChange": true,
@ -58,6 +80,7 @@
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true
"allowAccentChange": true,
"allowPanelPatternChange": true
}
}

View File

@ -65,6 +65,44 @@ const defaultServiceMeta = {
let serviceMeta = { ...defaultServiceMeta };
const availablePanelPatterns = ["none", "hex", "dots", "grid", "diagonal", "cross", "mesh"];
const panelPatternTargetIds = ["nav", "section", "group", "item"];
const panelPatternTuningDefaults = {
opacity: 70,
height: 45,
fade: 10,
gradientOpacity: 80,
gradientHeight: 40,
gradientFade: 8,
};
const panelPatternTargetMeta = {
nav: { defaultPattern: "hex" },
section: { defaultPattern: "hex" },
group: { defaultPattern: "none" },
item: { defaultPattern: "none" },
};
function capitalizePanelPatternTarget(target) {
return target.charAt(0).toUpperCase() + target.slice(1);
}
function buildDefaultPanelPatternPreferences() {
const prefs = {};
for (const target of panelPatternTargetIds) {
const cap = capitalizePanelPatternTarget(target);
prefs[`panelPattern${cap}`] = panelPatternTargetMeta[target].defaultPattern;
prefs[`panelPattern${cap}Opacity`] = panelPatternTuningDefaults.opacity;
prefs[`panelPattern${cap}Height`] = panelPatternTuningDefaults.height;
prefs[`panelPattern${cap}Fade`] = panelPatternTuningDefaults.fade;
prefs[`panelPattern${cap}GradientOpacity`] = panelPatternTuningDefaults.gradientOpacity;
prefs[`panelPattern${cap}GradientHeight`] = panelPatternTuningDefaults.gradientHeight;
prefs[`panelPattern${cap}GradientFade`] = panelPatternTuningDefaults.gradientFade;
}
return prefs;
}
// ============================================================================
// Default UI preferences
// ============================================================================
@ -76,6 +114,7 @@ const defaultPreferences = {
customAccentFrom: "#8b5cf6",
customAccentTo: "#22d3ee",
customAccentAngle: 135,
...buildDefaultPanelPatternPreferences(),
navLayout: "sidebar",
showTree: true,
showResizeHandles: true,
@ -100,6 +139,7 @@ const defaultPreferences = {
catalogCompactCardsColumns: 5,
catalogCompactListColumns: 3,
searchPlaceholder: "Grafana, S3, журнал, сервер...",
settingsPanelWidth: 460,
};
const defaultPanelFeatures = {
@ -114,6 +154,7 @@ const defaultPanelFeatures = {
allowLayoutLimitsChange: true,
allowThemeChange: true,
allowAccentChange: true,
allowPanelPatternChange: true,
};
const defaultPanelLimits = {
@ -129,6 +170,7 @@ const defaultPanelLimits = {
catalogListColumns: { min: 1, max: 4 },
catalogCompactCardsColumns: { min: 2, max: 8 },
catalogCompactListColumns: { min: 1, max: 6 },
settingsPanelWidth: { min: 320, max: 720 },
};
const layoutLimitKeys = [
@ -149,7 +191,7 @@ const catalogColumnKeys = [
"catalogCompactListColumns",
];
const panelLimitKeys = [...layoutLimitKeys, ...catalogColumnKeys];
const panelLimitKeys = [...layoutLimitKeys, ...catalogColumnKeys, "settingsPanelWidth"];
const availableNavLayouts = ["sidebar", "top"];
const availableTreeMetaLayouts = ["inline", "stacked"];
@ -186,6 +228,7 @@ const themeButtons = document.querySelectorAll("[data-theme]");
const navLayoutButtons = document.querySelectorAll("[data-nav-layout]");
const viewButtons = document.querySelectorAll("[data-view]");
const accentButtons = document.querySelectorAll("[data-accent]");
const patternTargetsPanel = document.querySelector("[data-pattern-targets-panel]");
const accentCustomPanel = document.querySelector("[data-accent-custom-panel]");
const customAccentFromInput = document.querySelector("[data-custom-accent-from]");
const customAccentToInput = document.querySelector("[data-custom-accent-to]");
@ -201,6 +244,8 @@ const treeColumnsInput = document.querySelector("[data-tree-columns]");
const treeColumnsValue = document.querySelector("[data-tree-columns-value]");
const treeMetaLayoutButtons = document.querySelectorAll("[data-tree-meta-layout]");
const autoCollapseSearchInput = document.querySelector("[data-auto-collapse-search]");
const settingsPanelWidthInput = document.querySelector("[data-settings-panel-width]");
const settingsPanelWidthValue = document.querySelector("[data-settings-panel-width-value]");
const layoutLimitInputs = document.querySelectorAll("[data-layout-limit]");
const catalogColumnInputs = document.querySelectorAll("[data-catalog-columns]");
const settingsSections = document.querySelectorAll("[data-settings-section]");
@ -388,6 +433,35 @@ accentButtons.forEach((button) => {
});
});
patternTargetsPanel?.addEventListener("click", (event) => {
const button = event.target.closest("[data-panel-pattern-target][data-panel-pattern]");
if (!button || !panelFeatures.allowPanelPatternChange) {
return;
}
const target = button.dataset.panelPatternTarget;
preferences[getPanelPatternPreferenceKey(target, "pattern")] = button.dataset.panelPattern;
applyPreferences(preferences);
savePreferences(preferences);
});
patternTargetsPanel?.addEventListener("input", (event) => {
const input = event.target.closest("[data-panel-pattern-tuning]");
if (!input || !panelFeatures.allowPanelPatternChange) {
return;
}
const target = input.dataset.panelPatternTuning;
const field = input.dataset.panelPatternField;
const key = getPanelPatternPreferenceKey(target, field);
const fallback = defaultPreferences[key] ?? panelPatternTuningDefaults[field] ?? 50;
preferences[key] = clampPanelPatternPercent(input.value, fallback);
applyPreferences(preferences);
savePreferences(preferences);
});
// Изменение параметров своего градиента сразу переключает accent на custom.
function handleCustomAccentChange() {
if (!panelFeatures.allowAccentChange) {
@ -459,6 +533,17 @@ if (treeColumnsInput) {
treeColumnsInput.addEventListener("input", handleTreeColumnsChange);
}
function handleSettingsPanelWidthChange() {
preferences.settingsPanelWidth = clampLayoutPreference(
"settingsPanelWidth",
settingsPanelWidthInput?.value,
);
applyPreferences(preferences);
savePreferences(preferences);
}
settingsPanelWidthInput?.addEventListener("input", handleSettingsPanelWidthChange);
treeMetaLayoutButtons.forEach((button) => {
button.addEventListener("click", () => {
if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) {
@ -625,6 +710,9 @@ function normalizePreferences(raw) {
next.accent = defaultPreferences.accent;
}
migrateLegacyPanelPatternPreferences(next);
normalizePanelPatternPreferences(next);
if (!availableNavLayouts.includes(next.navLayout)) {
next.navLayout = defaultPreferences.navLayout;
}
@ -659,6 +747,7 @@ function normalizePreferences(raw) {
getTopBarMinHeight(next),
getTopBarMaxHeight(next),
);
next.settingsPanelWidth = clampLayoutPreference("settingsPanelWidth", next.settingsPanelWidth);
return next;
}
@ -724,6 +813,19 @@ function enforceFeatureLocks(rawPreferences) {
next.customAccentAngle = panelDefaults.customAccentAngle;
}
if (!panelFeatures.allowPanelPatternChange) {
for (const target of panelPatternTargetIds) {
const cap = capitalizePanelPatternTarget(target);
next[`panelPattern${cap}`] = panelDefaults[`panelPattern${cap}`];
next[`panelPattern${cap}Opacity`] = panelDefaults[`panelPattern${cap}Opacity`];
next[`panelPattern${cap}Height`] = panelDefaults[`panelPattern${cap}Height`];
next[`panelPattern${cap}Fade`] = panelDefaults[`panelPattern${cap}Fade`];
next[`panelPattern${cap}GradientOpacity`] = panelDefaults[`panelPattern${cap}GradientOpacity`];
next[`panelPattern${cap}GradientHeight`] = panelDefaults[`panelPattern${cap}GradientHeight`];
next[`panelPattern${cap}GradientFade`] = panelDefaults[`panelPattern${cap}GradientFade`];
}
}
if (!panelFeatures.allowShowNumbers) {
next.showNumbers = panelDefaults.showNumbers;
}
@ -1370,6 +1472,7 @@ function applyPreferences(nextPreferences) {
document.documentElement.dataset.theme = activePreferences.theme;
document.documentElement.dataset.accent = activePreferences.accent;
applyPanelPatternPreferences(activePreferences);
document.documentElement.dataset.navLayout =
activePreferences.navLayout === "top" ? "top" : "sidebar";
document.documentElement.dataset.showTree = String(isTreeVisible(activePreferences));
@ -1391,6 +1494,10 @@ function applyPreferences(nextPreferences) {
getTopBarMaxHeight(activePreferences),
)}px`,
);
document.documentElement.style.setProperty(
"--settings-panel-width",
`${clampLayoutPreference("settingsPanelWidth", activePreferences.settingsPanelWidth)}px`,
);
const treeColumns = getTreeColumns(activePreferences);
const sidebarMaxWidth = getSidebarMaxWidth(activePreferences);
@ -1461,9 +1568,11 @@ function applyPreferences(nextPreferences) {
setActiveButton(navLayoutButtons, "navLayout", activePreferences.navLayout);
setActiveButton(viewButtons, "view", activePreferences.view);
setActiveButton(accentButtons, "accent", activePreferences.accent);
syncPatternTargetPanels(activePreferences);
setActiveButton(treeMetaLayoutButtons, "treeMetaLayout", activePreferences.treeMetaLayout);
syncLayoutLimitInputs(activePreferences);
syncCatalogColumnInputs(activePreferences);
syncSettingsPanelWidthInput(activePreferences);
applyPanelFeatures(activePreferences);
}
@ -1524,7 +1633,11 @@ function isSettingsSectionVisible(sectionName, activePreferences) {
case "tree":
return panelFeatures.allowTreeOptions && activePreferences.showTree;
case "appearance":
return panelFeatures.allowThemeChange || panelFeatures.allowAccentChange;
return (
panelFeatures.allowThemeChange ||
panelFeatures.allowAccentChange ||
panelFeatures.allowPanelPatternChange
);
default:
return true;
}
@ -1776,6 +1889,22 @@ function syncLayoutLimitInputs(activePreferences) {
});
}
function syncSettingsPanelWidthInput(activePreferences) {
if (!settingsPanelWidthInput) {
return;
}
const limits = getPanelLimitRange("settingsPanelWidth");
settingsPanelWidthInput.min = String(limits.min);
settingsPanelWidthInput.max = String(limits.max);
settingsPanelWidthInput.step = "10";
settingsPanelWidthInput.value = String(activePreferences.settingsPanelWidth);
if (settingsPanelWidthValue) {
settingsPanelWidthValue.textContent = `${activePreferences.settingsPanelWidth}px`;
}
}
function syncCatalogColumnInputs(activePreferences) {
catalogColumnInputs.forEach((input) => {
const key = input.dataset.catalogColumns;
@ -1802,6 +1931,194 @@ function getPanelLimitRange(key) {
return getLayoutLimitRange(key);
}
function clampPanelPatternPercent(value, fallback = 50) {
const numeric = Number(value);
const base = Number.isFinite(numeric) ? numeric : fallback;
return clamp(Math.round(base), 0, 100);
}
function getPanelPatternPreferenceKey(target, field) {
const cap = capitalizePanelPatternTarget(target);
switch (field) {
case "pattern":
return `panelPattern${cap}`;
case "opacity":
return `panelPattern${cap}Opacity`;
case "height":
return `panelPattern${cap}Height`;
case "fade":
return `panelPattern${cap}Fade`;
case "gradientOpacity":
return `panelPattern${cap}GradientOpacity`;
case "gradientHeight":
return `panelPattern${cap}GradientHeight`;
case "gradientFade":
return `panelPattern${cap}GradientFade`;
default:
return `panelPattern${cap}`;
}
}
function migrateLegacyPanelPatternPreferences(next) {
if (next.panelPatternNav !== undefined) {
return;
}
const legacyPattern = availablePanelPatterns.includes(next.panelPattern)
? next.panelPattern
: panelPatternTargetMeta.nav.defaultPattern;
const legacyOpacity = next.panelPatternOpacity ?? panelPatternTuningDefaults.opacity;
const legacyHeight = next.panelPatternHeight ?? panelPatternTuningDefaults.height;
const legacyGradientOpacity = next.panelGradientOpacity ?? panelPatternTuningDefaults.gradientOpacity;
const legacyGradientHeight = next.panelGradientHeight ?? panelPatternTuningDefaults.gradientHeight;
const legacyTargets = [
["Nav", next.panelPatternOnNav !== false],
["Section", next.panelPatternOnSection !== false],
["Group", Boolean(next.panelPatternOnGroup)],
["Item", Boolean(next.panelPatternOnItem)],
];
for (const [suffix, enabled] of legacyTargets) {
next[`panelPattern${suffix}`] = enabled && legacyPattern !== "none" ? legacyPattern : "none";
next[`panelPattern${suffix}Opacity`] = legacyOpacity;
next[`panelPattern${suffix}Height`] = legacyHeight;
next[`panelPattern${suffix}Fade`] = panelPatternTuningDefaults.fade;
next[`panelPattern${suffix}GradientOpacity`] = legacyGradientOpacity;
next[`panelPattern${suffix}GradientHeight`] = legacyGradientHeight;
next[`panelPattern${suffix}GradientFade`] = panelPatternTuningDefaults.gradientFade;
}
}
function normalizePanelPatternPreferences(next) {
for (const target of panelPatternTargetIds) {
const cap = capitalizePanelPatternTarget(target);
const patternKey = `panelPattern${cap}`;
if (!availablePanelPatterns.includes(next[patternKey])) {
next[patternKey] = panelPatternTargetMeta[target].defaultPattern;
}
next[`panelPattern${cap}Opacity`] = clampPanelPatternPercent(
next[`panelPattern${cap}Opacity`],
panelPatternTuningDefaults.opacity,
);
next[`panelPattern${cap}Height`] = clampPanelPatternPercent(
next[`panelPattern${cap}Height`],
panelPatternTuningDefaults.height,
);
next[`panelPattern${cap}Fade`] = clampPanelPatternPercent(
next[`panelPattern${cap}Fade`],
panelPatternTuningDefaults.fade,
);
next[`panelPattern${cap}GradientOpacity`] = clampPanelPatternPercent(
next[`panelPattern${cap}GradientOpacity`],
panelPatternTuningDefaults.gradientOpacity,
);
next[`panelPattern${cap}GradientHeight`] = clampPanelPatternPercent(
next[`panelPattern${cap}GradientHeight`],
panelPatternTuningDefaults.gradientHeight,
);
next[`panelPattern${cap}GradientFade`] = clampPanelPatternPercent(
next[`panelPattern${cap}GradientFade`],
panelPatternTuningDefaults.gradientFade,
);
}
}
function getPanelPatternFadeStart(heightPercent, fadePercent) {
const height = clampPanelPatternPercent(heightPercent, panelPatternTuningDefaults.height);
const fade = clampPanelPatternPercent(fadePercent, panelPatternTuningDefaults.fade);
return `${Math.max(0, height - fade)}%`;
}
function applyPanelPatternPreferences(activePreferences) {
for (const target of panelPatternTargetIds) {
const cap = capitalizePanelPatternTarget(target);
const pattern = activePreferences[`panelPattern${cap}`] || "none";
const height = clampPanelPatternPercent(
activePreferences[`panelPattern${cap}Height`],
panelPatternTuningDefaults.height,
);
const opacityPercent = clampPanelPatternPercent(
activePreferences[`panelPattern${cap}Opacity`],
panelPatternTuningDefaults.opacity,
);
const patternOpacity = pattern === "none" || height === 0 ? 0 : opacityPercent / 100;
document.documentElement.dataset[`panelPattern${cap}`] = pattern;
document.documentElement.style.setProperty(`--panel-pattern-${target}-opacity`, String(patternOpacity));
document.documentElement.style.setProperty(`--panel-pattern-${target}-height`, `${height}%`);
document.documentElement.style.setProperty(
`--panel-pattern-${target}-fade-start`,
getPanelPatternFadeStart(height, activePreferences[`panelPattern${cap}Fade`]),
);
document.documentElement.style.setProperty(
`--panel-gradient-${target}-strength`,
String(
(clampPanelPatternPercent(
activePreferences[`panelPattern${cap}GradientOpacity`],
panelPatternTuningDefaults.gradientOpacity,
) /
100) *
0.28,
),
);
const gradientHeight = clampPanelPatternPercent(
activePreferences[`panelPattern${cap}GradientHeight`],
panelPatternTuningDefaults.gradientHeight,
);
document.documentElement.style.setProperty(
`--panel-gradient-${target}-height`,
`${gradientHeight}%`,
);
document.documentElement.style.setProperty(
`--panel-gradient-${target}-fade-start`,
getPanelPatternFadeStart(gradientHeight, activePreferences[`panelPattern${cap}GradientFade`]),
);
}
}
function syncPatternTargetPanels(activePreferences) {
if (!patternTargetsPanel) {
return;
}
const disabled = !panelFeatures.allowPanelPatternChange;
const tuningFields = ["opacity", "height", "fade", "gradientOpacity", "gradientHeight", "gradientFade"];
for (const target of panelPatternTargetIds) {
const pattern = activePreferences[getPanelPatternPreferenceKey(target, "pattern")];
patternTargetsPanel
.querySelectorAll(`[data-panel-pattern-target="${target}"][data-panel-pattern]`)
.forEach((button) => {
const isActive = button.dataset.panelPattern === pattern;
button.classList.toggle("is-active", isActive);
button.setAttribute("aria-pressed", String(isActive));
button.disabled = disabled;
});
for (const field of tuningFields) {
const key = getPanelPatternPreferenceKey(target, field);
const input = patternTargetsPanel.querySelector(
`[data-panel-pattern-tuning="${target}"][data-panel-pattern-field="${field}"]`,
);
const output = patternTargetsPanel.querySelector(
`[data-panel-pattern-value="${target}"][data-panel-pattern-field="${field}"]`,
);
if (input) {
input.value = String(activePreferences[key]);
input.disabled = disabled;
}
if (output) {
output.textContent = `${activePreferences[key]}%`;
}
}
}
}
function clampCatalogColumn(key, value) {
const limits = getPanelLimitRange(key);
const fallback = panelDefaults[key] ?? defaultPreferences[key];

View File

@ -31,6 +31,46 @@
--catalog-list-columns: 1;
--catalog-compact-cards-columns: 5;
--catalog-compact-list-columns: 3;
--pattern-color: rgba(255, 255, 255, 0.16);
--pattern-tile-hex-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
--pattern-tile-hex-size: 96px 84px;
--pattern-tile-dots-image: radial-gradient(circle at center, var(--pattern-color) 0.95px, transparent 1px);
--pattern-tile-dots-size: 18px 18px;
--pattern-tile-grid-image:
linear-gradient(var(--pattern-color) 1px, transparent 1px),
linear-gradient(90deg, var(--pattern-color) 1px, transparent 1px);
--pattern-tile-grid-size: 26px 26px;
--pattern-tile-diagonal-image: repeating-linear-gradient(
-45deg,
transparent,
transparent 10px,
var(--pattern-color) 10px,
var(--pattern-color) 11px
);
--pattern-tile-diagonal-size: auto;
--pattern-tile-cross-image:
repeating-linear-gradient(
0deg,
transparent,
transparent 14px,
var(--pattern-color) 14px,
var(--pattern-color) 15px
),
repeating-linear-gradient(
90deg,
transparent,
transparent 14px,
var(--pattern-color) 14px,
var(--pattern-color) 15px
);
--pattern-tile-cross-size: auto;
--pattern-tile-mesh-image: radial-gradient(
circle at 1px 1px,
rgba(var(--accent-rgb), 0.22) 1px,
transparent 0
);
--pattern-tile-mesh-size: 20px 20px;
--settings-panel-width: 460px;
}
/* Светлая тема переопределяет только токены, структура стилей остается общей. */
@ -45,8 +85,40 @@ html[data-theme="light"] {
--muted: #5f6b7a;
--border: rgba(15, 23, 42, 0.12);
--shadow: 0 24px 70px rgba(56, 65, 91, 0.18);
--pattern-color: rgba(15, 23, 42, 0.13);
--pattern-tile-hex-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
--pattern-tile-mesh-image: radial-gradient(
circle at 1px 1px,
rgba(var(--accent-rgb), 0.16) 1px,
transparent 0
);
}
html[data-panel-pattern-nav="hex"] { --panel-pattern-image-nav: var(--pattern-tile-hex-image); --panel-pattern-size-nav: var(--pattern-tile-hex-size); }
html[data-panel-pattern-nav="dots"] { --panel-pattern-image-nav: var(--pattern-tile-dots-image); --panel-pattern-size-nav: var(--pattern-tile-dots-size); }
html[data-panel-pattern-nav="grid"] { --panel-pattern-image-nav: var(--pattern-tile-grid-image); --panel-pattern-size-nav: var(--pattern-tile-grid-size); }
html[data-panel-pattern-nav="diagonal"] { --panel-pattern-image-nav: var(--pattern-tile-diagonal-image); --panel-pattern-size-nav: var(--pattern-tile-diagonal-size); }
html[data-panel-pattern-nav="cross"] { --panel-pattern-image-nav: var(--pattern-tile-cross-image); --panel-pattern-size-nav: var(--pattern-tile-cross-size); }
html[data-panel-pattern-nav="mesh"] { --panel-pattern-image-nav: var(--pattern-tile-mesh-image); --panel-pattern-size-nav: var(--pattern-tile-mesh-size); }
html[data-panel-pattern-section="hex"] { --panel-pattern-image-section: var(--pattern-tile-hex-image); --panel-pattern-size-section: var(--pattern-tile-hex-size); }
html[data-panel-pattern-section="dots"] { --panel-pattern-image-section: var(--pattern-tile-dots-image); --panel-pattern-size-section: var(--pattern-tile-dots-size); }
html[data-panel-pattern-section="grid"] { --panel-pattern-image-section: var(--pattern-tile-grid-image); --panel-pattern-size-section: var(--pattern-tile-grid-size); }
html[data-panel-pattern-section="diagonal"] { --panel-pattern-image-section: var(--pattern-tile-diagonal-image); --panel-pattern-size-section: var(--pattern-tile-diagonal-size); }
html[data-panel-pattern-section="cross"] { --panel-pattern-image-section: var(--pattern-tile-cross-image); --panel-pattern-size-section: var(--pattern-tile-cross-size); }
html[data-panel-pattern-section="mesh"] { --panel-pattern-image-section: var(--pattern-tile-mesh-image); --panel-pattern-size-section: var(--pattern-tile-mesh-size); }
html[data-panel-pattern-group="hex"] { --panel-pattern-image-group: var(--pattern-tile-hex-image); --panel-pattern-size-group: var(--pattern-tile-hex-size); }
html[data-panel-pattern-group="dots"] { --panel-pattern-image-group: var(--pattern-tile-dots-image); --panel-pattern-size-group: var(--pattern-tile-dots-size); }
html[data-panel-pattern-group="grid"] { --panel-pattern-image-group: var(--pattern-tile-grid-image); --panel-pattern-size-group: var(--pattern-tile-grid-size); }
html[data-panel-pattern-group="diagonal"] { --panel-pattern-image-group: var(--pattern-tile-diagonal-image); --panel-pattern-size-group: var(--pattern-tile-diagonal-size); }
html[data-panel-pattern-group="cross"] { --panel-pattern-image-group: var(--pattern-tile-cross-image); --panel-pattern-size-group: var(--pattern-tile-cross-size); }
html[data-panel-pattern-group="mesh"] { --panel-pattern-image-group: var(--pattern-tile-mesh-image); --panel-pattern-size-group: var(--pattern-tile-mesh-size); }
html[data-panel-pattern-item="hex"] { --panel-pattern-image-item: var(--pattern-tile-hex-image); --panel-pattern-size-item: var(--pattern-tile-hex-size); }
html[data-panel-pattern-item="dots"] { --panel-pattern-image-item: var(--pattern-tile-dots-image); --panel-pattern-size-item: var(--pattern-tile-dots-size); }
html[data-panel-pattern-item="grid"] { --panel-pattern-image-item: var(--pattern-tile-grid-image); --panel-pattern-size-item: var(--pattern-tile-grid-size); }
html[data-panel-pattern-item="diagonal"] { --panel-pattern-image-item: var(--pattern-tile-diagonal-image); --panel-pattern-size-item: var(--pattern-tile-diagonal-size); }
html[data-panel-pattern-item="cross"] { --panel-pattern-image-item: var(--pattern-tile-cross-image); --panel-pattern-size-item: var(--pattern-tile-cross-size); }
html[data-panel-pattern-item="mesh"] { --panel-pattern-image-item: var(--pattern-tile-mesh-image); --panel-pattern-size-item: var(--pattern-tile-mesh-size); }
/* Наборы акцентов. --accent-rgb нужен для прозрачных градиентов через rgba(). */
html[data-accent="cyan"] {
--accent: #06b6d4;
@ -324,6 +396,8 @@ body.is-resizing-top-bar {
background: var(--panel);
box-shadow: var(--shadow);
backdrop-filter: blur(22px);
position: relative;
overflow: hidden;
}
.hero-content {
@ -335,7 +409,6 @@ body.is-resizing-top-bar {
gap: 8px;
padding: clamp(16px, 2.6vw, 24px);
border-radius: 22px;
position: relative;
overflow: visible;
}
@ -344,21 +417,117 @@ body.is-resizing-top-bar {
overflow: auto;
}
/* Внутренний фон sidebar: градиент + hexagon pattern. */
.hero-content::before {
/* Акцентный градиент под узором — отдельно для каждого типа блоков. */
html:not([data-panel-pattern-nav="none"]) .hero-content::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background:
linear-gradient(120deg, rgba(var(--accent-rgb), 0.25), transparent 34%),
url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.10' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
mask-image: linear-gradient(to bottom, black, transparent 88%);
background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%);
opacity: var(--panel-gradient-nav-strength);
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-nav-fade-start), transparent var(--panel-gradient-nav-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-nav-fade-start), transparent var(--panel-gradient-nav-height));
pointer-events: none;
z-index: 0;
}
html:not([data-panel-pattern-section="none"]) .catalog-section::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%);
opacity: var(--panel-gradient-section-strength);
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-section-fade-start), transparent var(--panel-gradient-section-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-section-fade-start), transparent var(--panel-gradient-section-height));
pointer-events: none;
z-index: 0;
}
html:not([data-panel-pattern-group="none"]) .catalog-group::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%);
opacity: var(--panel-gradient-group-strength);
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-group-fade-start), transparent var(--panel-gradient-group-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-group-fade-start), transparent var(--panel-gradient-group-height));
pointer-events: none;
z-index: 0;
}
html:not([data-panel-pattern-nav="none"]) .hero-content::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
opacity: var(--panel-pattern-nav-opacity);
background-image: var(--panel-pattern-image-nav);
background-size: var(--panel-pattern-size-nav);
background-repeat: repeat;
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-nav-fade-start), transparent var(--panel-pattern-nav-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-nav-fade-start), transparent var(--panel-pattern-nav-height));
}
html:not([data-panel-pattern-section="none"]) .catalog-section::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
opacity: var(--panel-pattern-section-opacity);
background-image: var(--panel-pattern-image-section);
background-size: var(--panel-pattern-size-section);
background-repeat: repeat;
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-section-fade-start), transparent var(--panel-pattern-section-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-section-fade-start), transparent var(--panel-pattern-section-height));
}
html:not([data-panel-pattern-group="none"]) .catalog-group::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
opacity: var(--panel-pattern-group-opacity);
background-image: var(--panel-pattern-image-group);
background-size: var(--panel-pattern-size-group);
background-repeat: repeat;
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-group-fade-start), transparent var(--panel-pattern-group-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-group-fade-start), transparent var(--panel-pattern-group-height));
}
html:not([data-panel-pattern-item="none"]) .nav-card::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
opacity: var(--panel-pattern-item-opacity);
background-image: var(--panel-pattern-image-item);
background-size: var(--panel-pattern-size-item);
background-repeat: repeat;
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-item-fade-start), transparent var(--panel-pattern-item-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-item-fade-start), transparent var(--panel-pattern-item-height));
}
.catalog-section > summary,
.catalog-section > .section-groups,
.catalog-group > summary,
.catalog-group > .items-grid {
position: relative;
z-index: 1;
}
.hero-content > * {
position: relative;
z-index: 1;
}
/* Общий стиль маленьких uppercase-меток и номеров. */
@ -414,7 +583,7 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta {
z-index: 1001;
top: 24px;
right: 24px;
width: min(460px, calc(100% - 48px));
width: min(var(--settings-panel-width), calc(100% - 48px));
max-height: calc(100vh - 48px);
overflow: auto;
display: grid;
@ -512,6 +681,27 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta {
line-height: 1;
}
.settings-width-row {
grid-template-columns: minmax(0, 1fr) auto;
grid-template-rows: auto auto;
gap: 6px 10px;
}
.settings-width-row span {
grid-column: 1;
grid-row: 1;
}
.settings-width-row output {
grid-column: 2;
grid-row: 1;
}
.settings-width-row input[type="range"] {
grid-column: 1 / -1;
grid-row: 2;
}
.settings-section {
display: grid;
gap: 10px;
@ -901,6 +1091,166 @@ html[data-theme="light"] .accent-swatch-custom {
transform: scale(1.05);
}
.pattern-group {
align-items: stretch;
}
.pattern-targets-panel {
display: grid;
gap: 10px;
width: 100%;
}
.pattern-target-panel .settings-section-body {
display: grid;
gap: 10px;
padding-top: 10px;
}
.pattern-swatches {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
width: 100%;
}
.pattern-swatch {
min-height: 54px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
color: var(--muted);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
cursor: pointer;
position: relative;
overflow: hidden;
padding: 0;
}
.pattern-swatch::before {
content: "";
position: absolute;
inset: 0;
opacity: 0.95;
pointer-events: none;
}
.pattern-swatch[data-panel-pattern="none"]::before {
background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.12), transparent 70%);
}
.pattern-swatch[data-panel-pattern="hex"]::before {
background-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
background-size: 48px 42px;
}
html[data-theme="light"] .pattern-swatch[data-panel-pattern="hex"]::before {
background-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
}
.pattern-swatch[data-panel-pattern="dots"]::before {
background-image: radial-gradient(circle at center, var(--pattern-color) 0.95px, transparent 1px);
background-size: 12px 12px;
}
.pattern-swatch[data-panel-pattern="grid"]::before {
background-image:
linear-gradient(var(--pattern-color) 1px, transparent 1px),
linear-gradient(90deg, var(--pattern-color) 1px, transparent 1px);
background-size: 16px 16px;
}
.pattern-swatch[data-panel-pattern="diagonal"]::before {
background-image: repeating-linear-gradient(
-45deg,
transparent,
transparent 7px,
var(--pattern-color) 7px,
var(--pattern-color) 8px
);
}
.pattern-swatch[data-panel-pattern="cross"]::before {
background-image:
repeating-linear-gradient(
0deg,
transparent,
transparent 9px,
var(--pattern-color) 9px,
var(--pattern-color) 10px
),
repeating-linear-gradient(
90deg,
transparent,
transparent 9px,
var(--pattern-color) 9px,
var(--pattern-color) 10px
);
}
.pattern-swatch[data-panel-pattern="mesh"]::before {
background-image: radial-gradient(
circle at 1px 1px,
rgba(var(--accent-rgb), 0.24) 1px,
transparent 0
);
background-size: 12px 12px;
}
.pattern-swatch-label {
position: relative;
z-index: 1;
display: grid;
place-items: end start;
width: 100%;
height: 100%;
padding: 6px 8px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.28), transparent 58%);
color: #f8fafc;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}
html[data-theme="light"] .pattern-swatch-label {
background: linear-gradient(to top, rgba(255, 255, 255, 0.82), transparent 58%);
color: #101827;
text-shadow: none;
}
.pattern-swatch.is-active {
border-color: rgba(var(--accent-rgb), 0.72);
box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.18);
}
.pattern-tuning {
display: grid;
gap: 12px;
width: 100%;
}
.pattern-tuning .accent-custom-row {
grid-template-columns: minmax(0, 1fr) auto;
grid-template-rows: auto auto;
gap: 6px 10px;
}
.pattern-tuning .accent-custom-row span {
grid-column: 1;
grid-row: 1;
}
.pattern-tuning .accent-custom-row output {
grid-column: 2;
grid-row: 1;
}
.pattern-tuning .accent-custom-row input[type="range"] {
grid-column: 1 / -1;
grid-row: 2;
}
/* Панель настройки своего градиента появляется при data-accent="custom". */
.accent-custom-panel {
display: grid;
@ -1329,6 +1679,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
}
.catalog-group {
position: relative;
border: 1px solid var(--border);
border-radius: 18px;
background: rgba(255, 255, 255, 0.035);
@ -1427,6 +1778,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
opacity: 0;
transition: opacity 180ms ease;
pointer-events: none;
z-index: 1;
}
/* Hover задается классом .is-hovered из JS, чтобы убрать скачки на границах. */
@ -1445,6 +1797,8 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
/* Иконка сервиса берется из serviceMeta. */
.nav-card img {
position: relative;
z-index: 1;
width: 50px;
height: 50px;
object-fit: contain;
@ -1453,6 +1807,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
.card-content {
position: relative;
z-index: 1;
display: grid;
gap: 5px;
}
@ -1493,6 +1848,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
position: absolute;
right: 16px;
bottom: 14px;
z-index: 1;
width: 36px;
height: 36px;
display: grid;
@ -1515,6 +1871,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
.shine {
position: absolute;
inset: 0;
z-index: 2;
background: linear-gradient(
110deg,
transparent 0%,

View File

@ -127,6 +127,11 @@
</span>
<input type="checkbox" data-show-resize-handles checked />
</label>
<label class="accent-custom-row settings-width-row">
<span>Ширина настроек</span>
<input type="range" data-settings-panel-width />
<output data-settings-panel-width-value>460px</output>
</label>
</section>
<details class="settings-section settings-section-collapsible" data-settings-section="dimensions">
@ -369,6 +374,114 @@
</button>
</div>
<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">
<summary class="settings-section-summary">
<span class="settings-section-title">Панель навигации</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="nav" data-panel-pattern-field="fade" /><output data-panel-pattern-value="nav" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="nav" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="nav" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="section">
<summary class="settings-section-summary">
<span class="settings-section-title">Раздел</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="section" data-panel-pattern-field="fade" /><output data-panel-pattern-value="section" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="section" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="section" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="group">
<summary class="settings-section-summary">
<span class="settings-section-title">Подраздел</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="group" data-panel-pattern-field="fade" /><output data-panel-pattern-value="group" data-panel-pattern-field="fade">10%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: прозрачность</span><input type="range" min="0" max="100" step="1" value="80" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientOpacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientOpacity">80%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: высота</span><input type="range" min="0" max="100" step="1" value="40" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientHeight" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientHeight">40%</output></label>
<label class="accent-custom-row pattern-gradient-tuning"><span>Градиент: переход</span><input type="range" min="0" max="100" step="1" value="8" data-panel-pattern-tuning="group" data-panel-pattern-field="gradientFade" /><output data-panel-pattern-value="group" data-panel-pattern-field="gradientFade">8%</output></label>
</div>
</div>
</details>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="item">
<summary class="settings-section-summary">
<span class="settings-section-title">Элемент</span>
<span class="collapse-indicator" aria-hidden="true">⌄</span>
</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-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>
<label class="accent-custom-row"><span>Узор: переход</span><input type="range" min="0" max="100" step="1" value="10" data-panel-pattern-tuning="item" data-panel-pattern-field="fade" /><output data-panel-pattern-value="item" data-panel-pattern-field="fade">10%</output></label>
</div>
</div>
</details>
</div>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>

321
script.js
View File

@ -65,6 +65,44 @@ const defaultServiceMeta = {
let serviceMeta = { ...defaultServiceMeta };
const availablePanelPatterns = ["none", "hex", "dots", "grid", "diagonal", "cross", "mesh"];
const panelPatternTargetIds = ["nav", "section", "group", "item"];
const panelPatternTuningDefaults = {
opacity: 70,
height: 45,
fade: 10,
gradientOpacity: 80,
gradientHeight: 40,
gradientFade: 8,
};
const panelPatternTargetMeta = {
nav: { defaultPattern: "hex" },
section: { defaultPattern: "hex" },
group: { defaultPattern: "none" },
item: { defaultPattern: "none" },
};
function capitalizePanelPatternTarget(target) {
return target.charAt(0).toUpperCase() + target.slice(1);
}
function buildDefaultPanelPatternPreferences() {
const prefs = {};
for (const target of panelPatternTargetIds) {
const cap = capitalizePanelPatternTarget(target);
prefs[`panelPattern${cap}`] = panelPatternTargetMeta[target].defaultPattern;
prefs[`panelPattern${cap}Opacity`] = panelPatternTuningDefaults.opacity;
prefs[`panelPattern${cap}Height`] = panelPatternTuningDefaults.height;
prefs[`panelPattern${cap}Fade`] = panelPatternTuningDefaults.fade;
prefs[`panelPattern${cap}GradientOpacity`] = panelPatternTuningDefaults.gradientOpacity;
prefs[`panelPattern${cap}GradientHeight`] = panelPatternTuningDefaults.gradientHeight;
prefs[`panelPattern${cap}GradientFade`] = panelPatternTuningDefaults.gradientFade;
}
return prefs;
}
// ============================================================================
// Default UI preferences
// ============================================================================
@ -76,6 +114,7 @@ const defaultPreferences = {
customAccentFrom: "#8b5cf6",
customAccentTo: "#22d3ee",
customAccentAngle: 135,
...buildDefaultPanelPatternPreferences(),
navLayout: "sidebar",
showTree: true,
showResizeHandles: true,
@ -100,6 +139,7 @@ const defaultPreferences = {
catalogCompactCardsColumns: 5,
catalogCompactListColumns: 3,
searchPlaceholder: "Grafana, S3, журнал, сервер...",
settingsPanelWidth: 460,
};
const defaultPanelFeatures = {
@ -114,6 +154,7 @@ const defaultPanelFeatures = {
allowLayoutLimitsChange: true,
allowThemeChange: true,
allowAccentChange: true,
allowPanelPatternChange: true,
};
const defaultPanelLimits = {
@ -129,6 +170,7 @@ const defaultPanelLimits = {
catalogListColumns: { min: 1, max: 4 },
catalogCompactCardsColumns: { min: 2, max: 8 },
catalogCompactListColumns: { min: 1, max: 6 },
settingsPanelWidth: { min: 320, max: 720 },
};
const layoutLimitKeys = [
@ -149,7 +191,7 @@ const catalogColumnKeys = [
"catalogCompactListColumns",
];
const panelLimitKeys = [...layoutLimitKeys, ...catalogColumnKeys];
const panelLimitKeys = [...layoutLimitKeys, ...catalogColumnKeys, "settingsPanelWidth"];
const availableNavLayouts = ["sidebar", "top"];
const availableTreeMetaLayouts = ["inline", "stacked"];
@ -186,6 +228,7 @@ const themeButtons = document.querySelectorAll("[data-theme]");
const navLayoutButtons = document.querySelectorAll("[data-nav-layout]");
const viewButtons = document.querySelectorAll("[data-view]");
const accentButtons = document.querySelectorAll("[data-accent]");
const patternTargetsPanel = document.querySelector("[data-pattern-targets-panel]");
const accentCustomPanel = document.querySelector("[data-accent-custom-panel]");
const customAccentFromInput = document.querySelector("[data-custom-accent-from]");
const customAccentToInput = document.querySelector("[data-custom-accent-to]");
@ -201,6 +244,8 @@ const treeColumnsInput = document.querySelector("[data-tree-columns]");
const treeColumnsValue = document.querySelector("[data-tree-columns-value]");
const treeMetaLayoutButtons = document.querySelectorAll("[data-tree-meta-layout]");
const autoCollapseSearchInput = document.querySelector("[data-auto-collapse-search]");
const settingsPanelWidthInput = document.querySelector("[data-settings-panel-width]");
const settingsPanelWidthValue = document.querySelector("[data-settings-panel-width-value]");
const layoutLimitInputs = document.querySelectorAll("[data-layout-limit]");
const catalogColumnInputs = document.querySelectorAll("[data-catalog-columns]");
const settingsSections = document.querySelectorAll("[data-settings-section]");
@ -388,6 +433,35 @@ accentButtons.forEach((button) => {
});
});
patternTargetsPanel?.addEventListener("click", (event) => {
const button = event.target.closest("[data-panel-pattern-target][data-panel-pattern]");
if (!button || !panelFeatures.allowPanelPatternChange) {
return;
}
const target = button.dataset.panelPatternTarget;
preferences[getPanelPatternPreferenceKey(target, "pattern")] = button.dataset.panelPattern;
applyPreferences(preferences);
savePreferences(preferences);
});
patternTargetsPanel?.addEventListener("input", (event) => {
const input = event.target.closest("[data-panel-pattern-tuning]");
if (!input || !panelFeatures.allowPanelPatternChange) {
return;
}
const target = input.dataset.panelPatternTuning;
const field = input.dataset.panelPatternField;
const key = getPanelPatternPreferenceKey(target, field);
const fallback = defaultPreferences[key] ?? panelPatternTuningDefaults[field] ?? 50;
preferences[key] = clampPanelPatternPercent(input.value, fallback);
applyPreferences(preferences);
savePreferences(preferences);
});
// Изменение параметров своего градиента сразу переключает accent на custom.
function handleCustomAccentChange() {
if (!panelFeatures.allowAccentChange) {
@ -459,6 +533,17 @@ if (treeColumnsInput) {
treeColumnsInput.addEventListener("input", handleTreeColumnsChange);
}
function handleSettingsPanelWidthChange() {
preferences.settingsPanelWidth = clampLayoutPreference(
"settingsPanelWidth",
settingsPanelWidthInput?.value,
);
applyPreferences(preferences);
savePreferences(preferences);
}
settingsPanelWidthInput?.addEventListener("input", handleSettingsPanelWidthChange);
treeMetaLayoutButtons.forEach((button) => {
button.addEventListener("click", () => {
if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) {
@ -625,6 +710,9 @@ function normalizePreferences(raw) {
next.accent = defaultPreferences.accent;
}
migrateLegacyPanelPatternPreferences(next);
normalizePanelPatternPreferences(next);
if (!availableNavLayouts.includes(next.navLayout)) {
next.navLayout = defaultPreferences.navLayout;
}
@ -659,6 +747,7 @@ function normalizePreferences(raw) {
getTopBarMinHeight(next),
getTopBarMaxHeight(next),
);
next.settingsPanelWidth = clampLayoutPreference("settingsPanelWidth", next.settingsPanelWidth);
return next;
}
@ -724,6 +813,19 @@ function enforceFeatureLocks(rawPreferences) {
next.customAccentAngle = panelDefaults.customAccentAngle;
}
if (!panelFeatures.allowPanelPatternChange) {
for (const target of panelPatternTargetIds) {
const cap = capitalizePanelPatternTarget(target);
next[`panelPattern${cap}`] = panelDefaults[`panelPattern${cap}`];
next[`panelPattern${cap}Opacity`] = panelDefaults[`panelPattern${cap}Opacity`];
next[`panelPattern${cap}Height`] = panelDefaults[`panelPattern${cap}Height`];
next[`panelPattern${cap}Fade`] = panelDefaults[`panelPattern${cap}Fade`];
next[`panelPattern${cap}GradientOpacity`] = panelDefaults[`panelPattern${cap}GradientOpacity`];
next[`panelPattern${cap}GradientHeight`] = panelDefaults[`panelPattern${cap}GradientHeight`];
next[`panelPattern${cap}GradientFade`] = panelDefaults[`panelPattern${cap}GradientFade`];
}
}
if (!panelFeatures.allowShowNumbers) {
next.showNumbers = panelDefaults.showNumbers;
}
@ -1370,6 +1472,7 @@ function applyPreferences(nextPreferences) {
document.documentElement.dataset.theme = activePreferences.theme;
document.documentElement.dataset.accent = activePreferences.accent;
applyPanelPatternPreferences(activePreferences);
document.documentElement.dataset.navLayout =
activePreferences.navLayout === "top" ? "top" : "sidebar";
document.documentElement.dataset.showTree = String(isTreeVisible(activePreferences));
@ -1391,6 +1494,10 @@ function applyPreferences(nextPreferences) {
getTopBarMaxHeight(activePreferences),
)}px`,
);
document.documentElement.style.setProperty(
"--settings-panel-width",
`${clampLayoutPreference("settingsPanelWidth", activePreferences.settingsPanelWidth)}px`,
);
const treeColumns = getTreeColumns(activePreferences);
const sidebarMaxWidth = getSidebarMaxWidth(activePreferences);
@ -1461,9 +1568,11 @@ function applyPreferences(nextPreferences) {
setActiveButton(navLayoutButtons, "navLayout", activePreferences.navLayout);
setActiveButton(viewButtons, "view", activePreferences.view);
setActiveButton(accentButtons, "accent", activePreferences.accent);
syncPatternTargetPanels(activePreferences);
setActiveButton(treeMetaLayoutButtons, "treeMetaLayout", activePreferences.treeMetaLayout);
syncLayoutLimitInputs(activePreferences);
syncCatalogColumnInputs(activePreferences);
syncSettingsPanelWidthInput(activePreferences);
applyPanelFeatures(activePreferences);
}
@ -1524,7 +1633,11 @@ function isSettingsSectionVisible(sectionName, activePreferences) {
case "tree":
return panelFeatures.allowTreeOptions && activePreferences.showTree;
case "appearance":
return panelFeatures.allowThemeChange || panelFeatures.allowAccentChange;
return (
panelFeatures.allowThemeChange ||
panelFeatures.allowAccentChange ||
panelFeatures.allowPanelPatternChange
);
default:
return true;
}
@ -1776,6 +1889,22 @@ function syncLayoutLimitInputs(activePreferences) {
});
}
function syncSettingsPanelWidthInput(activePreferences) {
if (!settingsPanelWidthInput) {
return;
}
const limits = getPanelLimitRange("settingsPanelWidth");
settingsPanelWidthInput.min = String(limits.min);
settingsPanelWidthInput.max = String(limits.max);
settingsPanelWidthInput.step = "10";
settingsPanelWidthInput.value = String(activePreferences.settingsPanelWidth);
if (settingsPanelWidthValue) {
settingsPanelWidthValue.textContent = `${activePreferences.settingsPanelWidth}px`;
}
}
function syncCatalogColumnInputs(activePreferences) {
catalogColumnInputs.forEach((input) => {
const key = input.dataset.catalogColumns;
@ -1802,6 +1931,194 @@ function getPanelLimitRange(key) {
return getLayoutLimitRange(key);
}
function clampPanelPatternPercent(value, fallback = 50) {
const numeric = Number(value);
const base = Number.isFinite(numeric) ? numeric : fallback;
return clamp(Math.round(base), 0, 100);
}
function getPanelPatternPreferenceKey(target, field) {
const cap = capitalizePanelPatternTarget(target);
switch (field) {
case "pattern":
return `panelPattern${cap}`;
case "opacity":
return `panelPattern${cap}Opacity`;
case "height":
return `panelPattern${cap}Height`;
case "fade":
return `panelPattern${cap}Fade`;
case "gradientOpacity":
return `panelPattern${cap}GradientOpacity`;
case "gradientHeight":
return `panelPattern${cap}GradientHeight`;
case "gradientFade":
return `panelPattern${cap}GradientFade`;
default:
return `panelPattern${cap}`;
}
}
function migrateLegacyPanelPatternPreferences(next) {
if (next.panelPatternNav !== undefined) {
return;
}
const legacyPattern = availablePanelPatterns.includes(next.panelPattern)
? next.panelPattern
: panelPatternTargetMeta.nav.defaultPattern;
const legacyOpacity = next.panelPatternOpacity ?? panelPatternTuningDefaults.opacity;
const legacyHeight = next.panelPatternHeight ?? panelPatternTuningDefaults.height;
const legacyGradientOpacity = next.panelGradientOpacity ?? panelPatternTuningDefaults.gradientOpacity;
const legacyGradientHeight = next.panelGradientHeight ?? panelPatternTuningDefaults.gradientHeight;
const legacyTargets = [
["Nav", next.panelPatternOnNav !== false],
["Section", next.panelPatternOnSection !== false],
["Group", Boolean(next.panelPatternOnGroup)],
["Item", Boolean(next.panelPatternOnItem)],
];
for (const [suffix, enabled] of legacyTargets) {
next[`panelPattern${suffix}`] = enabled && legacyPattern !== "none" ? legacyPattern : "none";
next[`panelPattern${suffix}Opacity`] = legacyOpacity;
next[`panelPattern${suffix}Height`] = legacyHeight;
next[`panelPattern${suffix}Fade`] = panelPatternTuningDefaults.fade;
next[`panelPattern${suffix}GradientOpacity`] = legacyGradientOpacity;
next[`panelPattern${suffix}GradientHeight`] = legacyGradientHeight;
next[`panelPattern${suffix}GradientFade`] = panelPatternTuningDefaults.gradientFade;
}
}
function normalizePanelPatternPreferences(next) {
for (const target of panelPatternTargetIds) {
const cap = capitalizePanelPatternTarget(target);
const patternKey = `panelPattern${cap}`;
if (!availablePanelPatterns.includes(next[patternKey])) {
next[patternKey] = panelPatternTargetMeta[target].defaultPattern;
}
next[`panelPattern${cap}Opacity`] = clampPanelPatternPercent(
next[`panelPattern${cap}Opacity`],
panelPatternTuningDefaults.opacity,
);
next[`panelPattern${cap}Height`] = clampPanelPatternPercent(
next[`panelPattern${cap}Height`],
panelPatternTuningDefaults.height,
);
next[`panelPattern${cap}Fade`] = clampPanelPatternPercent(
next[`panelPattern${cap}Fade`],
panelPatternTuningDefaults.fade,
);
next[`panelPattern${cap}GradientOpacity`] = clampPanelPatternPercent(
next[`panelPattern${cap}GradientOpacity`],
panelPatternTuningDefaults.gradientOpacity,
);
next[`panelPattern${cap}GradientHeight`] = clampPanelPatternPercent(
next[`panelPattern${cap}GradientHeight`],
panelPatternTuningDefaults.gradientHeight,
);
next[`panelPattern${cap}GradientFade`] = clampPanelPatternPercent(
next[`panelPattern${cap}GradientFade`],
panelPatternTuningDefaults.gradientFade,
);
}
}
function getPanelPatternFadeStart(heightPercent, fadePercent) {
const height = clampPanelPatternPercent(heightPercent, panelPatternTuningDefaults.height);
const fade = clampPanelPatternPercent(fadePercent, panelPatternTuningDefaults.fade);
return `${Math.max(0, height - fade)}%`;
}
function applyPanelPatternPreferences(activePreferences) {
for (const target of panelPatternTargetIds) {
const cap = capitalizePanelPatternTarget(target);
const pattern = activePreferences[`panelPattern${cap}`] || "none";
const height = clampPanelPatternPercent(
activePreferences[`panelPattern${cap}Height`],
panelPatternTuningDefaults.height,
);
const opacityPercent = clampPanelPatternPercent(
activePreferences[`panelPattern${cap}Opacity`],
panelPatternTuningDefaults.opacity,
);
const patternOpacity = pattern === "none" || height === 0 ? 0 : opacityPercent / 100;
document.documentElement.dataset[`panelPattern${cap}`] = pattern;
document.documentElement.style.setProperty(`--panel-pattern-${target}-opacity`, String(patternOpacity));
document.documentElement.style.setProperty(`--panel-pattern-${target}-height`, `${height}%`);
document.documentElement.style.setProperty(
`--panel-pattern-${target}-fade-start`,
getPanelPatternFadeStart(height, activePreferences[`panelPattern${cap}Fade`]),
);
document.documentElement.style.setProperty(
`--panel-gradient-${target}-strength`,
String(
(clampPanelPatternPercent(
activePreferences[`panelPattern${cap}GradientOpacity`],
panelPatternTuningDefaults.gradientOpacity,
) /
100) *
0.28,
),
);
const gradientHeight = clampPanelPatternPercent(
activePreferences[`panelPattern${cap}GradientHeight`],
panelPatternTuningDefaults.gradientHeight,
);
document.documentElement.style.setProperty(
`--panel-gradient-${target}-height`,
`${gradientHeight}%`,
);
document.documentElement.style.setProperty(
`--panel-gradient-${target}-fade-start`,
getPanelPatternFadeStart(gradientHeight, activePreferences[`panelPattern${cap}GradientFade`]),
);
}
}
function syncPatternTargetPanels(activePreferences) {
if (!patternTargetsPanel) {
return;
}
const disabled = !panelFeatures.allowPanelPatternChange;
const tuningFields = ["opacity", "height", "fade", "gradientOpacity", "gradientHeight", "gradientFade"];
for (const target of panelPatternTargetIds) {
const pattern = activePreferences[getPanelPatternPreferenceKey(target, "pattern")];
patternTargetsPanel
.querySelectorAll(`[data-panel-pattern-target="${target}"][data-panel-pattern]`)
.forEach((button) => {
const isActive = button.dataset.panelPattern === pattern;
button.classList.toggle("is-active", isActive);
button.setAttribute("aria-pressed", String(isActive));
button.disabled = disabled;
});
for (const field of tuningFields) {
const key = getPanelPatternPreferenceKey(target, field);
const input = patternTargetsPanel.querySelector(
`[data-panel-pattern-tuning="${target}"][data-panel-pattern-field="${field}"]`,
);
const output = patternTargetsPanel.querySelector(
`[data-panel-pattern-value="${target}"][data-panel-pattern-field="${field}"]`,
);
if (input) {
input.value = String(activePreferences[key]);
input.disabled = disabled;
}
if (output) {
output.textContent = `${activePreferences[key]}%`;
}
}
}
}
function clampCatalogColumn(key, value) {
const limits = getPanelLimitRange(key);
const fallback = panelDefaults[key] ?? defaultPreferences[key];

View File

@ -11,6 +11,26 @@
"customAccentFrom": "#8b5cf6",
"customAccentTo": "#22d3ee",
"customAccentAngle": 135,
"panelPatternNav": "hex",
"panelPatternNavOpacity": 70,
"panelPatternNavHeight": 45,
"panelPatternNavGradientOpacity": 80,
"panelPatternNavGradientHeight": 40,
"panelPatternSection": "hex",
"panelPatternSectionOpacity": 70,
"panelPatternSectionHeight": 45,
"panelPatternSectionGradientOpacity": 80,
"panelPatternSectionGradientHeight": 40,
"panelPatternGroup": "none",
"panelPatternGroupOpacity": 70,
"panelPatternGroupHeight": 45,
"panelPatternGroupGradientOpacity": 80,
"panelPatternGroupGradientHeight": 40,
"panelPatternItem": "none",
"panelPatternItemOpacity": 70,
"panelPatternItemHeight": 45,
"panelPatternItemGradientOpacity": 80,
"panelPatternItemGradientHeight": 40,
"showNumbers": false,
"showTreeTags": true,
"showTreeType": false,
@ -31,7 +51,8 @@
"catalogListColumns": 1,
"catalogCompactCardsColumns": 5,
"catalogCompactListColumns": 3,
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
"settingsPanelWidth": 460
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
@ -45,7 +66,8 @@
"catalogCardsColumns": { "min": 1, "max": 6 },
"catalogListColumns": { "min": 1, "max": 4 },
"catalogCompactCardsColumns": { "min": 2, "max": 8 },
"catalogCompactListColumns": { "min": 1, "max": 6 }
"catalogCompactListColumns": { "min": 1, "max": 6 },
"settingsPanelWidth": { "min": 320, "max": 720 }
},
"features": {
"allowNavLayoutChange": true,
@ -58,6 +80,7 @@
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true
"allowAccentChange": true,
"allowPanelPatternChange": true
}
}

View File

@ -31,6 +31,46 @@
--catalog-list-columns: 1;
--catalog-compact-cards-columns: 5;
--catalog-compact-list-columns: 3;
--pattern-color: rgba(255, 255, 255, 0.16);
--pattern-tile-hex-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
--pattern-tile-hex-size: 96px 84px;
--pattern-tile-dots-image: radial-gradient(circle at center, var(--pattern-color) 0.95px, transparent 1px);
--pattern-tile-dots-size: 18px 18px;
--pattern-tile-grid-image:
linear-gradient(var(--pattern-color) 1px, transparent 1px),
linear-gradient(90deg, var(--pattern-color) 1px, transparent 1px);
--pattern-tile-grid-size: 26px 26px;
--pattern-tile-diagonal-image: repeating-linear-gradient(
-45deg,
transparent,
transparent 10px,
var(--pattern-color) 10px,
var(--pattern-color) 11px
);
--pattern-tile-diagonal-size: auto;
--pattern-tile-cross-image:
repeating-linear-gradient(
0deg,
transparent,
transparent 14px,
var(--pattern-color) 14px,
var(--pattern-color) 15px
),
repeating-linear-gradient(
90deg,
transparent,
transparent 14px,
var(--pattern-color) 14px,
var(--pattern-color) 15px
);
--pattern-tile-cross-size: auto;
--pattern-tile-mesh-image: radial-gradient(
circle at 1px 1px,
rgba(var(--accent-rgb), 0.22) 1px,
transparent 0
);
--pattern-tile-mesh-size: 20px 20px;
--settings-panel-width: 460px;
}
/* Светлая тема переопределяет только токены, структура стилей остается общей. */
@ -45,8 +85,40 @@ html[data-theme="light"] {
--muted: #5f6b7a;
--border: rgba(15, 23, 42, 0.12);
--shadow: 0 24px 70px rgba(56, 65, 91, 0.18);
--pattern-color: rgba(15, 23, 42, 0.13);
--pattern-tile-hex-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
--pattern-tile-mesh-image: radial-gradient(
circle at 1px 1px,
rgba(var(--accent-rgb), 0.16) 1px,
transparent 0
);
}
html[data-panel-pattern-nav="hex"] { --panel-pattern-image-nav: var(--pattern-tile-hex-image); --panel-pattern-size-nav: var(--pattern-tile-hex-size); }
html[data-panel-pattern-nav="dots"] { --panel-pattern-image-nav: var(--pattern-tile-dots-image); --panel-pattern-size-nav: var(--pattern-tile-dots-size); }
html[data-panel-pattern-nav="grid"] { --panel-pattern-image-nav: var(--pattern-tile-grid-image); --panel-pattern-size-nav: var(--pattern-tile-grid-size); }
html[data-panel-pattern-nav="diagonal"] { --panel-pattern-image-nav: var(--pattern-tile-diagonal-image); --panel-pattern-size-nav: var(--pattern-tile-diagonal-size); }
html[data-panel-pattern-nav="cross"] { --panel-pattern-image-nav: var(--pattern-tile-cross-image); --panel-pattern-size-nav: var(--pattern-tile-cross-size); }
html[data-panel-pattern-nav="mesh"] { --panel-pattern-image-nav: var(--pattern-tile-mesh-image); --panel-pattern-size-nav: var(--pattern-tile-mesh-size); }
html[data-panel-pattern-section="hex"] { --panel-pattern-image-section: var(--pattern-tile-hex-image); --panel-pattern-size-section: var(--pattern-tile-hex-size); }
html[data-panel-pattern-section="dots"] { --panel-pattern-image-section: var(--pattern-tile-dots-image); --panel-pattern-size-section: var(--pattern-tile-dots-size); }
html[data-panel-pattern-section="grid"] { --panel-pattern-image-section: var(--pattern-tile-grid-image); --panel-pattern-size-section: var(--pattern-tile-grid-size); }
html[data-panel-pattern-section="diagonal"] { --panel-pattern-image-section: var(--pattern-tile-diagonal-image); --panel-pattern-size-section: var(--pattern-tile-diagonal-size); }
html[data-panel-pattern-section="cross"] { --panel-pattern-image-section: var(--pattern-tile-cross-image); --panel-pattern-size-section: var(--pattern-tile-cross-size); }
html[data-panel-pattern-section="mesh"] { --panel-pattern-image-section: var(--pattern-tile-mesh-image); --panel-pattern-size-section: var(--pattern-tile-mesh-size); }
html[data-panel-pattern-group="hex"] { --panel-pattern-image-group: var(--pattern-tile-hex-image); --panel-pattern-size-group: var(--pattern-tile-hex-size); }
html[data-panel-pattern-group="dots"] { --panel-pattern-image-group: var(--pattern-tile-dots-image); --panel-pattern-size-group: var(--pattern-tile-dots-size); }
html[data-panel-pattern-group="grid"] { --panel-pattern-image-group: var(--pattern-tile-grid-image); --panel-pattern-size-group: var(--pattern-tile-grid-size); }
html[data-panel-pattern-group="diagonal"] { --panel-pattern-image-group: var(--pattern-tile-diagonal-image); --panel-pattern-size-group: var(--pattern-tile-diagonal-size); }
html[data-panel-pattern-group="cross"] { --panel-pattern-image-group: var(--pattern-tile-cross-image); --panel-pattern-size-group: var(--pattern-tile-cross-size); }
html[data-panel-pattern-group="mesh"] { --panel-pattern-image-group: var(--pattern-tile-mesh-image); --panel-pattern-size-group: var(--pattern-tile-mesh-size); }
html[data-panel-pattern-item="hex"] { --panel-pattern-image-item: var(--pattern-tile-hex-image); --panel-pattern-size-item: var(--pattern-tile-hex-size); }
html[data-panel-pattern-item="dots"] { --panel-pattern-image-item: var(--pattern-tile-dots-image); --panel-pattern-size-item: var(--pattern-tile-dots-size); }
html[data-panel-pattern-item="grid"] { --panel-pattern-image-item: var(--pattern-tile-grid-image); --panel-pattern-size-item: var(--pattern-tile-grid-size); }
html[data-panel-pattern-item="diagonal"] { --panel-pattern-image-item: var(--pattern-tile-diagonal-image); --panel-pattern-size-item: var(--pattern-tile-diagonal-size); }
html[data-panel-pattern-item="cross"] { --panel-pattern-image-item: var(--pattern-tile-cross-image); --panel-pattern-size-item: var(--pattern-tile-cross-size); }
html[data-panel-pattern-item="mesh"] { --panel-pattern-image-item: var(--pattern-tile-mesh-image); --panel-pattern-size-item: var(--pattern-tile-mesh-size); }
/* Наборы акцентов. --accent-rgb нужен для прозрачных градиентов через rgba(). */
html[data-accent="cyan"] {
--accent: #06b6d4;
@ -324,6 +396,8 @@ body.is-resizing-top-bar {
background: var(--panel);
box-shadow: var(--shadow);
backdrop-filter: blur(22px);
position: relative;
overflow: hidden;
}
.hero-content {
@ -335,7 +409,6 @@ body.is-resizing-top-bar {
gap: 8px;
padding: clamp(16px, 2.6vw, 24px);
border-radius: 22px;
position: relative;
overflow: visible;
}
@ -344,21 +417,117 @@ body.is-resizing-top-bar {
overflow: auto;
}
/* Внутренний фон sidebar: градиент + hexagon pattern. */
.hero-content::before {
/* Акцентный градиент под узором — отдельно для каждого типа блоков. */
html:not([data-panel-pattern-nav="none"]) .hero-content::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background:
linear-gradient(120deg, rgba(var(--accent-rgb), 0.25), transparent 34%),
url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.10' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
mask-image: linear-gradient(to bottom, black, transparent 88%);
background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%);
opacity: var(--panel-gradient-nav-strength);
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-nav-fade-start), transparent var(--panel-gradient-nav-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-nav-fade-start), transparent var(--panel-gradient-nav-height));
pointer-events: none;
z-index: 0;
}
html:not([data-panel-pattern-section="none"]) .catalog-section::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%);
opacity: var(--panel-gradient-section-strength);
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-section-fade-start), transparent var(--panel-gradient-section-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-section-fade-start), transparent var(--panel-gradient-section-height));
pointer-events: none;
z-index: 0;
}
html:not([data-panel-pattern-group="none"]) .catalog-group::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(120deg, rgba(var(--accent-rgb), 1), transparent 38%);
opacity: var(--panel-gradient-group-strength);
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-group-fade-start), transparent var(--panel-gradient-group-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-gradient-group-fade-start), transparent var(--panel-gradient-group-height));
pointer-events: none;
z-index: 0;
}
html:not([data-panel-pattern-nav="none"]) .hero-content::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
opacity: var(--panel-pattern-nav-opacity);
background-image: var(--panel-pattern-image-nav);
background-size: var(--panel-pattern-size-nav);
background-repeat: repeat;
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-nav-fade-start), transparent var(--panel-pattern-nav-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-nav-fade-start), transparent var(--panel-pattern-nav-height));
}
html:not([data-panel-pattern-section="none"]) .catalog-section::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
opacity: var(--panel-pattern-section-opacity);
background-image: var(--panel-pattern-image-section);
background-size: var(--panel-pattern-size-section);
background-repeat: repeat;
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-section-fade-start), transparent var(--panel-pattern-section-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-section-fade-start), transparent var(--panel-pattern-section-height));
}
html:not([data-panel-pattern-group="none"]) .catalog-group::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
opacity: var(--panel-pattern-group-opacity);
background-image: var(--panel-pattern-image-group);
background-size: var(--panel-pattern-size-group);
background-repeat: repeat;
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-group-fade-start), transparent var(--panel-pattern-group-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-group-fade-start), transparent var(--panel-pattern-group-height));
}
html:not([data-panel-pattern-item="none"]) .nav-card::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
opacity: var(--panel-pattern-item-opacity);
background-image: var(--panel-pattern-image-item);
background-size: var(--panel-pattern-size-item);
background-repeat: repeat;
mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-item-fade-start), transparent var(--panel-pattern-item-height));
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--panel-pattern-item-fade-start), transparent var(--panel-pattern-item-height));
}
.catalog-section > summary,
.catalog-section > .section-groups,
.catalog-group > summary,
.catalog-group > .items-grid {
position: relative;
z-index: 1;
}
.hero-content > * {
position: relative;
z-index: 1;
}
/* Общий стиль маленьких uppercase-меток и номеров. */
@ -414,7 +583,7 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta {
z-index: 1001;
top: 24px;
right: 24px;
width: min(460px, calc(100% - 48px));
width: min(var(--settings-panel-width), calc(100% - 48px));
max-height: calc(100vh - 48px);
overflow: auto;
display: grid;
@ -512,6 +681,27 @@ html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta {
line-height: 1;
}
.settings-width-row {
grid-template-columns: minmax(0, 1fr) auto;
grid-template-rows: auto auto;
gap: 6px 10px;
}
.settings-width-row span {
grid-column: 1;
grid-row: 1;
}
.settings-width-row output {
grid-column: 2;
grid-row: 1;
}
.settings-width-row input[type="range"] {
grid-column: 1 / -1;
grid-row: 2;
}
.settings-section {
display: grid;
gap: 10px;
@ -901,6 +1091,166 @@ html[data-theme="light"] .accent-swatch-custom {
transform: scale(1.05);
}
.pattern-group {
align-items: stretch;
}
.pattern-targets-panel {
display: grid;
gap: 10px;
width: 100%;
}
.pattern-target-panel .settings-section-body {
display: grid;
gap: 10px;
padding-top: 10px;
}
.pattern-swatches {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
width: 100%;
}
.pattern-swatch {
min-height: 54px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--surface);
color: var(--muted);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
cursor: pointer;
position: relative;
overflow: hidden;
padding: 0;
}
.pattern-swatch::before {
content: "";
position: absolute;
inset: 0;
opacity: 0.95;
pointer-events: none;
}
.pattern-swatch[data-panel-pattern="none"]::before {
background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.12), transparent 70%);
}
.pattern-swatch[data-panel-pattern="hex"]::before {
background-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='white' stroke-opacity='.18' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
background-size: 48px 42px;
}
html[data-theme="light"] .pattern-swatch[data-panel-pattern="hex"]::before {
background-image: url("data:image/svg+xml,%3Csvg width='96' height='84' viewBox='0 0 96 84' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%23101827' stroke-opacity='.14' stroke-width='1.4'%3E%3Cpath d='M24 2 46 14v25L24 52 2 39V14L24 2Z'/%3E%3Cpath d='M72 32 94 44v25L72 82 50 69V44l22-12Z'/%3E%3C/g%3E%3C/svg%3E");
}
.pattern-swatch[data-panel-pattern="dots"]::before {
background-image: radial-gradient(circle at center, var(--pattern-color) 0.95px, transparent 1px);
background-size: 12px 12px;
}
.pattern-swatch[data-panel-pattern="grid"]::before {
background-image:
linear-gradient(var(--pattern-color) 1px, transparent 1px),
linear-gradient(90deg, var(--pattern-color) 1px, transparent 1px);
background-size: 16px 16px;
}
.pattern-swatch[data-panel-pattern="diagonal"]::before {
background-image: repeating-linear-gradient(
-45deg,
transparent,
transparent 7px,
var(--pattern-color) 7px,
var(--pattern-color) 8px
);
}
.pattern-swatch[data-panel-pattern="cross"]::before {
background-image:
repeating-linear-gradient(
0deg,
transparent,
transparent 9px,
var(--pattern-color) 9px,
var(--pattern-color) 10px
),
repeating-linear-gradient(
90deg,
transparent,
transparent 9px,
var(--pattern-color) 9px,
var(--pattern-color) 10px
);
}
.pattern-swatch[data-panel-pattern="mesh"]::before {
background-image: radial-gradient(
circle at 1px 1px,
rgba(var(--accent-rgb), 0.24) 1px,
transparent 0
);
background-size: 12px 12px;
}
.pattern-swatch-label {
position: relative;
z-index: 1;
display: grid;
place-items: end start;
width: 100%;
height: 100%;
padding: 6px 8px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.28), transparent 58%);
color: #f8fafc;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}
html[data-theme="light"] .pattern-swatch-label {
background: linear-gradient(to top, rgba(255, 255, 255, 0.82), transparent 58%);
color: #101827;
text-shadow: none;
}
.pattern-swatch.is-active {
border-color: rgba(var(--accent-rgb), 0.72);
box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.18);
}
.pattern-tuning {
display: grid;
gap: 12px;
width: 100%;
}
.pattern-tuning .accent-custom-row {
grid-template-columns: minmax(0, 1fr) auto;
grid-template-rows: auto auto;
gap: 6px 10px;
}
.pattern-tuning .accent-custom-row span {
grid-column: 1;
grid-row: 1;
}
.pattern-tuning .accent-custom-row output {
grid-column: 2;
grid-row: 1;
}
.pattern-tuning .accent-custom-row input[type="range"] {
grid-column: 1 / -1;
grid-row: 2;
}
/* Панель настройки своего градиента появляется при data-accent="custom". */
.accent-custom-panel {
display: grid;
@ -1329,6 +1679,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
}
.catalog-group {
position: relative;
border: 1px solid var(--border);
border-radius: 18px;
background: rgba(255, 255, 255, 0.035);
@ -1427,6 +1778,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
opacity: 0;
transition: opacity 180ms ease;
pointer-events: none;
z-index: 1;
}
/* Hover задается классом .is-hovered из JS, чтобы убрать скачки на границах. */
@ -1445,6 +1797,8 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
/* Иконка сервиса берется из serviceMeta. */
.nav-card img {
position: relative;
z-index: 1;
width: 50px;
height: 50px;
object-fit: contain;
@ -1453,6 +1807,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
.card-content {
position: relative;
z-index: 1;
display: grid;
gap: 5px;
}
@ -1493,6 +1848,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
position: absolute;
right: 16px;
bottom: 14px;
z-index: 1;
width: 36px;
height: 36px;
display: grid;
@ -1515,6 +1871,7 @@ html:not([data-tree-columns="1"]) .tree-node-icon {
.shine {
position: absolute;
inset: 0;
z-index: 2;
background: linear-gradient(
110deg,
transparent 0%,