Добавлена возможность все очень гибко настраивать. Добавлены новые типы. Сделаны примеры.

This commit is contained in:
Илья Братских 2026-06-28 18:06:17 +03:00 committed by KBAHTNET
parent c1547cc24e
commit 459fc98bf1
45 changed files with 3387 additions and 527 deletions

View File

@ -124,11 +124,32 @@ ecosystem/
"accent": "violet",
"showNumbers": false,
"showTreeTags": true,
"treeTwoColumn": false,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 2,
"autoCollapseSearch": true,
"sidebarWidth": 390,
"topBarHeight": 140,
"shellMaxWidth": 2800,
"sidebarMinWidth": 260,
"sidebarMaxWidth": 800,
"sidebarMaxWidthWide": 1400,
"treeSidebarBaseWidth": 420,
"treeSidebarColumnWidth": 120,
"topBarMinHeight": 80,
"topBarMaxHeight": 560,
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
"sidebarMinWidth": { "min": 200, "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 }
},
"features": {
"allowNavLayoutChange": true,
"allowSearch": true,
@ -136,6 +157,7 @@ ecosystem/
"allowShowNumbers": true,
"allowTreeOptions": true,
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true
}
@ -144,7 +166,8 @@ ecosystem/
| `defaults` | Варианты | Описание |
|------------|----------|----------|
| `navLayout` | `"sidebar"`, `"top"` | `"sidebar"` — дерево слева; `"top"` — шапка с названием и поиском, без дерева |
| `navLayout` | `"sidebar"`, `"top"` | `"sidebar"` — панель слева; `"top"` — шапка с названием и поиском |
| `showTree` | `true`, `false` | Показывать дерево навигации (в любом режиме макета) |
| `theme` | `"dark"`, `"light"` | Стартовая цветовая тема |
| `view` | `"cards"`, `"list"`, `"compact-cards"`, `"compact-list"` | Режим отображения каталога |
| `accent` | `"violet"`, `"cyan"`, `"lime"`, `"amber"`, `"custom"` | Пресет акцента; при `"custom"` используются поля градиента ниже |
@ -152,26 +175,54 @@ ecosystem/
| `customAccentTo` | `#rrggbb` | Цвет «до» для `accent: "custom"` (по умолчанию `#22d3ee`) |
| `customAccentAngle` | `0`–`360` | Угол градиента в градусах (по умолчанию `135`) |
| `showNumbers` | `true`, `false` | Показывать номера разделов, подразделов и элементов |
| `showTreeTags` | `true`, `false` | Теги рядом с элементами в дереве (взаимоисключимо с `treeTwoColumn`) |
| `treeTwoColumn` | `true`, `false` | Элементы папок дерева в 2 колонки (взаимоисключимо с `showTreeTags`) |
| `showTreeTags` | `true`, `false` | Теги элементов в дереве (оформление как на карточках) |
| `showTreeType` | `true`, `false` | Подпись типа сервиса в дереве |
| `treeMetaLayout` | `"inline"`, `"stacked"` | Расположение тегов и типа: сбоку от названия или снизу |
| `treeColumns` | `1`–`6` (по умолчанию `2`) | Число колонок элементов внутри папок дерева |
| `autoCollapseSearch` | `true`, `false` | При поиске сворачивать ветки дерева без совпадений |
| `sidebarWidth` | `260`–`560` (`760` при `treeTwoColumn: true`) | Ширина боковой панели в px; игнорируется при `navLayout: "top"` |
| `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% окна) |
| `searchPlaceholder` | произвольная строка | Текст-подсказка в поле поиска |
| `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" }` | Диапазон «Шапка: макс. высота» |
При `treeColumns > 1` фактический потолок ширины меню: `min(sidebarMaxWidthWide, treeSidebarBaseWidth + treeColumns × treeSidebarColumnWidth)`.
`shellMaxWidth` автоматически не опускается ниже `sidebarMaxWidthWide + 320`, чтобы оставалось место для каталога.
| `features` | Варианты | Описание |
|------------|----------|----------|
| `allowNavLayoutChange` | `true`, `false` | Переключатель «Сбоку / Сверху» |
| `allowShowTreeChange` | `true`, `false` | Показ/скрытие дерева и кнопка «Скрыть дерево» в панели |
| `allowSearch` | `true`, `false` | Поле поиска |
| `allowViewChange` | `true`, `false` | Режимы каталога |
| `allowShowNumbers` | `true`, `false` | Нумерация |
| `allowTreeOptions` | `true`, `false` | Теги, 2 колонки, авто-раскрытие дерева |
| `allowSidebarResize` | `true`, `false` | Drag-resize боковой панели |
| `allowTreeOptions` | `true`, `false` | Колонки, теги, тип, расположение мета, авто-раскрытие дерева |
| `allowSidebarResize` | `true`, `false` | Drag-resize боковой панели (ширина) или верхней шапки (высота) |
| `allowLayoutLimitsChange` | `true`, `false` | Раздел ⚙ «Габариты»: лимиты ширины контента и навигации |
| `allowThemeChange` | `true`, `false` | Светлая / тёмная тема |
| `allowAccentChange` | `true`, `false` | Пресеты и свой градиент |
Пользовательские изменения сохраняются в `localStorage`, но заблокированные `features` всегда возвращаются к `defaults`.
Пример пресета «только шапка»: [`settings-top-bar.example.json`](settings-top-bar.example.json) — `navLayout: "top"`, дерево и resize отключены.
Пример пресета «шапка сверху»: [`settings-top-bar.example.json`](settings-top-bar.example.json) — `navLayout: "top"`, дерево по умолчанию скрыто (`showTree: false`), но его можно включить.
### Форматы иерархии
@ -282,7 +333,7 @@ npx serve .
Базовые типы заданы в `script.js` (`defaultServiceMeta`):
`ceph`, `cloudbeaver`, `config`, `gitea`, `grafana`, `ipmi`, `kubernetes`, `ldap`, `messages`, `opensearch`, `postgresql`, `prometheus`, `rabbitmq`, `redmine`, `rules`, `xpath`, `xmpp`
`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`). Иконка показывается на карточке каталога и в дереве навигации.
@ -329,14 +380,18 @@ Popup сгруппирован по разделам. Доступные бло
| Раздел | Настройки |
|--------|-----------|
| **Макет** | Панель навигации: **Сбоку** (дерево + поиск) или **Сверху** (только название и поиск) |
| **Макет** | **Сбоку** / **Сверху**; при «Сбоку» — toggle «Показывать дерево» и кнопка «Скрыть/Показать дерево» в панели |
| **Каталог** | Вид карточек, нумерация |
| **Дерево** | Теги, 2 колонки, авто-раскрытие поиска (только при `navLayout: sidebar`) |
| **Дерево** | Теги, 2 колонки, авто-раскрытие (при `showTree: true`; 2 колонки — только «Сбоку») |
| **Оформление** | Тема, акцент, свой градиент |
### Режим «Сверху»
Компактная шапка над каталогом: заголовок панели, поиск и ⚙. Дерево навигации скрыто — переход только через карточки каталога. Подходит для простых порталов и когда дерево избыточно.
Компактная шапка: заголовок, поиск и ⚙. Дерево по умолчанию скрыто, но его можно включить (`showTree: true`) — оно появится под шапкой на всю ширину.
### Скрытие дерева
Дерево управляется отдельно от макета (`showTree`). Переключение — в ⚙ → **Макет** или кнопкой **Скрыть/Показать дерево** под поиском.
### Свой градиент

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/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/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

View File

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Демо панели: плоский список элементов без разделов."
content="Навигационная панель экосистемы с разделами, подразделами, поиском и быстрыми переходами."
/>
<title>Пример: плоский список</title>
<title>Панель экосистемы</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
@ -42,7 +42,7 @@
⚙
</button>
<p class="eyebrow" id="page-title">Пример: плоский список</p>
<p class="eyebrow" id="page-title">Панель экосистемы</p>
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
<label class="search-field" data-search-field>
@ -57,6 +57,15 @@
<!-- Дерево генерируется из 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>
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
@ -82,10 +91,7 @@
-->
<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>
@ -106,8 +112,109 @@
<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>
</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>
@ -128,6 +235,34 @@
</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>
@ -141,25 +276,38 @@
<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>

View File

@ -15,6 +15,13 @@
"tags": ["prometheus", "метрики"],
"url": "https://prometheus.example.com"
},
{
"id": "analytics",
"title": "Портал аналитики",
"type": "analytics",
"tags": ["аналитика", "отчёты"],
"url": "https://analytics.example.com"
},
{
"id": "opensearch",
"title": "OpenSearch",
@ -29,6 +36,20 @@
"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",
@ -36,6 +57,13 @@
"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",
@ -57,6 +85,76 @@
"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",

View File

@ -3,23 +3,60 @@
"data": null,
"defaults": {
"navLayout": "sidebar",
"showTree": true,
"showResizeHandles": true,
"theme": "dark",
"view": "cards",
"accent": "violet",
"customAccentFrom": "#8b5cf6",
"customAccentTo": "#22d3ee",
"customAccentAngle": 135,
"showNumbers": false,
"showTreeTags": true,
"treeTwoColumn": false,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 2,
"autoCollapseSearch": true,
"sidebarWidth": 390,
"topBarHeight": 140,
"shellMaxWidth": 2800,
"sidebarMinWidth": 260,
"sidebarMaxWidth": 800,
"sidebarMaxWidthWide": 1400,
"treeSidebarBaseWidth": 420,
"treeSidebarColumnWidth": 120,
"topBarMinHeight": 80,
"topBarMaxHeight": 560,
"catalogCardsColumns": 3,
"catalogListColumns": 1,
"catalogCompactCardsColumns": 5,
"catalogCompactListColumns": 3,
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
"sidebarMinWidth": { "min": 200, "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
}

View File

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Демо панели: полная иерархия section → group → item."
content="Навигационная панель экосистемы с разделами, подразделами, поиском и быстрыми переходами."
/>
<title>Пример: section → group → item</title>
<title>Панель экосистемы</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
@ -42,7 +42,7 @@
⚙
</button>
<p class="eyebrow" id="page-title">Пример: section → group → item</p>
<p class="eyebrow" id="page-title">Панель экосистемы</p>
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
<label class="search-field" data-search-field>
@ -57,6 +57,15 @@
<!-- Дерево генерируется из 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>
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
@ -82,10 +91,7 @@
-->
<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>
@ -106,8 +112,109 @@
<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>
</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>
@ -128,6 +235,34 @@
</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>
@ -141,25 +276,38 @@
<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>

View File

@ -30,8 +30,16 @@
"url": "https://prometheus.example.com"
},
{
"id": "alerts",
"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": ["алерты", "правила"],
@ -92,6 +100,22 @@
"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"
}
]
},
@ -110,8 +134,16 @@
"url": "https://s3.example.com"
},
{
"id": "postgresql",
"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"],
@ -119,7 +151,7 @@
},
{
"id": "cloudbeaver",
"number": "2.2.3",
"number": "2.2.4",
"title": "CloudBeaver",
"type": "cloudbeaver",
"tags": ["sql", "база", "редактор"],
@ -128,6 +160,156 @@
]
}
]
},
{
"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"
}
]
}
]
}
]
}

View File

@ -3,23 +3,60 @@
"data": null,
"defaults": {
"navLayout": "sidebar",
"showTree": true,
"showResizeHandles": true,
"theme": "dark",
"view": "cards",
"accent": "violet",
"customAccentFrom": "#8b5cf6",
"customAccentTo": "#22d3ee",
"customAccentAngle": 135,
"showNumbers": false,
"showTreeTags": true,
"treeTwoColumn": false,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 2,
"autoCollapseSearch": true,
"sidebarWidth": 390,
"topBarHeight": 140,
"shellMaxWidth": 2800,
"sidebarMinWidth": 260,
"sidebarMaxWidth": 800,
"sidebarMaxWidthWide": 1400,
"treeSidebarBaseWidth": 420,
"treeSidebarColumnWidth": 120,
"topBarMinHeight": 80,
"topBarMaxHeight": 560,
"catalogCardsColumns": 3,
"catalogListColumns": 1,
"catalogCompactCardsColumns": 5,
"catalogCompactListColumns": 3,
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
"sidebarMinWidth": { "min": 200, "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
}

View File

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Демо панели: раздел с прямыми элементами и подразделами одновременно."
content="Навигационная панель экосистемы с разделами, подразделами, поиском и быстрыми переходами."
/>
<title>Пример: смешанный раздел</title>
<title>Панель экосистемы</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
@ -42,7 +42,7 @@
⚙
</button>
<p class="eyebrow" id="page-title">Пример: смешанный раздел</p>
<p class="eyebrow" id="page-title">Панель экосистемы</p>
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
<label class="search-field" data-search-field>
@ -57,6 +57,15 @@
<!-- Дерево генерируется из 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>
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
@ -82,10 +91,7 @@
-->
<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>
@ -106,8 +112,109 @@
<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>
</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>
@ -128,6 +235,34 @@
</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>
@ -141,25 +276,38 @@
<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>

View File

@ -15,14 +15,38 @@
"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": "Документация",
"type": "config",
"tags": ["docs", "wiki"],
"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",
@ -56,8 +80,16 @@
"url": "https://prometheus.example.com"
},
{
"id": "logs",
"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": ["логи"],
@ -66,14 +98,54 @@
]
},
{
"id": "data",
"id": "communications",
"number": "1.2",
"title": "Данные и очереди",
"description": "Базы данных и брокеры сообщений.",
"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.2.1",
"number": "1.3.1",
"title": "PostgreSQL",
"type": "postgresql",
"tags": ["postgresql", "sql"],
@ -81,11 +153,43 @@
},
{
"id": "mq",
"number": "1.2.2",
"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

@ -3,23 +3,60 @@
"data": null,
"defaults": {
"navLayout": "sidebar",
"showTree": true,
"showResizeHandles": true,
"theme": "dark",
"view": "cards",
"accent": "violet",
"customAccentFrom": "#8b5cf6",
"customAccentTo": "#22d3ee",
"customAccentAngle": 135,
"showNumbers": false,
"showTreeTags": true,
"treeTwoColumn": false,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 2,
"autoCollapseSearch": true,
"sidebarWidth": 390,
"topBarHeight": 140,
"shellMaxWidth": 2800,
"sidebarMinWidth": 260,
"sidebarMaxWidth": 800,
"sidebarMaxWidthWide": 1400,
"treeSidebarBaseWidth": 420,
"treeSidebarColumnWidth": 120,
"topBarMinHeight": 80,
"topBarMaxHeight": 560,
"catalogCardsColumns": 3,
"catalogListColumns": 1,
"catalogCompactCardsColumns": 5,
"catalogCompactListColumns": 3,
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
"sidebarMinWidth": { "min": 200, "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
}

View File

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Демо панели: раздел с элементами без подразделов (section → item)."
content="Навигационная панель экосистемы с разделами, подразделами, поиском и быстрыми переходами."
/>
<title>Пример: section → item</title>
<title>Панель экосистемы</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
@ -42,7 +42,7 @@
⚙
</button>
<p class="eyebrow" id="page-title">Пример: section → item</p>
<p class="eyebrow" id="page-title">Панель экосистемы</p>
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
<label class="search-field" data-search-field>
@ -57,6 +57,15 @@
<!-- Дерево генерируется из 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>
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
@ -82,10 +91,7 @@
-->
<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>
@ -106,8 +112,109 @@
<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>
</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>
@ -128,6 +235,34 @@
</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>
@ -141,25 +276,38 @@
<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>

View File

@ -8,24 +8,56 @@
"description": "Часто используемые сервисы без промежуточных подразделов.",
"items": [
{
"id": "mail",
"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": "chat",
"number": "1.2",
"title": "Корпоративный чат",
"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.3",
"number": "1.7",
"title": "Задачи",
"type": "redmine",
"tags": ["redmine", "задачи"],
@ -34,22 +66,78 @@
]
},
{
"id": "dev",
"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": "2.1",
"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": "2.2",
"number": "3.4",
"title": "Конфигурации",
"type": "config",
"tags": ["config", "настройки"],
@ -57,31 +145,55 @@
},
{
"id": "xpath-debug",
"number": "2.3",
"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": "3",
"number": "4",
"title": "Администрирование",
"description": "Управление пользователями и железом.",
"description": "Управление пользователями, доступом и железом.",
"items": [
{
"id": "ldap",
"number": "3.1",
"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": "3.2",
"number": "4.4",
"title": "Сервер 01 — IPMI",
"type": "ipmi",
"tags": ["ipmi", "сервер", "железо"],
@ -89,7 +201,7 @@
},
{
"id": "server-02",
"number": "3.3",
"number": "4.5",
"title": "Сервер 02 — IPMI",
"type": "ipmi",
"tags": ["ipmi", "сервер"],

View File

@ -3,23 +3,60 @@
"data": null,
"defaults": {
"navLayout": "sidebar",
"showTree": true,
"showResizeHandles": true,
"theme": "dark",
"view": "cards",
"accent": "violet",
"customAccentFrom": "#8b5cf6",
"customAccentTo": "#22d3ee",
"customAccentAngle": 135,
"showNumbers": false,
"showTreeTags": true,
"treeTwoColumn": false,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 2,
"autoCollapseSearch": true,
"sidebarWidth": 390,
"topBarHeight": 140,
"shellMaxWidth": 2800,
"sidebarMinWidth": 260,
"sidebarMaxWidth": 800,
"sidebarMaxWidthWide": 1400,
"treeSidebarBaseWidth": 420,
"treeSidebarColumnWidth": 120,
"topBarMinHeight": 80,
"topBarMaxHeight": 560,
"catalogCardsColumns": 3,
"catalogListColumns": 1,
"catalogCompactCardsColumns": 5,
"catalogCompactListColumns": 3,
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
"sidebarMinWidth": { "min": 200, "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
}

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

@ -32,9 +32,16 @@ let ecosystemSections = [];
let panelTitle = "Панель экосистемы";
const defaultServiceMeta = {
analytics: { label: "Аналитика", icon: "assets/analytics.svg" },
api: { label: "API", icon: "assets/api.svg" },
calendar: { label: "Календарь", icon: "assets/calendar.svg" },
ceph: { label: "Ceph", icon: "assets/ceph.svg" },
chat: { label: "Беседа", icon: "assets/chat.svg" },
cicd: { label: "CI/CD", icon: "assets/cicd.svg" },
cloudbeaver: { label: "CloudBeaver", icon: "assets/cloudbeaver.svg" },
config: { label: "Config", icon: "assets/config.svg" },
drive: { label: "Диск", icon: "assets/drive.svg" },
forum: { label: "Форум", icon: "assets/forum.svg" },
gitea: { label: "Gitea", icon: "assets/gitea.svg" },
grafana: { label: "Grafana", icon: "assets/grafana.svg" },
ipmi: { label: "IPMI", icon: "assets/server.svg" },
@ -47,6 +54,11 @@ const defaultServiceMeta = {
rabbitmq: { label: "RabbitMQ", icon: "assets/rabbitmq.svg" },
redmine: { label: "Redmine", icon: "assets/redmine.svg" },
rules: { label: "Rules", icon: "assets/rules.svg" },
social: { label: "Соцсеть", icon: "assets/social.svg" },
vault: { label: "Vault", icon: "assets/vault.svg" },
video: { label: "Видео", icon: "assets/video.svg" },
vpn: { label: "VPN", icon: "assets/vpn.svg" },
wiki: { label: "Wiki", icon: "assets/wiki.svg" },
xpath: { label: "xPath", icon: "assets/xpath.svg" },
xmpp: { label: "XMPP", icon: "assets/xmpp.svg" },
};
@ -65,26 +77,84 @@ const defaultPreferences = {
customAccentTo: "#22d3ee",
customAccentAngle: 135,
navLayout: "sidebar",
showTree: true,
showResizeHandles: true,
showNumbers: false,
showTreeTags: true,
treeTwoColumn: false,
showTreeType: false,
treeMetaLayout: "inline",
treeColumns: 2,
autoCollapseSearch: true,
sidebarWidth: 390,
topBarHeight: 140,
shellMaxWidth: 2800,
sidebarMinWidth: 260,
sidebarMaxWidth: 800,
sidebarMaxWidthWide: 1400,
treeSidebarBaseWidth: 420,
treeSidebarColumnWidth: 120,
topBarMinHeight: 80,
topBarMaxHeight: 560,
catalogCardsColumns: 3,
catalogListColumns: 1,
catalogCompactCardsColumns: 5,
catalogCompactListColumns: 3,
searchPlaceholder: "Grafana, S3, журнал, сервер...",
};
const defaultPanelFeatures = {
allowNavLayoutChange: true,
allowShowTreeChange: true,
allowSearch: true,
allowViewChange: true,
allowCatalogColumnsChange: true,
allowShowNumbers: true,
allowTreeOptions: true,
allowSidebarResize: true,
allowLayoutLimitsChange: true,
allowThemeChange: true,
allowAccentChange: true,
};
const defaultPanelLimits = {
shellMaxWidth: { min: 1200, max: 3840 },
sidebarMinWidth: { min: 200, 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 },
};
const layoutLimitKeys = [
"shellMaxWidth",
"sidebarMinWidth",
"sidebarMaxWidth",
"sidebarMaxWidthWide",
"treeSidebarBaseWidth",
"treeSidebarColumnWidth",
"topBarMinHeight",
"topBarMaxHeight",
];
const catalogColumnKeys = [
"catalogCardsColumns",
"catalogListColumns",
"catalogCompactCardsColumns",
"catalogCompactListColumns",
];
const panelLimitKeys = [...layoutLimitKeys, ...catalogColumnKeys];
const availableNavLayouts = ["sidebar", "top"];
const availableTreeMetaLayouts = ["inline", "stacked"];
const MIN_TREE_COLUMNS = 1;
const MAX_TREE_COLUMNS = 6;
const availableViews = ["cards", "list", "compact-cards", "compact-list"];
const availableAccents = ["violet", "cyan", "lime", "amber", "custom"];
@ -94,6 +164,7 @@ const availableAccents = ["violet", "cyan", "lime", "amber", "custom"];
// preferences - текущие настройки интерфейса; итоговые значения собираются в bootstrap().
let panelDefaults = { ...defaultPreferences };
let panelFeatures = { ...defaultPanelFeatures };
let panelLimits = structuredClone(defaultPanelLimits);
let preferences = { ...defaultPreferences };
// collapseState - объект вида { [collapseKey]: boolean } для details.
@ -104,6 +175,7 @@ const appLayout = document.querySelector(".app-layout");
const catalog = document.querySelector("[data-catalog-root]");
const navTree = document.querySelector("[data-nav-tree]");
const sidebarResizer = document.querySelector("[data-sidebar-resizer]");
const topBarResizer = document.querySelector("[data-top-bar-resizer]");
const searchField = document.querySelector("[data-search-field]");
const searchInput = document.querySelector("[data-search-input]");
const settingsToggle = document.querySelector("[data-settings-toggle]");
@ -120,10 +192,17 @@ const customAccentToInput = document.querySelector("[data-custom-accent-to]");
const customAccentAngleInput = document.querySelector("[data-custom-accent-angle]");
const customAccentAngleValue = document.querySelector("[data-custom-accent-angle-value]");
const customAccentPreview = document.querySelector("[data-custom-accent-preview]");
const showTreeInput = document.querySelector("[data-show-tree]");
const showResizeHandlesInput = document.querySelector("[data-show-resize-handles]");
const showNumbersInput = document.querySelector("[data-show-numbers]");
const showTreeTagsInput = document.querySelector("[data-show-tree-tags]");
const treeTwoColumnInput = document.querySelector("[data-tree-two-column]");
const showTreeTypeInput = document.querySelector("[data-show-tree-type]");
const treeColumnsInput = document.querySelector("[data-tree-columns]");
const treeColumnsValue = document.querySelector("[data-tree-columns-value]");
const treeMetaLayoutButtons = document.querySelectorAll("[data-tree-meta-layout]");
const autoCollapseSearchInput = document.querySelector("[data-auto-collapse-search]");
const layoutLimitInputs = document.querySelectorAll("[data-layout-limit]");
const catalogColumnInputs = document.querySelectorAll("[data-catalog-columns]");
const settingsSections = document.querySelectorAll("[data-settings-section]");
const emptyTemplate = document.querySelector("#empty-template");
const configErrorTemplate = document.querySelector("#config-error-template");
@ -236,14 +315,53 @@ navLayoutButtons.forEach((button) => {
preferences.navLayout = button.dataset.navLayout;
applyPreferences(preferences);
if (preferences.navLayout === "sidebar") {
if (isTreeVisible(preferences)) {
renderNavigationTree();
} else {
navTree.replaceChildren();
}
savePreferences(preferences);
});
});
function setShowTree(nextValue, { persist = true } = {}) {
if (!panelFeatures.allowShowTreeChange) {
return;
}
preferences.showTree = Boolean(nextValue);
applyPreferences(preferences);
if (preferences.showTree) {
renderNavigationTree();
} else {
navTree.replaceChildren();
}
if (persist) {
savePreferences(preferences);
}
}
if (showTreeInput) {
showTreeInput.addEventListener("change", () => {
setShowTree(showTreeInput.checked);
});
}
if (showResizeHandlesInput) {
showResizeHandlesInput.addEventListener("change", () => {
if (!panelFeatures.allowSidebarResize) {
return;
}
preferences.showResizeHandles = showResizeHandlesInput.checked;
applyPreferences(preferences);
savePreferences(preferences);
});
}
// Переключение режима отображения каталога.
viewButtons.forEach((button) => {
button.addEventListener("click", () => {
@ -300,41 +418,62 @@ showNumbersInput.addEventListener("change", () => {
});
// Показывает/скрывает поисковые теги в файловом дереве.
// Взаимоисключимо с двухколоночным режимом элементов папок.
showTreeTagsInput.addEventListener("change", () => {
if (!panelFeatures.allowTreeOptions || preferences.navLayout === "top") {
if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) {
return;
}
preferences.showTreeTags = showTreeTagsInput.checked;
if (preferences.showTreeTags) {
preferences.treeTwoColumn = false;
}
applyPreferences(preferences);
renderNavigationTree();
savePreferences(preferences);
});
// Компактный двухколоночный режим элементов внутри папок дерева.
// Взаимоисключимо с отображением тегов.
treeTwoColumnInput.addEventListener("change", () => {
if (!panelFeatures.allowTreeOptions || preferences.navLayout === "top") {
showTreeTypeInput.addEventListener("change", () => {
if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) {
return;
}
preferences.treeTwoColumn = treeTwoColumnInput.checked;
if (preferences.treeTwoColumn) {
preferences.showTreeTags = false;
if (preferences.sidebarWidth < 480) {
preferences.sidebarWidth = 480;
}
preferences.showTreeType = showTreeTypeInput.checked;
applyPreferences(preferences);
renderNavigationTree();
savePreferences(preferences);
});
function handleTreeColumnsChange() {
if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) {
return;
}
preferences.treeColumns = getTreeColumns({ treeColumns: treeColumnsInput.value });
if (preferences.treeColumns > 1 && preferences.sidebarWidth < getSidebarMinWidth(preferences) + preferences.treeColumns * 70) {
preferences.sidebarWidth = getSidebarMinWidth(preferences) + preferences.treeColumns * 70;
}
applyPreferences(preferences);
savePreferences(preferences);
}
if (treeColumnsInput) {
treeColumnsInput.addEventListener("input", handleTreeColumnsChange);
}
treeMetaLayoutButtons.forEach((button) => {
button.addEventListener("click", () => {
if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) {
return;
}
preferences.treeMetaLayout = button.dataset.treeMetaLayout;
applyPreferences(preferences);
savePreferences(preferences);
});
});
// Включает/выключает авто-раскрытие веток дерева при поиске.
autoCollapseSearchInput.addEventListener("change", () => {
if (!panelFeatures.allowTreeOptions || preferences.navLayout === "top") {
if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) {
return;
}
@ -344,10 +483,38 @@ autoCollapseSearchInput.addEventListener("change", () => {
savePreferences(preferences);
});
layoutLimitInputs.forEach((input) => {
input.addEventListener("input", () => {
if (!panelFeatures.allowLayoutLimitsChange) {
return;
}
const key = input.dataset.layoutLimit;
preferences[key] = clampLayoutPreference(key, input.value);
preferences = normalizePreferences(preferences);
applyPreferences(preferences);
savePreferences(preferences);
});
});
catalogColumnInputs.forEach((input) => {
input.addEventListener("input", () => {
if (!panelFeatures.allowCatalogColumnsChange) {
return;
}
const key = input.dataset.catalogColumns;
preferences[key] = clampCatalogColumn(key, input.value);
preferences = normalizePreferences(preferences);
applyPreferences(preferences);
savePreferences(preferences);
});
});
// Drag-resize бокового меню.
// Ширина сохраняется в preferences.sidebarWidth и применяется через CSS-переменную.
sidebarResizer.addEventListener("pointerdown", (event) => {
if (preferences.navLayout === "top" || !panelFeatures.allowSidebarResize) {
if (preferences.navLayout === "top" || !isResizeVisible(preferences)) {
return;
}
@ -358,7 +525,7 @@ sidebarResizer.addEventListener("pointerdown", (event) => {
const handlePointerMove = (moveEvent) => {
const layoutLeft = document.querySelector(".app-layout").getBoundingClientRect().left;
preferences.sidebarWidth = Math.round(
clamp(moveEvent.clientX - layoutLeft, 260, getSidebarMaxWidth(preferences)),
clamp(moveEvent.clientX - layoutLeft, getSidebarMinWidth(preferences), getSidebarMaxWidth(preferences)),
);
applyPreferences(preferences);
};
@ -376,6 +543,38 @@ sidebarResizer.addEventListener("pointerdown", (event) => {
sidebarResizer.addEventListener("pointercancel", handlePointerUp);
});
// Drag-resize верхней панели в режиме navLayout: top.
topBarResizer?.addEventListener("pointerdown", (event) => {
if (preferences.navLayout !== "top" || !isResizeVisible(preferences)) {
return;
}
event.preventDefault();
document.body.classList.add("is-resizing-top-bar");
topBarResizer.setPointerCapture(event.pointerId);
const panelTop = document.querySelector(".side-panel")?.getBoundingClientRect().top ?? 0;
const handlePointerMove = (moveEvent) => {
preferences.topBarHeight = Math.round(
clamp(moveEvent.clientY - panelTop, getTopBarMinHeight(preferences), getTopBarMaxHeight(preferences)),
);
applyPreferences(preferences);
};
const handlePointerUp = () => {
document.body.classList.remove("is-resizing-top-bar");
savePreferences(preferences);
topBarResizer.removeEventListener("pointermove", handlePointerMove);
topBarResizer.removeEventListener("pointerup", handlePointerUp);
topBarResizer.removeEventListener("pointercancel", handlePointerUp);
};
topBarResizer.addEventListener("pointermove", handlePointerMove);
topBarResizer.addEventListener("pointerup", handlePointerUp);
topBarResizer.addEventListener("pointercancel", handlePointerUp);
});
// ============================================================================
// Panel config loading
// ============================================================================
@ -395,6 +594,7 @@ async function loadPanelDataFromSettings(settings) {
}
function applyOrganizationConfig(settings) {
panelLimits = normalizePanelLimits(settings?.limits);
panelDefaults = normalizePreferences({
...defaultPreferences,
...(settings?.defaults && typeof settings.defaults === "object" ? settings.defaults : {}),
@ -438,29 +638,60 @@ function normalizePreferences(raw) {
360,
);
next.showNumbers = Boolean(next.showNumbers);
next.showTree = Boolean(next.showTree);
next.showResizeHandles = next.showResizeHandles !== false;
next.showTreeTags = Boolean(next.showTreeTags);
next.treeTwoColumn = Boolean(next.treeTwoColumn);
next.showTreeType = Boolean(next.showTreeType);
next.treeColumns = getTreeColumns(next);
next.treeMetaLayout = availableTreeMetaLayouts.includes(next.treeMetaLayout)
? next.treeMetaLayout
: defaultPreferences.treeMetaLayout;
next.autoCollapseSearch = Boolean(next.autoCollapseSearch);
normalizeLayoutPreferences(next);
normalizeCatalogColumns(next);
next.sidebarWidth = clamp(
Number(next.sidebarWidth) || defaultPreferences.sidebarWidth,
260,
getSidebarMinWidth(next),
getSidebarMaxWidth(next),
);
if (next.treeTwoColumn) {
next.showTreeTags = false;
} else if (next.showTreeTags) {
next.treeTwoColumn = false;
}
if (next.navLayout === "top") {
next.treeTwoColumn = false;
next.showTreeTags = false;
}
next.topBarHeight = clamp(
Number(next.topBarHeight) || defaultPreferences.topBarHeight,
getTopBarMinHeight(next),
getTopBarMaxHeight(next),
);
return next;
}
function normalizeLayoutPreferences(next) {
for (const key of layoutLimitKeys) {
next[key] = clampLayoutPreference(key, next[key]);
}
if (next.sidebarMinWidth > next.sidebarMaxWidth) {
next.sidebarMinWidth = next.sidebarMaxWidth;
}
if (next.sidebarMaxWidth > next.sidebarMaxWidthWide) {
next.sidebarMaxWidth = next.sidebarMaxWidthWide;
}
if (next.topBarMinHeight > next.topBarMaxHeight) {
next.topBarMinHeight = next.topBarMaxHeight;
}
const minShellWidth = getSidebarMaxWidth(next) + 320;
if (next.shellMaxWidth < minShellWidth) {
next.shellMaxWidth = clampLayoutPreference("shellMaxWidth", minShellWidth);
}
}
function normalizeCatalogColumns(next) {
for (const key of catalogColumnKeys) {
next[key] = clampCatalogColumn(key, next[key]);
}
}
function enforceFeatureLocks(rawPreferences) {
const next = normalizePreferences(rawPreferences);
@ -468,6 +699,10 @@ function enforceFeatureLocks(rawPreferences) {
next.navLayout = panelDefaults.navLayout;
}
if (!panelFeatures.allowShowTreeChange) {
next.showTree = panelDefaults.showTree;
}
if (!panelFeatures.allowThemeChange) {
next.theme = panelDefaults.theme;
}
@ -476,6 +711,12 @@ function enforceFeatureLocks(rawPreferences) {
next.view = panelDefaults.view;
}
if (!panelFeatures.allowCatalogColumnsChange) {
for (const key of catalogColumnKeys) {
next[key] = panelDefaults[key];
}
}
if (!panelFeatures.allowAccentChange) {
next.accent = panelDefaults.accent;
next.customAccentFrom = panelDefaults.customAccentFrom;
@ -487,16 +728,33 @@ function enforceFeatureLocks(rawPreferences) {
next.showNumbers = panelDefaults.showNumbers;
}
if (!panelFeatures.allowTreeOptions || next.navLayout === "top") {
next.showTreeTags = false;
next.treeTwoColumn = false;
if (!panelFeatures.allowTreeOptions || !next.showTree) {
next.showTreeTags = panelDefaults.showTreeTags;
next.showTreeType = panelDefaults.showTreeType;
next.treeColumns = panelDefaults.treeColumns;
next.treeMetaLayout = panelDefaults.treeMetaLayout;
next.autoCollapseSearch = panelDefaults.autoCollapseSearch;
}
if (!panelFeatures.allowSidebarResize) {
next.showResizeHandles = panelDefaults.showResizeHandles;
}
if (!panelFeatures.allowSidebarResize || next.navLayout === "top") {
next.sidebarWidth = panelDefaults.sidebarWidth;
}
if (!panelFeatures.allowSidebarResize || next.navLayout !== "top") {
next.topBarHeight = panelDefaults.topBarHeight;
}
if (!panelFeatures.allowLayoutLimitsChange) {
for (const key of layoutLimitKeys) {
next[key] = panelDefaults[key];
}
normalizeLayoutPreferences(next);
}
return next;
}
@ -893,7 +1151,7 @@ function matchesText(values, query) {
// Строит левое файловое дерево из той же структуры данных, что и каталог.
// Это важно: дерево и карточки всегда остаются синхронизированными.
function renderNavigationTree() {
if (preferences.navLayout === "top") {
if (!isTreeVisible(preferences)) {
navTree.replaceChildren();
return;
}
@ -964,22 +1222,39 @@ function createTreeItemList(items, query = "", className = "tree-items") {
itemList.className = className;
items.forEach((item) => {
const meta = getItemMeta(item);
const itemButton = document.createElement("button");
itemButton.type = "button";
itemButton.className = "tree-node tree-file";
itemButton.dataset.focusTarget = getItemId(item);
itemButton.innerHTML = `
<img class="tree-node-icon" src="${resolveAssetPath(meta.icon)}" alt="" loading="lazy" />
<span>${highlightText(item.title, query)}</span>
<small>${highlightText(item.tags.join(", "), query)}</small>
`;
itemList.append(itemButton);
itemList.append(createTreeItemButton(item, query));
});
return itemList;
}
function createTreeItemButton(item, query = "") {
const meta = getItemMeta(item);
const itemButton = document.createElement("button");
itemButton.type = "button";
itemButton.className = "tree-node tree-file";
itemButton.dataset.focusTarget = getItemId(item);
itemButton.innerHTML = `
<img class="tree-node-icon" src="${resolveAssetPath(meta.icon)}" alt="" loading="lazy" />
<div class="tree-file-body">
<span class="tree-file-title">${highlightText(item.title, query)}</span>
<div class="tree-file-meta">
<span class="tree-file-type">${highlightText(meta.label, query)}</span>
<div class="tree-tags tags">${renderTreeTagSpans(item.tags, query)}</div>
</div>
</div>
`;
return itemButton;
}
function renderTreeTagSpans(tags, query = "") {
if (!Array.isArray(tags) || !tags.length) {
return "";
}
return tags.map((tag) => `<span>${highlightText(tag, query)}</span>`).join("");
}
// ============================================================================
// Focusing from tree to content
// ============================================================================
@ -1097,31 +1372,84 @@ function applyPreferences(nextPreferences) {
document.documentElement.dataset.accent = activePreferences.accent;
document.documentElement.dataset.navLayout =
activePreferences.navLayout === "top" ? "top" : "sidebar";
document.documentElement.dataset.showTree = String(isTreeVisible(activePreferences));
document.documentElement.dataset.showResizeHandles = String(isResizeVisible(activePreferences));
applyAccentVariables(activePreferences);
document.documentElement.style.setProperty(
"--sidebar-width",
`${clamp(
Number(activePreferences.sidebarWidth) || defaultPreferences.sidebarWidth,
260,
getSidebarMinWidth(activePreferences),
getSidebarMaxWidth(activePreferences),
)}px`,
);
document.documentElement.style.setProperty(
"--top-bar-height",
`${clamp(
Number(activePreferences.topBarHeight) || defaultPreferences.topBarHeight,
getTopBarMinHeight(activePreferences),
getTopBarMaxHeight(activePreferences),
)}px`,
);
const treeColumns = getTreeColumns(activePreferences);
const sidebarMaxWidth = getSidebarMaxWidth(activePreferences);
document.documentElement.style.setProperty(
"--shell-max-width",
activePreferences.treeTwoColumn ? "1920px" : "1560px",
`${getShellMaxWidth(activePreferences)}px`,
);
document.documentElement.style.setProperty("--sidebar-max-width", `${sidebarMaxWidth}px`);
document.documentElement.style.setProperty(
"--tree-sidebar-base-width",
`${activePreferences.treeSidebarBaseWidth}px`,
);
document.documentElement.style.setProperty(
"--sidebar-max-width",
`${getSidebarMaxWidth(activePreferences)}px`,
"--tree-sidebar-column-width",
`${activePreferences.treeSidebarColumnWidth}px`,
);
document.documentElement.style.setProperty(
"--catalog-cards-columns",
String(activePreferences.catalogCardsColumns),
);
document.documentElement.style.setProperty(
"--catalog-list-columns",
String(activePreferences.catalogListColumns),
);
document.documentElement.style.setProperty(
"--catalog-compact-cards-columns",
String(activePreferences.catalogCompactCardsColumns),
);
document.documentElement.style.setProperty(
"--catalog-compact-list-columns",
String(activePreferences.catalogCompactListColumns),
);
document.documentElement.style.setProperty("--tree-columns", String(treeColumns));
catalog.classList.remove("cards-view", "list-view", "compact-cards-view", "compact-list-view");
catalog.classList.add(`${activePreferences.view}-view`);
document.documentElement.dataset.showNumbers = String(Boolean(activePreferences.showNumbers));
document.documentElement.dataset.showTreeTags = String(Boolean(activePreferences.showTreeTags));
document.documentElement.dataset.treeTwoColumn = String(Boolean(activePreferences.treeTwoColumn));
document.documentElement.dataset.showTreeType = String(Boolean(activePreferences.showTreeType));
document.documentElement.dataset.treeMetaLayout = activePreferences.treeMetaLayout;
document.documentElement.dataset.treeColumns = String(treeColumns);
if (showTreeInput) {
showTreeInput.checked = Boolean(activePreferences.showTree);
}
if (showResizeHandlesInput) {
showResizeHandlesInput.checked = Boolean(activePreferences.showResizeHandles);
}
showNumbersInput.checked = Boolean(activePreferences.showNumbers);
showTreeTagsInput.checked = Boolean(activePreferences.showTreeTags);
treeTwoColumnInput.checked = Boolean(activePreferences.treeTwoColumn);
if (showTreeTypeInput) {
showTreeTypeInput.checked = Boolean(activePreferences.showTreeType);
}
if (treeColumnsInput) {
treeColumnsInput.value = String(treeColumns);
}
if (treeColumnsValue) {
treeColumnsValue.textContent = String(treeColumns);
}
autoCollapseSearchInput.checked = Boolean(activePreferences.autoCollapseSearch);
customAccentFromInput.value = activePreferences.customAccentFrom;
customAccentToInput.value = activePreferences.customAccentTo;
@ -1133,6 +1461,9 @@ function applyPreferences(nextPreferences) {
setActiveButton(navLayoutButtons, "navLayout", activePreferences.navLayout);
setActiveButton(viewButtons, "view", activePreferences.view);
setActiveButton(accentButtons, "accent", activePreferences.accent);
setActiveButton(treeMetaLayoutButtons, "treeMetaLayout", activePreferences.treeMetaLayout);
syncLayoutLimitInputs(activePreferences);
syncCatalogColumnInputs(activePreferences);
applyPanelFeatures(activePreferences);
}
@ -1147,12 +1478,17 @@ function applyPanelFeatures(activePreferences) {
}
if (navTree) {
navTree.hidden = activePreferences.navLayout === "top";
navTree.hidden = !isTreeVisible(activePreferences);
}
if (sidebarResizer) {
sidebarResizer.hidden =
activePreferences.navLayout === "top" || !panelFeatures.allowSidebarResize;
activePreferences.navLayout === "top" || !isResizeVisible(activePreferences);
}
if (topBarResizer) {
topBarResizer.hidden =
activePreferences.navLayout !== "top" || !isResizeVisible(activePreferences);
}
if (appLayout) {
@ -1163,16 +1499,30 @@ function applyPanelFeatures(activePreferences) {
const featureName = element.dataset.feature;
element.hidden = featureName ? !panelFeatures[featureName] : false;
});
document.querySelectorAll("[data-show-when-sidebar]").forEach((element) => {
element.hidden = activePreferences.navLayout === "top";
});
}
function isSettingsSectionVisible(sectionName, activePreferences) {
switch (sectionName) {
case "layout":
return panelFeatures.allowNavLayoutChange;
return (
panelFeatures.allowNavLayoutChange ||
panelFeatures.allowShowTreeChange ||
panelFeatures.allowSidebarResize
);
case "dimensions":
return panelFeatures.allowLayoutLimitsChange;
case "catalog":
return panelFeatures.allowViewChange || panelFeatures.allowShowNumbers;
return (
panelFeatures.allowViewChange ||
panelFeatures.allowShowNumbers ||
panelFeatures.allowCatalogColumnsChange
);
case "tree":
return panelFeatures.allowTreeOptions && activePreferences.navLayout !== "top";
return panelFeatures.allowTreeOptions && activePreferences.showTree;
case "appearance":
return panelFeatures.allowThemeChange || panelFeatures.allowAccentChange;
default:
@ -1180,6 +1530,14 @@ function isSettingsSectionVisible(sectionName, activePreferences) {
}
}
function isTreeVisible(activePreferences) {
return Boolean(activePreferences.showTree);
}
function isResizeVisible(activePreferences) {
return panelFeatures.allowSidebarResize && Boolean(activePreferences.showResizeHandles);
}
// Применяет CSS-переменные акцента для пресетов или пользовательского градиента.
function applyAccentVariables(nextPreferences) {
const root = document.documentElement;
@ -1331,9 +1689,162 @@ function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
// Максимальная ширина sidebar зависит от режима дерева.
function getTreeColumns(nextPreferences) {
let columns = Number(nextPreferences.treeColumns);
if (!Number.isFinite(columns)) {
columns = nextPreferences.treeTwoColumn ? 2 : defaultPreferences.treeColumns;
}
return clamp(Math.round(columns), MIN_TREE_COLUMNS, MAX_TREE_COLUMNS);
}
function getLayoutLimitRange(key) {
const limits = panelLimits[key] ?? defaultPanelLimits[key];
if (!limits || !Number.isFinite(limits.min) || !Number.isFinite(limits.max)) {
return defaultPanelLimits[key];
}
return {
min: Math.min(limits.min, limits.max),
max: Math.max(limits.min, limits.max),
};
}
function clampLayoutPreference(key, value) {
const limits = getLayoutLimitRange(key);
const fallback = panelDefaults[key] ?? defaultPreferences[key];
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return fallback;
}
return clamp(Math.round(numeric), limits.min, limits.max);
}
function normalizePanelLimits(rawLimits) {
const next = structuredClone(defaultPanelLimits);
if (!rawLimits || typeof rawLimits !== "object") {
return next;
}
for (const key of panelLimitKeys) {
const source = rawLimits[key];
if (!source || typeof source !== "object") {
continue;
}
const min = Math.round(Number(source.min));
const max = Math.round(Number(source.max));
if (!Number.isFinite(min) || !Number.isFinite(max)) {
continue;
}
next[key] = {
min: Math.min(min, max),
max: Math.max(min, max),
};
}
return next;
}
function syncLayoutLimitInputs(activePreferences) {
layoutLimitInputs.forEach((input) => {
const key = input.dataset.layoutLimit;
if (!layoutLimitKeys.includes(key)) {
return;
}
const limits = getPanelLimitRange(key);
input.min = String(limits.min);
input.max = String(limits.max);
input.step = key === "treeSidebarColumnWidth" ? "5" : "10";
input.value = String(activePreferences[key]);
input.disabled = !panelFeatures.allowLayoutLimitsChange;
const output = document.querySelector(`[data-layout-limit-value="${key}"]`);
if (output) {
output.textContent = String(activePreferences[key]);
}
});
}
function syncCatalogColumnInputs(activePreferences) {
catalogColumnInputs.forEach((input) => {
const key = input.dataset.catalogColumns;
if (!catalogColumnKeys.includes(key)) {
return;
}
const limits = getPanelLimitRange(key);
input.min = String(limits.min);
input.max = String(limits.max);
input.step = "1";
input.value = String(activePreferences[key]);
input.disabled = !panelFeatures.allowCatalogColumnsChange;
const output = document.querySelector(`[data-catalog-columns-value="${key}"]`);
if (output) {
output.textContent = String(activePreferences[key]);
}
});
}
function getPanelLimitRange(key) {
return getLayoutLimitRange(key);
}
function clampCatalogColumn(key, value) {
const limits = getPanelLimitRange(key);
const fallback = panelDefaults[key] ?? defaultPreferences[key];
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return fallback;
}
return clamp(Math.round(numeric), limits.min, limits.max);
}
function getSidebarMinWidth(nextPreferences) {
return Number(nextPreferences.sidebarMinWidth);
}
function getSidebarMaxWidth(nextPreferences) {
return nextPreferences.treeTwoColumn ? 760 : 560;
const columns = getTreeColumns(nextPreferences);
if (columns <= 1) {
return Number(nextPreferences.sidebarMaxWidth);
}
return Math.min(
Number(nextPreferences.sidebarMaxWidthWide),
Number(nextPreferences.treeSidebarBaseWidth) +
columns * Number(nextPreferences.treeSidebarColumnWidth),
);
}
function getShellMaxWidth(nextPreferences) {
return Number(nextPreferences.shellMaxWidth);
}
function getTopBarMinHeight(nextPreferences) {
return Number(nextPreferences.topBarMinHeight);
}
function getTopBarMaxHeight(nextPreferences) {
return Math.min(
Number(nextPreferences.topBarMaxHeight),
Math.round(window.innerHeight * 0.75),
);
}
// ============================================================================

View File

@ -24,8 +24,13 @@
--card-x: 50%;
--card-y: 50%;
--sidebar-width: 390px;
--shell-max-width: 1560px;
--sidebar-max-width: 560px;
--top-bar-height: 140px;
--shell-max-width: 1920px;
--sidebar-max-width: 800px;
--catalog-cards-columns: 3;
--catalog-list-columns: 1;
--catalog-compact-cards-columns: 5;
--catalog-compact-list-columns: 3;
}
/* Светлая тема переопределяет только токены, структура стилей остается общей. */
@ -138,11 +143,15 @@ a {
/* Левая колонка получает ширину из --sidebar-width, которую меняет JS. */
.app-layout {
display: grid;
grid-template-columns: var(--sidebar-width) 8px minmax(0, 1fr);
gap: 8px;
grid-template-columns: var(--sidebar-width) 6px minmax(0, 1fr);
gap: 4px;
align-items: start;
}
html[data-nav-layout="sidebar"][data-show-resize-handles="false"] .app-layout {
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
}
/* Компактная шапка: название + поиск, без дерева и resizer. */
html[data-nav-layout="top"] .app-layout,
.app-layout[data-nav-layout="top"] {
@ -155,17 +164,22 @@ html[data-nav-layout="top"] .sidebar-resizer {
html[data-nav-layout="top"] .side-panel {
position: static;
display: flex;
flex-direction: column;
gap: 0;
}
html[data-nav-layout="top"] .side-panel .hero-content {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
align-items: flex-start;
align-content: flex-start;
justify-content: flex-start;
gap: 12px 16px;
max-height: none;
min-height: auto;
overflow: visible;
height: var(--top-bar-height);
min-height: var(--top-bar-height);
max-height: var(--top-bar-height);
overflow: auto;
padding: 14px 58px 14px 18px;
}
@ -173,6 +187,13 @@ html[data-nav-layout="top"] .side-panel .hero-content > .eyebrow {
margin: 0;
flex: 0 1 auto;
max-width: min(100%, 360px);
align-self: center;
padding-right: 0;
}
.side-panel .hero-content > .eyebrow#page-title {
padding-right: 46px;
line-height: 1.35;
}
html[data-nav-layout="top"] .search-field {
@ -181,22 +202,31 @@ html[data-nav-layout="top"] .search-field {
max-width: min(100%, 560px);
}
html[data-nav-layout="top"] .nav-tree {
html[data-show-tree="false"] .nav-tree {
display: none;
}
/* ==========================================================================
04. Sidebar Resizer
--------------------------------------------------------------------------
Узкая колонка между меню и каталогом. Pointer Events в script.js двигают
CSS-переменную --sidebar-width и сохраняют значение в localStorage.
========================================================================== */
html[data-nav-layout="top"][data-show-tree="true"] .nav-tree {
flex: 1 1 100%;
width: 100%;
max-height: none;
min-height: 0;
overflow: auto;
margin-top: 4px;
padding-top: 10px;
border-top: 1px solid var(--border);
}
html[data-show-tree="false"] .side-panel .hero-content {
max-height: none;
}
.sidebar-resizer {
position: sticky;
top: 20px;
width: 8px;
width: 6px;
height: calc(100vh - 40px);
margin: 0 -1px;
border-radius: 999px;
cursor: col-resize;
background: transparent;
@ -224,6 +254,49 @@ body.is-resizing-sidebar {
user-select: none;
}
/* ==========================================================================
04b. Top Bar Resizer
--------------------------------------------------------------------------
Горизонтальная ручка под шапкой в режиме navLayout: top.
========================================================================== */
.top-bar-resizer {
display: none;
width: 100%;
height: 8px;
margin-top: 8px;
margin-bottom: 6px;
border-radius: 999px;
cursor: row-resize;
background: transparent;
transition: background 160ms ease;
flex: 0 0 auto;
}
html[data-nav-layout="top"] .top-bar-resizer:not([hidden]) {
display: block;
}
.top-bar-resizer::before {
content: "";
display: block;
width: 100%;
height: 2px;
margin: 3px auto;
border-radius: inherit;
background: var(--border);
}
.top-bar-resizer:hover::before,
body.is-resizing-top-bar .top-bar-resizer::before {
background: var(--accent);
}
body.is-resizing-top-bar {
cursor: row-resize;
user-select: none;
}
/* ==========================================================================
05. Sidebar Panel
--------------------------------------------------------------------------
@ -308,35 +381,16 @@ html:not([data-show-numbers="true"]) .item-number {
display: none;
}
/* Теги в дереве управляются toggle-настройкой "Показывать теги дерева". */
html[data-show-tree-tags="false"] .tree-node small {
/* Теги и тип в дереве управляются настройками showTreeTags / showTreeType. */
html[data-show-tree-tags="false"] .tree-tags {
display: none;
}
/* Двухколоночный режим элементов внутри папок дерева. */
html[data-tree-two-column="true"] {
--shell-max-width: 1920px;
--sidebar-max-width: 760px;
html[data-show-tree-type="false"] .tree-file-type {
display: none;
}
html[data-tree-two-column="true"] .tree-items {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 2px 8px;
}
html[data-tree-two-column="true"] .tree-node.tree-file {
grid-template-columns: 20px minmax(0, 1fr);
min-height: 24px;
font-size: 0.8rem;
}
html[data-tree-two-column="true"] .tree-node-icon {
width: 16px;
height: 16px;
border-radius: 4px;
}
html[data-tree-two-column="true"] .tree-node.tree-file small {
html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta {
display: none;
}
@ -440,13 +494,13 @@ html[data-tree-two-column="true"] .tree-node.tree-file small {
display: flex;
justify-content: space-between;
gap: 14px;
align-items: flex-start;
align-items: center;
}
.settings-panel-head h2 {
margin: 4px 0 0;
font-size: 1.35rem;
letter-spacing: -0.04em;
.settings-panel-eyebrow {
margin: 0;
flex: 1 1 auto;
min-width: 0;
}
.settings-close {
@ -463,7 +517,8 @@ html[data-tree-two-column="true"] .tree-node.tree-file small {
gap: 10px;
}
.settings-section[hidden] {
.settings-section[hidden],
.settings-section-collapsible[hidden] {
display: none;
}
@ -483,6 +538,98 @@ html[data-tree-two-column="true"] .tree-node.tree-file small {
line-height: 1.45;
}
.settings-section-hint code {
font-size: 0.78rem;
}
.catalog-columns-group {
display: grid;
gap: 8px;
}
.settings-section-collapsible {
border: 1px solid var(--border);
border-radius: 18px;
padding: 10px 12px;
background: var(--surface);
}
.settings-section-collapsible > summary {
list-style: none;
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}
.settings-section-collapsible > summary::-webkit-details-marker {
display: none;
}
.settings-section-summary .settings-section-title {
margin: 0;
}
.settings-section-collapsible:not([open]) > .settings-section-summary .collapse-indicator {
transform: rotate(-90deg);
}
.settings-section-body {
display: grid;
gap: 10px;
padding-top: 10px;
}
.layout-limit-row {
position: relative;
grid-template-columns: minmax(128px, 1.5fr) minmax(0, 2fr) auto;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 14px;
background: rgba(255, 255, 255, 0.02);
cursor: help;
}
.layout-limit-row[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
z-index: 30;
left: 8px;
right: 8px;
bottom: calc(100% + 6px);
top: auto;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--panel-strong);
color: var(--text);
font-size: 0.78rem;
font-weight: 500;
line-height: 1.45;
box-shadow: var(--shadow);
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: translateY(4px);
transition:
opacity 140ms ease,
visibility 140ms ease,
transform 140ms ease;
}
.layout-limit-row[data-tooltip]:hover::after,
.layout-limit-row[data-tooltip]:focus-within::after {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.layout-limit-row span {
line-height: 1.25;
}
/* ==========================================================================
07. Search Field And Control Inputs
--------------------------------------------------------------------------
@ -936,13 +1083,91 @@ html[data-theme="light"] .accent-swatch-custom {
}
.tree-file {
grid-template-columns: 20px minmax(0, 1fr) auto;
grid-template-columns: 20px minmax(0, 1fr);
align-items: start;
gap: 8px;
}
.tree-file::before {
content: none;
}
.tree-file-body {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.tree-file-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tree-file-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
min-width: 0;
}
html[data-tree-meta-layout="inline"] .tree-file-body {
flex-direction: row;
align-items: center;
gap: 8px;
}
html[data-tree-meta-layout="inline"] .tree-file-title {
flex: 1 1 auto;
}
html[data-tree-meta-layout="inline"] .tree-file-meta {
flex: 0 1 auto;
justify-content: flex-end;
}
html[data-tree-meta-layout="stacked"] .tree-file-body {
align-items: stretch;
}
.tree-file-type {
margin: 0;
color: var(--accent);
font-size: 0.72rem;
font-weight: 750;
white-space: nowrap;
}
.tree-tags.tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 0;
}
.tree-tags.tags span {
border: 1px solid var(--border);
border-radius: 999px;
padding: 2px 6px;
background: rgba(var(--accent-rgb), 0.1);
color: var(--muted);
font-size: 0.68rem;
font-weight: 800;
}
html:not([data-tree-columns="1"]) .tree-file {
font-size: 0.8rem;
}
html:not([data-tree-columns="1"]) .tree-node-icon {
width: 16px;
height: 16px;
border-radius: 4px;
}
.tree-node-icon {
width: 18px;
height: 18px;
@ -955,7 +1180,8 @@ html[data-theme="light"] .accent-swatch-custom {
.tree-items {
position: relative;
display: grid;
gap: 1px;
grid-template-columns: repeat(var(--tree-columns, 1), minmax(0, 1fr));
gap: 2px 8px;
margin-left: 18px;
padding-left: 10px;
border-left: 1px solid var(--border);
@ -967,17 +1193,6 @@ html[data-theme="light"] .accent-swatch-custom {
background: rgba(var(--accent-rgb), 0.14);
}
/* Теги дерева усечены ellipsis, чтобы длинные списки не ломали sidebar. */
.tree-node small {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--accent);
font-size: 0.68rem;
font-weight: 800;
}
/* ==========================================================================
09. Catalog Structure
--------------------------------------------------------------------------
@ -1155,11 +1370,14 @@ html[data-theme="light"] .accent-swatch-custom {
.items-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
padding: 10px;
}
.catalog.cards-view .items-grid {
grid-template-columns: repeat(var(--catalog-cards-columns, 3), minmax(0, 1fr));
}
/* Базовая карточка с иконкой, контентом, тегами, стрелкой и shine-слоем. */
.nav-card {
--tilt-x: 0deg;
@ -1321,7 +1539,7 @@ html[data-theme="light"] .accent-swatch-custom {
========================================================================== */
.catalog.list-view .items-grid {
grid-template-columns: 1fr;
grid-template-columns: repeat(var(--catalog-list-columns, 1), minmax(0, 1fr));
}
/* В list-режимах карточка вытягивается в строку. */
@ -1342,12 +1560,12 @@ html[data-theme="light"] .accent-swatch-custom {
/* Compact modes скрывают второстепенную информацию и уменьшают отступы. */
.catalog.compact-cards-view .items-grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-template-columns: repeat(var(--catalog-compact-cards-columns, 5), minmax(0, 1fr));
}
/* Compact list на широких экранах использует 3 колонки. */
/* Compact list на широких экранах использует настраиваемое число колонок. */
.catalog.compact-list-view .items-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(var(--catalog-compact-list-columns, 3), minmax(0, 1fr));
}
.catalog.compact-cards-view .section-head,
@ -1466,15 +1684,15 @@ html[data-theme="light"] .accent-swatch-custom {
/* Узкое окно на устройствах с мышью: sidebar и сетка карточек чуть компактнее. */
@media (hover: hover) and (pointer: fine) and (max-width: 1120px) {
.app-layout {
grid-template-columns: minmax(240px, min(var(--sidebar-width), 320px)) 8px minmax(0, 1fr);
grid-template-columns: minmax(240px, min(var(--sidebar-width), 320px)) 6px minmax(0, 1fr);
}
.items-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
.catalog.cards-view .items-grid {
grid-template-columns: repeat(min(var(--catalog-cards-columns, 3), 2), minmax(0, 1fr));
}
.catalog.compact-cards-view .items-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(min(var(--catalog-compact-cards-columns, 5), 3), minmax(0, 1fr));
}
}
@ -1489,7 +1707,8 @@ html[data-theme="light"] .accent-swatch-custom {
grid-template-columns: 1fr;
}
.sidebar-resizer {
.sidebar-resizer,
.top-bar-resizer {
display: none;
}
@ -1509,21 +1728,6 @@ html[data-theme="light"] .accent-swatch-custom {
overflow: auto;
}
/*
Двухколоночное дерево на touch не отключается полностью:
- в portrait — всегда, если включена настройка;
- в landscape — начиная с 520px ширины viewport.
*/
html[data-tree-two-column="true"] .tree-items {
grid-template-columns: 1fr;
}
@media (min-width: 520px), (orientation: portrait) {
html[data-tree-two-column="true"] .tree-items {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.control-panel,
.catalog-section,
.empty-state {
@ -1567,17 +1771,24 @@ html[data-theme="light"] .accent-swatch-custom {
padding: 8px;
}
.items-grid {
grid-template-columns: 1fr;
.catalog.cards-view .items-grid {
grid-template-columns: repeat(min(var(--catalog-cards-columns, 3), 1), minmax(0, 1fr));
padding: 8px;
}
.catalog.list-view .items-grid {
grid-template-columns: repeat(min(var(--catalog-list-columns, 1), 1), minmax(0, 1fr));
padding: 8px;
}
.catalog.compact-list-view .items-grid {
grid-template-columns: 1fr;
grid-template-columns: repeat(min(var(--catalog-compact-list-columns, 3), 1), minmax(0, 1fr));
padding: 8px;
}
.catalog.compact-cards-view .items-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(min(var(--catalog-compact-cards-columns, 5), 2), minmax(0, 1fr));
padding: 8px;
}
.nav-card {

View File

@ -57,6 +57,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>
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
@ -82,10 +92,7 @@
-->
<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>
@ -104,10 +111,111 @@
</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>
</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>
@ -128,6 +236,34 @@
</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>
@ -141,25 +277,38 @@
<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>

View File

@ -1,113 +1,7 @@
{
"title": "Панель экосистемы",
"sections": [
{
"id": "user",
"number": "1",
"title": "Интерфейс пользователя",
"description": "Рабочие инструменты пользователя для фильтрации и обработки сообщений.",
"groups": [
{
"id": "message-filtering",
"number": "1.1",
"title": "Управление фильтрацией сообщений",
"description": "Правила, задания, события и статистика фильтрации.",
"items": [
{ "number": "1.1.1", "title": "Редактирование правил", "type": "rules", "tags": ["правила", "фильтрация", "редактор"], "url": "#" },
{ "number": "1.1.2", "title": "Управление заданиями", "type": "redmine", "tags": ["redmine", "задачи", "задания"], "url": "#" },
{ "number": "1.1.3", "title": "Применение правил", "type": "rules", "tags": ["правила", "применение", "фильтрация"], "url": "#" },
{ "number": "1.1.4", "title": "Просмотр статистики", "type": "grafana", "tags": ["grafana", "статистика", "дашборд"], "url": "#" },
{ "number": "1.1.5", "title": "Управление событиями фильтрации", "type": "grafana", "tags": ["grafana", "события", "алерты"], "url": "#" },
{ "number": "1.1.6", "title": "Просмотр журнала событий фильтрации", "type": "grafana", "tags": ["grafana", "журнал", "события"], "url": "#" }
]
},
{
"id": "message-processing",
"number": "1.2",
"title": "Обработка сообщений",
"description": "Просмотр, оформление и доступ к хранилищам сообщений.",
"items": [
{ "number": "1.2.1", "title": "Просмотр и оформление", "type": "messages", "tags": ["сообщения", "просмотр", "оформление"], "url": "#" },
{ "number": "1.2.2", "title": "Прямой доступ", "type": "ceph", "tags": ["ceph", "доступ", "объекты"], "url": "#" },
{ "number": "1.2.3", "title": "S3 Хранилище", "type": "ceph", "tags": ["ceph", "s3", "хранилище"], "url": "#" }
]
}
]
},
{
"id": "admin",
"number": "2",
"title": "Интерфейс администратора",
"description": "Администрирование сервисов, кластеров, мониторинга и аппаратной части.",
"groups": [
{
"id": "service-monitoring",
"number": "2.1",
"title": "Управление и мониторинг сервисов",
"description": "Кластер, модули, события мониторинга и журналы ошибок.",
"items": [
{ "number": "2.1.1", "title": "Управление кластером", "type": "kubernetes", "tags": ["kubernetes", "кластер", "k8s"], "url": "#" },
{ "number": "2.1.2", "title": "Мониторинг состояния модулей", "type": "grafana", "tags": ["grafana", "модули", "мониторинг"], "url": "#" },
{ "number": "2.1.3", "title": "Управление событиями мониторинга", "type": "grafana", "tags": ["grafana", "события", "алерты"], "url": "#" },
{ "number": "2.1.4", "title": "Просмотр журнала событий мониторинга", "type": "grafana", "tags": ["grafana", "журнал", "мониторинг"], "url": "#" },
{ "number": "2.1.5", "title": "Просмотр журналов ошибок", "type": "opensearch", "tags": ["opensearch", "ошибки", "логи"], "url": "#" }
]
},
{
"id": "service-configuration",
"number": "2.2",
"title": "Настройка сервисов",
"description": "Пользователи, очереди, xPath, модули и конфигурации.",
"items": [
{ "number": "2.2.1", "title": "Управление пользователями и очередями", "type": "rabbitmq", "tags": ["пользователи", "очереди", "rabbitmq"], "url": "#" },
{ "number": "2.2.2", "title": "Отладка xPath-запросов", "type": "xpath", "tags": ["xpath", "отладка", "запросы"], "url": "#" },
{ "number": "2.2.3", "title": "Конфигурирование модулей", "type": "config", "tags": ["конфигурация", "модули", "настройки"], "url": "#" },
{ "number": "2.2.4", "title": "Просмотр журнала конфигурирования", "type": "gitea", "tags": ["gitea", "журнал", "конфигурация"], "url": "#" },
{ "number": "2.2.5", "title": "Прямой доступ к конфигурации", "type": "gitea", "tags": ["gitea", "конфигурация", "git"], "url": "#" }
]
},
{
"id": "hardware",
"number": "2.3",
"title": "Управление и мониторинг аппаратной части",
"description": "Сенсоры, ресурсы и прямые панели серверов.",
"items": [
{ "number": "2.3.1", "title": "Мониторинг показаний сенсоров", "type": "grafana", "tags": ["grafana", "сенсоры", "железо"], "url": "#" },
{ "number": "2.3.2", "title": "Мониторинг потребления ресурсов", "type": "grafana", "tags": ["grafana", "ресурсы", "метрики"], "url": "#" },
{ "number": "2.3.3", "title": "СЕРВЕР01", "type": "ipmi", "tags": ["ipmi", "сервер01", "железо"], "url": "#" },
{ "number": "2.3.4", "title": "СЕРВЕР02", "type": "ipmi", "tags": ["ipmi", "сервер02", "железо"], "url": "#" },
{ "number": "2.3.5", "title": "СЕРВЕР03", "type": "ipmi", "tags": ["ipmi", "сервер03", "железо"], "url": "#" },
{ "number": "2.3.6", "title": "СЕРВЕР04", "type": "ipmi", "tags": ["ipmi", "сервер04", "железо"], "url": "#" },
{ "number": "2.3.7", "title": "СЕРВЕР05", "type": "ipmi", "tags": ["ipmi", "сервер05", "железо"], "url": "#" }
]
},
{
"id": "service-panels",
"number": "2.4",
"title": "Настройка отдельных сервисов",
"description": "Панели управления инфраструктурными сервисами.",
"items": [
{ "number": "2.4.1", "title": "Панель управления LDAP Account Manager", "type": "ldap", "tags": ["ldap", "accounts", "пользователи"], "url": "#" },
{ "number": "2.4.2", "title": "Панель управления кластера RabbitMQ", "type": "rabbitmq", "tags": ["rabbitmq", "кластер", "очереди"], "url": "#" },
{ "number": "2.4.3", "title": "Панель управления кластера Ceph", "type": "ceph", "tags": ["ceph", "кластер", "storage"], "url": "#" },
{ "number": "2.4.4", "title": "Панель управления Prometheus", "type": "prometheus", "tags": ["prometheus", "метрики", "мониторинг"], "url": "#" },
{ "number": "2.4.5", "title": "Панель управления XMPP-сервера", "type": "xmpp", "tags": ["xmpp", "чат", "сервер"], "url": "#" },
{ "number": "2.4.6", "title": "Панель управления PostgreSQL", "type": "postgresql", "tags": ["postgresql", "база", "db"], "url": "#" },
{ "number": "2.4.7", "title": "Подключение к БД", "type": "cloudbeaver", "tags": ["cloudbeaver", "database", "sql"], "url": "#" }
]
},
{
"id": "packages",
"number": "2.5",
"title": "Образы и пакеты",
"description": "Реестры образов и deb-пакетов.",
"items": [
{ "number": "2.5.1", "title": "Реестр docker-образов", "type": "gitea", "tags": ["gitea", "docker", "registry"], "url": "#" },
{ "number": "2.5.2", "title": "Репозиторий deb-пакетов", "type": "gitea", "tags": ["gitea", "deb", "packages"], "url": "#" }
]
}
]
}
],
"serviceMeta": {}
}

635
script.js
View File

@ -32,9 +32,16 @@ let ecosystemSections = [];
let panelTitle = "Панель экосистемы";
const defaultServiceMeta = {
analytics: { label: "Аналитика", icon: "assets/analytics.svg" },
api: { label: "API", icon: "assets/api.svg" },
calendar: { label: "Календарь", icon: "assets/calendar.svg" },
ceph: { label: "Ceph", icon: "assets/ceph.svg" },
chat: { label: "Беседа", icon: "assets/chat.svg" },
cicd: { label: "CI/CD", icon: "assets/cicd.svg" },
cloudbeaver: { label: "CloudBeaver", icon: "assets/cloudbeaver.svg" },
config: { label: "Config", icon: "assets/config.svg" },
drive: { label: "Диск", icon: "assets/drive.svg" },
forum: { label: "Форум", icon: "assets/forum.svg" },
gitea: { label: "Gitea", icon: "assets/gitea.svg" },
grafana: { label: "Grafana", icon: "assets/grafana.svg" },
ipmi: { label: "IPMI", icon: "assets/server.svg" },
@ -47,6 +54,11 @@ const defaultServiceMeta = {
rabbitmq: { label: "RabbitMQ", icon: "assets/rabbitmq.svg" },
redmine: { label: "Redmine", icon: "assets/redmine.svg" },
rules: { label: "Rules", icon: "assets/rules.svg" },
social: { label: "Соцсеть", icon: "assets/social.svg" },
vault: { label: "Vault", icon: "assets/vault.svg" },
video: { label: "Видео", icon: "assets/video.svg" },
vpn: { label: "VPN", icon: "assets/vpn.svg" },
wiki: { label: "Wiki", icon: "assets/wiki.svg" },
xpath: { label: "xPath", icon: "assets/xpath.svg" },
xmpp: { label: "XMPP", icon: "assets/xmpp.svg" },
};
@ -65,26 +77,84 @@ const defaultPreferences = {
customAccentTo: "#22d3ee",
customAccentAngle: 135,
navLayout: "sidebar",
showTree: true,
showResizeHandles: true,
showNumbers: false,
showTreeTags: true,
treeTwoColumn: false,
showTreeType: false,
treeMetaLayout: "inline",
treeColumns: 2,
autoCollapseSearch: true,
sidebarWidth: 390,
topBarHeight: 140,
shellMaxWidth: 2800,
sidebarMinWidth: 260,
sidebarMaxWidth: 800,
sidebarMaxWidthWide: 1400,
treeSidebarBaseWidth: 420,
treeSidebarColumnWidth: 120,
topBarMinHeight: 80,
topBarMaxHeight: 560,
catalogCardsColumns: 3,
catalogListColumns: 1,
catalogCompactCardsColumns: 5,
catalogCompactListColumns: 3,
searchPlaceholder: "Grafana, S3, журнал, сервер...",
};
const defaultPanelFeatures = {
allowNavLayoutChange: true,
allowShowTreeChange: true,
allowSearch: true,
allowViewChange: true,
allowCatalogColumnsChange: true,
allowShowNumbers: true,
allowTreeOptions: true,
allowSidebarResize: true,
allowLayoutLimitsChange: true,
allowThemeChange: true,
allowAccentChange: true,
};
const defaultPanelLimits = {
shellMaxWidth: { min: 1200, max: 3840 },
sidebarMinWidth: { min: 200, 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 },
};
const layoutLimitKeys = [
"shellMaxWidth",
"sidebarMinWidth",
"sidebarMaxWidth",
"sidebarMaxWidthWide",
"treeSidebarBaseWidth",
"treeSidebarColumnWidth",
"topBarMinHeight",
"topBarMaxHeight",
];
const catalogColumnKeys = [
"catalogCardsColumns",
"catalogListColumns",
"catalogCompactCardsColumns",
"catalogCompactListColumns",
];
const panelLimitKeys = [...layoutLimitKeys, ...catalogColumnKeys];
const availableNavLayouts = ["sidebar", "top"];
const availableTreeMetaLayouts = ["inline", "stacked"];
const MIN_TREE_COLUMNS = 1;
const MAX_TREE_COLUMNS = 6;
const availableViews = ["cards", "list", "compact-cards", "compact-list"];
const availableAccents = ["violet", "cyan", "lime", "amber", "custom"];
@ -94,6 +164,7 @@ const availableAccents = ["violet", "cyan", "lime", "amber", "custom"];
// preferences - текущие настройки интерфейса; итоговые значения собираются в bootstrap().
let panelDefaults = { ...defaultPreferences };
let panelFeatures = { ...defaultPanelFeatures };
let panelLimits = structuredClone(defaultPanelLimits);
let preferences = { ...defaultPreferences };
// collapseState - объект вида { [collapseKey]: boolean } для details.
@ -104,6 +175,7 @@ const appLayout = document.querySelector(".app-layout");
const catalog = document.querySelector("[data-catalog-root]");
const navTree = document.querySelector("[data-nav-tree]");
const sidebarResizer = document.querySelector("[data-sidebar-resizer]");
const topBarResizer = document.querySelector("[data-top-bar-resizer]");
const searchField = document.querySelector("[data-search-field]");
const searchInput = document.querySelector("[data-search-input]");
const settingsToggle = document.querySelector("[data-settings-toggle]");
@ -120,10 +192,17 @@ const customAccentToInput = document.querySelector("[data-custom-accent-to]");
const customAccentAngleInput = document.querySelector("[data-custom-accent-angle]");
const customAccentAngleValue = document.querySelector("[data-custom-accent-angle-value]");
const customAccentPreview = document.querySelector("[data-custom-accent-preview]");
const showTreeInput = document.querySelector("[data-show-tree]");
const showResizeHandlesInput = document.querySelector("[data-show-resize-handles]");
const showNumbersInput = document.querySelector("[data-show-numbers]");
const showTreeTagsInput = document.querySelector("[data-show-tree-tags]");
const treeTwoColumnInput = document.querySelector("[data-tree-two-column]");
const showTreeTypeInput = document.querySelector("[data-show-tree-type]");
const treeColumnsInput = document.querySelector("[data-tree-columns]");
const treeColumnsValue = document.querySelector("[data-tree-columns-value]");
const treeMetaLayoutButtons = document.querySelectorAll("[data-tree-meta-layout]");
const autoCollapseSearchInput = document.querySelector("[data-auto-collapse-search]");
const layoutLimitInputs = document.querySelectorAll("[data-layout-limit]");
const catalogColumnInputs = document.querySelectorAll("[data-catalog-columns]");
const settingsSections = document.querySelectorAll("[data-settings-section]");
const emptyTemplate = document.querySelector("#empty-template");
const configErrorTemplate = document.querySelector("#config-error-template");
@ -236,14 +315,53 @@ navLayoutButtons.forEach((button) => {
preferences.navLayout = button.dataset.navLayout;
applyPreferences(preferences);
if (preferences.navLayout === "sidebar") {
if (isTreeVisible(preferences)) {
renderNavigationTree();
} else {
navTree.replaceChildren();
}
savePreferences(preferences);
});
});
function setShowTree(nextValue, { persist = true } = {}) {
if (!panelFeatures.allowShowTreeChange) {
return;
}
preferences.showTree = Boolean(nextValue);
applyPreferences(preferences);
if (preferences.showTree) {
renderNavigationTree();
} else {
navTree.replaceChildren();
}
if (persist) {
savePreferences(preferences);
}
}
if (showTreeInput) {
showTreeInput.addEventListener("change", () => {
setShowTree(showTreeInput.checked);
});
}
if (showResizeHandlesInput) {
showResizeHandlesInput.addEventListener("change", () => {
if (!panelFeatures.allowSidebarResize) {
return;
}
preferences.showResizeHandles = showResizeHandlesInput.checked;
applyPreferences(preferences);
savePreferences(preferences);
});
}
// Переключение режима отображения каталога.
viewButtons.forEach((button) => {
button.addEventListener("click", () => {
@ -300,41 +418,62 @@ showNumbersInput.addEventListener("change", () => {
});
// Показывает/скрывает поисковые теги в файловом дереве.
// Взаимоисключимо с двухколоночным режимом элементов папок.
showTreeTagsInput.addEventListener("change", () => {
if (!panelFeatures.allowTreeOptions || preferences.navLayout === "top") {
if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) {
return;
}
preferences.showTreeTags = showTreeTagsInput.checked;
if (preferences.showTreeTags) {
preferences.treeTwoColumn = false;
}
applyPreferences(preferences);
renderNavigationTree();
savePreferences(preferences);
});
// Компактный двухколоночный режим элементов внутри папок дерева.
// Взаимоисключимо с отображением тегов.
treeTwoColumnInput.addEventListener("change", () => {
if (!panelFeatures.allowTreeOptions || preferences.navLayout === "top") {
showTreeTypeInput.addEventListener("change", () => {
if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) {
return;
}
preferences.treeTwoColumn = treeTwoColumnInput.checked;
if (preferences.treeTwoColumn) {
preferences.showTreeTags = false;
if (preferences.sidebarWidth < 480) {
preferences.sidebarWidth = 480;
}
preferences.showTreeType = showTreeTypeInput.checked;
applyPreferences(preferences);
renderNavigationTree();
savePreferences(preferences);
});
function handleTreeColumnsChange() {
if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) {
return;
}
preferences.treeColumns = getTreeColumns({ treeColumns: treeColumnsInput.value });
if (preferences.treeColumns > 1 && preferences.sidebarWidth < getSidebarMinWidth(preferences) + preferences.treeColumns * 70) {
preferences.sidebarWidth = getSidebarMinWidth(preferences) + preferences.treeColumns * 70;
}
applyPreferences(preferences);
savePreferences(preferences);
}
if (treeColumnsInput) {
treeColumnsInput.addEventListener("input", handleTreeColumnsChange);
}
treeMetaLayoutButtons.forEach((button) => {
button.addEventListener("click", () => {
if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) {
return;
}
preferences.treeMetaLayout = button.dataset.treeMetaLayout;
applyPreferences(preferences);
savePreferences(preferences);
});
});
// Включает/выключает авто-раскрытие веток дерева при поиске.
autoCollapseSearchInput.addEventListener("change", () => {
if (!panelFeatures.allowTreeOptions || preferences.navLayout === "top") {
if (!panelFeatures.allowTreeOptions || !isTreeVisible(preferences)) {
return;
}
@ -344,10 +483,38 @@ autoCollapseSearchInput.addEventListener("change", () => {
savePreferences(preferences);
});
layoutLimitInputs.forEach((input) => {
input.addEventListener("input", () => {
if (!panelFeatures.allowLayoutLimitsChange) {
return;
}
const key = input.dataset.layoutLimit;
preferences[key] = clampLayoutPreference(key, input.value);
preferences = normalizePreferences(preferences);
applyPreferences(preferences);
savePreferences(preferences);
});
});
catalogColumnInputs.forEach((input) => {
input.addEventListener("input", () => {
if (!panelFeatures.allowCatalogColumnsChange) {
return;
}
const key = input.dataset.catalogColumns;
preferences[key] = clampCatalogColumn(key, input.value);
preferences = normalizePreferences(preferences);
applyPreferences(preferences);
savePreferences(preferences);
});
});
// Drag-resize бокового меню.
// Ширина сохраняется в preferences.sidebarWidth и применяется через CSS-переменную.
sidebarResizer.addEventListener("pointerdown", (event) => {
if (preferences.navLayout === "top" || !panelFeatures.allowSidebarResize) {
if (preferences.navLayout === "top" || !isResizeVisible(preferences)) {
return;
}
@ -358,7 +525,7 @@ sidebarResizer.addEventListener("pointerdown", (event) => {
const handlePointerMove = (moveEvent) => {
const layoutLeft = document.querySelector(".app-layout").getBoundingClientRect().left;
preferences.sidebarWidth = Math.round(
clamp(moveEvent.clientX - layoutLeft, 260, getSidebarMaxWidth(preferences)),
clamp(moveEvent.clientX - layoutLeft, getSidebarMinWidth(preferences), getSidebarMaxWidth(preferences)),
);
applyPreferences(preferences);
};
@ -376,6 +543,38 @@ sidebarResizer.addEventListener("pointerdown", (event) => {
sidebarResizer.addEventListener("pointercancel", handlePointerUp);
});
// Drag-resize верхней панели в режиме navLayout: top.
topBarResizer?.addEventListener("pointerdown", (event) => {
if (preferences.navLayout !== "top" || !isResizeVisible(preferences)) {
return;
}
event.preventDefault();
document.body.classList.add("is-resizing-top-bar");
topBarResizer.setPointerCapture(event.pointerId);
const panelTop = document.querySelector(".side-panel")?.getBoundingClientRect().top ?? 0;
const handlePointerMove = (moveEvent) => {
preferences.topBarHeight = Math.round(
clamp(moveEvent.clientY - panelTop, getTopBarMinHeight(preferences), getTopBarMaxHeight(preferences)),
);
applyPreferences(preferences);
};
const handlePointerUp = () => {
document.body.classList.remove("is-resizing-top-bar");
savePreferences(preferences);
topBarResizer.removeEventListener("pointermove", handlePointerMove);
topBarResizer.removeEventListener("pointerup", handlePointerUp);
topBarResizer.removeEventListener("pointercancel", handlePointerUp);
};
topBarResizer.addEventListener("pointermove", handlePointerMove);
topBarResizer.addEventListener("pointerup", handlePointerUp);
topBarResizer.addEventListener("pointercancel", handlePointerUp);
});
// ============================================================================
// Panel config loading
// ============================================================================
@ -395,6 +594,7 @@ async function loadPanelDataFromSettings(settings) {
}
function applyOrganizationConfig(settings) {
panelLimits = normalizePanelLimits(settings?.limits);
panelDefaults = normalizePreferences({
...defaultPreferences,
...(settings?.defaults && typeof settings.defaults === "object" ? settings.defaults : {}),
@ -438,29 +638,60 @@ function normalizePreferences(raw) {
360,
);
next.showNumbers = Boolean(next.showNumbers);
next.showTree = Boolean(next.showTree);
next.showResizeHandles = next.showResizeHandles !== false;
next.showTreeTags = Boolean(next.showTreeTags);
next.treeTwoColumn = Boolean(next.treeTwoColumn);
next.showTreeType = Boolean(next.showTreeType);
next.treeColumns = getTreeColumns(next);
next.treeMetaLayout = availableTreeMetaLayouts.includes(next.treeMetaLayout)
? next.treeMetaLayout
: defaultPreferences.treeMetaLayout;
next.autoCollapseSearch = Boolean(next.autoCollapseSearch);
normalizeLayoutPreferences(next);
normalizeCatalogColumns(next);
next.sidebarWidth = clamp(
Number(next.sidebarWidth) || defaultPreferences.sidebarWidth,
260,
getSidebarMinWidth(next),
getSidebarMaxWidth(next),
);
if (next.treeTwoColumn) {
next.showTreeTags = false;
} else if (next.showTreeTags) {
next.treeTwoColumn = false;
}
if (next.navLayout === "top") {
next.treeTwoColumn = false;
next.showTreeTags = false;
}
next.topBarHeight = clamp(
Number(next.topBarHeight) || defaultPreferences.topBarHeight,
getTopBarMinHeight(next),
getTopBarMaxHeight(next),
);
return next;
}
function normalizeLayoutPreferences(next) {
for (const key of layoutLimitKeys) {
next[key] = clampLayoutPreference(key, next[key]);
}
if (next.sidebarMinWidth > next.sidebarMaxWidth) {
next.sidebarMinWidth = next.sidebarMaxWidth;
}
if (next.sidebarMaxWidth > next.sidebarMaxWidthWide) {
next.sidebarMaxWidth = next.sidebarMaxWidthWide;
}
if (next.topBarMinHeight > next.topBarMaxHeight) {
next.topBarMinHeight = next.topBarMaxHeight;
}
const minShellWidth = getSidebarMaxWidth(next) + 320;
if (next.shellMaxWidth < minShellWidth) {
next.shellMaxWidth = clampLayoutPreference("shellMaxWidth", minShellWidth);
}
}
function normalizeCatalogColumns(next) {
for (const key of catalogColumnKeys) {
next[key] = clampCatalogColumn(key, next[key]);
}
}
function enforceFeatureLocks(rawPreferences) {
const next = normalizePreferences(rawPreferences);
@ -468,6 +699,10 @@ function enforceFeatureLocks(rawPreferences) {
next.navLayout = panelDefaults.navLayout;
}
if (!panelFeatures.allowShowTreeChange) {
next.showTree = panelDefaults.showTree;
}
if (!panelFeatures.allowThemeChange) {
next.theme = panelDefaults.theme;
}
@ -476,6 +711,12 @@ function enforceFeatureLocks(rawPreferences) {
next.view = panelDefaults.view;
}
if (!panelFeatures.allowCatalogColumnsChange) {
for (const key of catalogColumnKeys) {
next[key] = panelDefaults[key];
}
}
if (!panelFeatures.allowAccentChange) {
next.accent = panelDefaults.accent;
next.customAccentFrom = panelDefaults.customAccentFrom;
@ -487,16 +728,33 @@ function enforceFeatureLocks(rawPreferences) {
next.showNumbers = panelDefaults.showNumbers;
}
if (!panelFeatures.allowTreeOptions || next.navLayout === "top") {
next.showTreeTags = false;
next.treeTwoColumn = false;
if (!panelFeatures.allowTreeOptions || !next.showTree) {
next.showTreeTags = panelDefaults.showTreeTags;
next.showTreeType = panelDefaults.showTreeType;
next.treeColumns = panelDefaults.treeColumns;
next.treeMetaLayout = panelDefaults.treeMetaLayout;
next.autoCollapseSearch = panelDefaults.autoCollapseSearch;
}
if (!panelFeatures.allowSidebarResize) {
next.showResizeHandles = panelDefaults.showResizeHandles;
}
if (!panelFeatures.allowSidebarResize || next.navLayout === "top") {
next.sidebarWidth = panelDefaults.sidebarWidth;
}
if (!panelFeatures.allowSidebarResize || next.navLayout !== "top") {
next.topBarHeight = panelDefaults.topBarHeight;
}
if (!panelFeatures.allowLayoutLimitsChange) {
for (const key of layoutLimitKeys) {
next[key] = panelDefaults[key];
}
normalizeLayoutPreferences(next);
}
return next;
}
@ -893,7 +1151,7 @@ function matchesText(values, query) {
// Строит левое файловое дерево из той же структуры данных, что и каталог.
// Это важно: дерево и карточки всегда остаются синхронизированными.
function renderNavigationTree() {
if (preferences.navLayout === "top") {
if (!isTreeVisible(preferences)) {
navTree.replaceChildren();
return;
}
@ -964,22 +1222,39 @@ function createTreeItemList(items, query = "", className = "tree-items") {
itemList.className = className;
items.forEach((item) => {
const meta = getItemMeta(item);
const itemButton = document.createElement("button");
itemButton.type = "button";
itemButton.className = "tree-node tree-file";
itemButton.dataset.focusTarget = getItemId(item);
itemButton.innerHTML = `
<img class="tree-node-icon" src="${resolveAssetPath(meta.icon)}" alt="" loading="lazy" />
<span>${highlightText(item.title, query)}</span>
<small>${highlightText(item.tags.join(", "), query)}</small>
`;
itemList.append(itemButton);
itemList.append(createTreeItemButton(item, query));
});
return itemList;
}
function createTreeItemButton(item, query = "") {
const meta = getItemMeta(item);
const itemButton = document.createElement("button");
itemButton.type = "button";
itemButton.className = "tree-node tree-file";
itemButton.dataset.focusTarget = getItemId(item);
itemButton.innerHTML = `
<img class="tree-node-icon" src="${resolveAssetPath(meta.icon)}" alt="" loading="lazy" />
<div class="tree-file-body">
<span class="tree-file-title">${highlightText(item.title, query)}</span>
<div class="tree-file-meta">
<span class="tree-file-type">${highlightText(meta.label, query)}</span>
<div class="tree-tags tags">${renderTreeTagSpans(item.tags, query)}</div>
</div>
</div>
`;
return itemButton;
}
function renderTreeTagSpans(tags, query = "") {
if (!Array.isArray(tags) || !tags.length) {
return "";
}
return tags.map((tag) => `<span>${highlightText(tag, query)}</span>`).join("");
}
// ============================================================================
// Focusing from tree to content
// ============================================================================
@ -1097,31 +1372,84 @@ function applyPreferences(nextPreferences) {
document.documentElement.dataset.accent = activePreferences.accent;
document.documentElement.dataset.navLayout =
activePreferences.navLayout === "top" ? "top" : "sidebar";
document.documentElement.dataset.showTree = String(isTreeVisible(activePreferences));
document.documentElement.dataset.showResizeHandles = String(isResizeVisible(activePreferences));
applyAccentVariables(activePreferences);
document.documentElement.style.setProperty(
"--sidebar-width",
`${clamp(
Number(activePreferences.sidebarWidth) || defaultPreferences.sidebarWidth,
260,
getSidebarMinWidth(activePreferences),
getSidebarMaxWidth(activePreferences),
)}px`,
);
document.documentElement.style.setProperty(
"--top-bar-height",
`${clamp(
Number(activePreferences.topBarHeight) || defaultPreferences.topBarHeight,
getTopBarMinHeight(activePreferences),
getTopBarMaxHeight(activePreferences),
)}px`,
);
const treeColumns = getTreeColumns(activePreferences);
const sidebarMaxWidth = getSidebarMaxWidth(activePreferences);
document.documentElement.style.setProperty(
"--shell-max-width",
activePreferences.treeTwoColumn ? "1920px" : "1560px",
`${getShellMaxWidth(activePreferences)}px`,
);
document.documentElement.style.setProperty("--sidebar-max-width", `${sidebarMaxWidth}px`);
document.documentElement.style.setProperty(
"--tree-sidebar-base-width",
`${activePreferences.treeSidebarBaseWidth}px`,
);
document.documentElement.style.setProperty(
"--sidebar-max-width",
`${getSidebarMaxWidth(activePreferences)}px`,
"--tree-sidebar-column-width",
`${activePreferences.treeSidebarColumnWidth}px`,
);
document.documentElement.style.setProperty(
"--catalog-cards-columns",
String(activePreferences.catalogCardsColumns),
);
document.documentElement.style.setProperty(
"--catalog-list-columns",
String(activePreferences.catalogListColumns),
);
document.documentElement.style.setProperty(
"--catalog-compact-cards-columns",
String(activePreferences.catalogCompactCardsColumns),
);
document.documentElement.style.setProperty(
"--catalog-compact-list-columns",
String(activePreferences.catalogCompactListColumns),
);
document.documentElement.style.setProperty("--tree-columns", String(treeColumns));
catalog.classList.remove("cards-view", "list-view", "compact-cards-view", "compact-list-view");
catalog.classList.add(`${activePreferences.view}-view`);
document.documentElement.dataset.showNumbers = String(Boolean(activePreferences.showNumbers));
document.documentElement.dataset.showTreeTags = String(Boolean(activePreferences.showTreeTags));
document.documentElement.dataset.treeTwoColumn = String(Boolean(activePreferences.treeTwoColumn));
document.documentElement.dataset.showTreeType = String(Boolean(activePreferences.showTreeType));
document.documentElement.dataset.treeMetaLayout = activePreferences.treeMetaLayout;
document.documentElement.dataset.treeColumns = String(treeColumns);
if (showTreeInput) {
showTreeInput.checked = Boolean(activePreferences.showTree);
}
if (showResizeHandlesInput) {
showResizeHandlesInput.checked = Boolean(activePreferences.showResizeHandles);
}
showNumbersInput.checked = Boolean(activePreferences.showNumbers);
showTreeTagsInput.checked = Boolean(activePreferences.showTreeTags);
treeTwoColumnInput.checked = Boolean(activePreferences.treeTwoColumn);
if (showTreeTypeInput) {
showTreeTypeInput.checked = Boolean(activePreferences.showTreeType);
}
if (treeColumnsInput) {
treeColumnsInput.value = String(treeColumns);
}
if (treeColumnsValue) {
treeColumnsValue.textContent = String(treeColumns);
}
autoCollapseSearchInput.checked = Boolean(activePreferences.autoCollapseSearch);
customAccentFromInput.value = activePreferences.customAccentFrom;
customAccentToInput.value = activePreferences.customAccentTo;
@ -1133,6 +1461,9 @@ function applyPreferences(nextPreferences) {
setActiveButton(navLayoutButtons, "navLayout", activePreferences.navLayout);
setActiveButton(viewButtons, "view", activePreferences.view);
setActiveButton(accentButtons, "accent", activePreferences.accent);
setActiveButton(treeMetaLayoutButtons, "treeMetaLayout", activePreferences.treeMetaLayout);
syncLayoutLimitInputs(activePreferences);
syncCatalogColumnInputs(activePreferences);
applyPanelFeatures(activePreferences);
}
@ -1147,12 +1478,17 @@ function applyPanelFeatures(activePreferences) {
}
if (navTree) {
navTree.hidden = activePreferences.navLayout === "top";
navTree.hidden = !isTreeVisible(activePreferences);
}
if (sidebarResizer) {
sidebarResizer.hidden =
activePreferences.navLayout === "top" || !panelFeatures.allowSidebarResize;
activePreferences.navLayout === "top" || !isResizeVisible(activePreferences);
}
if (topBarResizer) {
topBarResizer.hidden =
activePreferences.navLayout !== "top" || !isResizeVisible(activePreferences);
}
if (appLayout) {
@ -1163,16 +1499,30 @@ function applyPanelFeatures(activePreferences) {
const featureName = element.dataset.feature;
element.hidden = featureName ? !panelFeatures[featureName] : false;
});
document.querySelectorAll("[data-show-when-sidebar]").forEach((element) => {
element.hidden = activePreferences.navLayout === "top";
});
}
function isSettingsSectionVisible(sectionName, activePreferences) {
switch (sectionName) {
case "layout":
return panelFeatures.allowNavLayoutChange;
return (
panelFeatures.allowNavLayoutChange ||
panelFeatures.allowShowTreeChange ||
panelFeatures.allowSidebarResize
);
case "dimensions":
return panelFeatures.allowLayoutLimitsChange;
case "catalog":
return panelFeatures.allowViewChange || panelFeatures.allowShowNumbers;
return (
panelFeatures.allowViewChange ||
panelFeatures.allowShowNumbers ||
panelFeatures.allowCatalogColumnsChange
);
case "tree":
return panelFeatures.allowTreeOptions && activePreferences.navLayout !== "top";
return panelFeatures.allowTreeOptions && activePreferences.showTree;
case "appearance":
return panelFeatures.allowThemeChange || panelFeatures.allowAccentChange;
default:
@ -1180,6 +1530,14 @@ function isSettingsSectionVisible(sectionName, activePreferences) {
}
}
function isTreeVisible(activePreferences) {
return Boolean(activePreferences.showTree);
}
function isResizeVisible(activePreferences) {
return panelFeatures.allowSidebarResize && Boolean(activePreferences.showResizeHandles);
}
// Применяет CSS-переменные акцента для пресетов или пользовательского градиента.
function applyAccentVariables(nextPreferences) {
const root = document.documentElement;
@ -1331,9 +1689,162 @@ function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
// Максимальная ширина sidebar зависит от режима дерева.
function getTreeColumns(nextPreferences) {
let columns = Number(nextPreferences.treeColumns);
if (!Number.isFinite(columns)) {
columns = nextPreferences.treeTwoColumn ? 2 : defaultPreferences.treeColumns;
}
return clamp(Math.round(columns), MIN_TREE_COLUMNS, MAX_TREE_COLUMNS);
}
function getLayoutLimitRange(key) {
const limits = panelLimits[key] ?? defaultPanelLimits[key];
if (!limits || !Number.isFinite(limits.min) || !Number.isFinite(limits.max)) {
return defaultPanelLimits[key];
}
return {
min: Math.min(limits.min, limits.max),
max: Math.max(limits.min, limits.max),
};
}
function clampLayoutPreference(key, value) {
const limits = getLayoutLimitRange(key);
const fallback = panelDefaults[key] ?? defaultPreferences[key];
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return fallback;
}
return clamp(Math.round(numeric), limits.min, limits.max);
}
function normalizePanelLimits(rawLimits) {
const next = structuredClone(defaultPanelLimits);
if (!rawLimits || typeof rawLimits !== "object") {
return next;
}
for (const key of panelLimitKeys) {
const source = rawLimits[key];
if (!source || typeof source !== "object") {
continue;
}
const min = Math.round(Number(source.min));
const max = Math.round(Number(source.max));
if (!Number.isFinite(min) || !Number.isFinite(max)) {
continue;
}
next[key] = {
min: Math.min(min, max),
max: Math.max(min, max),
};
}
return next;
}
function syncLayoutLimitInputs(activePreferences) {
layoutLimitInputs.forEach((input) => {
const key = input.dataset.layoutLimit;
if (!layoutLimitKeys.includes(key)) {
return;
}
const limits = getPanelLimitRange(key);
input.min = String(limits.min);
input.max = String(limits.max);
input.step = key === "treeSidebarColumnWidth" ? "5" : "10";
input.value = String(activePreferences[key]);
input.disabled = !panelFeatures.allowLayoutLimitsChange;
const output = document.querySelector(`[data-layout-limit-value="${key}"]`);
if (output) {
output.textContent = String(activePreferences[key]);
}
});
}
function syncCatalogColumnInputs(activePreferences) {
catalogColumnInputs.forEach((input) => {
const key = input.dataset.catalogColumns;
if (!catalogColumnKeys.includes(key)) {
return;
}
const limits = getPanelLimitRange(key);
input.min = String(limits.min);
input.max = String(limits.max);
input.step = "1";
input.value = String(activePreferences[key]);
input.disabled = !panelFeatures.allowCatalogColumnsChange;
const output = document.querySelector(`[data-catalog-columns-value="${key}"]`);
if (output) {
output.textContent = String(activePreferences[key]);
}
});
}
function getPanelLimitRange(key) {
return getLayoutLimitRange(key);
}
function clampCatalogColumn(key, value) {
const limits = getPanelLimitRange(key);
const fallback = panelDefaults[key] ?? defaultPreferences[key];
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return fallback;
}
return clamp(Math.round(numeric), limits.min, limits.max);
}
function getSidebarMinWidth(nextPreferences) {
return Number(nextPreferences.sidebarMinWidth);
}
function getSidebarMaxWidth(nextPreferences) {
return nextPreferences.treeTwoColumn ? 760 : 560;
const columns = getTreeColumns(nextPreferences);
if (columns <= 1) {
return Number(nextPreferences.sidebarMaxWidth);
}
return Math.min(
Number(nextPreferences.sidebarMaxWidthWide),
Number(nextPreferences.treeSidebarBaseWidth) +
columns * Number(nextPreferences.treeSidebarColumnWidth),
);
}
function getShellMaxWidth(nextPreferences) {
return Number(nextPreferences.shellMaxWidth);
}
function getTopBarMinHeight(nextPreferences) {
return Number(nextPreferences.topBarMinHeight);
}
function getTopBarMaxHeight(nextPreferences) {
return Math.min(
Number(nextPreferences.topBarMaxHeight),
Math.round(window.innerHeight * 0.75),
);
}
// ============================================================================

View File

@ -3,23 +3,47 @@
"data": null,
"defaults": {
"navLayout": "top",
"showTree": false,
"theme": "dark",
"view": "cards",
"accent": "cyan",
"showNumbers": false,
"showTreeTags": false,
"treeTwoColumn": false,
"showTreeTags": true,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 2,
"autoCollapseSearch": true,
"sidebarWidth": 390,
"topBarHeight": 180,
"shellMaxWidth": 2800,
"sidebarMinWidth": 260,
"sidebarMaxWidth": 800,
"sidebarMaxWidthWide": 1400,
"treeSidebarBaseWidth": 420,
"treeSidebarColumnWidth": 120,
"topBarMinHeight": 80,
"topBarMaxHeight": 560,
"searchPlaceholder": "Поиск сервиса..."
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
"sidebarMinWidth": { "min": 200, "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 }
},
"features": {
"allowNavLayoutChange": true,
"allowShowTreeChange": true,
"allowSearch": true,
"allowViewChange": true,
"allowShowNumbers": false,
"allowTreeOptions": false,
"allowSidebarResize": false,
"allowTreeOptions": true,
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true
}

View File

@ -3,6 +3,8 @@
"data": null,
"defaults": {
"navLayout": "sidebar",
"showTree": true,
"showResizeHandles": true,
"theme": "dark",
"view": "cards",
"accent": "violet",
@ -11,18 +13,50 @@
"customAccentAngle": 135,
"showNumbers": false,
"showTreeTags": true,
"treeTwoColumn": false,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 2,
"autoCollapseSearch": true,
"sidebarWidth": 390,
"topBarHeight": 140,
"shellMaxWidth": 2800,
"sidebarMinWidth": 260,
"sidebarMaxWidth": 800,
"sidebarMaxWidthWide": 1400,
"treeSidebarBaseWidth": 420,
"treeSidebarColumnWidth": 120,
"topBarMinHeight": 80,
"topBarMaxHeight": 560,
"catalogCardsColumns": 3,
"catalogListColumns": 1,
"catalogCompactCardsColumns": 5,
"catalogCompactListColumns": 3,
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
"sidebarMinWidth": { "min": 200, "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
}

View File

@ -24,8 +24,13 @@
--card-x: 50%;
--card-y: 50%;
--sidebar-width: 390px;
--shell-max-width: 1560px;
--sidebar-max-width: 560px;
--top-bar-height: 140px;
--shell-max-width: 1920px;
--sidebar-max-width: 800px;
--catalog-cards-columns: 3;
--catalog-list-columns: 1;
--catalog-compact-cards-columns: 5;
--catalog-compact-list-columns: 3;
}
/* Светлая тема переопределяет только токены, структура стилей остается общей. */
@ -138,11 +143,15 @@ a {
/* Левая колонка получает ширину из --sidebar-width, которую меняет JS. */
.app-layout {
display: grid;
grid-template-columns: var(--sidebar-width) 8px minmax(0, 1fr);
gap: 8px;
grid-template-columns: var(--sidebar-width) 6px minmax(0, 1fr);
gap: 4px;
align-items: start;
}
html[data-nav-layout="sidebar"][data-show-resize-handles="false"] .app-layout {
grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
}
/* Компактная шапка: название + поиск, без дерева и resizer. */
html[data-nav-layout="top"] .app-layout,
.app-layout[data-nav-layout="top"] {
@ -155,17 +164,22 @@ html[data-nav-layout="top"] .sidebar-resizer {
html[data-nav-layout="top"] .side-panel {
position: static;
display: flex;
flex-direction: column;
gap: 0;
}
html[data-nav-layout="top"] .side-panel .hero-content {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
align-items: flex-start;
align-content: flex-start;
justify-content: flex-start;
gap: 12px 16px;
max-height: none;
min-height: auto;
overflow: visible;
height: var(--top-bar-height);
min-height: var(--top-bar-height);
max-height: var(--top-bar-height);
overflow: auto;
padding: 14px 58px 14px 18px;
}
@ -173,6 +187,13 @@ html[data-nav-layout="top"] .side-panel .hero-content > .eyebrow {
margin: 0;
flex: 0 1 auto;
max-width: min(100%, 360px);
align-self: center;
padding-right: 0;
}
.side-panel .hero-content > .eyebrow#page-title {
padding-right: 46px;
line-height: 1.35;
}
html[data-nav-layout="top"] .search-field {
@ -181,22 +202,31 @@ html[data-nav-layout="top"] .search-field {
max-width: min(100%, 560px);
}
html[data-nav-layout="top"] .nav-tree {
html[data-show-tree="false"] .nav-tree {
display: none;
}
/* ==========================================================================
04. Sidebar Resizer
--------------------------------------------------------------------------
Узкая колонка между меню и каталогом. Pointer Events в script.js двигают
CSS-переменную --sidebar-width и сохраняют значение в localStorage.
========================================================================== */
html[data-nav-layout="top"][data-show-tree="true"] .nav-tree {
flex: 1 1 100%;
width: 100%;
max-height: none;
min-height: 0;
overflow: auto;
margin-top: 4px;
padding-top: 10px;
border-top: 1px solid var(--border);
}
html[data-show-tree="false"] .side-panel .hero-content {
max-height: none;
}
.sidebar-resizer {
position: sticky;
top: 20px;
width: 8px;
width: 6px;
height: calc(100vh - 40px);
margin: 0 -1px;
border-radius: 999px;
cursor: col-resize;
background: transparent;
@ -224,6 +254,49 @@ body.is-resizing-sidebar {
user-select: none;
}
/* ==========================================================================
04b. Top Bar Resizer
--------------------------------------------------------------------------
Горизонтальная ручка под шапкой в режиме navLayout: top.
========================================================================== */
.top-bar-resizer {
display: none;
width: 100%;
height: 8px;
margin-top: 8px;
margin-bottom: 6px;
border-radius: 999px;
cursor: row-resize;
background: transparent;
transition: background 160ms ease;
flex: 0 0 auto;
}
html[data-nav-layout="top"] .top-bar-resizer:not([hidden]) {
display: block;
}
.top-bar-resizer::before {
content: "";
display: block;
width: 100%;
height: 2px;
margin: 3px auto;
border-radius: inherit;
background: var(--border);
}
.top-bar-resizer:hover::before,
body.is-resizing-top-bar .top-bar-resizer::before {
background: var(--accent);
}
body.is-resizing-top-bar {
cursor: row-resize;
user-select: none;
}
/* ==========================================================================
05. Sidebar Panel
--------------------------------------------------------------------------
@ -308,35 +381,16 @@ html:not([data-show-numbers="true"]) .item-number {
display: none;
}
/* Теги в дереве управляются toggle-настройкой "Показывать теги дерева". */
html[data-show-tree-tags="false"] .tree-node small {
/* Теги и тип в дереве управляются настройками showTreeTags / showTreeType. */
html[data-show-tree-tags="false"] .tree-tags {
display: none;
}
/* Двухколоночный режим элементов внутри папок дерева. */
html[data-tree-two-column="true"] {
--shell-max-width: 1920px;
--sidebar-max-width: 760px;
html[data-show-tree-type="false"] .tree-file-type {
display: none;
}
html[data-tree-two-column="true"] .tree-items {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 2px 8px;
}
html[data-tree-two-column="true"] .tree-node.tree-file {
grid-template-columns: 20px minmax(0, 1fr);
min-height: 24px;
font-size: 0.8rem;
}
html[data-tree-two-column="true"] .tree-node-icon {
width: 16px;
height: 16px;
border-radius: 4px;
}
html[data-tree-two-column="true"] .tree-node.tree-file small {
html[data-show-tree-tags="false"][data-show-tree-type="false"] .tree-file-meta {
display: none;
}
@ -440,13 +494,13 @@ html[data-tree-two-column="true"] .tree-node.tree-file small {
display: flex;
justify-content: space-between;
gap: 14px;
align-items: flex-start;
align-items: center;
}
.settings-panel-head h2 {
margin: 4px 0 0;
font-size: 1.35rem;
letter-spacing: -0.04em;
.settings-panel-eyebrow {
margin: 0;
flex: 1 1 auto;
min-width: 0;
}
.settings-close {
@ -463,7 +517,8 @@ html[data-tree-two-column="true"] .tree-node.tree-file small {
gap: 10px;
}
.settings-section[hidden] {
.settings-section[hidden],
.settings-section-collapsible[hidden] {
display: none;
}
@ -483,6 +538,98 @@ html[data-tree-two-column="true"] .tree-node.tree-file small {
line-height: 1.45;
}
.settings-section-hint code {
font-size: 0.78rem;
}
.catalog-columns-group {
display: grid;
gap: 8px;
}
.settings-section-collapsible {
border: 1px solid var(--border);
border-radius: 18px;
padding: 10px 12px;
background: var(--surface);
}
.settings-section-collapsible > summary {
list-style: none;
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}
.settings-section-collapsible > summary::-webkit-details-marker {
display: none;
}
.settings-section-summary .settings-section-title {
margin: 0;
}
.settings-section-collapsible:not([open]) > .settings-section-summary .collapse-indicator {
transform: rotate(-90deg);
}
.settings-section-body {
display: grid;
gap: 10px;
padding-top: 10px;
}
.layout-limit-row {
position: relative;
grid-template-columns: minmax(128px, 1.5fr) minmax(0, 2fr) auto;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 14px;
background: rgba(255, 255, 255, 0.02);
cursor: help;
}
.layout-limit-row[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
z-index: 30;
left: 8px;
right: 8px;
bottom: calc(100% + 6px);
top: auto;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--panel-strong);
color: var(--text);
font-size: 0.78rem;
font-weight: 500;
line-height: 1.45;
box-shadow: var(--shadow);
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: translateY(4px);
transition:
opacity 140ms ease,
visibility 140ms ease,
transform 140ms ease;
}
.layout-limit-row[data-tooltip]:hover::after,
.layout-limit-row[data-tooltip]:focus-within::after {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.layout-limit-row span {
line-height: 1.25;
}
/* ==========================================================================
07. Search Field And Control Inputs
--------------------------------------------------------------------------
@ -936,13 +1083,91 @@ html[data-theme="light"] .accent-swatch-custom {
}
.tree-file {
grid-template-columns: 20px minmax(0, 1fr) auto;
grid-template-columns: 20px minmax(0, 1fr);
align-items: start;
gap: 8px;
}
.tree-file::before {
content: none;
}
.tree-file-body {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.tree-file-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tree-file-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
min-width: 0;
}
html[data-tree-meta-layout="inline"] .tree-file-body {
flex-direction: row;
align-items: center;
gap: 8px;
}
html[data-tree-meta-layout="inline"] .tree-file-title {
flex: 1 1 auto;
}
html[data-tree-meta-layout="inline"] .tree-file-meta {
flex: 0 1 auto;
justify-content: flex-end;
}
html[data-tree-meta-layout="stacked"] .tree-file-body {
align-items: stretch;
}
.tree-file-type {
margin: 0;
color: var(--accent);
font-size: 0.72rem;
font-weight: 750;
white-space: nowrap;
}
.tree-tags.tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 0;
}
.tree-tags.tags span {
border: 1px solid var(--border);
border-radius: 999px;
padding: 2px 6px;
background: rgba(var(--accent-rgb), 0.1);
color: var(--muted);
font-size: 0.68rem;
font-weight: 800;
}
html:not([data-tree-columns="1"]) .tree-file {
font-size: 0.8rem;
}
html:not([data-tree-columns="1"]) .tree-node-icon {
width: 16px;
height: 16px;
border-radius: 4px;
}
.tree-node-icon {
width: 18px;
height: 18px;
@ -955,7 +1180,8 @@ html[data-theme="light"] .accent-swatch-custom {
.tree-items {
position: relative;
display: grid;
gap: 1px;
grid-template-columns: repeat(var(--tree-columns, 1), minmax(0, 1fr));
gap: 2px 8px;
margin-left: 18px;
padding-left: 10px;
border-left: 1px solid var(--border);
@ -967,17 +1193,6 @@ html[data-theme="light"] .accent-swatch-custom {
background: rgba(var(--accent-rgb), 0.14);
}
/* Теги дерева усечены ellipsis, чтобы длинные списки не ломали sidebar. */
.tree-node small {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--accent);
font-size: 0.68rem;
font-weight: 800;
}
/* ==========================================================================
09. Catalog Structure
--------------------------------------------------------------------------
@ -1155,11 +1370,14 @@ html[data-theme="light"] .accent-swatch-custom {
.items-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
padding: 10px;
}
.catalog.cards-view .items-grid {
grid-template-columns: repeat(var(--catalog-cards-columns, 3), minmax(0, 1fr));
}
/* Базовая карточка с иконкой, контентом, тегами, стрелкой и shine-слоем. */
.nav-card {
--tilt-x: 0deg;
@ -1321,7 +1539,7 @@ html[data-theme="light"] .accent-swatch-custom {
========================================================================== */
.catalog.list-view .items-grid {
grid-template-columns: 1fr;
grid-template-columns: repeat(var(--catalog-list-columns, 1), minmax(0, 1fr));
}
/* В list-режимах карточка вытягивается в строку. */
@ -1342,12 +1560,12 @@ html[data-theme="light"] .accent-swatch-custom {
/* Compact modes скрывают второстепенную информацию и уменьшают отступы. */
.catalog.compact-cards-view .items-grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-template-columns: repeat(var(--catalog-compact-cards-columns, 5), minmax(0, 1fr));
}
/* Compact list на широких экранах использует 3 колонки. */
/* Compact list на широких экранах использует настраиваемое число колонок. */
.catalog.compact-list-view .items-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(var(--catalog-compact-list-columns, 3), minmax(0, 1fr));
}
.catalog.compact-cards-view .section-head,
@ -1466,15 +1684,15 @@ html[data-theme="light"] .accent-swatch-custom {
/* Узкое окно на устройствах с мышью: sidebar и сетка карточек чуть компактнее. */
@media (hover: hover) and (pointer: fine) and (max-width: 1120px) {
.app-layout {
grid-template-columns: minmax(240px, min(var(--sidebar-width), 320px)) 8px minmax(0, 1fr);
grid-template-columns: minmax(240px, min(var(--sidebar-width), 320px)) 6px minmax(0, 1fr);
}
.items-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
.catalog.cards-view .items-grid {
grid-template-columns: repeat(min(var(--catalog-cards-columns, 3), 2), minmax(0, 1fr));
}
.catalog.compact-cards-view .items-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(min(var(--catalog-compact-cards-columns, 5), 3), minmax(0, 1fr));
}
}
@ -1489,7 +1707,8 @@ html[data-theme="light"] .accent-swatch-custom {
grid-template-columns: 1fr;
}
.sidebar-resizer {
.sidebar-resizer,
.top-bar-resizer {
display: none;
}
@ -1509,21 +1728,6 @@ html[data-theme="light"] .accent-swatch-custom {
overflow: auto;
}
/*
Двухколоночное дерево на touch не отключается полностью:
- в portrait — всегда, если включена настройка;
- в landscape — начиная с 520px ширины viewport.
*/
html[data-tree-two-column="true"] .tree-items {
grid-template-columns: 1fr;
}
@media (min-width: 520px), (orientation: portrait) {
html[data-tree-two-column="true"] .tree-items {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.control-panel,
.catalog-section,
.empty-state {
@ -1567,17 +1771,24 @@ html[data-theme="light"] .accent-swatch-custom {
padding: 8px;
}
.items-grid {
grid-template-columns: 1fr;
.catalog.cards-view .items-grid {
grid-template-columns: repeat(min(var(--catalog-cards-columns, 3), 1), minmax(0, 1fr));
padding: 8px;
}
.catalog.list-view .items-grid {
grid-template-columns: repeat(min(var(--catalog-list-columns, 1), 1), minmax(0, 1fr));
padding: 8px;
}
.catalog.compact-list-view .items-grid {
grid-template-columns: 1fr;
grid-template-columns: repeat(min(var(--catalog-compact-list-columns, 3), 1), minmax(0, 1fr));
padding: 8px;
}
.catalog.compact-cards-view .items-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(min(var(--catalog-compact-cards-columns, 5), 2), minmax(0, 1fr));
padding: 8px;
}
.nav-card {