Compare commits

...

10 Commits

Author SHA1 Message Date
e1fd7d2713 глобальные изменения кастомизации каждой панели, импорт-экспорт-сброс настроек, плавное закрытие скрытие 2026-06-29 11:27:36 +03:00
26dc3bac2b добавлена возможность настраивать каждый элемент (узоры) 2026-06-28 19:12:47 +03:00
50a04e3856 Обновление документации 2026-06-28 18:18:45 +03:00
459fc98bf1 Добавлена возможность все очень гибко настраивать. Добавлены новые типы. Сделаны примеры. 2026-06-28 18:18:44 +03:00
c1547cc24e в меню иконки items соответствуют их типу 2026-06-28 18:18:43 +03:00
f3508bf7f9 добавление дефолтных настроек, возможность меню сбоку и сверху 2026-06-28 18:18:43 +03:00
284a7f6b75 более гибкая архитектура размещения элементов с примерами и документацией 2026-06-28 18:18:42 +03:00
91ca4e8d57 вынесение настроек в отдельный файл, добавление readme 2026-06-28 18:18:41 +03:00
75ed13712a возможность 2х рядного дерева на мобильных 2026-06-28 18:18:41 +03:00
f540181c84 переключение адаптива для разных устройств по поинтеру,а не по ширине 2026-06-28 18:18:40 +03:00
73 changed files with 13841 additions and 367 deletions

731
README.md Normal file
View File

@ -0,0 +1,731 @@
# Панель экосистемы
Статическая навигационная панель для быстрого доступа к сервисам инфраструктуры. Поддерживает гибкую иерархию данных, поиск, дерево навигации, несколько режимов отображения и настройку внешнего вида.
Стек: HTML, CSS, vanilla JavaScript. Без сборщиков и фреймворков.
## Оглавление
- [Возможности](#возможности)
- [Быстрый старт](#быстрый-старт)
- [Структура проекта](#структура-проекта)
- [Конфигурация](#конфигурация)
- [`settings.json`](#settingsjson)
- [Пресет организации (`defaults` + `features`)](#пресет-организации-defaults--features)
- [Форматы иерархии](#форматы-иерархии)
- [Схема `panel-data`](#схема-panel-data)
- [Поля данных](#поля-данных)
- [Примеры данных](#примеры-данных)
- [Готовые демо-страницы (`examples/`)](#готовые-демо-страницы-examples)
- [Типы сервисов (`serviceMeta`)](#типы-сервисов-servicemeta)
- [Рекомендуемые типы сервисов](#рекомендуемые-типы-сервисов)
- [Настройки интерфейса (⚙)](#настройки-интерфейса-)
- [Узоры панелей](#узоры-панелей)
- [Разделение слоёв узоров](#разделение-слоёв-узоров)
- [Свой узор (custom)](#свой-узор-custom)
- [Экспорт и импорт настроек](#экспорт-и-импорт-настроек)
- [Навигация по коду](#навигация-по-коду)
- [Поведение интерфейса](#поведение-интерфейса)
- [localStorage](#localstorage)
- [Разработка](#разработка)
- [Ограничения](#ограничения)
- [Лицензия](#лицензия)
## Возможности
- **Древовидная навигация** слева в стиле файлового менеджера
- **Каталог карточек** справа с collapsible-разделами
- **Поиск** по названию, тегам, типу сервиса и родительским разделам
- **4 режима отображения**: плитки, список, компактные плитки, компактный список
- **Темы**: светлая / тёмная, 4 пресета акцента + свой градиент
- **Узоры панелей** — 13 встроенных пресетов + свой URL/SVG; отдельно для nav / section / group / item; слайдеры прозрачности, высоты и перехода
- **Разделение слоёв** — подложка и blur, чтобы вложенные узоры не «наезжали» друг на друга
- **Экспорт / импорт / сброс** настроек ⚙ в JSON-файл
- **Настройки** через popup (иконка ⚙)
- **Изменяемая ширина** бокового меню и **высота** верхней шапки (drag, можно скрыть ручки)
- **Габариты** — лимиты ширины контента, меню и шапки (⚙, для администратора)
- **Колонки каталога** — число элементов в строке для каждого из 4 режимов отображения
- **Сохранение состояния** в `localStorage` (настройки UI, раскрытие веток)
- **Конфигурация через JSON** — локальный объект или загрузка по URL
- **Гибкая иерархия**: полная вложенность, раздел без подраздела, плоский список
## Быстрый старт
Панель использует `fetch`, поэтому её нужно открывать через HTTP-сервер, а не напрямую как `file://`.
```bash
cd ecosystem
npx serve .
```
Открой в браузере адрес, который покажет сервер (обычно `http://localhost:3000`).
## Структура проекта
```
ecosystem/
├── index.html # Разметка страницы
├── styles.css # Стили, темы, адаптив
├── script.js # Логика, рендер, примеры структур
├── settings.json # Данные + пресет UI организации
├── settings-top-bar.example.json # Пример пресета «шапка сверху»
├── panel-data.json # Данные навигации (рабочий файл)
├── examples/ # Готовые демо-страницы (см. examples/README.md)
│ ├── index.html # Оглавление примеров
│ ├── shared/ # styles, script, assets для демо
│ ├── full/ # section → group → item
│ ├── section-items/ # section → item
│ ├── flat/ # плоский items[]
│ └── mixed/ # items + groups
├── panel-data-full.example.json # JSON-сниппет (без страницы)
├── assets/ # SVG-иконки сервисов
└── README.md
```
## Конфигурация
### `settings.json`
Указывает, откуда загружать данные панели.
**Вариант 1 — загрузка из файла или URL:**
```json
{
"dataUrl": "./panel-data.json"
}
```
Можно указать внешний адрес или любой из example-файлов:
```json
{
"dataUrl": "./panel-data-flat.example.json"
}
```
**Вариант 2 — inline-объект в том же файле:**
```json
{
"dataUrl": "",
"data": {
"title": "Заголовок панели",
"sections": []
}
}
```
> Если `dataUrl` не пустой, он имеет **приоритет** над полем `data`.
При ошибке загрузки или невалидном JSON на странице показывается экран с описанием проблемы.
#### Пресет организации (`defaults` + `features`)
В `settings.json` можно задать дефолтный вид панели для организации и ограничить, что пользователь может менять в ⚙.
```json
{
"dataUrl": "./panel-data.json",
"defaults": {
"navLayout": "sidebar",
"showTree": true,
"showResizeHandles": true,
"theme": "dark",
"view": "cards",
"accent": "violet",
"customAccentFrom": "#8b5cf6",
"customAccentTo": "#22d3ee",
"customAccentAngle": 135,
"panelPatternNav": "hex",
"panelPatternNavOpacity": 58,
"panelPatternNavHeight": 38,
"panelPatternNavFade": 18,
"panelPatternNavGradientOpacity": 62,
"panelPatternNavGradientHeight": 34,
"panelPatternNavGradientFade": 16,
"panelPatternSection": "hex",
"panelPatternSectionOpacity": 58,
"panelPatternSectionHeight": 38,
"panelPatternSectionFade": 18,
"panelPatternSectionGradientOpacity": 62,
"panelPatternSectionGradientHeight": 34,
"panelPatternSectionGradientFade": 16,
"panelPatternGroup": "none",
"panelPatternGroupOpacity": 58,
"panelPatternGroupHeight": 38,
"panelPatternGroupFade": 18,
"panelPatternGroupGradientOpacity": 62,
"panelPatternGroupGradientHeight": 34,
"panelPatternGroupGradientFade": 16,
"panelPatternItem": "none",
"panelPatternItemOpacity": 58,
"panelPatternItemHeight": 38,
"panelPatternItemFade": 18,
"panelPatternItemGradientOpacity": 62,
"panelPatternItemGradientHeight": 34,
"panelPatternItemGradientFade": 16,
"patternIsolation": 46,
"showNumbers": false,
"showTreeTags": true,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 1,
"autoCollapseSearch": true,
"sidebarWidth": 360,
"topBarHeight": 140,
"shellMaxWidth": 2240,
"sidebarMinWidth": 240,
"sidebarMaxWidth": 720,
"sidebarMaxWidthWide": 1040,
"treeSidebarBaseWidth": 360,
"treeSidebarColumnWidth": 96,
"topBarMinHeight": 80,
"topBarMaxHeight": 560,
"catalogCardsColumns": 3,
"catalogListColumns": 1,
"catalogCompactCardsColumns": 4,
"catalogCompactListColumns": 2,
"collapseAnimation": "animated",
"collapseDuration": 260,
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
"settingsPanelWidth": 440
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
"sidebarMinWidth": { "min": 220, "max": 480 },
"sidebarMaxWidth": { "min": 400, "max": 1200 },
"sidebarMaxWidthWide": { "min": 560, "max": 2000 },
"treeSidebarBaseWidth": { "min": 280, "max": 720 },
"treeSidebarColumnWidth": { "min": 60, "max": 200 },
"topBarMinHeight": { "min": 64, "max": 240 },
"topBarMaxHeight": { "min": 160, "max": 800 },
"catalogCardsColumns": { "min": 1, "max": 6 },
"catalogListColumns": { "min": 1, "max": 4 },
"catalogCompactCardsColumns": { "min": 2, "max": 8 },
"catalogCompactListColumns": { "min": 1, "max": 6 }
},
"features": {
"allowNavLayoutChange": true,
"allowShowTreeChange": true,
"allowSearch": true,
"allowViewChange": true,
"allowCatalogColumnsChange": true,
"allowShowNumbers": true,
"allowTreeOptions": true,
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true,
"allowPanelPatternChange": true
}
}
```
| `defaults` | Варианты | Описание |
|------------|----------|----------|
| `navLayout` | `"sidebar"`, `"top"` | `"sidebar"` — панель слева; `"top"` — шапка с названием и поиском |
| `showTree` | `true`, `false` | Показывать дерево навигации (в любом режиме макета) |
| `showResizeHandles` | `true`, `false` | Показывать ручки drag-resize меню / шапки |
| `theme` | `"dark"`, `"light"` | Стартовая цветовая тема |
| `view` | `"cards"`, `"list"`, `"compact-cards"`, `"compact-list"` | Режим отображения каталога |
| `accent` | `"violet"`, `"cyan"`, `"lime"`, `"amber"`, `"custom"` | Пресет акцента; при `"custom"` используются поля градиента ниже |
| `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` | Прозрачность узора в % (по умолчанию `58`) |
| `panelPattern{Nav\|Section\|Group\|Item}Height` | `0`–`100` | Высота узора в % (по умолчанию `38`; `0` — скрыть) |
| `panelPattern{Nav\|Section\|Group\|Item}Fade` | `0`–`100` | Ширина зоны затухания узора в % высоты блока: `0` — резкий обрыв на линии высоты, `100` — плавное затухание от верха (по умолчанию `18`) |
| `panelPattern{Nav\|Section\|Group\|Item}GradientOpacity` | `0`–`100` | Прозрачность акцентного градиента в % (по умолчанию `62`) |
| `panelPattern{Nav\|Section\|Group\|Item}GradientHeight` | `0`–`100` | Высота градиента в % (по умолчанию `34`) |
| `panelPattern{Nav\|Section\|Group\|Item}GradientFade` | `0`–`100` | Зона затухания градиента (по умолчанию `16`) |
| `panelPattern{Nav\|Section\|Group\|Item}CustomImage` | строка URL | Изображение для `custom` (пусто по умолчанию) |
| `panelPattern{Nav\|Section\|Group\|Item}CustomSize` | `"WxH"` в px | Размер плитки custom (по умолчанию `32px 32px`) |
| `patternIsolation` | `0`–`100` | Разделение слоёв узоров (подложка + blur; по умолчанию `46`) |
| `showNumbers` | `true`, `false` | Показывать номера разделов, подразделов и элементов |
| `showTreeTags` | `true`, `false` | Теги элементов в дереве (оформление как на карточках) |
| `showTreeType` | `true`, `false` | Подпись типа сервиса в дереве |
| `treeMetaLayout` | `"inline"`, `"stacked"` | Расположение тегов и типа: сбоку от названия или снизу |
| `treeColumns` | `1`–`6` (по умолчанию `1`) | Число колонок элементов внутри папок дерева |
| `autoCollapseSearch` | `true`, `false` | При поиске сворачивать ветки дерева без совпадений |
| `sidebarWidth` | `sidebarMinWidth`–`sidebarMaxWidth` (или `sidebarMaxWidthWide` при `treeColumns` > 1) | Текущая ширина бокового меню (drag-resize); при `navLayout: "top"` не применяется |
| `topBarHeight` | `topBarMinHeight`–`topBarMaxHeight` (до ~75% высоты окна) | Текущая высота верхней шапки (drag-resize); только при `navLayout: "top"` |
| `shellMaxWidth` | см. `limits.shellMaxWidth` | Максимальная ширина контейнера `.shell` (вся страница) |
| `sidebarMinWidth` | см. `limits.sidebarMinWidth` | Минимальная ширина бокового меню при drag-resize |
| `sidebarMaxWidth` | см. `limits.sidebarMaxWidth` | Максимальная ширина меню при `treeColumns: 1` |
| `sidebarMaxWidthWide` | см. `limits.sidebarMaxWidthWide` | Потолок ширины меню при нескольких колонках дерева |
| `treeSidebarBaseWidth` | см. `limits.treeSidebarBaseWidth` | База формулы ширины меню: `base + columns × step` |
| `treeSidebarColumnWidth` | см. `limits.treeSidebarColumnWidth` | Прирост ширины меню за каждую колонку дерева |
| `topBarMinHeight` | см. `limits.topBarMinHeight` | Минимальная высота шапки в режиме «Сверху» |
| `topBarMaxHeight` | см. `limits.topBarMaxHeight` | Максимальная высота шапки (также ограничена ~75% окна) |
| `catalogCardsColumns` | см. `limits.catalogCardsColumns` | Колонок в строке в режиме «Плитки» |
| `catalogListColumns` | см. `limits.catalogListColumns` | Колонок в строке в режиме «Список» |
| `catalogCompactCardsColumns` | см. `limits.catalogCompactCardsColumns` | Колонок в строке в режиме «Компакт плитки» |
| `catalogCompactListColumns` | см. `limits.catalogCompactListColumns` | Колонок в строке в режиме «Компакт список» |
| `collapseAnimation` | `"instant"`, `"animated"` | Режим раскрытия списков, дерева и collapsible-блоков |
| `collapseDuration` | `100`–`800` | Длительность плавного раскрытия в миллисекундах (по умолчанию `260`) |
| `searchPlaceholder` | произвольная строка | Текст-подсказка в поле поиска |
| `settingsPanelWidth` | `limits.settingsPanelWidth` (по умолчанию `320`–`720`) | Ширина popup настроек в px (по умолчанию `440`) |
| `limits` | Формат | Описание |
|----------|--------|----------|
| `shellMaxWidth` | `{ "min", "max" }` | Диапазон слайдера «Ширина контента» в ⚙ → Габариты |
| `sidebarMinWidth` | `{ "min", "max" }` | Диапазон «Меню: мин. ширина» |
| `sidebarMaxWidth` | `{ "min", "max" }` | Диапазон «Меню: макс. (1 кол.)» |
| `sidebarMaxWidthWide` | `{ "min", "max" }` | Диапазон «Меню: макс. (неск. кол.)» |
| `treeSidebarBaseWidth` | `{ "min", "max" }` | Диапазон «Дерево: база ширины» |
| `treeSidebarColumnWidth` | `{ "min", "max" }` | Диапазон «Дерево: ширина колонки» |
| `topBarMinHeight` | `{ "min", "max" }` | Диапазон «Шапка: мин. высота» |
| `topBarMaxHeight` | `{ "min", "max" }` | Диапазон «Шапка: макс. высота» |
| `catalogCardsColumns` | `{ "min", "max" }` | Диапазон колонок каталога «Плитки» |
| `catalogListColumns` | `{ "min", "max" }` | Диапазон колонок каталога «Список» |
| `catalogCompactCardsColumns` | `{ "min", "max" }` | Диапазон колонок «Компакт плитки» |
| `catalogCompactListColumns` | `{ "min", "max" }` | Диапазон колонок «Компакт список» |
При `treeColumns > 1` фактический потолок ширины меню: `min(sidebarMaxWidthWide, treeSidebarBaseWidth + treeColumns × treeSidebarColumnWidth)`.
`shellMaxWidth` автоматически не опускается ниже `sidebarMaxWidthWide + 320`, чтобы оставалось место для каталога.
| `features` | Варианты | Описание |
|------------|----------|----------|
| `allowNavLayoutChange` | `true`, `false` | Переключатель «Сбоку / Сверху» |
| `allowShowTreeChange` | `true`, `false` | Toggle «Показывать дерево» в ⚙ → Макет |
| `allowSearch` | `true`, `false` | Поле поиска |
| `allowViewChange` | `true`, `false` | Режимы каталога (плитки / список / компакт) |
| `allowCatalogColumnsChange` | `true`, `false` | Слайдеры «колонок в строке» для каждого вида каталога |
| `allowShowNumbers` | `true`, `false` | Нумерация |
| `allowTreeOptions` | `true`, `false` | Колонки дерева (1–6), теги, тип, расположение мета, авто-раскрытие |
| `allowSidebarResize` | `true`, `false` | Drag-resize и toggle «Ручки изменения размера» |
| `allowLayoutLimitsChange` | `true`, `false` | Раздел ⚙ «Габариты»: лимиты ширины контента и навигации |
| `allowThemeChange` | `true`, `false` | Светлая / тёмная тема |
| `allowAccentChange` | `true`, `false` | Пресеты и свой градиент |
| `allowPanelPatternChange` | `true`, `false` | Выбор узора панелей в ⚙ → Оформление |
Пользовательские изменения сохраняются в `localStorage`, но заблокированные `features` всегда возвращаются к `defaults`.
Пример пресета «шапка сверху»: [`settings-top-bar.example.json`](settings-top-bar.example.json) — `navLayout: "top"`, дерево по умолчанию скрыто (`showTree: false`), но его можно включить.
### Форматы иерархии
Панель поддерживает три базовых формата. Старый **section → group → item** работает без изменений.
| Формат | Структура | Дерево | Каталог |
|--------|-----------|--------|---------|
| **Полная** | `sections[].groups[].items[]` | раздел → подраздел → файлы | collapsible разделы и подразделы |
| **Раздел → item** | `sections[].items[]` | раздел → файлы | карточки внутри раздела |
| **Плоский список** | корневой `items[]` | плоский список файлов | только сетка карточек |
| **Смешанный** | `sections[].items[]` + `groups[]` | прямые файлы, затем подразделы | прямые карточки, затем подразделы |
Минимум данных: корень должен содержать `sections` **или** `items`. Каждый раздел — `groups` **или** `items`. Каждый элемент — `title` и `type`.
### Схема `panel-data`
Абстрактная структура файла данных (без привязки к конкретным сервисам):
```json
{
"title": "<заголовок панели>",
"sections": [
{
"id": "<slug-раздела>",
"number": "<опционально>",
"title": "<название раздела>",
"description": "<опционально>",
"items": [
{
"id": "<стабильный-ключ>",
"number": "<опционально>",
"title": "<название>",
"type": "<ключ из serviceMeta>",
"tags": ["<тег>"],
"url": "<ссылка>"
}
],
"groups": [
{
"id": "<slug-подраздела>",
"number": "<опционально>",
"title": "<название подраздела>",
"description": "<опционально>",
"items": ["<элементы — та же форма>"]
}
]
}
],
"items": ["<альтернатива sections: плоский список>"],
"serviceMeta": {
"<type-key>": {
"label": "<подпись типа>",
"icon": "assets/<иконка>.svg"
}
}
}
```
Используй **либо** `sections`, **либо** корневой `items`. Внутри раздела — **либо** `groups`, **либо** `items`, **либо** оба.
### Поля данных
| Уровень | Поля | Описание |
|---------|------|----------|
| **Корень** | `title` | Заголовок панели (sidebar + `<title>`) |
| | `sections` | Массив разделов (если нет корневого `items`) |
| | `items` | Плоский список без разделов |
| | `serviceMeta` | Дополнительные или переопределённые типы сервисов |
| **section** | `id`, `title` | Обязательные идентификатор и название |
| | `number`, `description` | Опционально |
| | `groups`, `items` | Хотя бы одно из двух |
| **group** | `id`, `title`, `items` | Подраздел с элементами |
| | `number`, `description` | Опционально |
| **item** | `title`, `type` | Обязательные название и тип |
| | `url` | Ссылка; `"#"` — заглушка |
| | `tags` | Массив строк для поиска |
| | `id` | Стабильный ключ (DOM, фокус из дерева) |
| | `number` | Отображаемый номер |
| | `icon` | Опционально: свой путь к SVG (иначе — иконка из `type`) |
- `type` — ключ из встроенного `defaultServiceMeta` или из `serviceMeta` в JSON; определяет иконку и подпись на карточке
- `id` элемента нужен, если нет `number` — иначе ключ генерируется автоматически
### Примеры данных
Готовые демо-страницы — в папке [`examples/`](examples/README.md). Корневые `panel-data-*.example.json` — те же структуры в виде JSON-сниппетов.
### Готовые демо-страницы (`examples/`)
Каждый пример — отдельная страница с `index.html`, `settings.json` и `panel-data.json`. Общие ресурсы в `examples/shared/`.
```bash
cd examples
npx serve .
```
| Путь | Формат |
|------|--------|
| `/` | оглавление примеров |
| `/full/` | section → group → item |
| `/section-items/` | section → item |
| `/flat/` | плоский `items[]` |
| `/mixed/` | items + groups в разделе |
Подробнее: [examples/README.md](examples/README.md).
### Типы сервисов (`serviceMeta`)
Базовые типы заданы в `script.js` (`defaultServiceMeta`):
`analytics`, `api`, `calendar`, `ceph`, `chat`, `cicd`, `cloudbeaver`, `config`, `drive`, `forum`, `gitea`, `grafana`, `ipmi`, `kubernetes`, `ldap`, `messages`, `opensearch`, `postgresql`, `prometheus`, `rabbitmq`, `redmine`, `rules`, `social`, `vault`, `video`, `vpn`, `wiki`, `xpath`, `xmpp`
Каждый тип имеет собственную SVG в `assets/` (например, `postgresql.svg`, `cloudbeaver.svg`). Иконка показывается на карточке каталога и в дереве навигации.
Переопределение или добавление типов в JSON:
```json
{
"serviceMeta": {
"my-type": {
"label": "Подпись",
"icon": "assets/config.svg"
}
}
}
```
### Рекомендуемые типы сервисов
Популярные `type`, которые можно добавить в `serviceMeta` (иконки — переиспользовать из `assets/`):
| Категория | Типы (`type`) | Комментарий |
|-----------|---------------|-------------|
| **Прокси / балансировка** | `nginx`, `traefik` | reverse proxy, ingress |
| **Контейнеры** | `docker`, `portainer`, `argocd` | runtime, UI, GitOps |
| **CI/CD** | `jenkins`, `gitlab`, `harbor` | сборка, репозиторий, registry |
| **Базы данных** | `mysql`, `mongodb`, `redis` | рядом с уже имеющимися `postgresql`, `opensearch` |
| **Очереди / стриминг** | `kafka` | аналог `rabbitmq` |
| **Мониторинг / логи** | `loki`, `zabbix`, `sentry` | логи, метрики, ошибки |
| **Безопасность / IAM** | `vault`, `keycloak`, `consul` | секреты, SSO, service mesh |
| **Поиск** | `elasticsearch`, `sonarqube` | полнотекст, качество кода |
| **Хранилище** | `minio` | S3-совместимое |
| **IaC** | `terraform` | инфраструктура как код |
| **Трекеры** | `jira` | задачи, рядом с `redmine` |
Чтобы подключить тип:
1. Добавь запись в `serviceMeta` вашего JSON **или** в `defaultServiceMeta` в `script.js`
2. При необходимости положи отдельную SVG в `assets/`
3. Укажи `"type": "nginx"` (и т.д.) в элементах
## Настройки интерфейса (⚙)
Popup сгруппирован по разделам. Доступные блоки зависят от `features` в `settings.json`. Изменения пользователя сохраняются в `localStorage` (`ecosystem-panel-preferences`).
| Раздел | Настройки |
|--------|-----------|
| **Макет** | **Сбоку** / **Сверху**; toggle «Показывать дерево»; toggle «Ручки изменения размера»; слайдер «Ширина настроек» |
| **Габариты** | Лимиты ширины контента, меню и шапки (сворачиваемый блок; при `allowLayoutLimitsChange`) |
| **Каталог** | Вид карточек, колонок в строке для каждого вида, нумерация |
| **Дерево** | Колонок 1–6, теги, тип, расположение мета, авто-раскрытие (при `showTree: true`) |
| **Оформление** | Тема, акцент, узоры по типам блоков, разделение слоёв, свой градиент |
| **(footer)** | Сброс, экспорт и импорт всех preferences в JSON |
### Режим «Сверху»
Компактная шапка: заголовок, поиск и ⚙. Дерево по умолчанию скрыто, но его можно включить (`showTree: true`) — оно появится под шапкой на всю ширину.
### Скрытие дерева
Дерево управляется отдельно от макета (`showTree`). Переключение — в ⚙ → **Макет**.
### Свой градиент
Кнопка **«Свой»** открывает настройки:
- угол (0–360°)
- цвет «от» и «до»
Значения сохраняются в `localStorage`.
### Узоры панелей
Настраиваются в ⚙ → **Оформление** (если `allowPanelPatternChange: true`).
#### Четыре цели (targets)
У каждого типа блока свой независимый набор: пресет, слайдеры и (опционально) свой URL.
| Цель | Preference (префикс) | DOM-элемент | Градиент под узором |
|------|----------------------|-------------|---------------------|
| Панель навигации | `panelPatternNav*` | `.hero-content` | да |
| Раздел | `panelPatternSection*` | `.catalog-section` | да |
| Подраздел | `panelPatternGroup*` | `.catalog-group` | да |
| Элемент | `panelPatternItem*` | `.nav-card` | нет (только узор) |
#### Встроенные пресеты (`panelPattern*`)
| id | Подпись в ⚙ | Описание |
|----|-------------|----------|
| `none` | Нет | Без повторяющейся текстуры |
| `hex` | Соты | Крупные гексагоны |
| `hex-sm` | Соты м. | Мелкие гексагоны |
| `dots` | Точки | Средняя точечная сетка |
| `dots-sm` | Точки м. | Мелкие кружки |
| `dots-lg` | Точки кр. | Крупные кружки |
| `grid` | Сетка | Линейная сетка |
| `diagonal` | Диаг. | Диагональные полосы |
| `cross` | Клетка | Перекрёстная штриховка |
| `mesh` | Mesh | Точки цвета акцента |
| `tri-sm` | △ мел. | Мелкие треугольники |
| `tri-lg` | △ кр. | Крупные треугольники |
| `diamond` | Ромбы | Ромбовидная сетка |
| `custom` | Свой | Своя текстура (см. ниже) |
По умолчанию: **навигация** и **разделы** — `hex`; **подразделы** и **элементы** — `none`.
#### Слайдеры (для каждой цели)
| Сuffix preference | UI | Диапазон | По умолчанию |
|-------------------|-----|----------|--------------|
| `Opacity` | Узор: прозрачность | 0–100 % | 58 |
| `Height` | Узор: высота | 0–100 % высоты блока | 38 |
| `Fade` | Узор: переход | 0–100 % | 18 |
| `GradientOpacity` | Градиент: прозрачность | 0–100 % | 62 |
| `GradientHeight` | Градиент: высота | 0–100 % | 34 |
| `GradientFade` | Градиент: переход | 0–100 % | 16 |
**Переход (fade):** `0%` — резкий обрыв на линии «высоты»; большее значение — плавнее затухание к низу блока.
Для цели **Элемент** в UI только три слайдера узора (без градиента).
#### Как узор попадает на экран
1. `preferences` → `applyPanelPatternPreferences()` в `script.js`
2. На `<html>`: `data-panel-pattern-nav="hex"` (и section / group / item)
3. CSS-переменные: `--panel-pattern-{target}-opacity`, `-height`, `-fade-start`, `--panel-gradient-{target}-*`
4. Встроенные пресеты: `styles.css` → `--pattern-tile-*` и селекторы `html[data-panel-pattern-*="…"]`
5. Слои на блоке: `::before` (градиент), `::after` (текстура с mask по высоте/переходу)
Кнопки пресетов в ⚙ **не** захардкожены в HTML: контейнеры `data-pattern-swatches="nav|section|group|item"`, список — `panelPatternCatalog` в `script.js`.
### Разделение слоёв узоров
Слайдер **«Разделение слоёв»** в ⚙ → Оформление (`patternIsolation`, 0–100 %, по умолчанию `46`).
Когда узоры включены на нескольких уровнях вложенности (раздел → подраздел → карточка), текстуры могут визуально накладываться. Настройка добавляет на `.catalog-section`, `.catalog-group` и `.nav-card`:
- полупрозрачную подложку (`--panel-layer-fill`);
- `backdrop-filter: blur(...)` (`--panel-layer-blur`).
Так каждый уровень «отрезает» узоры снизу и остаётся читаемой граница. Значение задаётся в `settings.json` → `defaults.patternIsolation` и сохраняется в `localStorage`.
### Свой узор (custom)
1. В панели нужной цели выберите пресет **«Свой»**
2. Появится блок с полями:
- **Изображение** — `panelPattern{Nav|Section|Group|Item}CustomImage`
- **Размер плитки** — `panelPattern*CustomSize` (например `32px 32px`)
Допустимые форматы URL:
- `data:image/svg+xml,…` или `data:image/png;base64,…`
- `https://…` / `http://…`
- относительный путь: `./assets/my-pattern.svg`, `assets/pattern.png`
- `javascript:` — **отклоняется** при нормализации
Пример минимального SVG (data URI):
```
data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Ccircle cx='8' cy='8' r='2' fill='white' fill-opacity='.2'/%3E%3C/svg%3E
```
При `custom` без URL узор не рисуется (opacity = 0), пока не задано изображение.
### Экспорт и импорт настроек
Внизу popup ⚙ — три кнопки:
| Кнопка | Действие |
|--------|----------|
| **Сбросить** | Подтверждение → `panelDefaults` из `settings.json`, очистка `ecosystem-panel-preferences` |
| **Экспортировать** | Скачивание `ecosystem-panel-preferences.json` |
| **Импортировать** | Выбор JSON; применение с `normalizePreferences` + `enforceFeatureLocks` |
Формат экспорта:
```json
{
"version": 1,
"exportedAt": "2026-06-29T12:00:00.000Z",
"preferences": { "theme": "dark", "panelPatternNav": "hex", "patternIsolation": 46 }
}
```
Импорт принимает и обёртку с `preferences`, и «голый» объект настроек.
### Раскрытие списков
В ⚙ → **Каталог** — режим **Резко** / **Плавно** и длительность (100–800 ms). Плавное раскрытие применяется к разделам и подразделам каталога, дереву навигации и схлопывающимся блокам в настройках. Кнопки сворачивания — chevron в стеклянной рамке с акцентной подсветкой при наведении.
| Preference | Значения | По умолчанию |
|------------|----------|--------------|
| `collapseAnimation` | `"instant"`, `"animated"` | `"animated"` |
| `collapseDuration` | `100`–`800` (ms) | `240` |
Старые ключи (`panelPattern`, `panelPatternOnNav`, …) при загрузке автоматически мигрируют в per-target формат, если в `localStorage` ещё нет `panelPatternNav`.
### Навигация по коду
Быстрый поиск: в `script.js` и `styles.css` — секции с разделителем `// =====` / `/* =====`; в `index.html` — комментарий-оглавление в начале файла.
#### script.js
| Секция | Содержимое |
|--------|------------|
| `Panel pattern catalog and defaults` | `panelPatternCatalog`, `availablePanelPatterns`, defaults по целям |
| `Panel pattern preferences` | normalize, apply, custom URL, UI (`renderPatternSwatches`, …) |
| `Settings import, export and reset` | `applyStoredPreferences`, export/import JSON |
| `Preferences application` | `applyPreferences`, sync контролов ⚙ |
| `Collapse state handling` | анимация раскрытия details |
Ключевые функции узоров: `applyPanelPatternPreferences`, `applyPatternIsolationPreference`, `getPanelPatternPreferenceKey`, `setupPatternControls`.
#### styles.css
| Секция | Содержимое |
|--------|------------|
| `01. Theme Tokens` | `--pattern-tile-*`, mapping `html[data-panel-pattern-*]`, `--panel-layer-*` |
| `05. Sidebar Panel` | `::before` / `::after` узоров, layer isolation на catalog/group/card |
| `06. Settings Popup` | `.pattern-swatches`, `.pattern-custom-panel`, `.settings-actions` |
#### index.html
| Блок | Атрибуты / классы |
|------|-------------------|
| Popup ⚙ | `[data-settings-panel]`, секции `[data-settings-section="…"]` |
| Узоры | `[data-pattern-targets-panel]`, `[data-pattern-swatches="{target}"]` |
| Разделение слоёв | `[data-pattern-isolation]` |
| Экспорт | `[data-settings-reset]`, `[data-settings-export]`, `[data-settings-import]` |
#### Добавить новый встроенный пресет
1. Добавить id в `availablePanelPatterns` и запись в `panelPatternCatalog` (`script.js`)
2. Определить `--pattern-tile-{id}-image` и `-size` в `:root` (`styles.css`)
3. Добавить строки `html[data-panel-pattern-{nav|section|group|item}="{id}"]` для всех четырёх целей
4. Добавить превью `.pattern-swatch[data-panel-pattern="{id}"]::before` в секции настроек
5. При необходимости — override SVG для `html[data-theme="light"]`
Значения сохраняются в `localStorage` и выставляют `data-panel-pattern-*` на `<html>` и CSS-переменные `--panel-pattern-{nav|section|group|item}-*`, `--panel-gradient-{target}-*`.
## Поведение интерфейса
### Поиск
Фильтрует каталог и подсвечивает совпадения в дереве и карточках. Ищет по:
- названию элемента
- номеру
- тегам
- типу сервиса
- названиям родительских разделов
### Дерево навигации
- Клик по **папке** — раскрытие/сворачивание
- Клик по **названию** — прокрутка к элементу в каталоге
- Состояние раскрытия сохраняется отдельно для дерева и контента (`ecosystem-panel-collapse-state`)
### Адаптив
- **Touch-устройства** (`pointer: coarse` / `hover: none`): stacked-layout, меню сверху, ручки resize скрыты
- **Мышь** (`pointer: fine` + `hover: hover`): sidebar + resizer + каталог в колонках
- На узких экранах число колонок каталога дополнительно ограничивается в CSS (чтобы сетка не ломалась)
### Ширина и габариты layout
Лимиты задаются в `settings.json` → `defaults` и `limits`, настраиваются пользователем в ⚙ → **Габариты** (если `allowLayoutLimitsChange: true`):
- **`shellMaxWidth`** — максимальная ширина всей панели (по умолчанию до 2800px)
- **`sidebarMaxWidth`** / **`sidebarMaxWidthWide`** — потолок ширины меню (1 колонка дерева / несколько)
- **`topBarMinHeight`** / **`topBarMaxHeight`** — диапазон высоты шапки в режиме «Сверху»
- Текущие **`sidebarWidth`** и **`topBarHeight`** — drag-resize; сохраняются в `localStorage`
## localStorage
| Ключ | Содержимое |
|------|------------|
| `ecosystem-panel-preferences` | Тема, акцент, узоры, вид, колонки, `patternIsolation`, custom-URL, ширина sidebar, export/import |
| `ecosystem-panel-collapse-state` | Раскрытые/свернутые ветки дерева и каталога |
## Разработка
### Добавить элемент
1. Открой `panel-data.json` (или свой JSON из `dataUrl`)
2. Найди нужный раздел / подраздел **или** добавь в `items` / корневой `items`
3. Добавь объект с полями `title`, `type`, при необходимости `tags`, `url`, `id`, `number`
4. Обнови страницу
### Добавить иконку и тип
1. Положи SVG в `assets/` (или переиспользуй существующую)
2. Зарегистрируй тип в `serviceMeta` JSON или в `defaultServiceMeta` в `script.js`
### Внешний источник данных
```json
{
"dataUrl": "https://your-server/panel-data.json"
}
```
Убедись, что сервер отдаёт `Content-Type: application/json` и разрешает CORS при другом домене.
## Ограничения
- Требуется HTTP-сервер (`fetch` не работает с `file://`)
- Внешний `dataUrl` должен быть доступен по CORS
- Настройки UI хранятся локально в браузере и не синхронизируются между устройствами
## Лицензия
Приватный проект. Используй и изменяй по своему усмотрению.

1
assets/analytics.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-analytics)"/><rect x="40" y="92" width="18" height="32" rx="6" fill="white"/><rect x="71" y="72" width="18" height="52" rx="6" fill="white" fill-opacity=".9"/><rect x="102" y="52" width="18" height="72" rx="6" fill="white" fill-opacity=".78"/><path d="M44 60l28 18 28-28 16 12" stroke="white" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><defs><linearGradient id="g-analytics" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#6EE7B7"/><stop offset="1" stop-color="#059669"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 680 B

1
assets/api.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-api)"/><path d="M52 52h-8v56h8M108 52h8v56h-8M68 80h24" stroke="white" stroke-width="10" stroke-linecap="round"/><circle cx="80" cy="80" r="28" stroke="white" stroke-width="8" fill="none"/><defs><linearGradient id="g-api" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FDE68A"/><stop offset="1" stop-color="#D97706"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 505 B

1
assets/calendar.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-calendar)"/><rect x="36" y="48" width="88" height="76" rx="14" fill="white"/><path d="M52 40v16M108 40v16" stroke="white" stroke-width="8" stroke-linecap="round"/><path d="M36 68h88" stroke="#4F46E5" stroke-width="6"/><rect x="50" y="82" width="14" height="14" rx="4" fill="#4F46E5"/><rect x="73" y="82" width="14" height="14" rx="4" fill="#4F46E5" fill-opacity=".7"/><rect x="96" y="82" width="14" height="14" rx="4" fill="#4F46E5" fill-opacity=".45"/><rect x="50" y="102" width="14" height="14" rx="4" fill="#4F46E5" fill-opacity=".7"/><rect x="73" y="102" width="14" height="14" rx="4" fill="#4F46E5"/><defs><linearGradient id="g-calendar" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#A5B4FC"/><stop offset="1" stop-color="#4F46E5"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 926 B

1
assets/chat.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-chat)"/><path d="M34 52h52c6 0 11 5 11 11v28c0 6-5 11-11 11H62L42 118V91H34c-6 0-11-5-11-11V63c0-6 5-11 11-11z" fill="white"/><path d="M74 58h52c6 0 11 5 11 11v28c0 6-5 11-11 11h-8l-20 27V98H74c-6 0-11-5-11-11V69c0-6 5-11 11-11z" fill="white" fill-opacity=".78"/><circle cx="52" cy="74" r="4" fill="#7C3AED"/><circle cx="68" cy="74" r="4" fill="#7C3AED"/><circle cx="84" cy="74" r="4" fill="#7C3AED"/><defs><linearGradient id="g-chat" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#C4B5FD"/><stop offset="1" stop-color="#7C3AED"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 718 B

1
assets/cicd.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-cicd)"/><path d="M80 44a36 36 0 1 1-25 59" stroke="white" stroke-width="10" stroke-linecap="round" fill="none"/><path d="M55 44h25v25" stroke="white" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/><circle cx="80" cy="80" r="10" fill="#1D4ED8"/><path d="M96 96l18 18" stroke="white" stroke-width="8" stroke-linecap="round"/><defs><linearGradient id="g-cicd" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#93C5FD"/><stop offset="1" stop-color="#1D4ED8"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 659 B

1
assets/cloudbeaver.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M34 88c0-18 14-32 32-34 4-18 20-30 38-30 22 0 40 16 40 38 0 2 0 4-.2 6 14 4 24 16 24 32 0 18-14 32-32 32H52c-10 0-18-8-18-18v-26z" fill="white" fill-opacity=".95"/><ellipse cx="80" cy="92" rx="34" ry="14" fill="white"/><path d="M46 92v24c0 8 15 14 34 14s34-6 34-14V92" fill="white" fill-opacity=".88"/><path d="M46 104c0 8 15 14 34 14s34-6 34-14" stroke="#1565C0" stroke-width="6"/><circle cx="66" cy="72" r="8" fill="#1565C0"/><circle cx="94" cy="72" r="8" fill="#1565C0"/><path d="M72 86h16" stroke="#1565C0" stroke-width="6" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#64B5F6"/><stop offset="1" stop-color="#1565C0"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 876 B

1
assets/drive.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-drive)"/><path d="M44 98l18-36h36l18 36H44z" fill="white"/><path d="M62 62l14-18h18l14 18" stroke="white" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><rect x="48" y="104" width="64" height="14" rx="7" fill="white" fill-opacity=".85"/><defs><linearGradient id="g-drive" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#93C5FD"/><stop offset="1" stop-color="#2563EB"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 573 B

1
assets/forum.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-forum)"/><rect x="36" y="44" width="88" height="18" rx="9" fill="white"/><rect x="36" y="72" width="72" height="18" rx="9" fill="white" fill-opacity=".88"/><rect x="36" y="100" width="80" height="18" rx="9" fill="white" fill-opacity=".76"/><circle cx="124" cy="53" r="8" fill="#EA580C"/><circle cx="116" cy="81" r="8" fill="#EA580C" fill-opacity=".85"/><circle cx="122" cy="109" r="8" fill="#EA580C" fill-opacity=".7"/><defs><linearGradient id="g-forum" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FDBA74"/><stop offset="1" stop-color="#EA580C"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 737 B

1
assets/postgresql.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M46 98c0-30 15-52 34-52s34 22 34 52c0 14-9 26-21 30v16H67v-16c-12-4-21-16-21-30z" fill="white"/><circle cx="69" cy="78" r="6" fill="#336791"/><circle cx="91" cy="78" r="6" fill="#336791"/><path d="M46 98c-10 10-16 24-16 38h18c2-14 9-26 18-32" fill="white" fill-opacity=".92"/><path d="M72 92c4 3 8 3 12 0" stroke="#336791" stroke-width="5" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#5B9BD5"/><stop offset="1" stop-color="#336791"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 688 B

1
assets/social.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-social)"/><circle cx="56" cy="68" r="16" fill="white"/><circle cx="104" cy="68" r="16" fill="white"/><circle cx="80" cy="108" r="18" fill="white" fill-opacity=".92"/><path d="M68 118c6 8 18 12 28 12s22-4 28-12" stroke="white" stroke-width="6" stroke-linecap="round"/><defs><linearGradient id="g-social" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#F9A8D4"/><stop offset="1" stop-color="#DB2777"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 586 B

1
assets/vault.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-vault)"/><rect x="38" y="48" width="84" height="72" rx="16" fill="white"/><circle cx="80" cy="84" r="18" stroke="#6D28D9" stroke-width="8" fill="none"/><circle cx="80" cy="84" r="6" fill="#6D28D9"/><rect x="74" y="84" width="12" height="18" rx="2" fill="#6D28D9"/><defs><linearGradient id="g-vault" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#DDD6FE"/><stop offset="1" stop-color="#6D28D9"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 582 B

1
assets/video.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-video)"/><rect x="34" y="50" width="92" height="60" rx="14" fill="white"/><path d="M88 68l28-16v56l-28-16V68z" fill="#DC2626"/><circle cx="58" cy="118" r="10" fill="white"/><circle cx="102" cy="118" r="10" fill="white" fill-opacity=".85"/><defs><linearGradient id="g-video" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FCA5A5"/><stop offset="1" stop-color="#DC2626"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 557 B

1
assets/vpn.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-vpn)"/><path d="M80 38l44 24v36c0 24-20 44-44 44S36 122 36 98V62l44-24z" fill="white"/><path d="M72 92c0-4 4-8 8-8s8 4 8 8v8H72v-8z" fill="#0F766E"/><rect x="68" y="76" width="24" height="18" rx="8" stroke="#0F766E" stroke-width="6"/><defs><linearGradient id="g-vpn" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#5EEAD4"/><stop offset="1" stop-color="#0F766E"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 550 B

1
assets/wiki.svg Normal file
View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-wiki)"/><path d="M48 44h64l8 92H40l8-92z" fill="white"/><path d="M56 56h48M56 76h40M56 96h44M56 116h32" stroke="#0369A1" stroke-width="6" stroke-linecap="round"/><path d="M80 44v16" stroke="white" stroke-width="8" stroke-linecap="round"/><defs><linearGradient id="g-wiki" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#7DD3FC"/><stop offset="1" stop-color="#0369A1"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 555 B

3
examples/.vscode/settings.json vendored Normal file
View File

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

73
examples/README.md Normal file
View File

@ -0,0 +1,73 @@
# Примеры панели экосистемы
Самодостаточные демо-страницы для всех поддерживаемых форматов данных. Каждый пример можно хостить отдельно — в папке есть всё необходимое для работы через HTTP-сервер.
## Структура
```
examples/
├── index.html # Оглавление примеров
├── shared/ # Общие ресурсы для всех демо
│ ├── styles.css
│ ├── script.js
│ └── assets/ # SVG-иконки
├── full/ # section → group → item
│ ├── index.html
│ ├── settings.json
│ └── panel-data.json
├── section-items/ # section → item
├── flat/ # корневой items[]
└── mixed/ # items + groups в одном разделе
```
## Запуск
Из папки `examples`:
```bash
cd examples
npx serve .
```
Открой:
- `http://localhost:3000/` — оглавление
- `http://localhost:3000/full/` — полная иерархия
- `http://localhost:3000/section-items/` — раздел без подразделов
- `http://localhost:3000/flat/` — плоский список
- `http://localhost:3000/mixed/` — смешанный раздел
Альтернатива — сервер из корня репозитория:
```bash
cd ecosystem
npx serve .
```
Тогда примеры доступны по пути `/examples/...`.
## Что внутри каждого примера
| Файл | Назначение |
|------|------------|
| `index.html` | Страница панели, подключает `../shared/` |
| `settings.json` | Указывает `./panel-data.json` и пресет UI (`defaults`, `features`) |
| `panel-data.json` | Демо-данные выбранного формата |
## Обновление shared/
Папка `shared/` — копия `styles.css`, `script.js` и `assets/` из корня проекта. После изменений в основной панели синхронизируй:
```powershell
Copy-Item ..\styles.css, ..\script.js shared\
Copy-Item ..\assets\* shared\assets\ -Recurse -Force
```
## Копирование как шаблон
Чтобы сделать свою панель на базе примера:
1. Скопируй папку нужного формата (например, `flat/`)
2. Отредактируй `panel-data.json`
3. При необходимости добавь типы в `serviceMeta`
4. Хости папку примера вместе с `shared/` (относительные пути уже настроены)

548
examples/flat/index.html Normal file
View File

@ -0,0 +1,548 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Навигационная панель экосистемы с разделами, подразделами, поиском и быстрыми переходами."
/>
<title>Панель экосистемы</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="../shared/styles.css" />
</head>
<body>
<!-- Декоративные фоновые пятна. Они не участвуют в логике и нужны только для глубины фона. -->
<div class="ambient ambient-one"></div>
<div class="ambient ambient-two"></div>
<main class="shell">
<!--
Основная сетка приложения:
слева sticky-меню с деревом навигации, справа каталог карточек.
Ширина левой колонки управляется CSS-переменной --sidebar-width и ресайзером.
-->
<div class="app-layout">
<!-- Боковая панель: поиск, настройки и дерево навигации по всей структуре. -->
<aside class="side-panel" aria-label="Навигация по экосистеме">
<div class="hero-content">
<!-- Шестеренка открывает fixed popup с настройками внешнего вида и поведения. -->
<button
class="settings-button"
type="button"
aria-label="Открыть настройки внешнего вида"
aria-expanded="false"
data-settings-toggle
>
⚙
</button>
<p class="eyebrow" id="page-title">Панель экосистемы</p>
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
<label class="search-field" data-search-field>
<input
type="search"
placeholder="Grafana, S3, журнал, сервер..."
autocomplete="off"
data-search-input
/>
</label>
<!-- Дерево генерируется из ecosystemSections в script.js. -->
<nav class="nav-tree" aria-label="Дерево навигации" data-nav-tree></nav>
</div>
<div
class="top-bar-resizer"
role="separator"
aria-orientation="horizontal"
aria-label="Изменить высоту верхней панели"
data-top-bar-resizer
hidden
></div>
</aside>
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
<div
class="sidebar-resizer"
role="separator"
aria-orientation="vertical"
aria-label="Изменить ширину бокового меню"
data-sidebar-resizer
></div>
<!-- Затемнение страницы за popup-настройками. -->
<div class="settings-overlay" data-settings-overlay></div>
<!--
Popup настроек:
- тема;
- режим отображения карточек;
- видимость нумерации;
- видимость тегов в дереве;
- авто-раскрытие дерева при поиске;
- цветовой акцент.
-->
<div class="control-panel" aria-label="Настройки панели" data-settings-panel>
<div class="settings-panel-head">
<p class="eyebrow settings-panel-eyebrow">Настройки</p>
<button class="settings-close" type="button" aria-label="Закрыть настройки" data-settings-close>
×
</button>
</div>
<!-- Переключение светлой/темной темы. Значение сохраняется в localStorage. -->
<section class="settings-section" data-settings-section="layout">
<p class="settings-section-title">Макет</p>
<div class="control-group">
<span>Панель навигации</span>
<button class="segmented is-active" type="button" data-nav-layout="sidebar">
Сбоку
</button>
<button class="segmented" type="button" data-nav-layout="top">
Сверху
</button>
</div>
<p class="settings-section-hint">
Режим «Сверху» — компактная шапка с названием и поиском, без дерева.
</p>
<label class="toggle-row" data-feature="allowShowTreeChange" data-show-when-sidebar>
<span>
<strong>Показывать дерево</strong>
<small>Только для панели «Сбоку»</small>
</span>
<input type="checkbox" data-show-tree checked />
</label>
<label class="toggle-row" data-feature="allowSidebarResize">
<span>
<strong>Ручки изменения размера</strong>
<small>Ширина меню или высота шапки</small>
</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">
<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">
Ограничения ширины страницы, бокового меню и верхней шапки. Значения применяются сразу;
текущий размер меню и шапки можно также менять перетаскиванием ручек на странице.
Допустимые диапазоны ползунков задаёт администратор в <code>settings.json</code> →
<code>limits</code>.
</p>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная ширина всей панели (.shell): насколько широко вместе могут располагаться меню и каталог."
>
<span>Ширина контента (max)</span>
<input type="range" data-layout-limit="shellMaxWidth" />
<output data-layout-limit-value="shellMaxWidth">2800</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Минимальная ширина бокового меню при перетаскивании вертикальной ручки между меню и каталогом."
>
<span>Меню: мин. ширина</span>
<input type="range" data-layout-limit="sidebarMinWidth" />
<output data-layout-limit-value="sidebarMinWidth">260</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная ширина меню, когда элементы дерева в папках показываются в одну колонку."
>
<span>Меню: макс. (1 кол.)</span>
<input type="range" data-layout-limit="sidebarMaxWidth" />
<output data-layout-limit-value="sidebarMaxWidth">800</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Верхний предел ширины меню при нескольких колонках в дереве. Фактический максимум также зависит от базы и шага колонки."
>
<span>Меню: макс. (неск. кол.)</span>
<input type="range" data-layout-limit="sidebarMaxWidthWide" />
<output data-layout-limit-value="sidebarMaxWidthWide">1400</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="База формулы ширины меню при нескольких колонках: min(макс. неск. кол., база + колонки × шаг)."
>
<span>Дерево: база ширины</span>
<input type="range" data-layout-limit="treeSidebarBaseWidth" />
<output data-layout-limit-value="treeSidebarBaseWidth">420</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Сколько пикселей добавляется к базовой ширине меню за каждую колонку элементов в папках дерева."
>
<span>Дерево: ширина колонки</span>
<input type="range" data-layout-limit="treeSidebarColumnWidth" />
<output data-layout-limit-value="treeSidebarColumnWidth">120</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Минимальная высота верхней шапки в режиме «Сверху» — нижняя граница при перетаскивании горизонтальной ручки."
>
<span>Шапка: мин. высота</span>
<input type="range" data-layout-limit="topBarMinHeight" />
<output data-layout-limit-value="topBarMinHeight">80</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная высота верхней шапки в режиме «Сверху». Также ограничена примерно 75% высоты окна браузера."
>
<span>Шапка: макс. высота</span>
<input type="range" data-layout-limit="topBarMaxHeight" />
<output data-layout-limit-value="topBarMaxHeight">560</output>
</label>
</div>
</details>
<section class="settings-section" data-settings-section="catalog">
<p class="settings-section-title">Каталог</p>
<!-- Режимы каталога: обычные/компактные плитки и списки. -->
<div class="control-group view-group" data-feature="allowViewChange">
<span>Вид</span>
<button class="segmented is-active" type="button" data-view="cards">
Плитки
</button>
<button class="segmented" type="button" data-view="list">
Список
</button>
<button class="segmented" type="button" data-view="compact-cards">
Компакт плитки
</button>
<button class="segmented" type="button" data-view="compact-list">
Компакт список
</button>
</div>
<div class="catalog-columns-group" data-feature="allowCatalogColumnsChange">
<p class="settings-section-hint">Колонок в строке для каждого вида каталога.</p>
<label class="accent-custom-row">
<span>Плитки</span>
<input type="range" data-catalog-columns="catalogCardsColumns" />
<output data-catalog-columns-value="catalogCardsColumns">3</output>
</label>
<label class="accent-custom-row">
<span>Список</span>
<input type="range" data-catalog-columns="catalogListColumns" />
<output data-catalog-columns-value="catalogListColumns">1</output>
</label>
<label class="accent-custom-row">
<span>Компакт плитки</span>
<input type="range" data-catalog-columns="catalogCompactCardsColumns" />
<output data-catalog-columns-value="catalogCompactCardsColumns">5</output>
</label>
<label class="accent-custom-row">
<span>Компакт список</span>
<input type="range" data-catalog-columns="catalogCompactListColumns" />
<output data-catalog-columns-value="catalogCompactListColumns">3</output>
</label>
</div>
<!-- Показывать номера разделов, подразделов и элементов. -->
<label class="toggle-row" data-feature="allowShowNumbers">
<span>
<strong>Показывать нумерацию</strong>
<small>Разделы, подразделы и элементы</small>
</span>
<input type="checkbox" data-show-numbers />
</label>
<div class="control-group collapse-animation-group">
<span>Раскрытие списков</span>
<button class="segmented" type="button" data-collapse-animation="instant">Резко</button>
<button class="segmented is-active" type="button" data-collapse-animation="animated">Плавно</button>
</div>
<p class="settings-section-hint">Разделы каталога, дерево навигации и блоки в настройках.</p>
<label class="accent-custom-row" data-collapse-duration-row>
<span>Длительность анимации</span>
<input type="range" min="100" max="800" step="25" value="240" data-collapse-duration />
<output data-collapse-duration-value>240 ms</output>
</label>
</section>
<section class="settings-section" data-settings-section="tree">
<p class="settings-section-title">Дерево</p>
<label class="accent-custom-row">
<span>Колонок</span>
<input type="range" min="1" max="6" step="1" value="2" data-tree-columns />
<output data-tree-columns-value>2</output>
</label>
<label class="toggle-row">
<span>
<strong>Показывать теги</strong>
<small>Оформление как на карточках</small>
</span>
<input type="checkbox" data-show-tree-tags />
</label>
<label class="toggle-row">
<span>
<strong>Показывать тип</strong>
<small>Подпись типа сервиса</small>
</span>
<input type="checkbox" data-show-tree-type />
</label>
<div class="control-group">
<span>Теги и тип</span>
<button class="segmented is-active" type="button" data-tree-meta-layout="inline">
Сбоку
</button>
<button class="segmented" type="button" data-tree-meta-layout="stacked">
Снизу
</button>
</div>
<label class="toggle-row">
<span>
<strong>Авто раскрывать поиск</strong>
<small>Сворачивать ветки без совпадений</small>
</span>
<input type="checkbox" data-auto-collapse-search />
</label>
</section>
<section class="settings-section" data-settings-section="appearance">
<p class="settings-section-title">Оформление</p>
<div class="control-group" data-feature="allowThemeChange">
<span>Тема</span>
<button class="segmented is-active" type="button" data-theme="dark">
Темная
</button>
<button class="segmented" type="button" data-theme="light">
Светлая
</button>
</div>
<!-- Выбор акцентного цвета интерфейса. -->
<div class="control-group accent-group" data-feature="allowAccentChange">
<span>Акцент</span>
<button
class="accent-swatch is-active"
type="button"
data-accent="violet"
aria-label="Фиолетовый акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="cyan"
aria-label="Голубой акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="lime"
aria-label="Лаймовый акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="amber"
aria-label="Янтарный акцент"
></button>
<button
class="accent-swatch accent-swatch-custom"
type="button"
data-accent="custom"
aria-label="Свой градиент"
title="Свой градиент"
>
<span class="accent-swatch-label">Свой</span>
</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" data-pattern-swatches="nav"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="nav" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="nav" data-panel-pattern-field="height" /><output data-panel-pattern-value="nav" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="section"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="section" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="section" data-panel-pattern-field="height" /><output data-panel-pattern-value="section" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="group"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="group" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="group" data-panel-pattern-field="height" /><output data-panel-pattern-value="group" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="item"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="item" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="item" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="item" data-panel-pattern-field="height" /><output data-panel-pattern-value="item" data-panel-pattern-field="height">45%</output></label>
<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>
<label class="accent-custom-row pattern-isolation-row" data-feature="allowPanelPatternChange">
<span>Разделение слоёв</span>
<input type="range" min="0" max="100" step="1" value="46" data-pattern-isolation />
<output data-pattern-isolation-value>46%</output>
</label>
<p class="settings-section-hint" data-feature="allowPanelPatternChange">
Подложка размывает узоры нижних уровней под разделами, подразделами и карточками.
</p>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>
<label class="accent-custom-row">
<span>Угол</span>
<input
type="range"
min="0"
max="360"
step="1"
value="135"
data-custom-accent-angle
/>
<output data-custom-accent-angle-value>135°</output>
</label>
<label class="accent-custom-row">
<span>Цвет от</span>
<input type="color" value="#8b5cf6" data-custom-accent-from />
</label>
<label class="accent-custom-row">
<span>Цвет до</span>
<input type="color" value="#22d3ee" data-custom-accent-to />
</label>
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
</div>
</section>
<div class="settings-actions">
<p class="settings-section-hint">
Сброс, экспорт и импорт настроек интерфейса на этом устройстве.
</p>
<div class="settings-actions-buttons">
<button class="settings-action-button" type="button" data-settings-reset>
Сбросить
</button>
<button class="settings-action-button" type="button" data-settings-export>
Экспортировать
</button>
<button class="settings-action-button" type="button" data-settings-import>
Импортировать
</button>
</div>
<input type="file" accept="application/json,.json" data-settings-import-input hidden />
</div>
</div>
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->
<section class="catalog cards-view" aria-live="polite" data-catalog-root></section>
<!-- Шаблон ошибки загрузки settings.json / panel data. -->
<template id="config-error-template">
<div class="empty-state config-error-state">
<img src="../shared/assets/config.svg" alt="" />
<h2>Не удалось загрузить панель</h2>
<p data-config-error-message></p>
</div>
</template>
<!-- Шаблон состояния, когда поиск не нашел ни одного элемента. -->
<template id="empty-template">
<div class="empty-state">
<img src="../shared/assets/search.svg" alt="" />
<h2>Ничего не найдено</h2>
<p>Попробуй изменить запрос или искать по названию системы, тегу или роли.</p>
</div>
</template>
</div>
</main>
<script src="../shared/script.js" data-ecosystem-panel data-asset-base="../shared/" defer></script>
</body>
</html>

View File

@ -0,0 +1,173 @@
{
"title": "Пример: плоский список",
"items": [
{
"id": "grafana",
"title": "Grafana",
"type": "grafana",
"tags": ["grafana", "метрики", "дашборд"],
"url": "https://grafana.example.com"
},
{
"id": "prometheus",
"title": "Prometheus",
"type": "prometheus",
"tags": ["prometheus", "метрики"],
"url": "https://prometheus.example.com"
},
{
"id": "analytics",
"title": "Портал аналитики",
"type": "analytics",
"tags": ["аналитика", "отчёты"],
"url": "https://analytics.example.com"
},
{
"id": "opensearch",
"title": "OpenSearch",
"type": "opensearch",
"tags": ["логи", "opensearch"],
"url": "https://opensearch.example.com"
},
{
"id": "kubernetes",
"title": "Kubernetes",
"type": "kubernetes",
"tags": ["k8s", "кластер"],
"url": "https://k8s.example.com"
},
{
"id": "cicd",
"title": "CI/CD",
"type": "cicd",
"tags": ["ci", "cd", "deploy"],
"url": "https://ci.example.com"
},
{
"id": "vault",
"title": "Vault — секреты",
"type": "vault",
"tags": ["vault", "секреты"],
"url": "https://vault.example.com"
},
{
"id": "gitea",
"title": "Gitea",
"type": "gitea",
"tags": ["git", "repo"],
"url": "https://gitea.example.com"
},
{
"id": "api",
"title": "API Gateway",
"type": "api",
"tags": ["api", "интеграции"],
"url": "https://api.example.com"
},
{
"id": "postgresql",
"title": "PostgreSQL",
"type": "postgresql",
"tags": ["база", "sql"],
"url": "https://postgres.example.com"
},
{
"id": "rabbitmq",
"title": "RabbitMQ",
"type": "rabbitmq",
"tags": ["очереди", "amqp"],
"url": "https://rabbitmq.example.com"
},
{
"id": "ceph",
"title": "Ceph S3",
"type": "ceph",
"tags": ["s3", "хранилище"],
"url": "https://s3.example.com"
},
{
"id": "drive",
"title": "Корпоративный диск",
"type": "drive",
"tags": ["файлы", "диск"],
"url": "https://drive.example.com"
},
{
"id": "social",
"title": "Корпоративная соцсеть",
"type": "social",
"tags": ["соцсеть", "лента"],
"url": "https://social.example.com"
},
{
"id": "chat",
"title": "Командные беседы",
"type": "chat",
"tags": ["беседа", "чат"],
"url": "https://chat.example.com"
},
{
"id": "forum",
"title": "Форум сотрудников",
"type": "forum",
"tags": ["форум", "обсуждения"],
"url": "https://forum.example.com"
},
{
"id": "wiki",
"title": "Wiki",
"type": "wiki",
"tags": ["wiki", "документация"],
"url": "https://wiki.example.com"
},
{
"id": "video",
"title": "Видеоконференции",
"type": "video",
"tags": ["видео", "meet"],
"url": "https://meet.example.com"
},
{
"id": "calendar",
"title": "Календарь",
"type": "calendar",
"tags": ["календарь", "встречи"],
"url": "https://calendar.example.com"
},
{
"id": "messages",
"title": "Почта",
"type": "messages",
"tags": ["почта", "email"],
"url": "https://mail.example.com"
},
{
"id": "xmpp",
"title": "XMPP-чат",
"type": "xmpp",
"tags": ["xmpp", "мессенджер"],
"url": "https://xmpp.example.com"
},
{
"id": "vpn",
"title": "VPN-портал",
"type": "vpn",
"tags": ["vpn", "доступ"],
"url": "https://vpn.example.com"
},
{
"id": "ldap",
"title": "LDAP",
"type": "ldap",
"tags": ["ldap", "iam"],
"url": "https://ldap.example.com"
},
{
"id": "redmine",
"title": "Redmine",
"type": "redmine",
"tags": ["задачи", "трекер"],
"url": "https://redmine.example.com"
}
]
}

105
examples/flat/settings.json Normal file
View File

@ -0,0 +1,105 @@
{
"dataUrl": "./panel-data.json",
"data": null,
"defaults": {
"navLayout": "top",
"showTree": false,
"showResizeHandles": false,
"theme": "dark",
"view": "cards",
"accent": "custom",
"customAccentFrom": "#ff428e",
"customAccentTo": "#5bd8ec",
"customAccentAngle": 135,
"panelPatternNav": "diagonal",
"panelPatternNavOpacity": 20,
"panelPatternNavHeight": 100,
"panelPatternNavFade": 100,
"panelPatternNavGradientOpacity": 100,
"panelPatternNavGradientHeight": 100,
"panelPatternNavGradientFade": 100,
"panelPatternNavCustomImage": "",
"panelPatternNavCustomSize": "32px 32px",
"panelPatternSection": "diamond",
"panelPatternSectionOpacity": 30,
"panelPatternSectionHeight": 50,
"panelPatternSectionFade": 60,
"panelPatternSectionGradientOpacity": 75,
"panelPatternSectionGradientHeight": 20,
"panelPatternSectionGradientFade": 15,
"panelPatternSectionCustomImage": "",
"panelPatternSectionCustomSize": "32px 32px",
"panelPatternGroup": "custom",
"panelPatternGroupOpacity": 10,
"panelPatternGroupHeight": 100,
"panelPatternGroupFade": 100,
"panelPatternGroupGradientOpacity": 20,
"panelPatternGroupGradientHeight": 100,
"panelPatternGroupGradientFade": 100,
"panelPatternGroupCustomImage": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='s2' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23E2E8F0' /%3E%3Cstop offset='100%25' stop-color='%2364748B' /%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='20' y='20' width='24' height='24' fill='none' stroke='url(%23s2)' stroke-width='1.5' transform='rotate(45 32 32)' /%3E%3Crect x='16' y='16' width='32' height='32' fill='none' stroke='url(%23s2)' stroke-width='1' opacity='0.4' transform='rotate(45 32 32)' /%3E%3C/svg%3E",
"panelPatternGroupCustomSize": "32px 32px",
"panelPatternItem": "mesh",
"panelPatternItemOpacity": 58,
"panelPatternItemHeight": 100,
"panelPatternItemFade": 18,
"panelPatternItemGradientOpacity": 62,
"panelPatternItemGradientHeight": 34,
"panelPatternItemGradientFade": 16,
"panelPatternItemCustomImage": "",
"panelPatternItemCustomSize": "32px 32px",
"patternIsolation": 65,
"showNumbers": false,
"showTreeTags": false,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 1,
"autoCollapseSearch": true,
"sidebarWidth": 360,
"topBarHeight": 72,
"shellMaxWidth": 1100,
"sidebarMinWidth": 240,
"sidebarMaxWidth": 720,
"sidebarMaxWidthWide": 1040,
"treeSidebarBaseWidth": 360,
"treeSidebarColumnWidth": 96,
"topBarMinHeight": 64,
"topBarMaxHeight": 800,
"catalogCardsColumns": 2,
"catalogListColumns": 2,
"catalogCompactCardsColumns": 3,
"catalogCompactListColumns": 3,
"collapseAnimation": "animated",
"collapseDuration": 250,
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
"settingsPanelWidth": 500
},
"limits": {
"shellMaxWidth": { "min": 1100, "max": 3840 },
"sidebarMinWidth": { "min": 220, "max": 480 },
"sidebarMaxWidth": { "min": 400, "max": 1200 },
"sidebarMaxWidthWide": { "min": 560, "max": 2000 },
"treeSidebarBaseWidth": { "min": 280, "max": 720 },
"treeSidebarColumnWidth": { "min": 60, "max": 200 },
"topBarMinHeight": { "min": 64, "max": 240 },
"topBarMaxHeight": { "min": 160, "max": 800 },
"catalogCardsColumns": { "min": 1, "max": 6 },
"catalogListColumns": { "min": 1, "max": 4 },
"catalogCompactCardsColumns": { "min": 2, "max": 8 },
"catalogCompactListColumns": { "min": 1, "max": 6 },
"settingsPanelWidth": { "min": 320, "max": 720 }
},
"features": {
"allowNavLayoutChange": true,
"allowShowTreeChange": true,
"allowSearch": true,
"allowViewChange": true,
"allowCatalogColumnsChange": true,
"allowShowNumbers": true,
"allowTreeOptions": true,
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true,
"allowPanelPatternChange": true
}
}

548
examples/full/index.html Normal file
View File

@ -0,0 +1,548 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Навигационная панель экосистемы с разделами, подразделами, поиском и быстрыми переходами."
/>
<title>Панель экосистемы</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="../shared/styles.css" />
</head>
<body>
<!-- Декоративные фоновые пятна. Они не участвуют в логике и нужны только для глубины фона. -->
<div class="ambient ambient-one"></div>
<div class="ambient ambient-two"></div>
<main class="shell">
<!--
Основная сетка приложения:
слева sticky-меню с деревом навигации, справа каталог карточек.
Ширина левой колонки управляется CSS-переменной --sidebar-width и ресайзером.
-->
<div class="app-layout">
<!-- Боковая панель: поиск, настройки и дерево навигации по всей структуре. -->
<aside class="side-panel" aria-label="Навигация по экосистеме">
<div class="hero-content">
<!-- Шестеренка открывает fixed popup с настройками внешнего вида и поведения. -->
<button
class="settings-button"
type="button"
aria-label="Открыть настройки внешнего вида"
aria-expanded="false"
data-settings-toggle
>
⚙
</button>
<p class="eyebrow" id="page-title">Панель экосистемы</p>
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
<label class="search-field" data-search-field>
<input
type="search"
placeholder="Grafana, S3, журнал, сервер..."
autocomplete="off"
data-search-input
/>
</label>
<!-- Дерево генерируется из ecosystemSections в script.js. -->
<nav class="nav-tree" aria-label="Дерево навигации" data-nav-tree></nav>
</div>
<div
class="top-bar-resizer"
role="separator"
aria-orientation="horizontal"
aria-label="Изменить высоту верхней панели"
data-top-bar-resizer
hidden
></div>
</aside>
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
<div
class="sidebar-resizer"
role="separator"
aria-orientation="vertical"
aria-label="Изменить ширину бокового меню"
data-sidebar-resizer
></div>
<!-- Затемнение страницы за popup-настройками. -->
<div class="settings-overlay" data-settings-overlay></div>
<!--
Popup настроек:
- тема;
- режим отображения карточек;
- видимость нумерации;
- видимость тегов в дереве;
- авто-раскрытие дерева при поиске;
- цветовой акцент.
-->
<div class="control-panel" aria-label="Настройки панели" data-settings-panel>
<div class="settings-panel-head">
<p class="eyebrow settings-panel-eyebrow">Настройки</p>
<button class="settings-close" type="button" aria-label="Закрыть настройки" data-settings-close>
×
</button>
</div>
<!-- Переключение светлой/темной темы. Значение сохраняется в localStorage. -->
<section class="settings-section" data-settings-section="layout">
<p class="settings-section-title">Макет</p>
<div class="control-group">
<span>Панель навигации</span>
<button class="segmented is-active" type="button" data-nav-layout="sidebar">
Сбоку
</button>
<button class="segmented" type="button" data-nav-layout="top">
Сверху
</button>
</div>
<p class="settings-section-hint">
Режим «Сверху» — компактная шапка с названием и поиском, без дерева.
</p>
<label class="toggle-row" data-feature="allowShowTreeChange" data-show-when-sidebar>
<span>
<strong>Показывать дерево</strong>
<small>Только для панели «Сбоку»</small>
</span>
<input type="checkbox" data-show-tree checked />
</label>
<label class="toggle-row" data-feature="allowSidebarResize">
<span>
<strong>Ручки изменения размера</strong>
<small>Ширина меню или высота шапки</small>
</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">
<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">
Ограничения ширины страницы, бокового меню и верхней шапки. Значения применяются сразу;
текущий размер меню и шапки можно также менять перетаскиванием ручек на странице.
Допустимые диапазоны ползунков задаёт администратор в <code>settings.json</code> →
<code>limits</code>.
</p>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная ширина всей панели (.shell): насколько широко вместе могут располагаться меню и каталог."
>
<span>Ширина контента (max)</span>
<input type="range" data-layout-limit="shellMaxWidth" />
<output data-layout-limit-value="shellMaxWidth">2800</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Минимальная ширина бокового меню при перетаскивании вертикальной ручки между меню и каталогом."
>
<span>Меню: мин. ширина</span>
<input type="range" data-layout-limit="sidebarMinWidth" />
<output data-layout-limit-value="sidebarMinWidth">260</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная ширина меню, когда элементы дерева в папках показываются в одну колонку."
>
<span>Меню: макс. (1 кол.)</span>
<input type="range" data-layout-limit="sidebarMaxWidth" />
<output data-layout-limit-value="sidebarMaxWidth">800</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Верхний предел ширины меню при нескольких колонках в дереве. Фактический максимум также зависит от базы и шага колонки."
>
<span>Меню: макс. (неск. кол.)</span>
<input type="range" data-layout-limit="sidebarMaxWidthWide" />
<output data-layout-limit-value="sidebarMaxWidthWide">1400</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="База формулы ширины меню при нескольких колонках: min(макс. неск. кол., база + колонки × шаг)."
>
<span>Дерево: база ширины</span>
<input type="range" data-layout-limit="treeSidebarBaseWidth" />
<output data-layout-limit-value="treeSidebarBaseWidth">420</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Сколько пикселей добавляется к базовой ширине меню за каждую колонку элементов в папках дерева."
>
<span>Дерево: ширина колонки</span>
<input type="range" data-layout-limit="treeSidebarColumnWidth" />
<output data-layout-limit-value="treeSidebarColumnWidth">120</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Минимальная высота верхней шапки в режиме «Сверху» — нижняя граница при перетаскивании горизонтальной ручки."
>
<span>Шапка: мин. высота</span>
<input type="range" data-layout-limit="topBarMinHeight" />
<output data-layout-limit-value="topBarMinHeight">80</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная высота верхней шапки в режиме «Сверху». Также ограничена примерно 75% высоты окна браузера."
>
<span>Шапка: макс. высота</span>
<input type="range" data-layout-limit="topBarMaxHeight" />
<output data-layout-limit-value="topBarMaxHeight">560</output>
</label>
</div>
</details>
<section class="settings-section" data-settings-section="catalog">
<p class="settings-section-title">Каталог</p>
<!-- Режимы каталога: обычные/компактные плитки и списки. -->
<div class="control-group view-group" data-feature="allowViewChange">
<span>Вид</span>
<button class="segmented is-active" type="button" data-view="cards">
Плитки
</button>
<button class="segmented" type="button" data-view="list">
Список
</button>
<button class="segmented" type="button" data-view="compact-cards">
Компакт плитки
</button>
<button class="segmented" type="button" data-view="compact-list">
Компакт список
</button>
</div>
<div class="catalog-columns-group" data-feature="allowCatalogColumnsChange">
<p class="settings-section-hint">Колонок в строке для каждого вида каталога.</p>
<label class="accent-custom-row">
<span>Плитки</span>
<input type="range" data-catalog-columns="catalogCardsColumns" />
<output data-catalog-columns-value="catalogCardsColumns">3</output>
</label>
<label class="accent-custom-row">
<span>Список</span>
<input type="range" data-catalog-columns="catalogListColumns" />
<output data-catalog-columns-value="catalogListColumns">1</output>
</label>
<label class="accent-custom-row">
<span>Компакт плитки</span>
<input type="range" data-catalog-columns="catalogCompactCardsColumns" />
<output data-catalog-columns-value="catalogCompactCardsColumns">5</output>
</label>
<label class="accent-custom-row">
<span>Компакт список</span>
<input type="range" data-catalog-columns="catalogCompactListColumns" />
<output data-catalog-columns-value="catalogCompactListColumns">3</output>
</label>
</div>
<!-- Показывать номера разделов, подразделов и элементов. -->
<label class="toggle-row" data-feature="allowShowNumbers">
<span>
<strong>Показывать нумерацию</strong>
<small>Разделы, подразделы и элементы</small>
</span>
<input type="checkbox" data-show-numbers />
</label>
<div class="control-group collapse-animation-group">
<span>Раскрытие списков</span>
<button class="segmented" type="button" data-collapse-animation="instant">Резко</button>
<button class="segmented is-active" type="button" data-collapse-animation="animated">Плавно</button>
</div>
<p class="settings-section-hint">Разделы каталога, дерево навигации и блоки в настройках.</p>
<label class="accent-custom-row" data-collapse-duration-row>
<span>Длительность анимации</span>
<input type="range" min="100" max="800" step="25" value="240" data-collapse-duration />
<output data-collapse-duration-value>240 ms</output>
</label>
</section>
<section class="settings-section" data-settings-section="tree">
<p class="settings-section-title">Дерево</p>
<label class="accent-custom-row">
<span>Колонок</span>
<input type="range" min="1" max="6" step="1" value="2" data-tree-columns />
<output data-tree-columns-value>2</output>
</label>
<label class="toggle-row">
<span>
<strong>Показывать теги</strong>
<small>Оформление как на карточках</small>
</span>
<input type="checkbox" data-show-tree-tags />
</label>
<label class="toggle-row">
<span>
<strong>Показывать тип</strong>
<small>Подпись типа сервиса</small>
</span>
<input type="checkbox" data-show-tree-type />
</label>
<div class="control-group">
<span>Теги и тип</span>
<button class="segmented is-active" type="button" data-tree-meta-layout="inline">
Сбоку
</button>
<button class="segmented" type="button" data-tree-meta-layout="stacked">
Снизу
</button>
</div>
<label class="toggle-row">
<span>
<strong>Авто раскрывать поиск</strong>
<small>Сворачивать ветки без совпадений</small>
</span>
<input type="checkbox" data-auto-collapse-search />
</label>
</section>
<section class="settings-section" data-settings-section="appearance">
<p class="settings-section-title">Оформление</p>
<div class="control-group" data-feature="allowThemeChange">
<span>Тема</span>
<button class="segmented is-active" type="button" data-theme="dark">
Темная
</button>
<button class="segmented" type="button" data-theme="light">
Светлая
</button>
</div>
<!-- Выбор акцентного цвета интерфейса. -->
<div class="control-group accent-group" data-feature="allowAccentChange">
<span>Акцент</span>
<button
class="accent-swatch is-active"
type="button"
data-accent="violet"
aria-label="Фиолетовый акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="cyan"
aria-label="Голубой акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="lime"
aria-label="Лаймовый акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="amber"
aria-label="Янтарный акцент"
></button>
<button
class="accent-swatch accent-swatch-custom"
type="button"
data-accent="custom"
aria-label="Свой градиент"
title="Свой градиент"
>
<span class="accent-swatch-label">Свой</span>
</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" data-pattern-swatches="nav"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="nav" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="nav" data-panel-pattern-field="height" /><output data-panel-pattern-value="nav" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="section"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="section" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="section" data-panel-pattern-field="height" /><output data-panel-pattern-value="section" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="group"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="group" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="group" data-panel-pattern-field="height" /><output data-panel-pattern-value="group" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="item"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="item" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="item" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="item" data-panel-pattern-field="height" /><output data-panel-pattern-value="item" data-panel-pattern-field="height">45%</output></label>
<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>
<label class="accent-custom-row pattern-isolation-row" data-feature="allowPanelPatternChange">
<span>Разделение слоёв</span>
<input type="range" min="0" max="100" step="1" value="46" data-pattern-isolation />
<output data-pattern-isolation-value>46%</output>
</label>
<p class="settings-section-hint" data-feature="allowPanelPatternChange">
Подложка размывает узоры нижних уровней под разделами, подразделами и карточками.
</p>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>
<label class="accent-custom-row">
<span>Угол</span>
<input
type="range"
min="0"
max="360"
step="1"
value="135"
data-custom-accent-angle
/>
<output data-custom-accent-angle-value>135°</output>
</label>
<label class="accent-custom-row">
<span>Цвет от</span>
<input type="color" value="#8b5cf6" data-custom-accent-from />
</label>
<label class="accent-custom-row">
<span>Цвет до</span>
<input type="color" value="#22d3ee" data-custom-accent-to />
</label>
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
</div>
</section>
<div class="settings-actions">
<p class="settings-section-hint">
Сброс, экспорт и импорт настроек интерфейса на этом устройстве.
</p>
<div class="settings-actions-buttons">
<button class="settings-action-button" type="button" data-settings-reset>
Сбросить
</button>
<button class="settings-action-button" type="button" data-settings-export>
Экспортировать
</button>
<button class="settings-action-button" type="button" data-settings-import>
Импортировать
</button>
</div>
<input type="file" accept="application/json,.json" data-settings-import-input hidden />
</div>
</div>
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->
<section class="catalog cards-view" aria-live="polite" data-catalog-root></section>
<!-- Шаблон ошибки загрузки settings.json / panel data. -->
<template id="config-error-template">
<div class="empty-state config-error-state">
<img src="../shared/assets/config.svg" alt="" />
<h2>Не удалось загрузить панель</h2>
<p data-config-error-message></p>
</div>
</template>
<!-- Шаблон состояния, когда поиск не нашел ни одного элемента. -->
<template id="empty-template">
<div class="empty-state">
<img src="../shared/assets/search.svg" alt="" />
<h2>Ничего не найдено</h2>
<p>Попробуй изменить запрос или искать по названию системы, тегу или роли.</p>
</div>
</template>
</div>
</main>
<script src="../shared/script.js" data-ecosystem-panel data-asset-base="../shared/" defer></script>
</body>
</html>

View File

@ -0,0 +1,315 @@
{
"title": "Пример: section → group → item",
"sections": [
{
"id": "monitoring",
"number": "1",
"title": "Мониторинг",
"description": "Метрики, алерты и журналы событий.",
"groups": [
{
"id": "metrics",
"number": "1.1",
"title": "Метрики и дашборды",
"description": "Визуализация и сбор показателей.",
"items": [
{
"id": "grafana-main",
"number": "1.1.1",
"title": "Grafana — основной",
"type": "grafana",
"tags": ["grafana", "дашборд", "метрики"],
"url": "https://grafana.example.com"
},
{
"id": "prometheus",
"number": "1.1.2",
"title": "Prometheus",
"type": "prometheus",
"tags": ["prometheus", "метрики", "tsdb"],
"url": "https://prometheus.example.com"
},
{
"id": "analytics",
"number": "1.1.3",
"title": "Портал аналитики",
"type": "analytics",
"tags": ["аналитика", "отчёты", "kpi"],
"url": "https://analytics.example.com"
},
{
"id": "alerts",
"number": "1.1.4",
"title": "Правила алертинга",
"type": "rules",
"tags": ["алерты", "правила"],
"url": "#"
}
]
},
{
"id": "logs",
"number": "1.2",
"title": "Логи и поиск",
"description": "Централизованный просмотр журналов.",
"items": [
{
"id": "opensearch",
"number": "1.2.1",
"title": "OpenSearch",
"type": "opensearch",
"tags": ["логи", "поиск", "opensearch"],
"url": "https://opensearch.example.com"
},
{
"id": "error-log",
"number": "1.2.2",
"title": "Журнал ошибок",
"type": "opensearch",
"tags": ["ошибки", "журнал"],
"url": "#"
}
]
}
]
},
{
"id": "infrastructure",
"number": "2",
"title": "Инфраструктура",
"description": "Кластеры, хранилища и сервисы данных.",
"groups": [
{
"id": "clusters",
"number": "2.1",
"title": "Кластеры и оркестрация",
"description": "Управление вычислительными ресурсами.",
"items": [
{
"id": "k8s",
"number": "2.1.1",
"title": "Kubernetes Dashboard",
"type": "kubernetes",
"tags": ["kubernetes", "k8s", "кластер"],
"url": "https://k8s.example.com"
},
{
"id": "rabbitmq",
"number": "2.1.2",
"title": "RabbitMQ",
"type": "rabbitmq",
"tags": ["очереди", "rabbitmq"],
"url": "https://rabbitmq.example.com"
},
{
"id": "cicd",
"number": "2.1.3",
"title": "CI/CD — пайплайны",
"type": "cicd",
"tags": ["ci", "cd", "deploy"],
"url": "https://ci.example.com"
},
{
"id": "vault",
"number": "2.1.4",
"title": "HashiCorp Vault",
"type": "vault",
"tags": ["секреты", "vault", "безопасность"],
"url": "https://vault.example.com"
}
]
},
{
"id": "storage",
"number": "2.2",
"title": "Хранилища и БД",
"description": "Объектное хранилище и реляционные базы.",
"items": [
{
"id": "ceph",
"number": "2.2.1",
"title": "Ceph — S3",
"type": "ceph",
"tags": ["ceph", "s3", "объекты"],
"url": "https://s3.example.com"
},
{
"id": "drive",
"number": "2.2.2",
"title": "Корпоративный диск",
"type": "drive",
"tags": ["файлы", "диск", "обмен"],
"url": "https://drive.example.com"
},
{
"id": "postgresql",
"number": "2.2.3",
"title": "PostgreSQL",
"type": "postgresql",
"tags": ["postgresql", "база", "sql"],
"url": "https://postgres.example.com"
},
{
"id": "cloudbeaver",
"number": "2.2.4",
"title": "CloudBeaver",
"type": "cloudbeaver",
"tags": ["sql", "база", "редактор"],
"url": "#"
}
]
}
]
},
{
"id": "communications",
"number": "3",
"title": "Коммуникации",
"description": "Соцсеть, чаты, форумы и видеосвязь.",
"groups": [
{
"id": "social-collab",
"number": "3.1",
"title": "Совместная работа",
"description": "Лента, обсуждения и база знаний.",
"items": [
{
"id": "social-feed",
"number": "3.1.1",
"title": "Корпоративная соцсеть",
"type": "social",
"tags": ["соцсеть", "лента", "новости"],
"url": "https://social.example.com"
},
{
"id": "forum",
"number": "3.1.2",
"title": "Форум сотрудников",
"type": "forum",
"tags": ["форум", "обсуждения", "q&a"],
"url": "https://forum.example.com"
},
{
"id": "wiki",
"number": "3.1.3",
"title": "Wiki — база знаний",
"type": "wiki",
"tags": ["wiki", "документация", "знания"],
"url": "https://wiki.example.com"
},
{
"id": "calendar",
"number": "3.1.4",
"title": "Календарь и встречи",
"type": "calendar",
"tags": ["календарь", "встречи", "расписание"],
"url": "https://calendar.example.com"
}
]
},
{
"id": "messaging",
"number": "3.2",
"title": "Сообщения и звонки",
"description": "Мессенджеры, почта и видеоконференции.",
"items": [
{
"id": "chat-team",
"number": "3.2.1",
"title": "Командные беседы",
"type": "chat",
"tags": ["беседа", "чат", "команда"],
"url": "https://chat.example.com"
},
{
"id": "xmpp",
"number": "3.2.2",
"title": "XMPP-сервер",
"type": "xmpp",
"tags": ["xmpp", "мессенджер"],
"url": "https://xmpp.example.com"
},
{
"id": "mail",
"number": "3.2.3",
"title": "Корпоративная почта",
"type": "messages",
"tags": ["почта", "email"],
"url": "https://mail.example.com"
},
{
"id": "video-conf",
"number": "3.2.4",
"title": "Видеоконференции",
"type": "video",
"tags": ["видео", "звонки", "meet"],
"url": "https://meet.example.com"
}
]
}
]
},
{
"id": "access",
"number": "4",
"title": "Доступ и интеграции",
"description": "VPN, API и внешние сервисы.",
"groups": [
{
"id": "remote-access",
"number": "4.1",
"title": "Удалённый доступ",
"items": [
{
"id": "vpn",
"number": "4.1.1",
"title": "VPN-портал",
"type": "vpn",
"tags": ["vpn", "удалёнка", "доступ"],
"url": "https://vpn.example.com"
},
{
"id": "ldap",
"number": "4.1.2",
"title": "LDAP Account Manager",
"type": "ldap",
"tags": ["ldap", "пользователи", "iam"],
"url": "https://ldap.example.com"
}
]
},
{
"id": "integrations",
"number": "4.2",
"title": "API и задачи",
"items": [
{
"id": "api-gateway",
"number": "4.2.1",
"title": "API Gateway",
"type": "api",
"tags": ["api", "gateway", "интеграции"],
"url": "https://api.example.com"
},
{
"id": "redmine",
"number": "4.2.2",
"title": "Redmine — задачи",
"type": "redmine",
"tags": ["redmine", "задачи", "трекер"],
"url": "https://redmine.example.com"
},
{
"id": "gitea",
"number": "4.2.3",
"title": "Gitea — репозитории",
"type": "gitea",
"tags": ["git", "repo", "gitea"],
"url": "https://gitea.example.com"
}
]
}
]
}
]
}

105
examples/full/settings.json Normal file
View File

@ -0,0 +1,105 @@
{
"dataUrl": "./panel-data.json",
"data": null,
"defaults": {
"navLayout": "top",
"showTree": false,
"showResizeHandles": false,
"theme": "dark",
"view": "cards",
"accent": "custom",
"customAccentFrom": "#ff428e",
"customAccentTo": "#5bd8ec",
"customAccentAngle": 135,
"panelPatternNav": "diagonal",
"panelPatternNavOpacity": 20,
"panelPatternNavHeight": 100,
"panelPatternNavFade": 100,
"panelPatternNavGradientOpacity": 100,
"panelPatternNavGradientHeight": 100,
"panelPatternNavGradientFade": 100,
"panelPatternNavCustomImage": "",
"panelPatternNavCustomSize": "32px 32px",
"panelPatternSection": "diamond",
"panelPatternSectionOpacity": 30,
"panelPatternSectionHeight": 50,
"panelPatternSectionFade": 60,
"panelPatternSectionGradientOpacity": 75,
"panelPatternSectionGradientHeight": 20,
"panelPatternSectionGradientFade": 15,
"panelPatternSectionCustomImage": "",
"panelPatternSectionCustomSize": "32px 32px",
"panelPatternGroup": "custom",
"panelPatternGroupOpacity": 10,
"panelPatternGroupHeight": 100,
"panelPatternGroupFade": 100,
"panelPatternGroupGradientOpacity": 20,
"panelPatternGroupGradientHeight": 100,
"panelPatternGroupGradientFade": 100,
"panelPatternGroupCustomImage": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='s2' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23E2E8F0' /%3E%3Cstop offset='100%25' stop-color='%2364748B' /%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='20' y='20' width='24' height='24' fill='none' stroke='url(%23s2)' stroke-width='1.5' transform='rotate(45 32 32)' /%3E%3Crect x='16' y='16' width='32' height='32' fill='none' stroke='url(%23s2)' stroke-width='1' opacity='0.4' transform='rotate(45 32 32)' /%3E%3C/svg%3E",
"panelPatternGroupCustomSize": "32px 32px",
"panelPatternItem": "mesh",
"panelPatternItemOpacity": 58,
"panelPatternItemHeight": 100,
"panelPatternItemFade": 18,
"panelPatternItemGradientOpacity": 62,
"panelPatternItemGradientHeight": 34,
"panelPatternItemGradientFade": 16,
"panelPatternItemCustomImage": "",
"panelPatternItemCustomSize": "32px 32px",
"patternIsolation": 65,
"showNumbers": false,
"showTreeTags": false,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 1,
"autoCollapseSearch": true,
"sidebarWidth": 360,
"topBarHeight": 72,
"shellMaxWidth": 1100,
"sidebarMinWidth": 240,
"sidebarMaxWidth": 720,
"sidebarMaxWidthWide": 1040,
"treeSidebarBaseWidth": 360,
"treeSidebarColumnWidth": 96,
"topBarMinHeight": 64,
"topBarMaxHeight": 800,
"catalogCardsColumns": 2,
"catalogListColumns": 2,
"catalogCompactCardsColumns": 3,
"catalogCompactListColumns": 3,
"collapseAnimation": "animated",
"collapseDuration": 250,
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
"settingsPanelWidth": 500
},
"limits": {
"shellMaxWidth": { "min": 1100, "max": 3840 },
"sidebarMinWidth": { "min": 220, "max": 480 },
"sidebarMaxWidth": { "min": 400, "max": 1200 },
"sidebarMaxWidthWide": { "min": 560, "max": 2000 },
"treeSidebarBaseWidth": { "min": 280, "max": 720 },
"treeSidebarColumnWidth": { "min": 60, "max": 200 },
"topBarMinHeight": { "min": 64, "max": 240 },
"topBarMaxHeight": { "min": 160, "max": 800 },
"catalogCardsColumns": { "min": 1, "max": 6 },
"catalogListColumns": { "min": 1, "max": 4 },
"catalogCompactCardsColumns": { "min": 2, "max": 8 },
"catalogCompactListColumns": { "min": 1, "max": 6 },
"settingsPanelWidth": { "min": 320, "max": 720 }
},
"features": {
"allowNavLayoutChange": true,
"allowShowTreeChange": true,
"allowSearch": true,
"allowViewChange": true,
"allowCatalogColumnsChange": true,
"allowShowNumbers": true,
"allowTreeOptions": true,
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true,
"allowPanelPatternChange": true
}
}

137
examples/index.html Normal file
View File

@ -0,0 +1,137 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Готовые демо-страницы панели экосистемы для всех форматов данных."
/>
<title>Примеры панели экосистемы</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="shared/styles.css" />
<style>
.examples-hub {
max-width: 960px;
margin: 0 auto;
padding: 48px 20px 64px;
}
.examples-hub h1 {
margin: 0 0 8px;
font-size: clamp(1.75rem, 4vw, 2.25rem);
}
.examples-hub > p {
margin: 0 0 28px;
color: var(--muted);
max-width: 56ch;
}
.examples-grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.example-card {
display: block;
padding: 20px;
border: 1px solid var(--border);
border-radius: var(--radius-lg, 16px);
background: var(--surface);
text-decoration: none;
color: inherit;
transition: border-color 0.2s, transform 0.2s;
}
.example-card:hover {
border-color: var(--accent);
transform: translateY(-2px);
}
.example-card h2 {
margin: 0 0 8px;
font-size: 1.05rem;
}
.example-card p {
margin: 0 0 12px;
color: var(--muted);
font-size: 0.92rem;
line-height: 1.45;
}
.example-card code {
font-size: 0.82rem;
color: var(--accent);
}
.examples-note {
margin-top: 32px;
padding: 16px 18px;
border-radius: 12px;
border: 1px solid var(--border);
background: color-mix(in srgb, var(--surface) 88%, var(--accent) 12%);
color: var(--muted);
font-size: 0.92rem;
line-height: 1.5;
}
.examples-note strong {
color: var(--text);
}
</style>
</head>
<body>
<div class="ambient ambient-one"></div>
<div class="ambient ambient-two"></div>
<main class="examples-hub">
<p class="eyebrow">Ecosystem panel</p>
<h1>Примеры структур данных</h1>
<p>
Каждый пример — самодостаточная страница с локальными <code>settings.json</code> и
<code>panel-data.json</code>. Общие стили, скрипт и иконки лежат в <code>shared/</code>.
</p>
<div class="examples-grid">
<a class="example-card" href="./full/">
<h2>Полная иерархия</h2>
<p>Разделы, подразделы и элементы — классический формат.</p>
<code>section → group → item</code>
</a>
<a class="example-card" href="./section-items/">
<h2>Раздел → item</h2>
<p>Элементы сразу внутри раздела, без промежуточных подразделов.</p>
<code>section → item</code>
</a>
<a class="example-card" href="./flat/">
<h2>Плоский список</h2>
<p>Только карточки сервисов, без дерева разделов.</p>
<code>items[]</code>
</a>
<a class="example-card" href="./mixed/">
<h2>Смешанный раздел</h2>
<p>Закреплённые ссылки и подразделы в одном разделе.</p>
<code>items[] + groups[]</code>
</a>
</div>
<div class="examples-note">
<strong>Запуск:</strong> из папки <code>examples</code> выполни
<code>npx serve .</code> и открой <code>/</code> или конкретный пример, например
<code>/full/</code>. Можно также поднять сервер из корня репозитория —
тогда адрес будет <code>/examples/full/</code>.
</div>
</main>
</body>
</html>

548
examples/mixed/index.html Normal file
View File

@ -0,0 +1,548 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Навигационная панель экосистемы с разделами, подразделами, поиском и быстрыми переходами."
/>
<title>Панель экосистемы</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="../shared/styles.css" />
</head>
<body>
<!-- Декоративные фоновые пятна. Они не участвуют в логике и нужны только для глубины фона. -->
<div class="ambient ambient-one"></div>
<div class="ambient ambient-two"></div>
<main class="shell">
<!--
Основная сетка приложения:
слева sticky-меню с деревом навигации, справа каталог карточек.
Ширина левой колонки управляется CSS-переменной --sidebar-width и ресайзером.
-->
<div class="app-layout">
<!-- Боковая панель: поиск, настройки и дерево навигации по всей структуре. -->
<aside class="side-panel" aria-label="Навигация по экосистеме">
<div class="hero-content">
<!-- Шестеренка открывает fixed popup с настройками внешнего вида и поведения. -->
<button
class="settings-button"
type="button"
aria-label="Открыть настройки внешнего вида"
aria-expanded="false"
data-settings-toggle
>
⚙
</button>
<p class="eyebrow" id="page-title">Панель экосистемы</p>
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
<label class="search-field" data-search-field>
<input
type="search"
placeholder="Grafana, S3, журнал, сервер..."
autocomplete="off"
data-search-input
/>
</label>
<!-- Дерево генерируется из ecosystemSections в script.js. -->
<nav class="nav-tree" aria-label="Дерево навигации" data-nav-tree></nav>
</div>
<div
class="top-bar-resizer"
role="separator"
aria-orientation="horizontal"
aria-label="Изменить высоту верхней панели"
data-top-bar-resizer
hidden
></div>
</aside>
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
<div
class="sidebar-resizer"
role="separator"
aria-orientation="vertical"
aria-label="Изменить ширину бокового меню"
data-sidebar-resizer
></div>
<!-- Затемнение страницы за popup-настройками. -->
<div class="settings-overlay" data-settings-overlay></div>
<!--
Popup настроек:
- тема;
- режим отображения карточек;
- видимость нумерации;
- видимость тегов в дереве;
- авто-раскрытие дерева при поиске;
- цветовой акцент.
-->
<div class="control-panel" aria-label="Настройки панели" data-settings-panel>
<div class="settings-panel-head">
<p class="eyebrow settings-panel-eyebrow">Настройки</p>
<button class="settings-close" type="button" aria-label="Закрыть настройки" data-settings-close>
×
</button>
</div>
<!-- Переключение светлой/темной темы. Значение сохраняется в localStorage. -->
<section class="settings-section" data-settings-section="layout">
<p class="settings-section-title">Макет</p>
<div class="control-group">
<span>Панель навигации</span>
<button class="segmented is-active" type="button" data-nav-layout="sidebar">
Сбоку
</button>
<button class="segmented" type="button" data-nav-layout="top">
Сверху
</button>
</div>
<p class="settings-section-hint">
Режим «Сверху» — компактная шапка с названием и поиском, без дерева.
</p>
<label class="toggle-row" data-feature="allowShowTreeChange" data-show-when-sidebar>
<span>
<strong>Показывать дерево</strong>
<small>Только для панели «Сбоку»</small>
</span>
<input type="checkbox" data-show-tree checked />
</label>
<label class="toggle-row" data-feature="allowSidebarResize">
<span>
<strong>Ручки изменения размера</strong>
<small>Ширина меню или высота шапки</small>
</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">
<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">
Ограничения ширины страницы, бокового меню и верхней шапки. Значения применяются сразу;
текущий размер меню и шапки можно также менять перетаскиванием ручек на странице.
Допустимые диапазоны ползунков задаёт администратор в <code>settings.json</code> →
<code>limits</code>.
</p>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная ширина всей панели (.shell): насколько широко вместе могут располагаться меню и каталог."
>
<span>Ширина контента (max)</span>
<input type="range" data-layout-limit="shellMaxWidth" />
<output data-layout-limit-value="shellMaxWidth">2800</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Минимальная ширина бокового меню при перетаскивании вертикальной ручки между меню и каталогом."
>
<span>Меню: мин. ширина</span>
<input type="range" data-layout-limit="sidebarMinWidth" />
<output data-layout-limit-value="sidebarMinWidth">260</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная ширина меню, когда элементы дерева в папках показываются в одну колонку."
>
<span>Меню: макс. (1 кол.)</span>
<input type="range" data-layout-limit="sidebarMaxWidth" />
<output data-layout-limit-value="sidebarMaxWidth">800</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Верхний предел ширины меню при нескольких колонках в дереве. Фактический максимум также зависит от базы и шага колонки."
>
<span>Меню: макс. (неск. кол.)</span>
<input type="range" data-layout-limit="sidebarMaxWidthWide" />
<output data-layout-limit-value="sidebarMaxWidthWide">1400</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="База формулы ширины меню при нескольких колонках: min(макс. неск. кол., база + колонки × шаг)."
>
<span>Дерево: база ширины</span>
<input type="range" data-layout-limit="treeSidebarBaseWidth" />
<output data-layout-limit-value="treeSidebarBaseWidth">420</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Сколько пикселей добавляется к базовой ширине меню за каждую колонку элементов в папках дерева."
>
<span>Дерево: ширина колонки</span>
<input type="range" data-layout-limit="treeSidebarColumnWidth" />
<output data-layout-limit-value="treeSidebarColumnWidth">120</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Минимальная высота верхней шапки в режиме «Сверху» — нижняя граница при перетаскивании горизонтальной ручки."
>
<span>Шапка: мин. высота</span>
<input type="range" data-layout-limit="topBarMinHeight" />
<output data-layout-limit-value="topBarMinHeight">80</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная высота верхней шапки в режиме «Сверху». Также ограничена примерно 75% высоты окна браузера."
>
<span>Шапка: макс. высота</span>
<input type="range" data-layout-limit="topBarMaxHeight" />
<output data-layout-limit-value="topBarMaxHeight">560</output>
</label>
</div>
</details>
<section class="settings-section" data-settings-section="catalog">
<p class="settings-section-title">Каталог</p>
<!-- Режимы каталога: обычные/компактные плитки и списки. -->
<div class="control-group view-group" data-feature="allowViewChange">
<span>Вид</span>
<button class="segmented is-active" type="button" data-view="cards">
Плитки
</button>
<button class="segmented" type="button" data-view="list">
Список
</button>
<button class="segmented" type="button" data-view="compact-cards">
Компакт плитки
</button>
<button class="segmented" type="button" data-view="compact-list">
Компакт список
</button>
</div>
<div class="catalog-columns-group" data-feature="allowCatalogColumnsChange">
<p class="settings-section-hint">Колонок в строке для каждого вида каталога.</p>
<label class="accent-custom-row">
<span>Плитки</span>
<input type="range" data-catalog-columns="catalogCardsColumns" />
<output data-catalog-columns-value="catalogCardsColumns">3</output>
</label>
<label class="accent-custom-row">
<span>Список</span>
<input type="range" data-catalog-columns="catalogListColumns" />
<output data-catalog-columns-value="catalogListColumns">1</output>
</label>
<label class="accent-custom-row">
<span>Компакт плитки</span>
<input type="range" data-catalog-columns="catalogCompactCardsColumns" />
<output data-catalog-columns-value="catalogCompactCardsColumns">5</output>
</label>
<label class="accent-custom-row">
<span>Компакт список</span>
<input type="range" data-catalog-columns="catalogCompactListColumns" />
<output data-catalog-columns-value="catalogCompactListColumns">3</output>
</label>
</div>
<!-- Показывать номера разделов, подразделов и элементов. -->
<label class="toggle-row" data-feature="allowShowNumbers">
<span>
<strong>Показывать нумерацию</strong>
<small>Разделы, подразделы и элементы</small>
</span>
<input type="checkbox" data-show-numbers />
</label>
<div class="control-group collapse-animation-group">
<span>Раскрытие списков</span>
<button class="segmented" type="button" data-collapse-animation="instant">Резко</button>
<button class="segmented is-active" type="button" data-collapse-animation="animated">Плавно</button>
</div>
<p class="settings-section-hint">Разделы каталога, дерево навигации и блоки в настройках.</p>
<label class="accent-custom-row" data-collapse-duration-row>
<span>Длительность анимации</span>
<input type="range" min="100" max="800" step="25" value="240" data-collapse-duration />
<output data-collapse-duration-value>240 ms</output>
</label>
</section>
<section class="settings-section" data-settings-section="tree">
<p class="settings-section-title">Дерево</p>
<label class="accent-custom-row">
<span>Колонок</span>
<input type="range" min="1" max="6" step="1" value="2" data-tree-columns />
<output data-tree-columns-value>2</output>
</label>
<label class="toggle-row">
<span>
<strong>Показывать теги</strong>
<small>Оформление как на карточках</small>
</span>
<input type="checkbox" data-show-tree-tags />
</label>
<label class="toggle-row">
<span>
<strong>Показывать тип</strong>
<small>Подпись типа сервиса</small>
</span>
<input type="checkbox" data-show-tree-type />
</label>
<div class="control-group">
<span>Теги и тип</span>
<button class="segmented is-active" type="button" data-tree-meta-layout="inline">
Сбоку
</button>
<button class="segmented" type="button" data-tree-meta-layout="stacked">
Снизу
</button>
</div>
<label class="toggle-row">
<span>
<strong>Авто раскрывать поиск</strong>
<small>Сворачивать ветки без совпадений</small>
</span>
<input type="checkbox" data-auto-collapse-search />
</label>
</section>
<section class="settings-section" data-settings-section="appearance">
<p class="settings-section-title">Оформление</p>
<div class="control-group" data-feature="allowThemeChange">
<span>Тема</span>
<button class="segmented is-active" type="button" data-theme="dark">
Темная
</button>
<button class="segmented" type="button" data-theme="light">
Светлая
</button>
</div>
<!-- Выбор акцентного цвета интерфейса. -->
<div class="control-group accent-group" data-feature="allowAccentChange">
<span>Акцент</span>
<button
class="accent-swatch is-active"
type="button"
data-accent="violet"
aria-label="Фиолетовый акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="cyan"
aria-label="Голубой акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="lime"
aria-label="Лаймовый акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="amber"
aria-label="Янтарный акцент"
></button>
<button
class="accent-swatch accent-swatch-custom"
type="button"
data-accent="custom"
aria-label="Свой градиент"
title="Свой градиент"
>
<span class="accent-swatch-label">Свой</span>
</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" data-pattern-swatches="nav"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="nav" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="nav" data-panel-pattern-field="height" /><output data-panel-pattern-value="nav" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="section"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="section" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="section" data-panel-pattern-field="height" /><output data-panel-pattern-value="section" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="group"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="group" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="group" data-panel-pattern-field="height" /><output data-panel-pattern-value="group" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="item"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="item" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="item" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="item" data-panel-pattern-field="height" /><output data-panel-pattern-value="item" data-panel-pattern-field="height">45%</output></label>
<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>
<label class="accent-custom-row pattern-isolation-row" data-feature="allowPanelPatternChange">
<span>Разделение слоёв</span>
<input type="range" min="0" max="100" step="1" value="46" data-pattern-isolation />
<output data-pattern-isolation-value>46%</output>
</label>
<p class="settings-section-hint" data-feature="allowPanelPatternChange">
Подложка размывает узоры нижних уровней под разделами, подразделами и карточками.
</p>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>
<label class="accent-custom-row">
<span>Угол</span>
<input
type="range"
min="0"
max="360"
step="1"
value="135"
data-custom-accent-angle
/>
<output data-custom-accent-angle-value>135°</output>
</label>
<label class="accent-custom-row">
<span>Цвет от</span>
<input type="color" value="#8b5cf6" data-custom-accent-from />
</label>
<label class="accent-custom-row">
<span>Цвет до</span>
<input type="color" value="#22d3ee" data-custom-accent-to />
</label>
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
</div>
</section>
<div class="settings-actions">
<p class="settings-section-hint">
Сброс, экспорт и импорт настроек интерфейса на этом устройстве.
</p>
<div class="settings-actions-buttons">
<button class="settings-action-button" type="button" data-settings-reset>
Сбросить
</button>
<button class="settings-action-button" type="button" data-settings-export>
Экспортировать
</button>
<button class="settings-action-button" type="button" data-settings-import>
Импортировать
</button>
</div>
<input type="file" accept="application/json,.json" data-settings-import-input hidden />
</div>
</div>
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->
<section class="catalog cards-view" aria-live="polite" data-catalog-root></section>
<!-- Шаблон ошибки загрузки settings.json / panel data. -->
<template id="config-error-template">
<div class="empty-state config-error-state">
<img src="../shared/assets/config.svg" alt="" />
<h2>Не удалось загрузить панель</h2>
<p data-config-error-message></p>
</div>
</template>
<!-- Шаблон состояния, когда поиск не нашел ни одного элемента. -->
<template id="empty-template">
<div class="empty-state">
<img src="../shared/assets/search.svg" alt="" />
<h2>Ничего не найдено</h2>
<p>Попробуй изменить запрос или искать по названию системы, тегу или роли.</p>
</div>
</template>
</div>
</main>
<script src="../shared/script.js" data-ecosystem-panel data-asset-base="../shared/" defer></script>
</body>
</html>

View File

@ -0,0 +1,199 @@
{
"title": "Пример: смешанный раздел",
"sections": [
{
"id": "platform",
"number": "1",
"title": "Платформа",
"description": "Закреплённые ссылки и тематические подразделы в одном разделе.",
"items": [
{
"id": "status",
"number": "1.0",
"title": "Статус платформы",
"type": "grafana",
"tags": ["статус", "overview"],
"url": "https://status.example.com"
},
{
"id": "social",
"number": "1.0",
"title": "Соцсеть",
"type": "social",
"tags": ["соцсеть", "лента"],
"url": "https://social.example.com"
},
{
"id": "chat",
"number": "1.0",
"title": "Беседы",
"type": "chat",
"tags": ["беседа", "чат"],
"url": "https://chat.example.com"
},
{
"id": "docs",
"number": "1.0",
"title": "Wiki",
"type": "wiki",
"tags": ["wiki", "docs"],
"url": "https://docs.example.com"
},
{
"id": "calendar",
"number": "1.0",
"title": "Календарь",
"type": "calendar",
"tags": ["календарь", "встречи"],
"url": "https://calendar.example.com"
},
{
"id": "support",
"number": "1.0",
"title": "Поддержка",
"type": "redmine",
"tags": ["support", "тикеты"],
"url": "https://support.example.com"
}
],
"groups": [
{
"id": "observability",
"number": "1.1",
"title": "Наблюдаемость",
"description": "Метрики, логи и трассировка.",
"items": [
{
"id": "grafana",
"number": "1.1.1",
"title": "Grafana",
"type": "grafana",
"tags": ["grafana"],
"url": "https://grafana.example.com"
},
{
"id": "prometheus",
"number": "1.1.2",
"title": "Prometheus",
"type": "prometheus",
"tags": ["prometheus"],
"url": "https://prometheus.example.com"
},
{
"id": "analytics",
"number": "1.1.3",
"title": "Аналитика",
"type": "analytics",
"tags": ["аналитика", "отчёты"],
"url": "https://analytics.example.com"
},
{
"id": "logs",
"number": "1.1.4",
"title": "Журнал событий",
"type": "opensearch",
"tags": ["логи"],
"url": "#"
}
]
},
{
"id": "communications",
"number": "1.2",
"title": "Коммуникации",
"description": "Форум, видео и почта.",
"items": [
{
"id": "forum",
"number": "1.2.1",
"title": "Форум",
"type": "forum",
"tags": ["форум", "обсуждения"],
"url": "https://forum.example.com"
},
{
"id": "video",
"number": "1.2.2",
"title": "Видеозвонки",
"type": "video",
"tags": ["видео", "meet"],
"url": "https://meet.example.com"
},
{
"id": "mail",
"number": "1.2.3",
"title": "Почта",
"type": "messages",
"tags": ["почта"],
"url": "https://mail.example.com"
},
{
"id": "xmpp",
"number": "1.2.4",
"title": "XMPP",
"type": "xmpp",
"tags": ["xmpp", "чат"],
"url": "https://xmpp.example.com"
}
]
},
{
"id": "data",
"number": "1.3",
"title": "Данные и интеграции",
"description": "Базы, очереди, API и секреты.",
"items": [
{
"id": "postgres",
"number": "1.3.1",
"title": "PostgreSQL",
"type": "postgresql",
"tags": ["postgresql", "sql"],
"url": "https://postgres.example.com"
},
{
"id": "mq",
"number": "1.3.2",
"title": "RabbitMQ",
"type": "rabbitmq",
"tags": ["очереди"],
"url": "https://rabbitmq.example.com"
},
{
"id": "drive",
"number": "1.3.3",
"title": "Корп. диск",
"type": "drive",
"tags": ["файлы", "диск"],
"url": "https://drive.example.com"
},
{
"id": "api",
"number": "1.3.4",
"title": "API Gateway",
"type": "api",
"tags": ["api", "gateway"],
"url": "https://api.example.com"
},
{
"id": "vault",
"number": "1.3.5",
"title": "Vault",
"type": "vault",
"tags": ["секреты", "vault"],
"url": "https://vault.example.com"
},
{
"id": "vpn",
"number": "1.3.6",
"title": "VPN",
"type": "vpn",
"tags": ["vpn", "доступ"],
"url": "https://vpn.example.com"
}
]
}
]
}
]
}

View File

@ -0,0 +1,105 @@
{
"dataUrl": "./panel-data.json",
"data": null,
"defaults": {
"navLayout": "top",
"showTree": false,
"showResizeHandles": false,
"theme": "dark",
"view": "cards",
"accent": "custom",
"customAccentFrom": "#ff428e",
"customAccentTo": "#5bd8ec",
"customAccentAngle": 135,
"panelPatternNav": "diagonal",
"panelPatternNavOpacity": 20,
"panelPatternNavHeight": 100,
"panelPatternNavFade": 100,
"panelPatternNavGradientOpacity": 100,
"panelPatternNavGradientHeight": 100,
"panelPatternNavGradientFade": 100,
"panelPatternNavCustomImage": "",
"panelPatternNavCustomSize": "32px 32px",
"panelPatternSection": "diamond",
"panelPatternSectionOpacity": 30,
"panelPatternSectionHeight": 50,
"panelPatternSectionFade": 60,
"panelPatternSectionGradientOpacity": 75,
"panelPatternSectionGradientHeight": 20,
"panelPatternSectionGradientFade": 15,
"panelPatternSectionCustomImage": "",
"panelPatternSectionCustomSize": "32px 32px",
"panelPatternGroup": "custom",
"panelPatternGroupOpacity": 10,
"panelPatternGroupHeight": 100,
"panelPatternGroupFade": 100,
"panelPatternGroupGradientOpacity": 20,
"panelPatternGroupGradientHeight": 100,
"panelPatternGroupGradientFade": 100,
"panelPatternGroupCustomImage": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='s2' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23E2E8F0' /%3E%3Cstop offset='100%25' stop-color='%2364748B' /%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='20' y='20' width='24' height='24' fill='none' stroke='url(%23s2)' stroke-width='1.5' transform='rotate(45 32 32)' /%3E%3Crect x='16' y='16' width='32' height='32' fill='none' stroke='url(%23s2)' stroke-width='1' opacity='0.4' transform='rotate(45 32 32)' /%3E%3C/svg%3E",
"panelPatternGroupCustomSize": "32px 32px",
"panelPatternItem": "mesh",
"panelPatternItemOpacity": 58,
"panelPatternItemHeight": 100,
"panelPatternItemFade": 18,
"panelPatternItemGradientOpacity": 62,
"panelPatternItemGradientHeight": 34,
"panelPatternItemGradientFade": 16,
"panelPatternItemCustomImage": "",
"panelPatternItemCustomSize": "32px 32px",
"patternIsolation": 65,
"showNumbers": false,
"showTreeTags": false,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 1,
"autoCollapseSearch": true,
"sidebarWidth": 360,
"topBarHeight": 72,
"shellMaxWidth": 1100,
"sidebarMinWidth": 240,
"sidebarMaxWidth": 720,
"sidebarMaxWidthWide": 1040,
"treeSidebarBaseWidth": 360,
"treeSidebarColumnWidth": 96,
"topBarMinHeight": 64,
"topBarMaxHeight": 800,
"catalogCardsColumns": 2,
"catalogListColumns": 2,
"catalogCompactCardsColumns": 3,
"catalogCompactListColumns": 3,
"collapseAnimation": "animated",
"collapseDuration": 250,
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
"settingsPanelWidth": 500
},
"limits": {
"shellMaxWidth": { "min": 1100, "max": 3840 },
"sidebarMinWidth": { "min": 220, "max": 480 },
"sidebarMaxWidth": { "min": 400, "max": 1200 },
"sidebarMaxWidthWide": { "min": 560, "max": 2000 },
"treeSidebarBaseWidth": { "min": 280, "max": 720 },
"treeSidebarColumnWidth": { "min": 60, "max": 200 },
"topBarMinHeight": { "min": 64, "max": 240 },
"topBarMaxHeight": { "min": 160, "max": 800 },
"catalogCardsColumns": { "min": 1, "max": 6 },
"catalogListColumns": { "min": 1, "max": 4 },
"catalogCompactCardsColumns": { "min": 2, "max": 8 },
"catalogCompactListColumns": { "min": 1, "max": 6 },
"settingsPanelWidth": { "min": 320, "max": 720 }
},
"features": {
"allowNavLayoutChange": true,
"allowShowTreeChange": true,
"allowSearch": true,
"allowViewChange": true,
"allowCatalogColumnsChange": true,
"allowShowNumbers": true,
"allowTreeOptions": true,
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true,
"allowPanelPatternChange": true
}
}

View File

@ -0,0 +1,548 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Навигационная панель экосистемы с разделами, подразделами, поиском и быстрыми переходами."
/>
<title>Панель экосистемы</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="../shared/styles.css" />
</head>
<body>
<!-- Декоративные фоновые пятна. Они не участвуют в логике и нужны только для глубины фона. -->
<div class="ambient ambient-one"></div>
<div class="ambient ambient-two"></div>
<main class="shell">
<!--
Основная сетка приложения:
слева sticky-меню с деревом навигации, справа каталог карточек.
Ширина левой колонки управляется CSS-переменной --sidebar-width и ресайзером.
-->
<div class="app-layout">
<!-- Боковая панель: поиск, настройки и дерево навигации по всей структуре. -->
<aside class="side-panel" aria-label="Навигация по экосистеме">
<div class="hero-content">
<!-- Шестеренка открывает fixed popup с настройками внешнего вида и поведения. -->
<button
class="settings-button"
type="button"
aria-label="Открыть настройки внешнего вида"
aria-expanded="false"
data-settings-toggle
>
⚙
</button>
<p class="eyebrow" id="page-title">Панель экосистемы</p>
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
<label class="search-field" data-search-field>
<input
type="search"
placeholder="Grafana, S3, журнал, сервер..."
autocomplete="off"
data-search-input
/>
</label>
<!-- Дерево генерируется из ecosystemSections в script.js. -->
<nav class="nav-tree" aria-label="Дерево навигации" data-nav-tree></nav>
</div>
<div
class="top-bar-resizer"
role="separator"
aria-orientation="horizontal"
aria-label="Изменить высоту верхней панели"
data-top-bar-resizer
hidden
></div>
</aside>
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
<div
class="sidebar-resizer"
role="separator"
aria-orientation="vertical"
aria-label="Изменить ширину бокового меню"
data-sidebar-resizer
></div>
<!-- Затемнение страницы за popup-настройками. -->
<div class="settings-overlay" data-settings-overlay></div>
<!--
Popup настроек:
- тема;
- режим отображения карточек;
- видимость нумерации;
- видимость тегов в дереве;
- авто-раскрытие дерева при поиске;
- цветовой акцент.
-->
<div class="control-panel" aria-label="Настройки панели" data-settings-panel>
<div class="settings-panel-head">
<p class="eyebrow settings-panel-eyebrow">Настройки</p>
<button class="settings-close" type="button" aria-label="Закрыть настройки" data-settings-close>
×
</button>
</div>
<!-- Переключение светлой/темной темы. Значение сохраняется в localStorage. -->
<section class="settings-section" data-settings-section="layout">
<p class="settings-section-title">Макет</p>
<div class="control-group">
<span>Панель навигации</span>
<button class="segmented is-active" type="button" data-nav-layout="sidebar">
Сбоку
</button>
<button class="segmented" type="button" data-nav-layout="top">
Сверху
</button>
</div>
<p class="settings-section-hint">
Режим «Сверху» — компактная шапка с названием и поиском, без дерева.
</p>
<label class="toggle-row" data-feature="allowShowTreeChange" data-show-when-sidebar>
<span>
<strong>Показывать дерево</strong>
<small>Только для панели «Сбоку»</small>
</span>
<input type="checkbox" data-show-tree checked />
</label>
<label class="toggle-row" data-feature="allowSidebarResize">
<span>
<strong>Ручки изменения размера</strong>
<small>Ширина меню или высота шапки</small>
</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">
<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">
Ограничения ширины страницы, бокового меню и верхней шапки. Значения применяются сразу;
текущий размер меню и шапки можно также менять перетаскиванием ручек на странице.
Допустимые диапазоны ползунков задаёт администратор в <code>settings.json</code> →
<code>limits</code>.
</p>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная ширина всей панели (.shell): насколько широко вместе могут располагаться меню и каталог."
>
<span>Ширина контента (max)</span>
<input type="range" data-layout-limit="shellMaxWidth" />
<output data-layout-limit-value="shellMaxWidth">2800</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Минимальная ширина бокового меню при перетаскивании вертикальной ручки между меню и каталогом."
>
<span>Меню: мин. ширина</span>
<input type="range" data-layout-limit="sidebarMinWidth" />
<output data-layout-limit-value="sidebarMinWidth">260</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная ширина меню, когда элементы дерева в папках показываются в одну колонку."
>
<span>Меню: макс. (1 кол.)</span>
<input type="range" data-layout-limit="sidebarMaxWidth" />
<output data-layout-limit-value="sidebarMaxWidth">800</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Верхний предел ширины меню при нескольких колонках в дереве. Фактический максимум также зависит от базы и шага колонки."
>
<span>Меню: макс. (неск. кол.)</span>
<input type="range" data-layout-limit="sidebarMaxWidthWide" />
<output data-layout-limit-value="sidebarMaxWidthWide">1400</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="База формулы ширины меню при нескольких колонках: min(макс. неск. кол., база + колонки × шаг)."
>
<span>Дерево: база ширины</span>
<input type="range" data-layout-limit="treeSidebarBaseWidth" />
<output data-layout-limit-value="treeSidebarBaseWidth">420</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Сколько пикселей добавляется к базовой ширине меню за каждую колонку элементов в папках дерева."
>
<span>Дерево: ширина колонки</span>
<input type="range" data-layout-limit="treeSidebarColumnWidth" />
<output data-layout-limit-value="treeSidebarColumnWidth">120</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Минимальная высота верхней шапки в режиме «Сверху» — нижняя граница при перетаскивании горизонтальной ручки."
>
<span>Шапка: мин. высота</span>
<input type="range" data-layout-limit="topBarMinHeight" />
<output data-layout-limit-value="topBarMinHeight">80</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная высота верхней шапки в режиме «Сверху». Также ограничена примерно 75% высоты окна браузера."
>
<span>Шапка: макс. высота</span>
<input type="range" data-layout-limit="topBarMaxHeight" />
<output data-layout-limit-value="topBarMaxHeight">560</output>
</label>
</div>
</details>
<section class="settings-section" data-settings-section="catalog">
<p class="settings-section-title">Каталог</p>
<!-- Режимы каталога: обычные/компактные плитки и списки. -->
<div class="control-group view-group" data-feature="allowViewChange">
<span>Вид</span>
<button class="segmented is-active" type="button" data-view="cards">
Плитки
</button>
<button class="segmented" type="button" data-view="list">
Список
</button>
<button class="segmented" type="button" data-view="compact-cards">
Компакт плитки
</button>
<button class="segmented" type="button" data-view="compact-list">
Компакт список
</button>
</div>
<div class="catalog-columns-group" data-feature="allowCatalogColumnsChange">
<p class="settings-section-hint">Колонок в строке для каждого вида каталога.</p>
<label class="accent-custom-row">
<span>Плитки</span>
<input type="range" data-catalog-columns="catalogCardsColumns" />
<output data-catalog-columns-value="catalogCardsColumns">3</output>
</label>
<label class="accent-custom-row">
<span>Список</span>
<input type="range" data-catalog-columns="catalogListColumns" />
<output data-catalog-columns-value="catalogListColumns">1</output>
</label>
<label class="accent-custom-row">
<span>Компакт плитки</span>
<input type="range" data-catalog-columns="catalogCompactCardsColumns" />
<output data-catalog-columns-value="catalogCompactCardsColumns">5</output>
</label>
<label class="accent-custom-row">
<span>Компакт список</span>
<input type="range" data-catalog-columns="catalogCompactListColumns" />
<output data-catalog-columns-value="catalogCompactListColumns">3</output>
</label>
</div>
<!-- Показывать номера разделов, подразделов и элементов. -->
<label class="toggle-row" data-feature="allowShowNumbers">
<span>
<strong>Показывать нумерацию</strong>
<small>Разделы, подразделы и элементы</small>
</span>
<input type="checkbox" data-show-numbers />
</label>
<div class="control-group collapse-animation-group">
<span>Раскрытие списков</span>
<button class="segmented" type="button" data-collapse-animation="instant">Резко</button>
<button class="segmented is-active" type="button" data-collapse-animation="animated">Плавно</button>
</div>
<p class="settings-section-hint">Разделы каталога, дерево навигации и блоки в настройках.</p>
<label class="accent-custom-row" data-collapse-duration-row>
<span>Длительность анимации</span>
<input type="range" min="100" max="800" step="25" value="240" data-collapse-duration />
<output data-collapse-duration-value>240 ms</output>
</label>
</section>
<section class="settings-section" data-settings-section="tree">
<p class="settings-section-title">Дерево</p>
<label class="accent-custom-row">
<span>Колонок</span>
<input type="range" min="1" max="6" step="1" value="2" data-tree-columns />
<output data-tree-columns-value>2</output>
</label>
<label class="toggle-row">
<span>
<strong>Показывать теги</strong>
<small>Оформление как на карточках</small>
</span>
<input type="checkbox" data-show-tree-tags />
</label>
<label class="toggle-row">
<span>
<strong>Показывать тип</strong>
<small>Подпись типа сервиса</small>
</span>
<input type="checkbox" data-show-tree-type />
</label>
<div class="control-group">
<span>Теги и тип</span>
<button class="segmented is-active" type="button" data-tree-meta-layout="inline">
Сбоку
</button>
<button class="segmented" type="button" data-tree-meta-layout="stacked">
Снизу
</button>
</div>
<label class="toggle-row">
<span>
<strong>Авто раскрывать поиск</strong>
<small>Сворачивать ветки без совпадений</small>
</span>
<input type="checkbox" data-auto-collapse-search />
</label>
</section>
<section class="settings-section" data-settings-section="appearance">
<p class="settings-section-title">Оформление</p>
<div class="control-group" data-feature="allowThemeChange">
<span>Тема</span>
<button class="segmented is-active" type="button" data-theme="dark">
Темная
</button>
<button class="segmented" type="button" data-theme="light">
Светлая
</button>
</div>
<!-- Выбор акцентного цвета интерфейса. -->
<div class="control-group accent-group" data-feature="allowAccentChange">
<span>Акцент</span>
<button
class="accent-swatch is-active"
type="button"
data-accent="violet"
aria-label="Фиолетовый акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="cyan"
aria-label="Голубой акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="lime"
aria-label="Лаймовый акцент"
></button>
<button
class="accent-swatch"
type="button"
data-accent="amber"
aria-label="Янтарный акцент"
></button>
<button
class="accent-swatch accent-swatch-custom"
type="button"
data-accent="custom"
aria-label="Свой градиент"
title="Свой градиент"
>
<span class="accent-swatch-label">Свой</span>
</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" data-pattern-swatches="nav"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="nav" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="nav" data-panel-pattern-field="height" /><output data-panel-pattern-value="nav" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="section"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="section" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="section" data-panel-pattern-field="height" /><output data-panel-pattern-value="section" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="group"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="group" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="group" data-panel-pattern-field="height" /><output data-panel-pattern-value="group" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="item"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="item" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="item" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="item" data-panel-pattern-field="height" /><output data-panel-pattern-value="item" data-panel-pattern-field="height">45%</output></label>
<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>
<label class="accent-custom-row pattern-isolation-row" data-feature="allowPanelPatternChange">
<span>Разделение слоёв</span>
<input type="range" min="0" max="100" step="1" value="46" data-pattern-isolation />
<output data-pattern-isolation-value>46%</output>
</label>
<p class="settings-section-hint" data-feature="allowPanelPatternChange">
Подложка размывает узоры нижних уровней под разделами, подразделами и карточками.
</p>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>
<label class="accent-custom-row">
<span>Угол</span>
<input
type="range"
min="0"
max="360"
step="1"
value="135"
data-custom-accent-angle
/>
<output data-custom-accent-angle-value>135°</output>
</label>
<label class="accent-custom-row">
<span>Цвет от</span>
<input type="color" value="#8b5cf6" data-custom-accent-from />
</label>
<label class="accent-custom-row">
<span>Цвет до</span>
<input type="color" value="#22d3ee" data-custom-accent-to />
</label>
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
</div>
</section>
<div class="settings-actions">
<p class="settings-section-hint">
Сброс, экспорт и импорт настроек интерфейса на этом устройстве.
</p>
<div class="settings-actions-buttons">
<button class="settings-action-button" type="button" data-settings-reset>
Сбросить
</button>
<button class="settings-action-button" type="button" data-settings-export>
Экспортировать
</button>
<button class="settings-action-button" type="button" data-settings-import>
Импортировать
</button>
</div>
<input type="file" accept="application/json,.json" data-settings-import-input hidden />
</div>
</div>
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->
<section class="catalog cards-view" aria-live="polite" data-catalog-root></section>
<!-- Шаблон ошибки загрузки settings.json / panel data. -->
<template id="config-error-template">
<div class="empty-state config-error-state">
<img src="../shared/assets/config.svg" alt="" />
<h2>Не удалось загрузить панель</h2>
<p data-config-error-message></p>
</div>
</template>
<!-- Шаблон состояния, когда поиск не нашел ни одного элемента. -->
<template id="empty-template">
<div class="empty-state">
<img src="../shared/assets/search.svg" alt="" />
<h2>Ничего не найдено</h2>
<p>Попробуй изменить запрос или искать по названию системы, тегу или роли.</p>
</div>
</template>
</div>
</main>
<script src="../shared/script.js" data-ecosystem-panel data-asset-base="../shared/" defer></script>
</body>
</html>

View File

@ -0,0 +1,213 @@
{
"title": "Пример: section → item",
"sections": [
{
"id": "quick",
"number": "1",
"title": "Быстрый доступ",
"description": "Часто используемые сервисы без промежуточных подразделов.",
"items": [
{
"id": "social",
"number": "1.1",
"title": "Корпоративная соцсеть",
"type": "social",
"tags": ["соцсеть", "лента", "новости"],
"url": "https://social.example.com"
},
{
"id": "chat",
"number": "1.2",
"title": "Командные беседы",
"type": "chat",
"tags": ["беседа", "чат", "команда"],
"url": "https://chat.example.com"
},
{
"id": "mail",
"number": "1.3",
"title": "Почта",
"type": "messages",
"tags": ["почта", "mail"],
"url": "https://mail.example.com"
},
{
"id": "xmpp",
"number": "1.4",
"title": "Корпоративный XMPP",
"type": "xmpp",
"tags": ["xmpp", "чат"],
"url": "https://chat.example.com"
},
{
"id": "video",
"number": "1.5",
"title": "Видеоконференции",
"type": "video",
"tags": ["видео", "meet", "звонки"],
"url": "https://meet.example.com"
},
{
"id": "calendar",
"number": "1.6",
"title": "Календарь",
"type": "calendar",
"tags": ["календарь", "встречи"],
"url": "https://calendar.example.com"
},
{
"id": "tasks",
"number": "1.7",
"title": "Задачи",
"type": "redmine",
"tags": ["redmine", "задачи"],
"url": "https://tasks.example.com"
}
]
},
{
"id": "knowledge",
"number": "2",
"title": "Знания и обсуждения",
"description": "Wiki, форум и аналитика.",
"items": [
{
"id": "wiki",
"number": "2.1",
"title": "Wiki — база знаний",
"type": "wiki",
"tags": ["wiki", "документация"],
"url": "https://wiki.example.com"
},
{
"id": "forum",
"number": "2.2",
"title": "Форум сотрудников",
"type": "forum",
"tags": ["форум", "обсуждения", "q&a"],
"url": "https://forum.example.com"
},
{
"id": "analytics",
"number": "2.3",
"title": "Портал аналитики",
"type": "analytics",
"tags": ["аналитика", "отчёты"],
"url": "https://analytics.example.com"
},
{
"id": "drive",
"number": "2.4",
"title": "Общий диск",
"type": "drive",
"tags": ["файлы", "диск", "обмен"],
"url": "https://drive.example.com"
}
]
},
{
"id": "dev",
"number": "3",
"title": "Разработка",
"description": "Репозитории, CI и конфигурации.",
"items": [
{
"id": "gitea",
"number": "3.1",
"title": "Gitea",
"type": "gitea",
"tags": ["git", "repo", "gitea"],
"url": "https://gitea.example.com"
},
{
"id": "cicd",
"number": "3.2",
"title": "CI/CD — пайплайны",
"type": "cicd",
"tags": ["ci", "cd", "deploy"],
"url": "https://ci.example.com"
},
{
"id": "api",
"number": "3.3",
"title": "API Gateway",
"type": "api",
"tags": ["api", "gateway", "интеграции"],
"url": "https://api.example.com"
},
{
"id": "config",
"number": "3.4",
"title": "Конфигурации",
"type": "config",
"tags": ["config", "настройки"],
"url": "#"
},
{
"id": "xpath-debug",
"number": "3.5",
"title": "Отладка xPath",
"type": "xpath",
"tags": ["xpath", "отладка"],
"url": "#"
},
{
"id": "vault",
"number": "3.6",
"title": "Vault — секреты",
"type": "vault",
"tags": ["vault", "секреты"],
"url": "https://vault.example.com"
}
]
},
{
"id": "admin",
"number": "4",
"title": "Администрирование",
"description": "Управление пользователями, доступом и железом.",
"items": [
{
"id": "ldap",
"number": "4.1",
"title": "LDAP Account Manager",
"type": "ldap",
"tags": ["ldap", "пользователи"],
"url": "https://ldap.example.com"
},
{
"id": "vpn",
"number": "4.2",
"title": "VPN-портал",
"type": "vpn",
"tags": ["vpn", "удалёнка"],
"url": "https://vpn.example.com"
},
{
"id": "grafana",
"number": "4.3",
"title": "Grafana — мониторинг",
"type": "grafana",
"tags": ["grafana", "метрики"],
"url": "https://grafana.example.com"
},
{
"id": "server-01",
"number": "4.4",
"title": "Сервер 01 — IPMI",
"type": "ipmi",
"tags": ["ipmi", "сервер", "железо"],
"url": "#"
},
{
"id": "server-02",
"number": "4.5",
"title": "Сервер 02 — IPMI",
"type": "ipmi",
"tags": ["ipmi", "сервер"],
"url": "#"
}
]
}
]
}

View File

@ -0,0 +1,97 @@
{
"dataUrl": "./panel-data.json",
"data": null,
"defaults": {
"navLayout": "sidebar",
"showTree": true,
"showResizeHandles": true,
"theme": "dark",
"view": "cards",
"accent": "violet",
"customAccentFrom": "#8b5cf6",
"customAccentTo": "#22d3ee",
"customAccentAngle": 135,
"panelPatternNav": "hex",
"panelPatternNavOpacity": 58,
"panelPatternNavHeight": 38,
"panelPatternNavFade": 18,
"panelPatternNavGradientOpacity": 62,
"panelPatternNavGradientHeight": 34,
"panelPatternNavGradientFade": 16,
"panelPatternSection": "hex",
"panelPatternSectionOpacity": 58,
"panelPatternSectionHeight": 38,
"panelPatternSectionFade": 18,
"panelPatternSectionGradientOpacity": 62,
"panelPatternSectionGradientHeight": 34,
"panelPatternSectionGradientFade": 16,
"panelPatternGroup": "none",
"panelPatternGroupOpacity": 58,
"panelPatternGroupHeight": 38,
"panelPatternGroupFade": 18,
"panelPatternGroupGradientOpacity": 62,
"panelPatternGroupGradientHeight": 34,
"panelPatternGroupGradientFade": 16,
"panelPatternItem": "none",
"panelPatternItemOpacity": 58,
"panelPatternItemHeight": 38,
"panelPatternItemFade": 18,
"panelPatternItemGradientOpacity": 62,
"panelPatternItemGradientHeight": 34,
"panelPatternItemGradientFade": 16,
"patternIsolation": 46,
"showNumbers": false,
"showTreeTags": true,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 1,
"autoCollapseSearch": true,
"sidebarWidth": 360,
"topBarHeight": 140,
"shellMaxWidth": 2240,
"sidebarMinWidth": 240,
"sidebarMaxWidth": 720,
"sidebarMaxWidthWide": 1040,
"treeSidebarBaseWidth": 360,
"treeSidebarColumnWidth": 96,
"topBarMinHeight": 80,
"topBarMaxHeight": 560,
"catalogCardsColumns": 3,
"catalogListColumns": 1,
"catalogCompactCardsColumns": 4,
"catalogCompactListColumns": 2,
"collapseAnimation": "animated",
"collapseDuration": 260,
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
"settingsPanelWidth": 440
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
"sidebarMinWidth": { "min": 220, "max": 480 },
"sidebarMaxWidth": { "min": 400, "max": 1200 },
"sidebarMaxWidthWide": { "min": 560, "max": 2000 },
"treeSidebarBaseWidth": { "min": 280, "max": 720 },
"treeSidebarColumnWidth": { "min": 60, "max": 200 },
"topBarMinHeight": { "min": 64, "max": 240 },
"topBarMaxHeight": { "min": 160, "max": 800 },
"catalogCardsColumns": { "min": 1, "max": 6 },
"catalogListColumns": { "min": 1, "max": 4 },
"catalogCompactCardsColumns": { "min": 2, "max": 8 },
"catalogCompactListColumns": { "min": 1, "max": 6 },
"settingsPanelWidth": { "min": 320, "max": 720 }
},
"features": {
"allowNavLayoutChange": true,
"allowShowTreeChange": true,
"allowSearch": true,
"allowViewChange": true,
"allowCatalogColumnsChange": true,
"allowShowNumbers": true,
"allowTreeOptions": true,
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true,
"allowPanelPatternChange": true
}
}

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-analytics)"/><rect x="40" y="92" width="18" height="32" rx="6" fill="white"/><rect x="71" y="72" width="18" height="52" rx="6" fill="white" fill-opacity=".9"/><rect x="102" y="52" width="18" height="72" rx="6" fill="white" fill-opacity=".78"/><path d="M44 60l28 18 28-28 16 12" stroke="white" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><defs><linearGradient id="g-analytics" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#6EE7B7"/><stop offset="1" stop-color="#059669"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 680 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-api)"/><path d="M52 52h-8v56h8M108 52h8v56h-8M68 80h24" stroke="white" stroke-width="10" stroke-linecap="round"/><circle cx="80" cy="80" r="28" stroke="white" stroke-width="8" fill="none"/><defs><linearGradient id="g-api" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FDE68A"/><stop offset="1" stop-color="#D97706"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 505 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-calendar)"/><rect x="36" y="48" width="88" height="76" rx="14" fill="white"/><path d="M52 40v16M108 40v16" stroke="white" stroke-width="8" stroke-linecap="round"/><path d="M36 68h88" stroke="#4F46E5" stroke-width="6"/><rect x="50" y="82" width="14" height="14" rx="4" fill="#4F46E5"/><rect x="73" y="82" width="14" height="14" rx="4" fill="#4F46E5" fill-opacity=".7"/><rect x="96" y="82" width="14" height="14" rx="4" fill="#4F46E5" fill-opacity=".45"/><rect x="50" y="102" width="14" height="14" rx="4" fill="#4F46E5" fill-opacity=".7"/><rect x="73" y="102" width="14" height="14" rx="4" fill="#4F46E5"/><defs><linearGradient id="g-calendar" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#A5B4FC"/><stop offset="1" stop-color="#4F46E5"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 926 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><circle cx="80" cy="80" r="40" fill="white" fill-opacity=".16"/><circle cx="80" cy="80" r="16" fill="white"/><circle cx="45" cy="65" r="15" fill="white"/><circle cx="115" cy="65" r="15" fill="white"/><circle cx="62" cy="112" r="15" fill="white"/><circle cx="98" cy="112" r="15" fill="white"/><path d="M57 71l16 7M103 71l-16 7M70 104l7-13M90 91l7 13" stroke="#BE123C" stroke-width="6" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FB7185"/><stop offset="1" stop-color="#BE123C"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 723 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-chat)"/><path d="M34 52h52c6 0 11 5 11 11v28c0 6-5 11-11 11H62L42 118V91H34c-6 0-11-5-11-11V63c0-6 5-11 11-11z" fill="white"/><path d="M74 58h52c6 0 11 5 11 11v28c0 6-5 11-11 11h-8l-20 27V98H74c-6 0-11-5-11-11V69c0-6 5-11 11-11z" fill="white" fill-opacity=".78"/><circle cx="52" cy="74" r="4" fill="#7C3AED"/><circle cx="68" cy="74" r="4" fill="#7C3AED"/><circle cx="84" cy="74" r="4" fill="#7C3AED"/><defs><linearGradient id="g-chat" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#C4B5FD"/><stop offset="1" stop-color="#7C3AED"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 718 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-cicd)"/><path d="M80 44a36 36 0 1 1-25 59" stroke="white" stroke-width="10" stroke-linecap="round" fill="none"/><path d="M55 44h25v25" stroke="white" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/><circle cx="80" cy="80" r="10" fill="#1D4ED8"/><path d="M96 96l18 18" stroke="white" stroke-width="8" stroke-linecap="round"/><defs><linearGradient id="g-cicd" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#93C5FD"/><stop offset="1" stop-color="#1D4ED8"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 659 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M34 88c0-18 14-32 32-34 4-18 20-30 38-30 22 0 40 16 40 38 0 2 0 4-.2 6 14 4 24 16 24 32 0 18-14 32-32 32H52c-10 0-18-8-18-18v-26z" fill="white" fill-opacity=".95"/><ellipse cx="80" cy="92" rx="34" ry="14" fill="white"/><path d="M46 92v24c0 8 15 14 34 14s34-6 34-14V92" fill="white" fill-opacity=".88"/><path d="M46 104c0 8 15 14 34 14s34-6 34-14" stroke="#1565C0" stroke-width="6"/><circle cx="66" cy="72" r="8" fill="#1565C0"/><circle cx="94" cy="72" r="8" fill="#1565C0"/><path d="M72 86h16" stroke="#1565C0" stroke-width="6" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#64B5F6"/><stop offset="1" stop-color="#1565C0"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 876 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><circle cx="80" cy="80" r="34" fill="white"/><path d="M80 44v18M80 98v18M44 80h18M98 80h18M55 55l13 13M92 92l13 13M105 55L92 68M68 92l-13 13" stroke="#4338CA" stroke-width="8" stroke-linecap="round"/><circle cx="80" cy="80" r="12" fill="#4338CA"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#818CF8"/><stop offset="1" stop-color="#4338CA"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 562 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><ellipse cx="80" cy="50" rx="42" ry="18" fill="white"/><path d="M38 50v60c0 10 19 18 42 18s42-8 42-18V50" fill="white" fill-opacity=".86"/><path d="M38 78c0 10 19 18 42 18s42-8 42-18M38 104c0 10 19 18 42 18s42-8 42-18" stroke="#0F766E" stroke-width="7"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#2DD4BF"/><stop offset="1" stop-color="#0F766E"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 569 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-drive)"/><path d="M44 98l18-36h36l18 36H44z" fill="white"/><path d="M62 62l14-18h18l14 18" stroke="white" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><rect x="48" y="104" width="64" height="14" rx="7" fill="white" fill-opacity=".85"/><defs><linearGradient id="g-drive" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#93C5FD"/><stop offset="1" stop-color="#2563EB"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 573 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-forum)"/><rect x="36" y="44" width="88" height="18" rx="9" fill="white"/><rect x="36" y="72" width="72" height="18" rx="9" fill="white" fill-opacity=".88"/><rect x="36" y="100" width="80" height="18" rx="9" fill="white" fill-opacity=".76"/><circle cx="124" cy="53" r="8" fill="#EA580C"/><circle cx="116" cy="81" r="8" fill="#EA580C" fill-opacity=".85"/><circle cx="122" cy="109" r="8" fill="#EA580C" fill-opacity=".7"/><defs><linearGradient id="g-forum" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FDBA74"/><stop offset="1" stop-color="#EA580C"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 737 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M49 60h53c16 0 28 12 28 28s-12 28-28 28H73c-22 0-40-18-40-40 0-9 7-16 16-16z" fill="white"/><circle cx="67" cy="86" r="7" fill="#4D7C0F"/><circle cx="101" cy="86" r="7" fill="#4D7C0F"/><path d="M75 102c7 5 16 5 24 0" stroke="#4D7C0F" stroke-width="6" stroke-linecap="round"/><path d="M103 57l13-13M113 45h14v14" stroke="#FEF3C7" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#A3E635"/><stop offset="1" stop-color="#65A30D"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 718 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M82 124c-27 0-49-18-49-43 0-22 18-40 41-40 18 0 33 12 33 29 0 14-11 25-25 25-11 0-20-8-20-18 0-8 7-15 16-15 6 0 11 4 11 10" stroke="white" stroke-width="10" stroke-linecap="round"/><circle cx="116" cy="43" r="13" fill="#FDE68A"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#F97316"/><stop offset="1" stop-color="#7C2D12"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 553 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M80 30l45 26v52l-45 26-45-26V56l45-26z" fill="white" fill-opacity=".18" stroke="white" stroke-width="8" stroke-linejoin="round"/><circle cx="80" cy="82" r="22" fill="white"/><path d="M80 48v68M46 82h68M56 58l48 48M104 58l-48 48" stroke="#2563EB" stroke-width="7" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#60A5FA"/><stop offset="1" stop-color="#1D4ED8"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 611 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><circle cx="80" cy="56" r="22" fill="white"/><path d="M42 122c6-28 20-42 38-42s32 14 38 42" fill="white"/><circle cx="43" cy="80" r="13" fill="white" fill-opacity=".82"/><circle cx="117" cy="80" r="13" fill="white" fill-opacity=".82"/><path d="M56 86h48" stroke="#047857" stroke-width="8" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#6EE7B7"/><stop offset="1" stop-color="#047857"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 628 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M36 48h88c8 0 14 6 14 14v38c0 8-6 14-14 14H78l-28 18v-18H36c-8 0-14-6-14-14V62c0-8 6-14 14-14z" fill="white"/><path d="M50 72h60M50 92h42" stroke="#0891B2" stroke-width="8" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#67E8F9"/><stop offset="1" stop-color="#0891B2"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 521 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><circle cx="70" cy="70" r="30" stroke="white" stroke-width="12"/><path d="M93 93l27 27" stroke="white" stroke-width="13" stroke-linecap="round"/><path d="M49 69h42M58 53h24M58 85h24" stroke="#0369A1" stroke-width="7" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#38BDF8"/><stop offset="1" stop-color="#0369A1"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 556 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M46 98c0-30 15-52 34-52s34 22 34 52c0 14-9 26-21 30v16H67v-16c-12-4-21-16-21-30z" fill="white"/><circle cx="69" cy="78" r="6" fill="#336791"/><circle cx="91" cy="78" r="6" fill="#336791"/><path d="M46 98c-10 10-16 24-16 38h18c2-14 9-26 18-32" fill="white" fill-opacity=".92"/><path d="M72 92c4 3 8 3 12 0" stroke="#336791" stroke-width="5" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#5B9BD5"/><stop offset="1" stop-color="#336791"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 688 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M80 34c22 24 34 44 34 62 0 20-15 34-34 34s-34-14-34-34c0-18 12-38 34-62z" fill="white"/><path d="M66 82c12 8 16 8 28 0M65 103h30M80 54v32" stroke="#DC2626" stroke-width="8" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FCA5A5"/><stop offset="1" stop-color="#DC2626"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 521 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M42 112V48c0-8 6-14 14-14s14 6 14 14v28h20V48c0-8 6-14 14-14s14 6 14 14v64H42z" fill="white"/><rect x="64" y="88" width="18" height="18" rx="4" fill="#EA580C"/><rect x="94" y="88" width="18" height="18" rx="4" fill="#EA580C"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FDBA74"/><stop offset="1" stop-color="#EA580C"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 550 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><rect x="38" y="38" width="84" height="84" rx="18" fill="white"/><path d="M58 66h44M58 86h30M58 106h44" stroke="#B91C1C" stroke-width="8" stroke-linecap="round"/><circle cx="104" cy="86" r="10" fill="#FCA5A5"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#F87171"/><stop offset="1" stop-color="#B91C1C"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 525 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><rect x="40" y="34" width="80" height="92" rx="16" fill="white"/><path d="M60 62h42M60 82h42M60 102h24" stroke="#6D28D9" stroke-width="8" stroke-linecap="round"/><path d="M47 62l6 6 12-15M47 102l6 6 12-15" stroke="#22C55E" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#C4B5FD"/><stop offset="1" stop-color="#6D28D9"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 603 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><circle cx="70" cy="70" r="30" stroke="white" stroke-width="12"/><path d="M94 94l28 28" stroke="white" stroke-width="13" stroke-linecap="round"/><path d="M60 70h20M70 60v20" stroke="#6D28D9" stroke-width="7" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#C4B5FD"/><stop offset="1" stop-color="#6D28D9"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 547 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><rect x="42" y="32" width="76" height="96" rx="14" fill="white"/><path d="M56 55h48M56 80h48M56 105h48" stroke="#334155" stroke-width="8" stroke-linecap="round"/><circle cx="103" cy="55" r="5" fill="#22C55E"/><circle cx="103" cy="80" r="5" fill="#FACC15"/><circle cx="103" cy="105" r="5" fill="#EF4444"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#94A3B8"/><stop offset="1" stop-color="#334155"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 619 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-social)"/><circle cx="56" cy="68" r="16" fill="white"/><circle cx="104" cy="68" r="16" fill="white"/><circle cx="80" cy="108" r="18" fill="white" fill-opacity=".92"/><path d="M68 118c6 8 18 12 28 12s22-4 28-12" stroke="white" stroke-width="6" stroke-linecap="round"/><defs><linearGradient id="g-social" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#F9A8D4"/><stop offset="1" stop-color="#DB2777"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-vault)"/><rect x="38" y="48" width="84" height="72" rx="16" fill="white"/><circle cx="80" cy="84" r="18" stroke="#6D28D9" stroke-width="8" fill="none"/><circle cx="80" cy="84" r="6" fill="#6D28D9"/><rect x="74" y="84" width="12" height="18" rx="2" fill="#6D28D9"/><defs><linearGradient id="g-vault" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#DDD6FE"/><stop offset="1" stop-color="#6D28D9"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 582 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-video)"/><rect x="34" y="50" width="92" height="60" rx="14" fill="white"/><path d="M88 68l28-16v56l-28-16V68z" fill="#DC2626"/><circle cx="58" cy="118" r="10" fill="white"/><circle cx="102" cy="118" r="10" fill="white" fill-opacity=".85"/><defs><linearGradient id="g-video" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FCA5A5"/><stop offset="1" stop-color="#DC2626"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 557 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-vpn)"/><path d="M80 38l44 24v36c0 24-20 44-44 44S36 122 36 98V62l44-24z" fill="white"/><path d="M72 92c0-4 4-8 8-8s8 4 8 8v8H72v-8z" fill="#0F766E"/><rect x="68" y="76" width="24" height="18" rx="8" stroke="#0F766E" stroke-width="6"/><defs><linearGradient id="g-vpn" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#5EEAD4"/><stop offset="1" stop-color="#0F766E"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 550 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g-wiki)"/><path d="M48 44h64l8 92H40l8-92z" fill="white"/><path d="M56 56h48M56 76h40M56 96h44M56 116h32" stroke="#0369A1" stroke-width="6" stroke-linecap="round"/><path d="M80 44v16" stroke="white" stroke-width="8" stroke-linecap="round"/><defs><linearGradient id="g-wiki" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#7DD3FC"/><stop offset="1" stop-color="#0369A1"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 555 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M46 44c10 38 21 61 34 82 13-21 24-44 34-82" stroke="white" stroke-width="12" stroke-linecap="round"/><path d="M46 44c24 29 44 45 68 60M114 44c-24 29-44 45-68 60" stroke="white" stroke-opacity=".62" stroke-width="8" stroke-linecap="round"/><circle cx="80" cy="84" r="10" fill="#15803D"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#86EFAC"/><stop offset="1" stop-color="#15803D"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 610 B

View File

@ -0,0 +1 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="44" fill="url(#g)"/><path d="M62 50L34 80l28 30M98 50l28 30-28 30" stroke="white" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/><path d="M87 42L69 118" stroke="#FDE68A" stroke-width="10" stroke-linecap="round"/><defs><linearGradient id="g" x1="18" y1="14" x2="140" y2="150"><stop stop-color="#FBBF24"/><stop offset="1" stop-color="#B45309"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 526 B

2916
examples/shared/script.js Normal file

File diff suppressed because it is too large Load Diff

2538
examples/shared/styles.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,19 @@
<!doctype html>
<!--
ecosystem-panel / index.html
ОГЛАВЛЕНИЕ РАЗМЕТКИ:
- .ambient-* — декоративный фон
- .side-panel — навигация: заголовок, поиск, дерево (.hero-content)
- .sidebar-resizer — drag ширины меню
- .top-bar-resizer — drag высоты шапки (режим «Сверху»)
- .control-panel — popup настроек ⚙ (секции data-settings-section)
- .catalog — каталог карточек (рендер в script.js)
- script.js — data-ecosystem-panel, settings.json по умолчанию
Настройки ⚙ (сверху вниз): layout → dimensions → catalog → tree → appearance → actions.
Узоры: data-pattern-targets-panel, контейнеры data-pattern-swatches="{nav|section|group|item}".
-->
<html lang="ru">
<head>
<meta charset="UTF-8" />
@ -45,7 +60,7 @@
<p class="eyebrow" id="page-title">Панель экосистемы</p>
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
<label class="search-field">
<label class="search-field" data-search-field>
<input
type="search"
placeholder="Grafana, S3, журнал, сервер..."
@ -57,6 +72,16 @@
<!-- Дерево генерируется из ecosystemSections в script.js. -->
<nav class="nav-tree" aria-label="Дерево навигации" data-nav-tree></nav>
</div>
<!-- Горизонтальная ручка высоты верхней панели (режим «Сверху»). -->
<div
class="top-bar-resizer"
role="separator"
aria-orientation="horizontal"
aria-label="Изменить высоту верхней панели"
data-top-bar-resizer
hidden
></div>
</aside>
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
@ -72,38 +97,151 @@
<div class="settings-overlay" data-settings-overlay></div>
<!--
Popup настроек:
- тема;
- режим отображения карточек;
- видимость нумерации;
- видимость тегов в дереве;
- авто-раскрытие дерева при поиске;
- цветовой акцент.
Popup настроек (⚙). Секции data-settings-section:
- layout — макет, дерево, ручки resize, ширина popup
- dimensions — лимиты shell/sidebar/topBar (collapsible)
- catalog — вид карточек, колонки, нумерация, анимация раскрытия
- tree — колонки дерева, теги, тип, авто-раскрытие поиска
- appearance — тема, акцент, узоры, разделение слоёв
- (footer) — settings-actions: сброс / экспорт / импорт preferences
Видимость блоков: features в settings.json + data-feature на элементах.
-->
<div class="control-panel" aria-label="Настройки панели" data-settings-panel>
<div class="settings-panel-head">
<div>
<p class="eyebrow">View settings</p>
<h2>Настройки</h2>
</div>
<p class="eyebrow settings-panel-eyebrow">Настройки</p>
<button class="settings-close" type="button" aria-label="Закрыть настройки" data-settings-close>
×
</button>
</div>
<!-- Переключение светлой/темной темы. Значение сохраняется в localStorage. -->
<section class="settings-section" data-settings-section="layout">
<p class="settings-section-title">Макет</p>
<div class="control-group">
<span>Тема</span>
<button class="segmented is-active" type="button" data-theme="dark">
Темная
<span>Панель навигации</span>
<button class="segmented is-active" type="button" data-nav-layout="sidebar">
Сбоку
</button>
<button class="segmented" type="button" data-theme="light">
Светлая
<button class="segmented" type="button" data-nav-layout="top">
Сверху
</button>
</div>
<p class="settings-section-hint">
«Сверху» — компактная шапка. Дерево включается отдельно переключателем ниже.
</p>
<label class="toggle-row" data-feature="allowShowTreeChange">
<span>
<strong>Показывать дерево</strong>
<small>В режимах «Сбоку» и «Сверху»</small>
</span>
<input type="checkbox" data-show-tree checked />
</label>
<label class="toggle-row" data-feature="allowSidebarResize">
<span>
<strong>Ручки изменения размера</strong>
<small>Ширина меню или высота шапки</small>
</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">
<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">
Ограничения ширины страницы, бокового меню и верхней шапки. Значения применяются сразу;
текущий размер меню и шапки можно также менять перетаскиванием ручек на странице.
Допустимые диапазоны ползунков задаёт администратор в <code>settings.json</code> →
<code>limits</code>.
</p>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная ширина всей панели (.shell): насколько широко вместе могут располагаться меню и каталог."
>
<span>Ширина контента (max)</span>
<input type="range" data-layout-limit="shellMaxWidth" />
<output data-layout-limit-value="shellMaxWidth">2800</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Минимальная ширина бокового меню при перетаскивании вертикальной ручки между меню и каталогом."
>
<span>Меню: мин. ширина</span>
<input type="range" data-layout-limit="sidebarMinWidth" />
<output data-layout-limit-value="sidebarMinWidth">260</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная ширина меню, когда элементы дерева в папках показываются в одну колонку."
>
<span>Меню: макс. (1 кол.)</span>
<input type="range" data-layout-limit="sidebarMaxWidth" />
<output data-layout-limit-value="sidebarMaxWidth">800</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Верхний предел ширины меню при нескольких колонках в дереве. Фактический максимум также зависит от базы и шага колонки."
>
<span>Меню: макс. (неск. кол.)</span>
<input type="range" data-layout-limit="sidebarMaxWidthWide" />
<output data-layout-limit-value="sidebarMaxWidthWide">1400</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="База формулы ширины меню при нескольких колонках: min(макс. неск. кол., база + колонки × шаг)."
>
<span>Дерево: база ширины</span>
<input type="range" data-layout-limit="treeSidebarBaseWidth" />
<output data-layout-limit-value="treeSidebarBaseWidth">420</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Сколько пикселей добавляется к базовой ширине меню за каждую колонку элементов в папках дерева."
>
<span>Дерево: ширина колонки</span>
<input type="range" data-layout-limit="treeSidebarColumnWidth" />
<output data-layout-limit-value="treeSidebarColumnWidth">120</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Минимальная высота верхней шапки в режиме «Сверху» — нижняя граница при перетаскивании горизонтальной ручки."
>
<span>Шапка: мин. высота</span>
<input type="range" data-layout-limit="topBarMinHeight" />
<output data-layout-limit-value="topBarMinHeight">80</output>
</label>
<label
class="accent-custom-row layout-limit-row"
data-tooltip="Максимальная высота верхней шапки в режиме «Сверху». Также ограничена примерно 75% высоты окна браузера."
>
<span>Шапка: макс. высота</span>
<input type="range" data-layout-limit="topBarMaxHeight" />
<output data-layout-limit-value="topBarMaxHeight">560</output>
</label>
</div>
</details>
<section class="settings-section" data-settings-section="catalog">
<p class="settings-section-title">Каталог</p>
<!-- Режимы каталога: обычные/компактные плитки и списки. -->
<div class="control-group view-group">
<div class="control-group view-group" data-feature="allowViewChange">
<span>Вид</span>
<button class="segmented is-active" type="button" data-view="cards">
Плитки
@ -119,8 +257,36 @@
</button>
</div>
<div class="catalog-columns-group" data-feature="allowCatalogColumnsChange">
<p class="settings-section-hint">Колонок в строке для каждого вида каталога.</p>
<label class="accent-custom-row">
<span>Плитки</span>
<input type="range" data-catalog-columns="catalogCardsColumns" />
<output data-catalog-columns-value="catalogCardsColumns">3</output>
</label>
<label class="accent-custom-row">
<span>Список</span>
<input type="range" data-catalog-columns="catalogListColumns" />
<output data-catalog-columns-value="catalogListColumns">1</output>
</label>
<label class="accent-custom-row">
<span>Компакт плитки</span>
<input type="range" data-catalog-columns="catalogCompactCardsColumns" />
<output data-catalog-columns-value="catalogCompactCardsColumns">5</output>
</label>
<label class="accent-custom-row">
<span>Компакт список</span>
<input type="range" data-catalog-columns="catalogCompactListColumns" />
<output data-catalog-columns-value="catalogCompactListColumns">3</output>
</label>
</div>
<!-- Показывать номера разделов, подразделов и элементов. -->
<label class="toggle-row">
<label class="toggle-row" data-feature="allowShowNumbers">
<span>
<strong>Показывать нумерацию</strong>
<small>Разделы, подразделы и элементы</small>
@ -128,25 +294,54 @@
<input type="checkbox" data-show-numbers />
</label>
<!-- Показывать поисковые теги рядом с файлами в дереве навигации. -->
<div class="control-group collapse-animation-group">
<span>Раскрытие списков</span>
<button class="segmented" type="button" data-collapse-animation="instant">Резко</button>
<button class="segmented is-active" type="button" data-collapse-animation="animated">Плавно</button>
</div>
<p class="settings-section-hint">Разделы каталога, дерево навигации и блоки в настройках.</p>
<label class="accent-custom-row" data-collapse-duration-row>
<span>Длительность анимации</span>
<input type="range" min="100" max="800" step="25" value="240" data-collapse-duration />
<output data-collapse-duration-value>240 ms</output>
</label>
</section>
<section class="settings-section" data-settings-section="tree">
<p class="settings-section-title">Дерево</p>
<label class="accent-custom-row">
<span>Колонок</span>
<input type="range" min="1" max="6" step="1" value="2" data-tree-columns />
<output data-tree-columns-value>2</output>
</label>
<label class="toggle-row">
<span>
<strong>Показывать теги дерева</strong>
<small>Теги рядом с элементом, без 2 колонок</small>
<strong>Показывать теги</strong>
<small>Оформление как на карточках</small>
</span>
<input type="checkbox" data-show-tree-tags />
</label>
<!-- Элементы внутри папок дерева отображаются в две колонки без тегов. -->
<label class="toggle-row">
<span>
<strong>2 колонки в дереве</strong>
<small>Компактный вид элементов папок</small>
<strong>Показывать тип</strong>
<small>Подпись типа сервиса</small>
</span>
<input type="checkbox" data-tree-two-column />
<input type="checkbox" data-show-tree-type />
</label>
<!-- Управляет тем, раскрывает ли поиск только ветки с совпадениями. -->
<div class="control-group">
<span>Теги и тип</span>
<button class="segmented is-active" type="button" data-tree-meta-layout="inline">
Сбоку
</button>
<button class="segmented" type="button" data-tree-meta-layout="stacked">
Снизу
</button>
</div>
<label class="toggle-row">
<span>
<strong>Авто раскрывать поиск</strong>
@ -154,9 +349,24 @@
</span>
<input type="checkbox" data-auto-collapse-search />
</label>
</section>
<!-- === Оформление: тема, акцент, узоры, разделение слоёв === -->
<section class="settings-section" data-settings-section="appearance">
<p class="settings-section-title">Оформление</p>
<div class="control-group" data-feature="allowThemeChange">
<span>Тема</span>
<button class="segmented is-active" type="button" data-theme="dark">
Темная
</button>
<button class="segmented" type="button" data-theme="light">
Светлая
</button>
</div>
<!-- Выбор акцентного цвета интерфейса. -->
<div class="control-group accent-group">
<div class="control-group accent-group" data-feature="allowAccentChange">
<span>Акцент</span>
<button
class="accent-swatch is-active"
@ -193,8 +403,99 @@
</button>
</div>
<!--
Узоры по типам блоков. Кнопки пресетов — пустые data-pattern-swatches;
script.js → renderPatternSwatches(). Панель «Свой» — ensurePatternCustomPanels().
Слайдеры data-panel-pattern-tuning синхронизируются в syncPatternTargetPanels().
-->
<div class="pattern-targets-panel" data-pattern-targets-panel data-feature="allowPanelPatternChange">
<p class="settings-section-hint">Узор и градиент настраиваются отдельно для каждого типа блоков.</p>
<details class="settings-section-collapsible pattern-target-panel" data-pattern-target="nav">
<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" data-pattern-swatches="nav"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="nav" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="nav" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="nav" data-panel-pattern-field="height" /><output data-panel-pattern-value="nav" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="section"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="section" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="section" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="section" data-panel-pattern-field="height" /><output data-panel-pattern-value="section" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="group"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="group" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="group" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="group" data-panel-pattern-field="height" /><output data-panel-pattern-value="group" data-panel-pattern-field="height">45%</output></label>
<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" data-pattern-swatches="item"></div>
<div class="pattern-tuning">
<label class="accent-custom-row"><span>Узор: прозрачность</span><input type="range" min="0" max="100" step="1" value="70" data-panel-pattern-tuning="item" data-panel-pattern-field="opacity" /><output data-panel-pattern-value="item" data-panel-pattern-field="opacity">70%</output></label>
<label class="accent-custom-row"><span>Узор: высота</span><input type="range" min="0" max="100" step="1" value="45" data-panel-pattern-tuning="item" data-panel-pattern-field="height" /><output data-panel-pattern-value="item" data-panel-pattern-field="height">45%</output></label>
<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>
<!-- patternIsolation → --panel-layer-* (размывает узоры нижних уровней) -->
<label class="accent-custom-row pattern-isolation-row" data-feature="allowPanelPatternChange">
<span>Разделение слоёв</span>
<input type="range" min="0" max="100" step="1" value="46" data-pattern-isolation />
<output data-pattern-isolation-value>46%</output>
</label>
<p class="settings-section-hint" data-feature="allowPanelPatternChange">
Подложка размывает узоры нижних уровней под разделами, подразделами и карточками.
</p>
<!-- Настройки пользовательского linear-gradient акцента. -->
<div class="accent-custom-panel" data-accent-custom-panel hidden>
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
<p class="accent-custom-title">Свой градиент</p>
<label class="accent-custom-row">
@ -222,11 +523,40 @@
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
</div>
</section>
<!-- Экспорт/импорт/сброс: ecosystem-panel-preferences JSON (script.js) -->
<div class="settings-actions">
<p class="settings-section-hint">
Сброс, экспорт и импорт настроек интерфейса на этом устройстве.
</p>
<div class="settings-actions-buttons">
<button class="settings-action-button" type="button" data-settings-reset>
Сбросить
</button>
<button class="settings-action-button" type="button" data-settings-export>
Экспортировать
</button>
<button class="settings-action-button" type="button" data-settings-import>
Импортировать
</button>
</div>
<input type="file" accept="application/json,.json" data-settings-import-input hidden />
</div>
</div>
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->
<section class="catalog cards-view" aria-live="polite" data-catalog-root></section>
<!-- Шаблон ошибки загрузки settings.json / panel data. -->
<template id="config-error-template">
<div class="empty-state config-error-state">
<img src="assets/config.svg" alt="" />
<h2>Не удалось загрузить панель</h2>
<p data-config-error-message></p>
</div>
</template>
<!-- Шаблон состояния, когда поиск не нашел ни одного элемента. -->
<template id="empty-template">
<div class="empty-state">
@ -238,6 +568,6 @@
</div>
</main>
<script src="script.js"></script>
<script src="script.js" data-ecosystem-panel defer></script>
</body>
</html>

View File

@ -0,0 +1,19 @@
{
"title": "Заголовок панели",
"items": [
{
"id": "item-a",
"title": "Элемент A",
"type": "grafana",
"tags": ["тег"],
"url": "https://example.com/a"
},
{
"id": "item-b",
"title": "Элемент B",
"type": "gitea",
"tags": ["тег"],
"url": "https://example.com/b"
}
]
}

View File

@ -0,0 +1,29 @@
{
"title": "Заголовок панели",
"sections": [
{
"id": "section-a",
"number": "1",
"title": "Раздел",
"description": "Описание раздела",
"groups": [
{
"id": "group-a",
"number": "1.1",
"title": "Подраздел",
"description": "Описание подраздела",
"items": [
{
"id": "item-a",
"number": "1.1.1",
"title": "Элемент",
"type": "grafana",
"tags": ["тег"],
"url": "https://example.com"
}
]
}
]
}
]
}

View File

@ -0,0 +1,33 @@
{
"title": "Заголовок панели",
"sections": [
{
"id": "section-a",
"title": "Раздел",
"items": [
{
"id": "item-direct",
"title": "Прямой элемент",
"type": "config",
"tags": ["быстрый доступ"],
"url": "https://example.com/direct"
}
],
"groups": [
{
"id": "group-a",
"title": "Подраздел",
"items": [
{
"id": "item-nested",
"title": "Вложенный элемент",
"type": "kubernetes",
"tags": ["кластер"],
"url": "https://example.com/nested"
}
]
}
]
}
]
}

View File

@ -0,0 +1,26 @@
{
"title": "Заголовок панели",
"sections": [
{
"id": "section-a",
"title": "Раздел",
"description": "Описание раздела",
"items": [
{
"id": "item-a",
"title": "Элемент A",
"type": "prometheus",
"tags": ["тег"],
"url": "https://example.com/a"
},
{
"id": "item-b",
"title": "Элемент B",
"type": "postgresql",
"tags": ["тег"],
"url": "https://example.com/b"
}
]
}
]
}

7
panel-data.json Normal file
View File

@ -0,0 +1,7 @@
{
"title": "Панель экосистемы",
"sections": [
],
"serviceMeta": {}
}

2353
script.js

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,55 @@
{
"dataUrl": "./panel-data.json",
"data": null,
"defaults": {
"navLayout": "top",
"showTree": false,
"theme": "dark",
"view": "cards",
"accent": "cyan",
"patternIsolation": 46,
"showNumbers": false,
"showTreeTags": true,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 1,
"autoCollapseSearch": true,
"sidebarWidth": 360,
"topBarHeight": 160,
"shellMaxWidth": 2240,
"sidebarMinWidth": 240,
"sidebarMaxWidth": 720,
"sidebarMaxWidthWide": 1040,
"treeSidebarBaseWidth": 360,
"treeSidebarColumnWidth": 96,
"topBarMinHeight": 80,
"topBarMaxHeight": 560,
"collapseAnimation": "animated",
"collapseDuration": 260,
"searchPlaceholder": "Поиск сервиса...",
"settingsPanelWidth": 440
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
"sidebarMinWidth": { "min": 220, "max": 480 },
"sidebarMaxWidth": { "min": 400, "max": 1200 },
"sidebarMaxWidthWide": { "min": 560, "max": 2000 },
"treeSidebarBaseWidth": { "min": 280, "max": 720 },
"treeSidebarColumnWidth": { "min": 60, "max": 200 },
"topBarMinHeight": { "min": 64, "max": 240 },
"topBarMaxHeight": { "min": 160, "max": 800 },
"settingsPanelWidth": { "min": 320, "max": 720 }
},
"features": {
"allowNavLayoutChange": true,
"allowShowTreeChange": true,
"allowSearch": true,
"allowViewChange": true,
"allowShowNumbers": false,
"allowTreeOptions": true,
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true
}
}

105
settings.json Normal file
View File

@ -0,0 +1,105 @@
{
"dataUrl": "./panel-data.json",
"data": null,
"defaults": {
"navLayout": "top",
"showTree": false,
"showResizeHandles": false,
"theme": "dark",
"view": "cards",
"accent": "custom",
"customAccentFrom": "#ff428e",
"customAccentTo": "#5bd8ec",
"customAccentAngle": 135,
"panelPatternNav": "diagonal",
"panelPatternNavOpacity": 20,
"panelPatternNavHeight": 100,
"panelPatternNavFade": 100,
"panelPatternNavGradientOpacity": 100,
"panelPatternNavGradientHeight": 100,
"panelPatternNavGradientFade": 100,
"panelPatternNavCustomImage": "",
"panelPatternNavCustomSize": "32px 32px",
"panelPatternSection": "diamond",
"panelPatternSectionOpacity": 30,
"panelPatternSectionHeight": 50,
"panelPatternSectionFade": 60,
"panelPatternSectionGradientOpacity": 75,
"panelPatternSectionGradientHeight": 20,
"panelPatternSectionGradientFade": 15,
"panelPatternSectionCustomImage": "",
"panelPatternSectionCustomSize": "32px 32px",
"panelPatternGroup": "custom",
"panelPatternGroupOpacity": 10,
"panelPatternGroupHeight": 100,
"panelPatternGroupFade": 100,
"panelPatternGroupGradientOpacity": 20,
"panelPatternGroupGradientHeight": 100,
"panelPatternGroupGradientFade": 100,
"panelPatternGroupCustomImage": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='s2' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23E2E8F0' /%3E%3Cstop offset='100%25' stop-color='%2364748B' /%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='20' y='20' width='24' height='24' fill='none' stroke='url(%23s2)' stroke-width='1.5' transform='rotate(45 32 32)' /%3E%3Crect x='16' y='16' width='32' height='32' fill='none' stroke='url(%23s2)' stroke-width='1' opacity='0.4' transform='rotate(45 32 32)' /%3E%3C/svg%3E",
"panelPatternGroupCustomSize": "32px 32px",
"panelPatternItem": "mesh",
"panelPatternItemOpacity": 58,
"panelPatternItemHeight": 100,
"panelPatternItemFade": 18,
"panelPatternItemGradientOpacity": 62,
"panelPatternItemGradientHeight": 34,
"panelPatternItemGradientFade": 16,
"panelPatternItemCustomImage": "",
"panelPatternItemCustomSize": "32px 32px",
"patternIsolation": 65,
"showNumbers": false,
"showTreeTags": false,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 1,
"autoCollapseSearch": true,
"sidebarWidth": 360,
"topBarHeight": 72,
"shellMaxWidth": 1100,
"sidebarMinWidth": 240,
"sidebarMaxWidth": 720,
"sidebarMaxWidthWide": 1040,
"treeSidebarBaseWidth": 360,
"treeSidebarColumnWidth": 96,
"topBarMinHeight": 64,
"topBarMaxHeight": 800,
"catalogCardsColumns": 2,
"catalogListColumns": 2,
"catalogCompactCardsColumns": 3,
"catalogCompactListColumns": 3,
"collapseAnimation": "animated",
"collapseDuration": 250,
"searchPlaceholder": "Grafana, S3, журнал, сервер...",
"settingsPanelWidth": 500
},
"limits": {
"shellMaxWidth": { "min": 1100, "max": 3840 },
"sidebarMinWidth": { "min": 220, "max": 480 },
"sidebarMaxWidth": { "min": 400, "max": 1200 },
"sidebarMaxWidthWide": { "min": 560, "max": 2000 },
"treeSidebarBaseWidth": { "min": 280, "max": 720 },
"treeSidebarColumnWidth": { "min": 60, "max": 200 },
"topBarMinHeight": { "min": 64, "max": 240 },
"topBarMaxHeight": { "min": 160, "max": 800 },
"catalogCardsColumns": { "min": 1, "max": 6 },
"catalogListColumns": { "min": 1, "max": 4 },
"catalogCompactCardsColumns": { "min": 2, "max": 8 },
"catalogCompactListColumns": { "min": 1, "max": 6 },
"settingsPanelWidth": { "min": 320, "max": 720 }
},
"features": {
"allowNavLayoutChange": true,
"allowShowTreeChange": true,
"allowSearch": true,
"allowViewChange": true,
"allowCatalogColumnsChange": true,
"allowShowNumbers": true,
"allowTreeOptions": true,
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true,
"allowPanelPatternChange": true
}
}

1234
styles.css

File diff suppressed because it is too large Load Diff