добавление дефолтных настроек, возможность меню сбоку и сверху
This commit is contained in:
parent
284a7f6b75
commit
f3508bf7f9
88
README.md
88
README.md
@ -11,6 +11,7 @@
|
||||
- [Структура проекта](#структура-проекта)
|
||||
- [Конфигурация](#конфигурация)
|
||||
- [`settings.json`](#settingsjson)
|
||||
- [Пресет организации (`defaults` + `features`)](#пресет-организации-defaults--features)
|
||||
- [Форматы иерархии](#форматы-иерархии)
|
||||
- [Схема `panel-data`](#схема-panel-data)
|
||||
- [Поля данных](#поля-данных)
|
||||
@ -56,7 +57,8 @@ ecosystem/
|
||||
├── index.html # Разметка страницы
|
||||
├── styles.css # Стили, темы, адаптив
|
||||
├── script.js # Логика, рендер, примеры структур
|
||||
├── settings.json # Источник данных (URL или inline)
|
||||
├── settings.json # Данные + пресет UI организации
|
||||
├── settings-top-bar.example.json # Пример пресета «шапка сверху»
|
||||
├── panel-data.json # Данные навигации (рабочий файл)
|
||||
├── examples/ # Готовые демо-страницы (см. examples/README.md)
|
||||
│ ├── index.html # Оглавление примеров
|
||||
@ -108,6 +110,69 @@ ecosystem/
|
||||
|
||||
При ошибке загрузки или невалидном JSON на странице показывается экран с описанием проблемы.
|
||||
|
||||
#### Пресет организации (`defaults` + `features`)
|
||||
|
||||
В `settings.json` можно задать дефолтный вид панели для организации и ограничить, что пользователь может менять в ⚙.
|
||||
|
||||
```json
|
||||
{
|
||||
"dataUrl": "./panel-data.json",
|
||||
"defaults": {
|
||||
"navLayout": "sidebar",
|
||||
"theme": "dark",
|
||||
"view": "cards",
|
||||
"accent": "violet",
|
||||
"showNumbers": false,
|
||||
"showTreeTags": true,
|
||||
"treeTwoColumn": false,
|
||||
"autoCollapseSearch": true,
|
||||
"sidebarWidth": 390,
|
||||
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
|
||||
},
|
||||
"features": {
|
||||
"allowNavLayoutChange": true,
|
||||
"allowSearch": true,
|
||||
"allowViewChange": true,
|
||||
"allowShowNumbers": true,
|
||||
"allowTreeOptions": true,
|
||||
"allowSidebarResize": true,
|
||||
"allowThemeChange": true,
|
||||
"allowAccentChange": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| `defaults` | Варианты | Описание |
|
||||
|------------|----------|----------|
|
||||
| `navLayout` | `"sidebar"`, `"top"` | `"sidebar"` — дерево слева; `"top"` — шапка с названием и поиском, без дерева |
|
||||
| `theme` | `"dark"`, `"light"` | Стартовая цветовая тема |
|
||||
| `view` | `"cards"`, `"list"`, `"compact-cards"`, `"compact-list"` | Режим отображения каталога |
|
||||
| `accent` | `"violet"`, `"cyan"`, `"lime"`, `"amber"`, `"custom"` | Пресет акцента; при `"custom"` используются поля градиента ниже |
|
||||
| `customAccentFrom` | `#rrggbb` | Цвет «от» для `accent: "custom"` (по умолчанию `#8b5cf6`) |
|
||||
| `customAccentTo` | `#rrggbb` | Цвет «до» для `accent: "custom"` (по умолчанию `#22d3ee`) |
|
||||
| `customAccentAngle` | `0`–`360` | Угол градиента в градусах (по умолчанию `135`) |
|
||||
| `showNumbers` | `true`, `false` | Показывать номера разделов, подразделов и элементов |
|
||||
| `showTreeTags` | `true`, `false` | Теги рядом с элементами в дереве (взаимоисключимо с `treeTwoColumn`) |
|
||||
| `treeTwoColumn` | `true`, `false` | Элементы папок дерева в 2 колонки (взаимоисключимо с `showTreeTags`) |
|
||||
| `autoCollapseSearch` | `true`, `false` | При поиске сворачивать ветки дерева без совпадений |
|
||||
| `sidebarWidth` | `260`–`560` (`760` при `treeTwoColumn: true`) | Ширина боковой панели в px; игнорируется при `navLayout: "top"` |
|
||||
| `searchPlaceholder` | произвольная строка | Текст-подсказка в поле поиска |
|
||||
|
||||
| `features` | Варианты | Описание |
|
||||
|------------|----------|----------|
|
||||
| `allowNavLayoutChange` | `true`, `false` | Переключатель «Сбоку / Сверху» |
|
||||
| `allowSearch` | `true`, `false` | Поле поиска |
|
||||
| `allowViewChange` | `true`, `false` | Режимы каталога |
|
||||
| `allowShowNumbers` | `true`, `false` | Нумерация |
|
||||
| `allowTreeOptions` | `true`, `false` | Теги, 2 колонки, авто-раскрытие дерева |
|
||||
| `allowSidebarResize` | `true`, `false` | Drag-resize боковой панели |
|
||||
| `allowThemeChange` | `true`, `false` | Светлая / тёмная тема |
|
||||
| `allowAccentChange` | `true`, `false` | Пресеты и свой градиент |
|
||||
|
||||
Пользовательские изменения сохраняются в `localStorage`, но заблокированные `features` всегда возвращаются к `defaults`.
|
||||
|
||||
Пример пресета «только шапка»: [`settings-top-bar.example.json`](settings-top-bar.example.json) — `navLayout: "top"`, дерево и resize отключены.
|
||||
|
||||
### Форматы иерархии
|
||||
|
||||
Панель поддерживает три базовых формата. Старый **section → group → item** работает без изменений.
|
||||
@ -257,17 +322,18 @@ npx serve .
|
||||
|
||||
## Настройки интерфейса (⚙)
|
||||
|
||||
Доступны в popup-окне и сохраняются в `localStorage` (`ecosystem-panel-preferences`):
|
||||
Popup сгруппирован по разделам. Доступные блоки зависят от `features` в `settings.json`. Изменения пользователя сохраняются в `localStorage` (`ecosystem-panel-preferences`).
|
||||
|
||||
| Настройка | Описание |
|
||||
|-----------|----------|
|
||||
| **Тема** | Светлая / тёмная |
|
||||
| **Вид** | Плитки, список, компакт плитки, компакт список |
|
||||
| **Нумерация** | Показывать номера разделов, подразделов и элементов |
|
||||
| **Теги дерева** | Поисковые теги рядом с элементами в дереве |
|
||||
| **2 колонки в дереве** | Элементы папок в 2 колонки (взаимоисключимо с тегами) |
|
||||
| **Авто раскрывать поиск** | При поиске сворачивать ветки без совпадений |
|
||||
| **Акцент** | violet, cyan, lime, amber или свой градиент |
|
||||
| Раздел | Настройки |
|
||||
|--------|-----------|
|
||||
| **Макет** | Панель навигации: **Сбоку** (дерево + поиск) или **Сверху** (только название и поиск) |
|
||||
| **Каталог** | Вид карточек, нумерация |
|
||||
| **Дерево** | Теги, 2 колонки, авто-раскрытие поиска (только при `navLayout: sidebar`) |
|
||||
| **Оформление** | Тема, акцент, свой градиент |
|
||||
|
||||
### Режим «Сверху»
|
||||
|
||||
Компактная шапка над каталогом: заголовок панели, поиск и ⚙. Дерево навигации скрыто — переход только через карточки каталога. Подходит для простых порталов и когда дерево избыточно.
|
||||
|
||||
### Свой градиент
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ npx serve .
|
||||
| Файл | Назначение |
|
||||
|------|------------|
|
||||
| `index.html` | Страница панели, подключает `../shared/` |
|
||||
| `settings.json` | Указывает `./panel-data.json` |
|
||||
| `settings.json` | Указывает `./panel-data.json` и пресет UI (`defaults`, `features`) |
|
||||
| `panel-data.json` | Демо-данные выбранного формата |
|
||||
|
||||
## Обновление shared/
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<!doctype html>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@ -17,13 +17,21 @@
|
||||
<link rel="stylesheet" href="../shared/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Декоративные фоновые пятна. Они не участвуют в логике и нужны только для глубины фона. -->
|
||||
<div class="ambient ambient-one"></div>
|
||||
<div class="ambient ambient-two"></div>
|
||||
|
||||
<main class="shell">
|
||||
<!--
|
||||
Основная сетка приложения:
|
||||
слева sticky-меню с деревом навигации, справа каталог карточек.
|
||||
Ширина левой колонки управляется CSS-переменной --sidebar-width и ресайзером.
|
||||
-->
|
||||
<div class="app-layout">
|
||||
<!-- Боковая панель: поиск, настройки и дерево навигации по всей структуре. -->
|
||||
<aside class="side-panel" aria-label="Навигация по экосистеме">
|
||||
<div class="hero-content">
|
||||
<!-- Шестеренка открывает fixed popup с настройками внешнего вида и поведения. -->
|
||||
<button
|
||||
class="settings-button"
|
||||
type="button"
|
||||
@ -36,7 +44,8 @@
|
||||
|
||||
<p class="eyebrow" id="page-title">Пример: плоский список</p>
|
||||
|
||||
<label class="search-field">
|
||||
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
|
||||
<label class="search-field" data-search-field>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Grafana, S3, журнал, сервер..."
|
||||
@ -45,10 +54,12 @@
|
||||
/>
|
||||
</label>
|
||||
|
||||
<!-- Дерево генерируется из ecosystemSections в script.js. -->
|
||||
<nav class="nav-tree" aria-label="Дерево навигации" data-nav-tree></nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
|
||||
<div
|
||||
class="sidebar-resizer"
|
||||
role="separator"
|
||||
@ -57,8 +68,18 @@
|
||||
data-sidebar-resizer
|
||||
></div>
|
||||
|
||||
<!-- Затемнение страницы за popup-настройками. -->
|
||||
<div class="settings-overlay" data-settings-overlay></div>
|
||||
|
||||
<!--
|
||||
Popup настроек:
|
||||
- тема;
|
||||
- режим отображения карточек;
|
||||
- видимость нумерации;
|
||||
- видимость тегов в дереве;
|
||||
- авто-раскрытие дерева при поиске;
|
||||
- цветовой акцент.
|
||||
-->
|
||||
<div class="control-panel" aria-label="Настройки панели" data-settings-panel>
|
||||
<div class="settings-panel-head">
|
||||
<div>
|
||||
@ -70,28 +91,57 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Переключение светлой/темной темы. Значение сохраняется в localStorage. -->
|
||||
<section class="settings-section" data-settings-section="layout">
|
||||
<p class="settings-section-title">Макет</p>
|
||||
<div class="control-group">
|
||||
<span>Тема</span>
|
||||
<button class="segmented is-active" type="button" data-theme="dark">Темная</button>
|
||||
<button class="segmented" type="button" data-theme="light">Светлая</button>
|
||||
<span>Панель навигации</span>
|
||||
<button class="segmented is-active" type="button" data-nav-layout="sidebar">
|
||||
Сбоку
|
||||
</button>
|
||||
<button class="segmented" type="button" data-nav-layout="top">
|
||||
Сверху
|
||||
</button>
|
||||
</div>
|
||||
<p class="settings-section-hint">
|
||||
Режим «Сверху» — компактная шапка с названием и поиском, без дерева.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="control-group view-group">
|
||||
<section class="settings-section" data-settings-section="catalog">
|
||||
<p class="settings-section-title">Каталог</p>
|
||||
|
||||
<!-- Режимы каталога: обычные/компактные плитки и списки. -->
|
||||
<div class="control-group view-group" data-feature="allowViewChange">
|
||||
<span>Вид</span>
|
||||
<button class="segmented is-active" type="button" data-view="cards">Плитки</button>
|
||||
<button class="segmented" type="button" data-view="list">Список</button>
|
||||
<button class="segmented" type="button" data-view="compact-cards">Компакт плитки</button>
|
||||
<button class="segmented" type="button" data-view="compact-list">Компакт список</button>
|
||||
<button class="segmented is-active" type="button" data-view="cards">
|
||||
Плитки
|
||||
</button>
|
||||
<button class="segmented" type="button" data-view="list">
|
||||
Список
|
||||
</button>
|
||||
<button class="segmented" type="button" data-view="compact-cards">
|
||||
Компакт плитки
|
||||
</button>
|
||||
<button class="segmented" type="button" data-view="compact-list">
|
||||
Компакт список
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label class="toggle-row">
|
||||
<!-- Показывать номера разделов, подразделов и элементов. -->
|
||||
<label class="toggle-row" data-feature="allowShowNumbers">
|
||||
<span>
|
||||
<strong>Показывать нумерацию</strong>
|
||||
<small>Разделы, подразделы и элементы</small>
|
||||
</span>
|
||||
<input type="checkbox" data-show-numbers />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="settings-section" data-settings-section="tree">
|
||||
<p class="settings-section-title">Дерево</p>
|
||||
|
||||
<!-- Показывать поисковые теги рядом с файлами в дереве навигации. -->
|
||||
<label class="toggle-row">
|
||||
<span>
|
||||
<strong>Показывать теги дерева</strong>
|
||||
@ -100,6 +150,7 @@
|
||||
<input type="checkbox" data-show-tree-tags />
|
||||
</label>
|
||||
|
||||
<!-- Элементы внутри папок дерева отображаются в две колонки без тегов. -->
|
||||
<label class="toggle-row">
|
||||
<span>
|
||||
<strong>2 колонки в дереве</strong>
|
||||
@ -108,6 +159,7 @@
|
||||
<input type="checkbox" data-tree-two-column />
|
||||
</label>
|
||||
|
||||
<!-- Управляет тем, раскрывает ли поиск только ветки с совпадениями. -->
|
||||
<label class="toggle-row">
|
||||
<span>
|
||||
<strong>Авто раскрывать поиск</strong>
|
||||
@ -115,39 +167,95 @@
|
||||
</span>
|
||||
<input type="checkbox" data-auto-collapse-search />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<div class="control-group accent-group">
|
||||
<section class="settings-section" data-settings-section="appearance">
|
||||
<p class="settings-section-title">Оформление</p>
|
||||
|
||||
<div class="control-group" data-feature="allowThemeChange">
|
||||
<span>Тема</span>
|
||||
<button class="segmented is-active" type="button" data-theme="dark">
|
||||
Темная
|
||||
</button>
|
||||
<button class="segmented" type="button" data-theme="light">
|
||||
Светлая
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Выбор акцентного цвета интерфейса. -->
|
||||
<div class="control-group accent-group" data-feature="allowAccentChange">
|
||||
<span>Акцент</span>
|
||||
<button class="accent-swatch is-active" type="button" data-accent="violet" aria-label="Фиолетовый акцент"></button>
|
||||
<button class="accent-swatch" type="button" data-accent="cyan" aria-label="Голубой акцент"></button>
|
||||
<button class="accent-swatch" type="button" data-accent="lime" aria-label="Лаймовый акцент"></button>
|
||||
<button class="accent-swatch" type="button" data-accent="amber" aria-label="Янтарный акцент"></button>
|
||||
<button class="accent-swatch accent-swatch-custom" type="button" data-accent="custom" aria-label="Свой градиент" title="Свой градиент">
|
||||
<button
|
||||
class="accent-swatch is-active"
|
||||
type="button"
|
||||
data-accent="violet"
|
||||
aria-label="Фиолетовый акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="cyan"
|
||||
aria-label="Голубой акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="lime"
|
||||
aria-label="Лаймовый акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="amber"
|
||||
aria-label="Янтарный акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch accent-swatch-custom"
|
||||
type="button"
|
||||
data-accent="custom"
|
||||
aria-label="Свой градиент"
|
||||
title="Свой градиент"
|
||||
>
|
||||
<span class="accent-swatch-label">Свой</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="accent-custom-panel" data-accent-custom-panel hidden>
|
||||
<!-- Настройки пользовательского linear-gradient акцента. -->
|
||||
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
|
||||
<p class="accent-custom-title">Свой градиент</p>
|
||||
|
||||
<label class="accent-custom-row">
|
||||
<span>Угол</span>
|
||||
<input type="range" min="0" max="360" step="1" value="135" data-custom-accent-angle />
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="360"
|
||||
step="1"
|
||||
value="135"
|
||||
data-custom-accent-angle
|
||||
/>
|
||||
<output data-custom-accent-angle-value>135°</output>
|
||||
</label>
|
||||
|
||||
<label class="accent-custom-row">
|
||||
<span>Цвет от</span>
|
||||
<input type="color" value="#8b5cf6" data-custom-accent-from />
|
||||
</label>
|
||||
|
||||
<label class="accent-custom-row">
|
||||
<span>Цвет до</span>
|
||||
<input type="color" value="#22d3ee" data-custom-accent-to />
|
||||
</label>
|
||||
|
||||
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->
|
||||
<section class="catalog cards-view" aria-live="polite" data-catalog-root></section>
|
||||
|
||||
<!-- Шаблон ошибки загрузки settings.json / panel data. -->
|
||||
<template id="config-error-template">
|
||||
<div class="empty-state config-error-state">
|
||||
<img src="../shared/assets/config.svg" alt="" />
|
||||
@ -156,6 +264,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Шаблон состояния, когда поиск не нашел ни одного элемента. -->
|
||||
<template id="empty-template">
|
||||
<div class="empty-state">
|
||||
<img src="../shared/assets/search.svg" alt="" />
|
||||
@ -166,11 +275,6 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script
|
||||
src="../shared/script.js"
|
||||
data-ecosystem-panel
|
||||
data-asset-base="../shared/"
|
||||
defer
|
||||
></script>
|
||||
<script src="../shared/script.js" data-ecosystem-panel data-asset-base="../shared/" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,4 +1,26 @@
|
||||
{
|
||||
"dataUrl": "./panel-data.json",
|
||||
"data": null
|
||||
"data": null,
|
||||
"defaults": {
|
||||
"navLayout": "sidebar",
|
||||
"theme": "dark",
|
||||
"view": "cards",
|
||||
"accent": "violet",
|
||||
"showNumbers": false,
|
||||
"showTreeTags": true,
|
||||
"treeTwoColumn": false,
|
||||
"autoCollapseSearch": true,
|
||||
"sidebarWidth": 390,
|
||||
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
|
||||
},
|
||||
"features": {
|
||||
"allowNavLayoutChange": true,
|
||||
"allowSearch": true,
|
||||
"allowViewChange": true,
|
||||
"allowShowNumbers": true,
|
||||
"allowTreeOptions": true,
|
||||
"allowSidebarResize": true,
|
||||
"allowThemeChange": true,
|
||||
"allowAccentChange": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<!doctype html>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@ -17,13 +17,21 @@
|
||||
<link rel="stylesheet" href="../shared/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Декоративные фоновые пятна. Они не участвуют в логике и нужны только для глубины фона. -->
|
||||
<div class="ambient ambient-one"></div>
|
||||
<div class="ambient ambient-two"></div>
|
||||
|
||||
<main class="shell">
|
||||
<!--
|
||||
Основная сетка приложения:
|
||||
слева sticky-меню с деревом навигации, справа каталог карточек.
|
||||
Ширина левой колонки управляется CSS-переменной --sidebar-width и ресайзером.
|
||||
-->
|
||||
<div class="app-layout">
|
||||
<!-- Боковая панель: поиск, настройки и дерево навигации по всей структуре. -->
|
||||
<aside class="side-panel" aria-label="Навигация по экосистеме">
|
||||
<div class="hero-content">
|
||||
<!-- Шестеренка открывает fixed popup с настройками внешнего вида и поведения. -->
|
||||
<button
|
||||
class="settings-button"
|
||||
type="button"
|
||||
@ -36,7 +44,8 @@
|
||||
|
||||
<p class="eyebrow" id="page-title">Пример: section → group → item</p>
|
||||
|
||||
<label class="search-field">
|
||||
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
|
||||
<label class="search-field" data-search-field>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Grafana, S3, журнал, сервер..."
|
||||
@ -45,10 +54,12 @@
|
||||
/>
|
||||
</label>
|
||||
|
||||
<!-- Дерево генерируется из ecosystemSections в script.js. -->
|
||||
<nav class="nav-tree" aria-label="Дерево навигации" data-nav-tree></nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
|
||||
<div
|
||||
class="sidebar-resizer"
|
||||
role="separator"
|
||||
@ -57,8 +68,18 @@
|
||||
data-sidebar-resizer
|
||||
></div>
|
||||
|
||||
<!-- Затемнение страницы за popup-настройками. -->
|
||||
<div class="settings-overlay" data-settings-overlay></div>
|
||||
|
||||
<!--
|
||||
Popup настроек:
|
||||
- тема;
|
||||
- режим отображения карточек;
|
||||
- видимость нумерации;
|
||||
- видимость тегов в дереве;
|
||||
- авто-раскрытие дерева при поиске;
|
||||
- цветовой акцент.
|
||||
-->
|
||||
<div class="control-panel" aria-label="Настройки панели" data-settings-panel>
|
||||
<div class="settings-panel-head">
|
||||
<div>
|
||||
@ -70,28 +91,57 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Переключение светлой/темной темы. Значение сохраняется в localStorage. -->
|
||||
<section class="settings-section" data-settings-section="layout">
|
||||
<p class="settings-section-title">Макет</p>
|
||||
<div class="control-group">
|
||||
<span>Тема</span>
|
||||
<button class="segmented is-active" type="button" data-theme="dark">Темная</button>
|
||||
<button class="segmented" type="button" data-theme="light">Светлая</button>
|
||||
<span>Панель навигации</span>
|
||||
<button class="segmented is-active" type="button" data-nav-layout="sidebar">
|
||||
Сбоку
|
||||
</button>
|
||||
<button class="segmented" type="button" data-nav-layout="top">
|
||||
Сверху
|
||||
</button>
|
||||
</div>
|
||||
<p class="settings-section-hint">
|
||||
Режим «Сверху» — компактная шапка с названием и поиском, без дерева.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="control-group view-group">
|
||||
<section class="settings-section" data-settings-section="catalog">
|
||||
<p class="settings-section-title">Каталог</p>
|
||||
|
||||
<!-- Режимы каталога: обычные/компактные плитки и списки. -->
|
||||
<div class="control-group view-group" data-feature="allowViewChange">
|
||||
<span>Вид</span>
|
||||
<button class="segmented is-active" type="button" data-view="cards">Плитки</button>
|
||||
<button class="segmented" type="button" data-view="list">Список</button>
|
||||
<button class="segmented" type="button" data-view="compact-cards">Компакт плитки</button>
|
||||
<button class="segmented" type="button" data-view="compact-list">Компакт список</button>
|
||||
<button class="segmented is-active" type="button" data-view="cards">
|
||||
Плитки
|
||||
</button>
|
||||
<button class="segmented" type="button" data-view="list">
|
||||
Список
|
||||
</button>
|
||||
<button class="segmented" type="button" data-view="compact-cards">
|
||||
Компакт плитки
|
||||
</button>
|
||||
<button class="segmented" type="button" data-view="compact-list">
|
||||
Компакт список
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label class="toggle-row">
|
||||
<!-- Показывать номера разделов, подразделов и элементов. -->
|
||||
<label class="toggle-row" data-feature="allowShowNumbers">
|
||||
<span>
|
||||
<strong>Показывать нумерацию</strong>
|
||||
<small>Разделы, подразделы и элементы</small>
|
||||
</span>
|
||||
<input type="checkbox" data-show-numbers />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="settings-section" data-settings-section="tree">
|
||||
<p class="settings-section-title">Дерево</p>
|
||||
|
||||
<!-- Показывать поисковые теги рядом с файлами в дереве навигации. -->
|
||||
<label class="toggle-row">
|
||||
<span>
|
||||
<strong>Показывать теги дерева</strong>
|
||||
@ -100,6 +150,7 @@
|
||||
<input type="checkbox" data-show-tree-tags />
|
||||
</label>
|
||||
|
||||
<!-- Элементы внутри папок дерева отображаются в две колонки без тегов. -->
|
||||
<label class="toggle-row">
|
||||
<span>
|
||||
<strong>2 колонки в дереве</strong>
|
||||
@ -108,6 +159,7 @@
|
||||
<input type="checkbox" data-tree-two-column />
|
||||
</label>
|
||||
|
||||
<!-- Управляет тем, раскрывает ли поиск только ветки с совпадениями. -->
|
||||
<label class="toggle-row">
|
||||
<span>
|
||||
<strong>Авто раскрывать поиск</strong>
|
||||
@ -115,39 +167,95 @@
|
||||
</span>
|
||||
<input type="checkbox" data-auto-collapse-search />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<div class="control-group accent-group">
|
||||
<section class="settings-section" data-settings-section="appearance">
|
||||
<p class="settings-section-title">Оформление</p>
|
||||
|
||||
<div class="control-group" data-feature="allowThemeChange">
|
||||
<span>Тема</span>
|
||||
<button class="segmented is-active" type="button" data-theme="dark">
|
||||
Темная
|
||||
</button>
|
||||
<button class="segmented" type="button" data-theme="light">
|
||||
Светлая
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Выбор акцентного цвета интерфейса. -->
|
||||
<div class="control-group accent-group" data-feature="allowAccentChange">
|
||||
<span>Акцент</span>
|
||||
<button class="accent-swatch is-active" type="button" data-accent="violet" aria-label="Фиолетовый акцент"></button>
|
||||
<button class="accent-swatch" type="button" data-accent="cyan" aria-label="Голубой акцент"></button>
|
||||
<button class="accent-swatch" type="button" data-accent="lime" aria-label="Лаймовый акцент"></button>
|
||||
<button class="accent-swatch" type="button" data-accent="amber" aria-label="Янтарный акцент"></button>
|
||||
<button class="accent-swatch accent-swatch-custom" type="button" data-accent="custom" aria-label="Свой градиент" title="Свой градиент">
|
||||
<button
|
||||
class="accent-swatch is-active"
|
||||
type="button"
|
||||
data-accent="violet"
|
||||
aria-label="Фиолетовый акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="cyan"
|
||||
aria-label="Голубой акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="lime"
|
||||
aria-label="Лаймовый акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="amber"
|
||||
aria-label="Янтарный акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch accent-swatch-custom"
|
||||
type="button"
|
||||
data-accent="custom"
|
||||
aria-label="Свой градиент"
|
||||
title="Свой градиент"
|
||||
>
|
||||
<span class="accent-swatch-label">Свой</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="accent-custom-panel" data-accent-custom-panel hidden>
|
||||
<!-- Настройки пользовательского linear-gradient акцента. -->
|
||||
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
|
||||
<p class="accent-custom-title">Свой градиент</p>
|
||||
|
||||
<label class="accent-custom-row">
|
||||
<span>Угол</span>
|
||||
<input type="range" min="0" max="360" step="1" value="135" data-custom-accent-angle />
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="360"
|
||||
step="1"
|
||||
value="135"
|
||||
data-custom-accent-angle
|
||||
/>
|
||||
<output data-custom-accent-angle-value>135°</output>
|
||||
</label>
|
||||
|
||||
<label class="accent-custom-row">
|
||||
<span>Цвет от</span>
|
||||
<input type="color" value="#8b5cf6" data-custom-accent-from />
|
||||
</label>
|
||||
|
||||
<label class="accent-custom-row">
|
||||
<span>Цвет до</span>
|
||||
<input type="color" value="#22d3ee" data-custom-accent-to />
|
||||
</label>
|
||||
|
||||
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->
|
||||
<section class="catalog cards-view" aria-live="polite" data-catalog-root></section>
|
||||
|
||||
<!-- Шаблон ошибки загрузки settings.json / panel data. -->
|
||||
<template id="config-error-template">
|
||||
<div class="empty-state config-error-state">
|
||||
<img src="../shared/assets/config.svg" alt="" />
|
||||
@ -156,6 +264,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Шаблон состояния, когда поиск не нашел ни одного элемента. -->
|
||||
<template id="empty-template">
|
||||
<div class="empty-state">
|
||||
<img src="../shared/assets/search.svg" alt="" />
|
||||
@ -166,11 +275,6 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script
|
||||
src="../shared/script.js"
|
||||
data-ecosystem-panel
|
||||
data-asset-base="../shared/"
|
||||
defer
|
||||
></script>
|
||||
<script src="../shared/script.js" data-ecosystem-panel data-asset-base="../shared/" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,4 +1,26 @@
|
||||
{
|
||||
"dataUrl": "./panel-data.json",
|
||||
"data": null
|
||||
"data": null,
|
||||
"defaults": {
|
||||
"navLayout": "sidebar",
|
||||
"theme": "dark",
|
||||
"view": "cards",
|
||||
"accent": "violet",
|
||||
"showNumbers": false,
|
||||
"showTreeTags": true,
|
||||
"treeTwoColumn": false,
|
||||
"autoCollapseSearch": true,
|
||||
"sidebarWidth": 390,
|
||||
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
|
||||
},
|
||||
"features": {
|
||||
"allowNavLayoutChange": true,
|
||||
"allowSearch": true,
|
||||
"allowViewChange": true,
|
||||
"allowShowNumbers": true,
|
||||
"allowTreeOptions": true,
|
||||
"allowSidebarResize": true,
|
||||
"allowThemeChange": true,
|
||||
"allowAccentChange": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<!doctype html>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@ -17,13 +17,21 @@
|
||||
<link rel="stylesheet" href="../shared/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Декоративные фоновые пятна. Они не участвуют в логике и нужны только для глубины фона. -->
|
||||
<div class="ambient ambient-one"></div>
|
||||
<div class="ambient ambient-two"></div>
|
||||
|
||||
<main class="shell">
|
||||
<!--
|
||||
Основная сетка приложения:
|
||||
слева sticky-меню с деревом навигации, справа каталог карточек.
|
||||
Ширина левой колонки управляется CSS-переменной --sidebar-width и ресайзером.
|
||||
-->
|
||||
<div class="app-layout">
|
||||
<!-- Боковая панель: поиск, настройки и дерево навигации по всей структуре. -->
|
||||
<aside class="side-panel" aria-label="Навигация по экосистеме">
|
||||
<div class="hero-content">
|
||||
<!-- Шестеренка открывает fixed popup с настройками внешнего вида и поведения. -->
|
||||
<button
|
||||
class="settings-button"
|
||||
type="button"
|
||||
@ -36,7 +44,8 @@
|
||||
|
||||
<p class="eyebrow" id="page-title">Пример: смешанный раздел</p>
|
||||
|
||||
<label class="search-field">
|
||||
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
|
||||
<label class="search-field" data-search-field>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Grafana, S3, журнал, сервер..."
|
||||
@ -45,10 +54,12 @@
|
||||
/>
|
||||
</label>
|
||||
|
||||
<!-- Дерево генерируется из ecosystemSections в script.js. -->
|
||||
<nav class="nav-tree" aria-label="Дерево навигации" data-nav-tree></nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
|
||||
<div
|
||||
class="sidebar-resizer"
|
||||
role="separator"
|
||||
@ -57,8 +68,18 @@
|
||||
data-sidebar-resizer
|
||||
></div>
|
||||
|
||||
<!-- Затемнение страницы за popup-настройками. -->
|
||||
<div class="settings-overlay" data-settings-overlay></div>
|
||||
|
||||
<!--
|
||||
Popup настроек:
|
||||
- тема;
|
||||
- режим отображения карточек;
|
||||
- видимость нумерации;
|
||||
- видимость тегов в дереве;
|
||||
- авто-раскрытие дерева при поиске;
|
||||
- цветовой акцент.
|
||||
-->
|
||||
<div class="control-panel" aria-label="Настройки панели" data-settings-panel>
|
||||
<div class="settings-panel-head">
|
||||
<div>
|
||||
@ -70,28 +91,57 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Переключение светлой/темной темы. Значение сохраняется в localStorage. -->
|
||||
<section class="settings-section" data-settings-section="layout">
|
||||
<p class="settings-section-title">Макет</p>
|
||||
<div class="control-group">
|
||||
<span>Тема</span>
|
||||
<button class="segmented is-active" type="button" data-theme="dark">Темная</button>
|
||||
<button class="segmented" type="button" data-theme="light">Светлая</button>
|
||||
<span>Панель навигации</span>
|
||||
<button class="segmented is-active" type="button" data-nav-layout="sidebar">
|
||||
Сбоку
|
||||
</button>
|
||||
<button class="segmented" type="button" data-nav-layout="top">
|
||||
Сверху
|
||||
</button>
|
||||
</div>
|
||||
<p class="settings-section-hint">
|
||||
Режим «Сверху» — компактная шапка с названием и поиском, без дерева.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="control-group view-group">
|
||||
<section class="settings-section" data-settings-section="catalog">
|
||||
<p class="settings-section-title">Каталог</p>
|
||||
|
||||
<!-- Режимы каталога: обычные/компактные плитки и списки. -->
|
||||
<div class="control-group view-group" data-feature="allowViewChange">
|
||||
<span>Вид</span>
|
||||
<button class="segmented is-active" type="button" data-view="cards">Плитки</button>
|
||||
<button class="segmented" type="button" data-view="list">Список</button>
|
||||
<button class="segmented" type="button" data-view="compact-cards">Компакт плитки</button>
|
||||
<button class="segmented" type="button" data-view="compact-list">Компакт список</button>
|
||||
<button class="segmented is-active" type="button" data-view="cards">
|
||||
Плитки
|
||||
</button>
|
||||
<button class="segmented" type="button" data-view="list">
|
||||
Список
|
||||
</button>
|
||||
<button class="segmented" type="button" data-view="compact-cards">
|
||||
Компакт плитки
|
||||
</button>
|
||||
<button class="segmented" type="button" data-view="compact-list">
|
||||
Компакт список
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label class="toggle-row">
|
||||
<!-- Показывать номера разделов, подразделов и элементов. -->
|
||||
<label class="toggle-row" data-feature="allowShowNumbers">
|
||||
<span>
|
||||
<strong>Показывать нумерацию</strong>
|
||||
<small>Разделы, подразделы и элементы</small>
|
||||
</span>
|
||||
<input type="checkbox" data-show-numbers />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="settings-section" data-settings-section="tree">
|
||||
<p class="settings-section-title">Дерево</p>
|
||||
|
||||
<!-- Показывать поисковые теги рядом с файлами в дереве навигации. -->
|
||||
<label class="toggle-row">
|
||||
<span>
|
||||
<strong>Показывать теги дерева</strong>
|
||||
@ -100,6 +150,7 @@
|
||||
<input type="checkbox" data-show-tree-tags />
|
||||
</label>
|
||||
|
||||
<!-- Элементы внутри папок дерева отображаются в две колонки без тегов. -->
|
||||
<label class="toggle-row">
|
||||
<span>
|
||||
<strong>2 колонки в дереве</strong>
|
||||
@ -108,6 +159,7 @@
|
||||
<input type="checkbox" data-tree-two-column />
|
||||
</label>
|
||||
|
||||
<!-- Управляет тем, раскрывает ли поиск только ветки с совпадениями. -->
|
||||
<label class="toggle-row">
|
||||
<span>
|
||||
<strong>Авто раскрывать поиск</strong>
|
||||
@ -115,39 +167,95 @@
|
||||
</span>
|
||||
<input type="checkbox" data-auto-collapse-search />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<div class="control-group accent-group">
|
||||
<section class="settings-section" data-settings-section="appearance">
|
||||
<p class="settings-section-title">Оформление</p>
|
||||
|
||||
<div class="control-group" data-feature="allowThemeChange">
|
||||
<span>Тема</span>
|
||||
<button class="segmented is-active" type="button" data-theme="dark">
|
||||
Темная
|
||||
</button>
|
||||
<button class="segmented" type="button" data-theme="light">
|
||||
Светлая
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Выбор акцентного цвета интерфейса. -->
|
||||
<div class="control-group accent-group" data-feature="allowAccentChange">
|
||||
<span>Акцент</span>
|
||||
<button class="accent-swatch is-active" type="button" data-accent="violet" aria-label="Фиолетовый акцент"></button>
|
||||
<button class="accent-swatch" type="button" data-accent="cyan" aria-label="Голубой акцент"></button>
|
||||
<button class="accent-swatch" type="button" data-accent="lime" aria-label="Лаймовый акцент"></button>
|
||||
<button class="accent-swatch" type="button" data-accent="amber" aria-label="Янтарный акцент"></button>
|
||||
<button class="accent-swatch accent-swatch-custom" type="button" data-accent="custom" aria-label="Свой градиент" title="Свой градиент">
|
||||
<button
|
||||
class="accent-swatch is-active"
|
||||
type="button"
|
||||
data-accent="violet"
|
||||
aria-label="Фиолетовый акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="cyan"
|
||||
aria-label="Голубой акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="lime"
|
||||
aria-label="Лаймовый акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="amber"
|
||||
aria-label="Янтарный акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch accent-swatch-custom"
|
||||
type="button"
|
||||
data-accent="custom"
|
||||
aria-label="Свой градиент"
|
||||
title="Свой градиент"
|
||||
>
|
||||
<span class="accent-swatch-label">Свой</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="accent-custom-panel" data-accent-custom-panel hidden>
|
||||
<!-- Настройки пользовательского linear-gradient акцента. -->
|
||||
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
|
||||
<p class="accent-custom-title">Свой градиент</p>
|
||||
|
||||
<label class="accent-custom-row">
|
||||
<span>Угол</span>
|
||||
<input type="range" min="0" max="360" step="1" value="135" data-custom-accent-angle />
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="360"
|
||||
step="1"
|
||||
value="135"
|
||||
data-custom-accent-angle
|
||||
/>
|
||||
<output data-custom-accent-angle-value>135°</output>
|
||||
</label>
|
||||
|
||||
<label class="accent-custom-row">
|
||||
<span>Цвет от</span>
|
||||
<input type="color" value="#8b5cf6" data-custom-accent-from />
|
||||
</label>
|
||||
|
||||
<label class="accent-custom-row">
|
||||
<span>Цвет до</span>
|
||||
<input type="color" value="#22d3ee" data-custom-accent-to />
|
||||
</label>
|
||||
|
||||
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->
|
||||
<section class="catalog cards-view" aria-live="polite" data-catalog-root></section>
|
||||
|
||||
<!-- Шаблон ошибки загрузки settings.json / panel data. -->
|
||||
<template id="config-error-template">
|
||||
<div class="empty-state config-error-state">
|
||||
<img src="../shared/assets/config.svg" alt="" />
|
||||
@ -156,6 +264,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Шаблон состояния, когда поиск не нашел ни одного элемента. -->
|
||||
<template id="empty-template">
|
||||
<div class="empty-state">
|
||||
<img src="../shared/assets/search.svg" alt="" />
|
||||
@ -166,11 +275,6 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script
|
||||
src="../shared/script.js"
|
||||
data-ecosystem-panel
|
||||
data-asset-base="../shared/"
|
||||
defer
|
||||
></script>
|
||||
<script src="../shared/script.js" data-ecosystem-panel data-asset-base="../shared/" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,4 +1,26 @@
|
||||
{
|
||||
"dataUrl": "./panel-data.json",
|
||||
"data": null
|
||||
"data": null,
|
||||
"defaults": {
|
||||
"navLayout": "sidebar",
|
||||
"theme": "dark",
|
||||
"view": "cards",
|
||||
"accent": "violet",
|
||||
"showNumbers": false,
|
||||
"showTreeTags": true,
|
||||
"treeTwoColumn": false,
|
||||
"autoCollapseSearch": true,
|
||||
"sidebarWidth": 390,
|
||||
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
|
||||
},
|
||||
"features": {
|
||||
"allowNavLayoutChange": true,
|
||||
"allowSearch": true,
|
||||
"allowViewChange": true,
|
||||
"allowShowNumbers": true,
|
||||
"allowTreeOptions": true,
|
||||
"allowSidebarResize": true,
|
||||
"allowThemeChange": true,
|
||||
"allowAccentChange": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<!doctype html>
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@ -17,13 +17,21 @@
|
||||
<link rel="stylesheet" href="../shared/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Декоративные фоновые пятна. Они не участвуют в логике и нужны только для глубины фона. -->
|
||||
<div class="ambient ambient-one"></div>
|
||||
<div class="ambient ambient-two"></div>
|
||||
|
||||
<main class="shell">
|
||||
<!--
|
||||
Основная сетка приложения:
|
||||
слева sticky-меню с деревом навигации, справа каталог карточек.
|
||||
Ширина левой колонки управляется CSS-переменной --sidebar-width и ресайзером.
|
||||
-->
|
||||
<div class="app-layout">
|
||||
<!-- Боковая панель: поиск, настройки и дерево навигации по всей структуре. -->
|
||||
<aside class="side-panel" aria-label="Навигация по экосистеме">
|
||||
<div class="hero-content">
|
||||
<!-- Шестеренка открывает fixed popup с настройками внешнего вида и поведения. -->
|
||||
<button
|
||||
class="settings-button"
|
||||
type="button"
|
||||
@ -36,7 +44,8 @@
|
||||
|
||||
<p class="eyebrow" id="page-title">Пример: section → item</p>
|
||||
|
||||
<label class="search-field">
|
||||
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
|
||||
<label class="search-field" data-search-field>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Grafana, S3, журнал, сервер..."
|
||||
@ -45,10 +54,12 @@
|
||||
/>
|
||||
</label>
|
||||
|
||||
<!-- Дерево генерируется из ecosystemSections в script.js. -->
|
||||
<nav class="nav-tree" aria-label="Дерево навигации" data-nav-tree></nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
|
||||
<div
|
||||
class="sidebar-resizer"
|
||||
role="separator"
|
||||
@ -57,8 +68,18 @@
|
||||
data-sidebar-resizer
|
||||
></div>
|
||||
|
||||
<!-- Затемнение страницы за popup-настройками. -->
|
||||
<div class="settings-overlay" data-settings-overlay></div>
|
||||
|
||||
<!--
|
||||
Popup настроек:
|
||||
- тема;
|
||||
- режим отображения карточек;
|
||||
- видимость нумерации;
|
||||
- видимость тегов в дереве;
|
||||
- авто-раскрытие дерева при поиске;
|
||||
- цветовой акцент.
|
||||
-->
|
||||
<div class="control-panel" aria-label="Настройки панели" data-settings-panel>
|
||||
<div class="settings-panel-head">
|
||||
<div>
|
||||
@ -70,28 +91,57 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Переключение светлой/темной темы. Значение сохраняется в localStorage. -->
|
||||
<section class="settings-section" data-settings-section="layout">
|
||||
<p class="settings-section-title">Макет</p>
|
||||
<div class="control-group">
|
||||
<span>Тема</span>
|
||||
<button class="segmented is-active" type="button" data-theme="dark">Темная</button>
|
||||
<button class="segmented" type="button" data-theme="light">Светлая</button>
|
||||
<span>Панель навигации</span>
|
||||
<button class="segmented is-active" type="button" data-nav-layout="sidebar">
|
||||
Сбоку
|
||||
</button>
|
||||
<button class="segmented" type="button" data-nav-layout="top">
|
||||
Сверху
|
||||
</button>
|
||||
</div>
|
||||
<p class="settings-section-hint">
|
||||
Режим «Сверху» — компактная шапка с названием и поиском, без дерева.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="control-group view-group">
|
||||
<section class="settings-section" data-settings-section="catalog">
|
||||
<p class="settings-section-title">Каталог</p>
|
||||
|
||||
<!-- Режимы каталога: обычные/компактные плитки и списки. -->
|
||||
<div class="control-group view-group" data-feature="allowViewChange">
|
||||
<span>Вид</span>
|
||||
<button class="segmented is-active" type="button" data-view="cards">Плитки</button>
|
||||
<button class="segmented" type="button" data-view="list">Список</button>
|
||||
<button class="segmented" type="button" data-view="compact-cards">Компакт плитки</button>
|
||||
<button class="segmented" type="button" data-view="compact-list">Компакт список</button>
|
||||
<button class="segmented is-active" type="button" data-view="cards">
|
||||
Плитки
|
||||
</button>
|
||||
<button class="segmented" type="button" data-view="list">
|
||||
Список
|
||||
</button>
|
||||
<button class="segmented" type="button" data-view="compact-cards">
|
||||
Компакт плитки
|
||||
</button>
|
||||
<button class="segmented" type="button" data-view="compact-list">
|
||||
Компакт список
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label class="toggle-row">
|
||||
<!-- Показывать номера разделов, подразделов и элементов. -->
|
||||
<label class="toggle-row" data-feature="allowShowNumbers">
|
||||
<span>
|
||||
<strong>Показывать нумерацию</strong>
|
||||
<small>Разделы, подразделы и элементы</small>
|
||||
</span>
|
||||
<input type="checkbox" data-show-numbers />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="settings-section" data-settings-section="tree">
|
||||
<p class="settings-section-title">Дерево</p>
|
||||
|
||||
<!-- Показывать поисковые теги рядом с файлами в дереве навигации. -->
|
||||
<label class="toggle-row">
|
||||
<span>
|
||||
<strong>Показывать теги дерева</strong>
|
||||
@ -100,6 +150,7 @@
|
||||
<input type="checkbox" data-show-tree-tags />
|
||||
</label>
|
||||
|
||||
<!-- Элементы внутри папок дерева отображаются в две колонки без тегов. -->
|
||||
<label class="toggle-row">
|
||||
<span>
|
||||
<strong>2 колонки в дереве</strong>
|
||||
@ -108,6 +159,7 @@
|
||||
<input type="checkbox" data-tree-two-column />
|
||||
</label>
|
||||
|
||||
<!-- Управляет тем, раскрывает ли поиск только ветки с совпадениями. -->
|
||||
<label class="toggle-row">
|
||||
<span>
|
||||
<strong>Авто раскрывать поиск</strong>
|
||||
@ -115,39 +167,95 @@
|
||||
</span>
|
||||
<input type="checkbox" data-auto-collapse-search />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<div class="control-group accent-group">
|
||||
<section class="settings-section" data-settings-section="appearance">
|
||||
<p class="settings-section-title">Оформление</p>
|
||||
|
||||
<div class="control-group" data-feature="allowThemeChange">
|
||||
<span>Тема</span>
|
||||
<button class="segmented is-active" type="button" data-theme="dark">
|
||||
Темная
|
||||
</button>
|
||||
<button class="segmented" type="button" data-theme="light">
|
||||
Светлая
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Выбор акцентного цвета интерфейса. -->
|
||||
<div class="control-group accent-group" data-feature="allowAccentChange">
|
||||
<span>Акцент</span>
|
||||
<button class="accent-swatch is-active" type="button" data-accent="violet" aria-label="Фиолетовый акцент"></button>
|
||||
<button class="accent-swatch" type="button" data-accent="cyan" aria-label="Голубой акцент"></button>
|
||||
<button class="accent-swatch" type="button" data-accent="lime" aria-label="Лаймовый акцент"></button>
|
||||
<button class="accent-swatch" type="button" data-accent="amber" aria-label="Янтарный акцент"></button>
|
||||
<button class="accent-swatch accent-swatch-custom" type="button" data-accent="custom" aria-label="Свой градиент" title="Свой градиент">
|
||||
<button
|
||||
class="accent-swatch is-active"
|
||||
type="button"
|
||||
data-accent="violet"
|
||||
aria-label="Фиолетовый акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="cyan"
|
||||
aria-label="Голубой акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="lime"
|
||||
aria-label="Лаймовый акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch"
|
||||
type="button"
|
||||
data-accent="amber"
|
||||
aria-label="Янтарный акцент"
|
||||
></button>
|
||||
<button
|
||||
class="accent-swatch accent-swatch-custom"
|
||||
type="button"
|
||||
data-accent="custom"
|
||||
aria-label="Свой градиент"
|
||||
title="Свой градиент"
|
||||
>
|
||||
<span class="accent-swatch-label">Свой</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="accent-custom-panel" data-accent-custom-panel hidden>
|
||||
<!-- Настройки пользовательского linear-gradient акцента. -->
|
||||
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
|
||||
<p class="accent-custom-title">Свой градиент</p>
|
||||
|
||||
<label class="accent-custom-row">
|
||||
<span>Угол</span>
|
||||
<input type="range" min="0" max="360" step="1" value="135" data-custom-accent-angle />
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="360"
|
||||
step="1"
|
||||
value="135"
|
||||
data-custom-accent-angle
|
||||
/>
|
||||
<output data-custom-accent-angle-value>135°</output>
|
||||
</label>
|
||||
|
||||
<label class="accent-custom-row">
|
||||
<span>Цвет от</span>
|
||||
<input type="color" value="#8b5cf6" data-custom-accent-from />
|
||||
</label>
|
||||
|
||||
<label class="accent-custom-row">
|
||||
<span>Цвет до</span>
|
||||
<input type="color" value="#22d3ee" data-custom-accent-to />
|
||||
</label>
|
||||
|
||||
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->
|
||||
<section class="catalog cards-view" aria-live="polite" data-catalog-root></section>
|
||||
|
||||
<!-- Шаблон ошибки загрузки settings.json / panel data. -->
|
||||
<template id="config-error-template">
|
||||
<div class="empty-state config-error-state">
|
||||
<img src="../shared/assets/config.svg" alt="" />
|
||||
@ -156,6 +264,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Шаблон состояния, когда поиск не нашел ни одного элемента. -->
|
||||
<template id="empty-template">
|
||||
<div class="empty-state">
|
||||
<img src="../shared/assets/search.svg" alt="" />
|
||||
@ -166,11 +275,6 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script
|
||||
src="../shared/script.js"
|
||||
data-ecosystem-panel
|
||||
data-asset-base="../shared/"
|
||||
defer
|
||||
></script>
|
||||
<script src="../shared/script.js" data-ecosystem-panel data-asset-base="../shared/" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,4 +1,26 @@
|
||||
{
|
||||
"dataUrl": "./panel-data.json",
|
||||
"data": null
|
||||
"data": null,
|
||||
"defaults": {
|
||||
"navLayout": "sidebar",
|
||||
"theme": "dark",
|
||||
"view": "cards",
|
||||
"accent": "violet",
|
||||
"showNumbers": false,
|
||||
"showTreeTags": true,
|
||||
"treeTwoColumn": false,
|
||||
"autoCollapseSearch": true,
|
||||
"sidebarWidth": 390,
|
||||
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
|
||||
},
|
||||
"features": {
|
||||
"allowNavLayoutChange": true,
|
||||
"allowSearch": true,
|
||||
"allowViewChange": true,
|
||||
"allowShowNumbers": true,
|
||||
"allowTreeOptions": true,
|
||||
"allowSidebarResize": true,
|
||||
"allowThemeChange": true,
|
||||
"allowAccentChange": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,34 +64,54 @@ const defaultPreferences = {
|
||||
customAccentFrom: "#8b5cf6",
|
||||
customAccentTo: "#22d3ee",
|
||||
customAccentAngle: 135,
|
||||
navLayout: "sidebar",
|
||||
showNumbers: false,
|
||||
showTreeTags: true,
|
||||
treeTwoColumn: false,
|
||||
autoCollapseSearch: true,
|
||||
sidebarWidth: 390,
|
||||
searchPlaceholder: "Grafana, S3, журнал, сервер...",
|
||||
};
|
||||
|
||||
const defaultPanelFeatures = {
|
||||
allowNavLayoutChange: true,
|
||||
allowSearch: true,
|
||||
allowViewChange: true,
|
||||
allowShowNumbers: true,
|
||||
allowTreeOptions: true,
|
||||
allowSidebarResize: true,
|
||||
allowThemeChange: true,
|
||||
allowAccentChange: true,
|
||||
};
|
||||
|
||||
const availableNavLayouts = ["sidebar", "top"];
|
||||
const availableViews = ["cards", "list", "compact-cards", "compact-list"];
|
||||
const availableAccents = ["violet", "cyan", "lime", "amber", "custom"];
|
||||
|
||||
// ============================================================================
|
||||
// Runtime state and DOM references
|
||||
// ============================================================================
|
||||
// preferences - текущие настройки интерфейса, объединенные с localStorage.
|
||||
const preferences = { ...defaultPreferences, ...readPreferences() };
|
||||
// preferences - текущие настройки интерфейса; итоговые значения собираются в bootstrap().
|
||||
let panelDefaults = { ...defaultPreferences };
|
||||
let panelFeatures = { ...defaultPanelFeatures };
|
||||
let preferences = { ...defaultPreferences };
|
||||
|
||||
// collapseState - объект вида { [collapseKey]: boolean } для details.
|
||||
const collapseState = readCollapseState();
|
||||
|
||||
// Корневые DOM-узлы, которые нужны для рендера и событий.
|
||||
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 searchField = document.querySelector("[data-search-field]");
|
||||
const searchInput = document.querySelector("[data-search-input]");
|
||||
const settingsToggle = document.querySelector("[data-settings-toggle]");
|
||||
const settingsPanel = document.querySelector("[data-settings-panel]");
|
||||
const settingsOverlay = document.querySelector("[data-settings-overlay]");
|
||||
const settingsClose = document.querySelector("[data-settings-close]");
|
||||
const themeButtons = document.querySelectorAll("[data-theme]");
|
||||
const navLayoutButtons = document.querySelectorAll("[data-nav-layout]");
|
||||
const viewButtons = document.querySelectorAll("[data-view]");
|
||||
const accentButtons = document.querySelectorAll("[data-accent]");
|
||||
const accentCustomPanel = document.querySelector("[data-accent-custom-panel]");
|
||||
@ -104,6 +124,7 @@ const showNumbersInput = document.querySelector("[data-show-numbers]");
|
||||
const showTreeTagsInput = document.querySelector("[data-show-tree-tags]");
|
||||
const treeTwoColumnInput = document.querySelector("[data-tree-two-column]");
|
||||
const autoCollapseSearchInput = document.querySelector("[data-auto-collapse-search]");
|
||||
const settingsSections = document.querySelectorAll("[data-settings-section]");
|
||||
const emptyTemplate = document.querySelector("#empty-template");
|
||||
const configErrorTemplate = document.querySelector("#config-error-template");
|
||||
const pageTitleElement = document.querySelector("#page-title");
|
||||
@ -115,7 +136,11 @@ bootstrap();
|
||||
|
||||
async function bootstrap() {
|
||||
try {
|
||||
const panelData = await loadPanelConfig();
|
||||
const settings = await fetchJson(SETTINGS_PATH);
|
||||
applyOrganizationConfig(settings);
|
||||
preferences = enforceFeatureLocks(normalizePreferences({ ...panelDefaults, ...readPreferences() }));
|
||||
|
||||
const panelData = await loadPanelDataFromSettings(settings);
|
||||
ecosystemSections = panelData.sections;
|
||||
serviceMeta = panelData.serviceMeta;
|
||||
panelTitle = panelData.title;
|
||||
@ -192,15 +217,40 @@ document.addEventListener("click", (event) => {
|
||||
|
||||
themeButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
if (!panelFeatures.allowThemeChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.theme = button.dataset.theme;
|
||||
applyPreferences(preferences);
|
||||
savePreferences(preferences);
|
||||
});
|
||||
});
|
||||
|
||||
navLayoutButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
if (!panelFeatures.allowNavLayoutChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.navLayout = button.dataset.navLayout;
|
||||
applyPreferences(preferences);
|
||||
|
||||
if (preferences.navLayout === "sidebar") {
|
||||
renderNavigationTree();
|
||||
}
|
||||
|
||||
savePreferences(preferences);
|
||||
});
|
||||
});
|
||||
|
||||
// Переключение режима отображения каталога.
|
||||
viewButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
if (!panelFeatures.allowViewChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.view = button.dataset.view;
|
||||
applyPreferences(preferences);
|
||||
savePreferences(preferences);
|
||||
@ -210,6 +260,10 @@ viewButtons.forEach((button) => {
|
||||
// Выбор цветового акцента из пресетов или пользовательского градиента.
|
||||
accentButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
if (!panelFeatures.allowAccentChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.accent = button.dataset.accent;
|
||||
applyPreferences(preferences);
|
||||
savePreferences(preferences);
|
||||
@ -218,6 +272,10 @@ accentButtons.forEach((button) => {
|
||||
|
||||
// Изменение параметров своего градиента сразу переключает accent на custom.
|
||||
function handleCustomAccentChange() {
|
||||
if (!panelFeatures.allowAccentChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.customAccentFrom = customAccentFromInput.value;
|
||||
preferences.customAccentTo = customAccentToInput.value;
|
||||
preferences.customAccentAngle = Number(customAccentAngleInput.value);
|
||||
@ -232,6 +290,10 @@ customAccentAngleInput.addEventListener("input", handleCustomAccentChange);
|
||||
|
||||
// Показывает/скрывает номера в секциях, группах и карточках.
|
||||
showNumbersInput.addEventListener("change", () => {
|
||||
if (!panelFeatures.allowShowNumbers) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.showNumbers = showNumbersInput.checked;
|
||||
applyPreferences(preferences);
|
||||
savePreferences(preferences);
|
||||
@ -240,6 +302,10 @@ showNumbersInput.addEventListener("change", () => {
|
||||
// Показывает/скрывает поисковые теги в файловом дереве.
|
||||
// Взаимоисключимо с двухколоночным режимом элементов папок.
|
||||
showTreeTagsInput.addEventListener("change", () => {
|
||||
if (!panelFeatures.allowTreeOptions || preferences.navLayout === "top") {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.showTreeTags = showTreeTagsInput.checked;
|
||||
if (preferences.showTreeTags) {
|
||||
preferences.treeTwoColumn = false;
|
||||
@ -251,6 +317,10 @@ showTreeTagsInput.addEventListener("change", () => {
|
||||
// Компактный двухколоночный режим элементов внутри папок дерева.
|
||||
// Взаимоисключимо с отображением тегов.
|
||||
treeTwoColumnInput.addEventListener("change", () => {
|
||||
if (!panelFeatures.allowTreeOptions || preferences.navLayout === "top") {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.treeTwoColumn = treeTwoColumnInput.checked;
|
||||
if (preferences.treeTwoColumn) {
|
||||
preferences.showTreeTags = false;
|
||||
@ -264,6 +334,10 @@ treeTwoColumnInput.addEventListener("change", () => {
|
||||
|
||||
// Включает/выключает авто-раскрытие веток дерева при поиске.
|
||||
autoCollapseSearchInput.addEventListener("change", () => {
|
||||
if (!panelFeatures.allowTreeOptions || preferences.navLayout === "top") {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.autoCollapseSearch = autoCollapseSearchInput.checked;
|
||||
applyPreferences(preferences);
|
||||
renderNavigationTree();
|
||||
@ -273,6 +347,10 @@ autoCollapseSearchInput.addEventListener("change", () => {
|
||||
// Drag-resize бокового меню.
|
||||
// Ширина сохраняется в preferences.sidebarWidth и применяется через CSS-переменную.
|
||||
sidebarResizer.addEventListener("pointerdown", (event) => {
|
||||
if (preferences.navLayout === "top" || !panelFeatures.allowSidebarResize) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
document.body.classList.add("is-resizing-sidebar");
|
||||
sidebarResizer.setPointerCapture(event.pointerId);
|
||||
@ -301,8 +379,7 @@ sidebarResizer.addEventListener("pointerdown", (event) => {
|
||||
// ============================================================================
|
||||
// Panel config loading
|
||||
// ============================================================================
|
||||
async function loadPanelConfig(settingsPath = SETTINGS_PATH) {
|
||||
const settings = await fetchJson(settingsPath);
|
||||
async function loadPanelDataFromSettings(settings) {
|
||||
const dataUrl = typeof settings.dataUrl === "string" ? settings.dataUrl.trim() : "";
|
||||
|
||||
let panelSource = settings.data;
|
||||
@ -317,6 +394,117 @@ async function loadPanelConfig(settingsPath = SETTINGS_PATH) {
|
||||
return normalizePanelData(panelSource);
|
||||
}
|
||||
|
||||
function applyOrganizationConfig(settings) {
|
||||
panelDefaults = normalizePreferences({
|
||||
...defaultPreferences,
|
||||
...(settings?.defaults && typeof settings.defaults === "object" ? settings.defaults : {}),
|
||||
});
|
||||
panelFeatures = {
|
||||
...defaultPanelFeatures,
|
||||
...(settings?.features && typeof settings.features === "object" ? settings.features : {}),
|
||||
};
|
||||
|
||||
const placeholder =
|
||||
typeof settings?.defaults?.searchPlaceholder === "string"
|
||||
? settings.defaults.searchPlaceholder.trim()
|
||||
: panelDefaults.searchPlaceholder;
|
||||
|
||||
if (searchInput && placeholder) {
|
||||
searchInput.placeholder = placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePreferences(raw) {
|
||||
const next = { ...raw };
|
||||
|
||||
if (!availableViews.includes(next.view)) {
|
||||
next.view = defaultPreferences.view;
|
||||
}
|
||||
|
||||
if (!availableAccents.includes(next.accent)) {
|
||||
next.accent = defaultPreferences.accent;
|
||||
}
|
||||
|
||||
if (!availableNavLayouts.includes(next.navLayout)) {
|
||||
next.navLayout = defaultPreferences.navLayout;
|
||||
}
|
||||
|
||||
next.customAccentFrom =
|
||||
normalizeHexColor(next.customAccentFrom) || defaultPreferences.customAccentFrom;
|
||||
next.customAccentTo = normalizeHexColor(next.customAccentTo) || defaultPreferences.customAccentTo;
|
||||
next.customAccentAngle = clamp(
|
||||
Number(next.customAccentAngle) || defaultPreferences.customAccentAngle,
|
||||
0,
|
||||
360,
|
||||
);
|
||||
next.showNumbers = Boolean(next.showNumbers);
|
||||
next.showTreeTags = Boolean(next.showTreeTags);
|
||||
next.treeTwoColumn = Boolean(next.treeTwoColumn);
|
||||
next.autoCollapseSearch = Boolean(next.autoCollapseSearch);
|
||||
next.sidebarWidth = clamp(
|
||||
Number(next.sidebarWidth) || defaultPreferences.sidebarWidth,
|
||||
260,
|
||||
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;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
function enforceFeatureLocks(rawPreferences) {
|
||||
const next = normalizePreferences(rawPreferences);
|
||||
|
||||
if (!panelFeatures.allowNavLayoutChange) {
|
||||
next.navLayout = panelDefaults.navLayout;
|
||||
}
|
||||
|
||||
if (!panelFeatures.allowThemeChange) {
|
||||
next.theme = panelDefaults.theme;
|
||||
}
|
||||
|
||||
if (!panelFeatures.allowViewChange) {
|
||||
next.view = panelDefaults.view;
|
||||
}
|
||||
|
||||
if (!panelFeatures.allowAccentChange) {
|
||||
next.accent = panelDefaults.accent;
|
||||
next.customAccentFrom = panelDefaults.customAccentFrom;
|
||||
next.customAccentTo = panelDefaults.customAccentTo;
|
||||
next.customAccentAngle = panelDefaults.customAccentAngle;
|
||||
}
|
||||
|
||||
if (!panelFeatures.allowShowNumbers) {
|
||||
next.showNumbers = panelDefaults.showNumbers;
|
||||
}
|
||||
|
||||
if (!panelFeatures.allowTreeOptions || next.navLayout === "top") {
|
||||
next.showTreeTags = false;
|
||||
next.treeTwoColumn = false;
|
||||
next.autoCollapseSearch = panelDefaults.autoCollapseSearch;
|
||||
}
|
||||
|
||||
if (!panelFeatures.allowSidebarResize || next.navLayout === "top") {
|
||||
next.sidebarWidth = panelDefaults.sidebarWidth;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
async function loadPanelConfig(settingsPath = SETTINGS_PATH) {
|
||||
const settings = await fetchJson(settingsPath);
|
||||
return loadPanelDataFromSettings(settings);
|
||||
}
|
||||
|
||||
async function fetchJson(url) {
|
||||
const response = await fetch(url);
|
||||
|
||||
@ -691,6 +879,11 @@ function matchesText(values, query) {
|
||||
// Строит левое файловое дерево из той же структуры данных, что и каталог.
|
||||
// Это важно: дерево и карточки всегда остаются синхронизированными.
|
||||
function renderNavigationTree() {
|
||||
if (preferences.navLayout === "top") {
|
||||
navTree.replaceChildren();
|
||||
return;
|
||||
}
|
||||
|
||||
const query = normalize(searchInput.value);
|
||||
const shouldAutoCollapse = Boolean(query && preferences.autoCollapseSearch);
|
||||
const fragment = document.createDocumentFragment();
|
||||
@ -877,65 +1070,95 @@ function attachCardInteractions() {
|
||||
// - класс режима отображения каталога;
|
||||
// - состояния toggle-контролов в настройках.
|
||||
function applyPreferences(nextPreferences) {
|
||||
if (!availableViews.includes(nextPreferences.view)) {
|
||||
nextPreferences.view = defaultPreferences.view;
|
||||
}
|
||||
const lockedPreferences = enforceFeatureLocks(nextPreferences);
|
||||
Object.assign(preferences, lockedPreferences);
|
||||
const activePreferences = preferences;
|
||||
|
||||
if (!availableAccents.includes(nextPreferences.accent)) {
|
||||
nextPreferences.accent = defaultPreferences.accent;
|
||||
}
|
||||
|
||||
nextPreferences.customAccentFrom = normalizeHexColor(nextPreferences.customAccentFrom) || defaultPreferences.customAccentFrom;
|
||||
nextPreferences.customAccentTo = normalizeHexColor(nextPreferences.customAccentTo) || defaultPreferences.customAccentTo;
|
||||
nextPreferences.customAccentAngle = clamp(
|
||||
Number(nextPreferences.customAccentAngle) || defaultPreferences.customAccentAngle,
|
||||
0,
|
||||
360,
|
||||
);
|
||||
|
||||
if (nextPreferences.treeTwoColumn) {
|
||||
nextPreferences.showTreeTags = false;
|
||||
} else if (nextPreferences.showTreeTags) {
|
||||
nextPreferences.treeTwoColumn = false;
|
||||
}
|
||||
|
||||
document.documentElement.dataset.theme = nextPreferences.theme;
|
||||
document.documentElement.dataset.accent = nextPreferences.accent;
|
||||
applyAccentVariables(nextPreferences);
|
||||
document.documentElement.dataset.theme = activePreferences.theme;
|
||||
document.documentElement.dataset.accent = activePreferences.accent;
|
||||
document.documentElement.dataset.navLayout =
|
||||
activePreferences.navLayout === "top" ? "top" : "sidebar";
|
||||
applyAccentVariables(activePreferences);
|
||||
document.documentElement.style.setProperty(
|
||||
"--sidebar-width",
|
||||
`${clamp(
|
||||
Number(nextPreferences.sidebarWidth) || defaultPreferences.sidebarWidth,
|
||||
Number(activePreferences.sidebarWidth) || defaultPreferences.sidebarWidth,
|
||||
260,
|
||||
getSidebarMaxWidth(nextPreferences),
|
||||
getSidebarMaxWidth(activePreferences),
|
||||
)}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--shell-max-width",
|
||||
nextPreferences.treeTwoColumn ? "1920px" : "1560px",
|
||||
activePreferences.treeTwoColumn ? "1920px" : "1560px",
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--sidebar-max-width",
|
||||
`${getSidebarMaxWidth(nextPreferences)}px`,
|
||||
`${getSidebarMaxWidth(activePreferences)}px`,
|
||||
);
|
||||
catalog.classList.remove("cards-view", "list-view", "compact-cards-view", "compact-list-view");
|
||||
catalog.classList.add(`${nextPreferences.view}-view`);
|
||||
document.documentElement.dataset.showNumbers = String(Boolean(nextPreferences.showNumbers));
|
||||
document.documentElement.dataset.showTreeTags = String(Boolean(nextPreferences.showTreeTags));
|
||||
document.documentElement.dataset.treeTwoColumn = String(Boolean(nextPreferences.treeTwoColumn));
|
||||
showNumbersInput.checked = Boolean(nextPreferences.showNumbers);
|
||||
showTreeTagsInput.checked = Boolean(nextPreferences.showTreeTags);
|
||||
treeTwoColumnInput.checked = Boolean(nextPreferences.treeTwoColumn);
|
||||
autoCollapseSearchInput.checked = Boolean(nextPreferences.autoCollapseSearch);
|
||||
customAccentFromInput.value = nextPreferences.customAccentFrom;
|
||||
customAccentToInput.value = nextPreferences.customAccentTo;
|
||||
customAccentAngleInput.value = String(nextPreferences.customAccentAngle);
|
||||
customAccentAngleValue.textContent = `${nextPreferences.customAccentAngle}°`;
|
||||
accentCustomPanel.hidden = nextPreferences.accent !== "custom";
|
||||
updateCustomAccentPreview(nextPreferences);
|
||||
setActiveButton(themeButtons, "theme", nextPreferences.theme);
|
||||
setActiveButton(viewButtons, "view", nextPreferences.view);
|
||||
setActiveButton(accentButtons, "accent", nextPreferences.accent);
|
||||
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));
|
||||
showNumbersInput.checked = Boolean(activePreferences.showNumbers);
|
||||
showTreeTagsInput.checked = Boolean(activePreferences.showTreeTags);
|
||||
treeTwoColumnInput.checked = Boolean(activePreferences.treeTwoColumn);
|
||||
autoCollapseSearchInput.checked = Boolean(activePreferences.autoCollapseSearch);
|
||||
customAccentFromInput.value = activePreferences.customAccentFrom;
|
||||
customAccentToInput.value = activePreferences.customAccentTo;
|
||||
customAccentAngleInput.value = String(activePreferences.customAccentAngle);
|
||||
customAccentAngleValue.textContent = `${activePreferences.customAccentAngle}°`;
|
||||
accentCustomPanel.hidden = activePreferences.accent !== "custom";
|
||||
updateCustomAccentPreview(activePreferences);
|
||||
setActiveButton(themeButtons, "theme", activePreferences.theme);
|
||||
setActiveButton(navLayoutButtons, "navLayout", activePreferences.navLayout);
|
||||
setActiveButton(viewButtons, "view", activePreferences.view);
|
||||
setActiveButton(accentButtons, "accent", activePreferences.accent);
|
||||
applyPanelFeatures(activePreferences);
|
||||
}
|
||||
|
||||
function applyPanelFeatures(activePreferences) {
|
||||
settingsSections.forEach((section) => {
|
||||
const featureName = section.dataset.settingsSection;
|
||||
section.hidden = !isSettingsSectionVisible(featureName, activePreferences);
|
||||
});
|
||||
|
||||
if (searchField) {
|
||||
searchField.hidden = !panelFeatures.allowSearch;
|
||||
}
|
||||
|
||||
if (navTree) {
|
||||
navTree.hidden = activePreferences.navLayout === "top";
|
||||
}
|
||||
|
||||
if (sidebarResizer) {
|
||||
sidebarResizer.hidden =
|
||||
activePreferences.navLayout === "top" || !panelFeatures.allowSidebarResize;
|
||||
}
|
||||
|
||||
if (appLayout) {
|
||||
appLayout.dataset.navLayout = activePreferences.navLayout === "top" ? "top" : "sidebar";
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-feature]").forEach((element) => {
|
||||
const featureName = element.dataset.feature;
|
||||
element.hidden = featureName ? !panelFeatures[featureName] : false;
|
||||
});
|
||||
}
|
||||
|
||||
function isSettingsSectionVisible(sectionName, activePreferences) {
|
||||
switch (sectionName) {
|
||||
case "layout":
|
||||
return panelFeatures.allowNavLayoutChange;
|
||||
case "catalog":
|
||||
return panelFeatures.allowViewChange || panelFeatures.allowShowNumbers;
|
||||
case "tree":
|
||||
return panelFeatures.allowTreeOptions && activePreferences.navLayout !== "top";
|
||||
case "appearance":
|
||||
return panelFeatures.allowThemeChange || panelFeatures.allowAccentChange;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Применяет CSS-переменные акцента для пресетов или пользовательского градиента.
|
||||
|
||||
@ -143,6 +143,48 @@ a {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* Компактная шапка: название + поиск, без дерева и resizer. */
|
||||
html[data-nav-layout="top"] .app-layout,
|
||||
.app-layout[data-nav-layout="top"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
html[data-nav-layout="top"] .sidebar-resizer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-nav-layout="top"] .side-panel {
|
||||
position: static;
|
||||
}
|
||||
|
||||
html[data-nav-layout="top"] .side-panel .hero-content {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px 16px;
|
||||
max-height: none;
|
||||
min-height: auto;
|
||||
overflow: visible;
|
||||
padding: 14px 58px 14px 18px;
|
||||
}
|
||||
|
||||
html[data-nav-layout="top"] .side-panel .hero-content > .eyebrow {
|
||||
margin: 0;
|
||||
flex: 0 1 auto;
|
||||
max-width: min(100%, 360px);
|
||||
}
|
||||
|
||||
html[data-nav-layout="top"] .search-field {
|
||||
margin-top: 0;
|
||||
flex: 1 1 220px;
|
||||
max-width: min(100%, 560px);
|
||||
}
|
||||
|
||||
html[data-nav-layout="top"] .nav-tree {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
04. Sidebar Resizer
|
||||
--------------------------------------------------------------------------
|
||||
@ -410,6 +452,31 @@ html[data-tree-two-column="true"] .tree-node.tree-file small {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.settings-section[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-section-title {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.settings-section-hint {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
07. Search Field And Control Inputs
|
||||
--------------------------------------------------------------------------
|
||||
|
||||
48
index.html
48
index.html
@ -45,7 +45,7 @@
|
||||
<p class="eyebrow" id="page-title">Панель экосистемы</p>
|
||||
|
||||
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
|
||||
<label class="search-field">
|
||||
<label class="search-field" data-search-field>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Grafana, S3, журнал, сервер..."
|
||||
@ -92,18 +92,27 @@
|
||||
</div>
|
||||
|
||||
<!-- Переключение светлой/темной темы. Значение сохраняется в localStorage. -->
|
||||
<section class="settings-section" data-settings-section="layout">
|
||||
<p class="settings-section-title">Макет</p>
|
||||
<div class="control-group">
|
||||
<span>Тема</span>
|
||||
<button class="segmented is-active" type="button" data-theme="dark">
|
||||
Темная
|
||||
<span>Панель навигации</span>
|
||||
<button class="segmented is-active" type="button" data-nav-layout="sidebar">
|
||||
Сбоку
|
||||
</button>
|
||||
<button class="segmented" type="button" data-theme="light">
|
||||
Светлая
|
||||
<button class="segmented" type="button" data-nav-layout="top">
|
||||
Сверху
|
||||
</button>
|
||||
</div>
|
||||
<p class="settings-section-hint">
|
||||
Режим «Сверху» — компактная шапка с названием и поиском, без дерева.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="settings-section" data-settings-section="catalog">
|
||||
<p class="settings-section-title">Каталог</p>
|
||||
|
||||
<!-- Режимы каталога: обычные/компактные плитки и списки. -->
|
||||
<div class="control-group view-group">
|
||||
<div class="control-group view-group" data-feature="allowViewChange">
|
||||
<span>Вид</span>
|
||||
<button class="segmented is-active" type="button" data-view="cards">
|
||||
Плитки
|
||||
@ -120,13 +129,17 @@
|
||||
</div>
|
||||
|
||||
<!-- Показывать номера разделов, подразделов и элементов. -->
|
||||
<label class="toggle-row">
|
||||
<label class="toggle-row" data-feature="allowShowNumbers">
|
||||
<span>
|
||||
<strong>Показывать нумерацию</strong>
|
||||
<small>Разделы, подразделы и элементы</small>
|
||||
</span>
|
||||
<input type="checkbox" data-show-numbers />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="settings-section" data-settings-section="tree">
|
||||
<p class="settings-section-title">Дерево</p>
|
||||
|
||||
<!-- Показывать поисковые теги рядом с файлами в дереве навигации. -->
|
||||
<label class="toggle-row">
|
||||
@ -154,9 +167,23 @@
|
||||
</span>
|
||||
<input type="checkbox" data-auto-collapse-search />
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="settings-section" data-settings-section="appearance">
|
||||
<p class="settings-section-title">Оформление</p>
|
||||
|
||||
<div class="control-group" data-feature="allowThemeChange">
|
||||
<span>Тема</span>
|
||||
<button class="segmented is-active" type="button" data-theme="dark">
|
||||
Темная
|
||||
</button>
|
||||
<button class="segmented" type="button" data-theme="light">
|
||||
Светлая
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Выбор акцентного цвета интерфейса. -->
|
||||
<div class="control-group accent-group">
|
||||
<div class="control-group accent-group" data-feature="allowAccentChange">
|
||||
<span>Акцент</span>
|
||||
<button
|
||||
class="accent-swatch is-active"
|
||||
@ -194,7 +221,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Настройки пользовательского linear-gradient акцента. -->
|
||||
<div class="accent-custom-panel" data-accent-custom-panel hidden>
|
||||
<div class="accent-custom-panel" data-accent-custom-panel data-feature="allowAccentChange" hidden>
|
||||
<p class="accent-custom-title">Свой градиент</p>
|
||||
|
||||
<label class="accent-custom-row">
|
||||
@ -222,6 +249,7 @@
|
||||
|
||||
<div class="accent-custom-preview" data-custom-accent-preview aria-hidden="true"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->
|
||||
|
||||
323
script.js
323
script.js
@ -64,34 +64,54 @@ const defaultPreferences = {
|
||||
customAccentFrom: "#8b5cf6",
|
||||
customAccentTo: "#22d3ee",
|
||||
customAccentAngle: 135,
|
||||
navLayout: "sidebar",
|
||||
showNumbers: false,
|
||||
showTreeTags: true,
|
||||
treeTwoColumn: false,
|
||||
autoCollapseSearch: true,
|
||||
sidebarWidth: 390,
|
||||
searchPlaceholder: "Grafana, S3, журнал, сервер...",
|
||||
};
|
||||
|
||||
const defaultPanelFeatures = {
|
||||
allowNavLayoutChange: true,
|
||||
allowSearch: true,
|
||||
allowViewChange: true,
|
||||
allowShowNumbers: true,
|
||||
allowTreeOptions: true,
|
||||
allowSidebarResize: true,
|
||||
allowThemeChange: true,
|
||||
allowAccentChange: true,
|
||||
};
|
||||
|
||||
const availableNavLayouts = ["sidebar", "top"];
|
||||
const availableViews = ["cards", "list", "compact-cards", "compact-list"];
|
||||
const availableAccents = ["violet", "cyan", "lime", "amber", "custom"];
|
||||
|
||||
// ============================================================================
|
||||
// Runtime state and DOM references
|
||||
// ============================================================================
|
||||
// preferences - текущие настройки интерфейса, объединенные с localStorage.
|
||||
const preferences = { ...defaultPreferences, ...readPreferences() };
|
||||
// preferences - текущие настройки интерфейса; итоговые значения собираются в bootstrap().
|
||||
let panelDefaults = { ...defaultPreferences };
|
||||
let panelFeatures = { ...defaultPanelFeatures };
|
||||
let preferences = { ...defaultPreferences };
|
||||
|
||||
// collapseState - объект вида { [collapseKey]: boolean } для details.
|
||||
const collapseState = readCollapseState();
|
||||
|
||||
// Корневые DOM-узлы, которые нужны для рендера и событий.
|
||||
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 searchField = document.querySelector("[data-search-field]");
|
||||
const searchInput = document.querySelector("[data-search-input]");
|
||||
const settingsToggle = document.querySelector("[data-settings-toggle]");
|
||||
const settingsPanel = document.querySelector("[data-settings-panel]");
|
||||
const settingsOverlay = document.querySelector("[data-settings-overlay]");
|
||||
const settingsClose = document.querySelector("[data-settings-close]");
|
||||
const themeButtons = document.querySelectorAll("[data-theme]");
|
||||
const navLayoutButtons = document.querySelectorAll("[data-nav-layout]");
|
||||
const viewButtons = document.querySelectorAll("[data-view]");
|
||||
const accentButtons = document.querySelectorAll("[data-accent]");
|
||||
const accentCustomPanel = document.querySelector("[data-accent-custom-panel]");
|
||||
@ -104,6 +124,7 @@ const showNumbersInput = document.querySelector("[data-show-numbers]");
|
||||
const showTreeTagsInput = document.querySelector("[data-show-tree-tags]");
|
||||
const treeTwoColumnInput = document.querySelector("[data-tree-two-column]");
|
||||
const autoCollapseSearchInput = document.querySelector("[data-auto-collapse-search]");
|
||||
const settingsSections = document.querySelectorAll("[data-settings-section]");
|
||||
const emptyTemplate = document.querySelector("#empty-template");
|
||||
const configErrorTemplate = document.querySelector("#config-error-template");
|
||||
const pageTitleElement = document.querySelector("#page-title");
|
||||
@ -115,7 +136,11 @@ bootstrap();
|
||||
|
||||
async function bootstrap() {
|
||||
try {
|
||||
const panelData = await loadPanelConfig();
|
||||
const settings = await fetchJson(SETTINGS_PATH);
|
||||
applyOrganizationConfig(settings);
|
||||
preferences = enforceFeatureLocks(normalizePreferences({ ...panelDefaults, ...readPreferences() }));
|
||||
|
||||
const panelData = await loadPanelDataFromSettings(settings);
|
||||
ecosystemSections = panelData.sections;
|
||||
serviceMeta = panelData.serviceMeta;
|
||||
panelTitle = panelData.title;
|
||||
@ -192,15 +217,40 @@ document.addEventListener("click", (event) => {
|
||||
|
||||
themeButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
if (!panelFeatures.allowThemeChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.theme = button.dataset.theme;
|
||||
applyPreferences(preferences);
|
||||
savePreferences(preferences);
|
||||
});
|
||||
});
|
||||
|
||||
navLayoutButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
if (!panelFeatures.allowNavLayoutChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.navLayout = button.dataset.navLayout;
|
||||
applyPreferences(preferences);
|
||||
|
||||
if (preferences.navLayout === "sidebar") {
|
||||
renderNavigationTree();
|
||||
}
|
||||
|
||||
savePreferences(preferences);
|
||||
});
|
||||
});
|
||||
|
||||
// Переключение режима отображения каталога.
|
||||
viewButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
if (!panelFeatures.allowViewChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.view = button.dataset.view;
|
||||
applyPreferences(preferences);
|
||||
savePreferences(preferences);
|
||||
@ -210,6 +260,10 @@ viewButtons.forEach((button) => {
|
||||
// Выбор цветового акцента из пресетов или пользовательского градиента.
|
||||
accentButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
if (!panelFeatures.allowAccentChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.accent = button.dataset.accent;
|
||||
applyPreferences(preferences);
|
||||
savePreferences(preferences);
|
||||
@ -218,6 +272,10 @@ accentButtons.forEach((button) => {
|
||||
|
||||
// Изменение параметров своего градиента сразу переключает accent на custom.
|
||||
function handleCustomAccentChange() {
|
||||
if (!panelFeatures.allowAccentChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.customAccentFrom = customAccentFromInput.value;
|
||||
preferences.customAccentTo = customAccentToInput.value;
|
||||
preferences.customAccentAngle = Number(customAccentAngleInput.value);
|
||||
@ -232,6 +290,10 @@ customAccentAngleInput.addEventListener("input", handleCustomAccentChange);
|
||||
|
||||
// Показывает/скрывает номера в секциях, группах и карточках.
|
||||
showNumbersInput.addEventListener("change", () => {
|
||||
if (!panelFeatures.allowShowNumbers) {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.showNumbers = showNumbersInput.checked;
|
||||
applyPreferences(preferences);
|
||||
savePreferences(preferences);
|
||||
@ -240,6 +302,10 @@ showNumbersInput.addEventListener("change", () => {
|
||||
// Показывает/скрывает поисковые теги в файловом дереве.
|
||||
// Взаимоисключимо с двухколоночным режимом элементов папок.
|
||||
showTreeTagsInput.addEventListener("change", () => {
|
||||
if (!panelFeatures.allowTreeOptions || preferences.navLayout === "top") {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.showTreeTags = showTreeTagsInput.checked;
|
||||
if (preferences.showTreeTags) {
|
||||
preferences.treeTwoColumn = false;
|
||||
@ -251,6 +317,10 @@ showTreeTagsInput.addEventListener("change", () => {
|
||||
// Компактный двухколоночный режим элементов внутри папок дерева.
|
||||
// Взаимоисключимо с отображением тегов.
|
||||
treeTwoColumnInput.addEventListener("change", () => {
|
||||
if (!panelFeatures.allowTreeOptions || preferences.navLayout === "top") {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.treeTwoColumn = treeTwoColumnInput.checked;
|
||||
if (preferences.treeTwoColumn) {
|
||||
preferences.showTreeTags = false;
|
||||
@ -264,6 +334,10 @@ treeTwoColumnInput.addEventListener("change", () => {
|
||||
|
||||
// Включает/выключает авто-раскрытие веток дерева при поиске.
|
||||
autoCollapseSearchInput.addEventListener("change", () => {
|
||||
if (!panelFeatures.allowTreeOptions || preferences.navLayout === "top") {
|
||||
return;
|
||||
}
|
||||
|
||||
preferences.autoCollapseSearch = autoCollapseSearchInput.checked;
|
||||
applyPreferences(preferences);
|
||||
renderNavigationTree();
|
||||
@ -273,6 +347,10 @@ autoCollapseSearchInput.addEventListener("change", () => {
|
||||
// Drag-resize бокового меню.
|
||||
// Ширина сохраняется в preferences.sidebarWidth и применяется через CSS-переменную.
|
||||
sidebarResizer.addEventListener("pointerdown", (event) => {
|
||||
if (preferences.navLayout === "top" || !panelFeatures.allowSidebarResize) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
document.body.classList.add("is-resizing-sidebar");
|
||||
sidebarResizer.setPointerCapture(event.pointerId);
|
||||
@ -301,8 +379,7 @@ sidebarResizer.addEventListener("pointerdown", (event) => {
|
||||
// ============================================================================
|
||||
// Panel config loading
|
||||
// ============================================================================
|
||||
async function loadPanelConfig(settingsPath = SETTINGS_PATH) {
|
||||
const settings = await fetchJson(settingsPath);
|
||||
async function loadPanelDataFromSettings(settings) {
|
||||
const dataUrl = typeof settings.dataUrl === "string" ? settings.dataUrl.trim() : "";
|
||||
|
||||
let panelSource = settings.data;
|
||||
@ -317,6 +394,117 @@ async function loadPanelConfig(settingsPath = SETTINGS_PATH) {
|
||||
return normalizePanelData(panelSource);
|
||||
}
|
||||
|
||||
function applyOrganizationConfig(settings) {
|
||||
panelDefaults = normalizePreferences({
|
||||
...defaultPreferences,
|
||||
...(settings?.defaults && typeof settings.defaults === "object" ? settings.defaults : {}),
|
||||
});
|
||||
panelFeatures = {
|
||||
...defaultPanelFeatures,
|
||||
...(settings?.features && typeof settings.features === "object" ? settings.features : {}),
|
||||
};
|
||||
|
||||
const placeholder =
|
||||
typeof settings?.defaults?.searchPlaceholder === "string"
|
||||
? settings.defaults.searchPlaceholder.trim()
|
||||
: panelDefaults.searchPlaceholder;
|
||||
|
||||
if (searchInput && placeholder) {
|
||||
searchInput.placeholder = placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePreferences(raw) {
|
||||
const next = { ...raw };
|
||||
|
||||
if (!availableViews.includes(next.view)) {
|
||||
next.view = defaultPreferences.view;
|
||||
}
|
||||
|
||||
if (!availableAccents.includes(next.accent)) {
|
||||
next.accent = defaultPreferences.accent;
|
||||
}
|
||||
|
||||
if (!availableNavLayouts.includes(next.navLayout)) {
|
||||
next.navLayout = defaultPreferences.navLayout;
|
||||
}
|
||||
|
||||
next.customAccentFrom =
|
||||
normalizeHexColor(next.customAccentFrom) || defaultPreferences.customAccentFrom;
|
||||
next.customAccentTo = normalizeHexColor(next.customAccentTo) || defaultPreferences.customAccentTo;
|
||||
next.customAccentAngle = clamp(
|
||||
Number(next.customAccentAngle) || defaultPreferences.customAccentAngle,
|
||||
0,
|
||||
360,
|
||||
);
|
||||
next.showNumbers = Boolean(next.showNumbers);
|
||||
next.showTreeTags = Boolean(next.showTreeTags);
|
||||
next.treeTwoColumn = Boolean(next.treeTwoColumn);
|
||||
next.autoCollapseSearch = Boolean(next.autoCollapseSearch);
|
||||
next.sidebarWidth = clamp(
|
||||
Number(next.sidebarWidth) || defaultPreferences.sidebarWidth,
|
||||
260,
|
||||
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;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
function enforceFeatureLocks(rawPreferences) {
|
||||
const next = normalizePreferences(rawPreferences);
|
||||
|
||||
if (!panelFeatures.allowNavLayoutChange) {
|
||||
next.navLayout = panelDefaults.navLayout;
|
||||
}
|
||||
|
||||
if (!panelFeatures.allowThemeChange) {
|
||||
next.theme = panelDefaults.theme;
|
||||
}
|
||||
|
||||
if (!panelFeatures.allowViewChange) {
|
||||
next.view = panelDefaults.view;
|
||||
}
|
||||
|
||||
if (!panelFeatures.allowAccentChange) {
|
||||
next.accent = panelDefaults.accent;
|
||||
next.customAccentFrom = panelDefaults.customAccentFrom;
|
||||
next.customAccentTo = panelDefaults.customAccentTo;
|
||||
next.customAccentAngle = panelDefaults.customAccentAngle;
|
||||
}
|
||||
|
||||
if (!panelFeatures.allowShowNumbers) {
|
||||
next.showNumbers = panelDefaults.showNumbers;
|
||||
}
|
||||
|
||||
if (!panelFeatures.allowTreeOptions || next.navLayout === "top") {
|
||||
next.showTreeTags = false;
|
||||
next.treeTwoColumn = false;
|
||||
next.autoCollapseSearch = panelDefaults.autoCollapseSearch;
|
||||
}
|
||||
|
||||
if (!panelFeatures.allowSidebarResize || next.navLayout === "top") {
|
||||
next.sidebarWidth = panelDefaults.sidebarWidth;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
async function loadPanelConfig(settingsPath = SETTINGS_PATH) {
|
||||
const settings = await fetchJson(settingsPath);
|
||||
return loadPanelDataFromSettings(settings);
|
||||
}
|
||||
|
||||
async function fetchJson(url) {
|
||||
const response = await fetch(url);
|
||||
|
||||
@ -691,6 +879,11 @@ function matchesText(values, query) {
|
||||
// Строит левое файловое дерево из той же структуры данных, что и каталог.
|
||||
// Это важно: дерево и карточки всегда остаются синхронизированными.
|
||||
function renderNavigationTree() {
|
||||
if (preferences.navLayout === "top") {
|
||||
navTree.replaceChildren();
|
||||
return;
|
||||
}
|
||||
|
||||
const query = normalize(searchInput.value);
|
||||
const shouldAutoCollapse = Boolean(query && preferences.autoCollapseSearch);
|
||||
const fragment = document.createDocumentFragment();
|
||||
@ -877,65 +1070,95 @@ function attachCardInteractions() {
|
||||
// - класс режима отображения каталога;
|
||||
// - состояния toggle-контролов в настройках.
|
||||
function applyPreferences(nextPreferences) {
|
||||
if (!availableViews.includes(nextPreferences.view)) {
|
||||
nextPreferences.view = defaultPreferences.view;
|
||||
}
|
||||
const lockedPreferences = enforceFeatureLocks(nextPreferences);
|
||||
Object.assign(preferences, lockedPreferences);
|
||||
const activePreferences = preferences;
|
||||
|
||||
if (!availableAccents.includes(nextPreferences.accent)) {
|
||||
nextPreferences.accent = defaultPreferences.accent;
|
||||
}
|
||||
|
||||
nextPreferences.customAccentFrom = normalizeHexColor(nextPreferences.customAccentFrom) || defaultPreferences.customAccentFrom;
|
||||
nextPreferences.customAccentTo = normalizeHexColor(nextPreferences.customAccentTo) || defaultPreferences.customAccentTo;
|
||||
nextPreferences.customAccentAngle = clamp(
|
||||
Number(nextPreferences.customAccentAngle) || defaultPreferences.customAccentAngle,
|
||||
0,
|
||||
360,
|
||||
);
|
||||
|
||||
if (nextPreferences.treeTwoColumn) {
|
||||
nextPreferences.showTreeTags = false;
|
||||
} else if (nextPreferences.showTreeTags) {
|
||||
nextPreferences.treeTwoColumn = false;
|
||||
}
|
||||
|
||||
document.documentElement.dataset.theme = nextPreferences.theme;
|
||||
document.documentElement.dataset.accent = nextPreferences.accent;
|
||||
applyAccentVariables(nextPreferences);
|
||||
document.documentElement.dataset.theme = activePreferences.theme;
|
||||
document.documentElement.dataset.accent = activePreferences.accent;
|
||||
document.documentElement.dataset.navLayout =
|
||||
activePreferences.navLayout === "top" ? "top" : "sidebar";
|
||||
applyAccentVariables(activePreferences);
|
||||
document.documentElement.style.setProperty(
|
||||
"--sidebar-width",
|
||||
`${clamp(
|
||||
Number(nextPreferences.sidebarWidth) || defaultPreferences.sidebarWidth,
|
||||
Number(activePreferences.sidebarWidth) || defaultPreferences.sidebarWidth,
|
||||
260,
|
||||
getSidebarMaxWidth(nextPreferences),
|
||||
getSidebarMaxWidth(activePreferences),
|
||||
)}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--shell-max-width",
|
||||
nextPreferences.treeTwoColumn ? "1920px" : "1560px",
|
||||
activePreferences.treeTwoColumn ? "1920px" : "1560px",
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--sidebar-max-width",
|
||||
`${getSidebarMaxWidth(nextPreferences)}px`,
|
||||
`${getSidebarMaxWidth(activePreferences)}px`,
|
||||
);
|
||||
catalog.classList.remove("cards-view", "list-view", "compact-cards-view", "compact-list-view");
|
||||
catalog.classList.add(`${nextPreferences.view}-view`);
|
||||
document.documentElement.dataset.showNumbers = String(Boolean(nextPreferences.showNumbers));
|
||||
document.documentElement.dataset.showTreeTags = String(Boolean(nextPreferences.showTreeTags));
|
||||
document.documentElement.dataset.treeTwoColumn = String(Boolean(nextPreferences.treeTwoColumn));
|
||||
showNumbersInput.checked = Boolean(nextPreferences.showNumbers);
|
||||
showTreeTagsInput.checked = Boolean(nextPreferences.showTreeTags);
|
||||
treeTwoColumnInput.checked = Boolean(nextPreferences.treeTwoColumn);
|
||||
autoCollapseSearchInput.checked = Boolean(nextPreferences.autoCollapseSearch);
|
||||
customAccentFromInput.value = nextPreferences.customAccentFrom;
|
||||
customAccentToInput.value = nextPreferences.customAccentTo;
|
||||
customAccentAngleInput.value = String(nextPreferences.customAccentAngle);
|
||||
customAccentAngleValue.textContent = `${nextPreferences.customAccentAngle}°`;
|
||||
accentCustomPanel.hidden = nextPreferences.accent !== "custom";
|
||||
updateCustomAccentPreview(nextPreferences);
|
||||
setActiveButton(themeButtons, "theme", nextPreferences.theme);
|
||||
setActiveButton(viewButtons, "view", nextPreferences.view);
|
||||
setActiveButton(accentButtons, "accent", nextPreferences.accent);
|
||||
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));
|
||||
showNumbersInput.checked = Boolean(activePreferences.showNumbers);
|
||||
showTreeTagsInput.checked = Boolean(activePreferences.showTreeTags);
|
||||
treeTwoColumnInput.checked = Boolean(activePreferences.treeTwoColumn);
|
||||
autoCollapseSearchInput.checked = Boolean(activePreferences.autoCollapseSearch);
|
||||
customAccentFromInput.value = activePreferences.customAccentFrom;
|
||||
customAccentToInput.value = activePreferences.customAccentTo;
|
||||
customAccentAngleInput.value = String(activePreferences.customAccentAngle);
|
||||
customAccentAngleValue.textContent = `${activePreferences.customAccentAngle}°`;
|
||||
accentCustomPanel.hidden = activePreferences.accent !== "custom";
|
||||
updateCustomAccentPreview(activePreferences);
|
||||
setActiveButton(themeButtons, "theme", activePreferences.theme);
|
||||
setActiveButton(navLayoutButtons, "navLayout", activePreferences.navLayout);
|
||||
setActiveButton(viewButtons, "view", activePreferences.view);
|
||||
setActiveButton(accentButtons, "accent", activePreferences.accent);
|
||||
applyPanelFeatures(activePreferences);
|
||||
}
|
||||
|
||||
function applyPanelFeatures(activePreferences) {
|
||||
settingsSections.forEach((section) => {
|
||||
const featureName = section.dataset.settingsSection;
|
||||
section.hidden = !isSettingsSectionVisible(featureName, activePreferences);
|
||||
});
|
||||
|
||||
if (searchField) {
|
||||
searchField.hidden = !panelFeatures.allowSearch;
|
||||
}
|
||||
|
||||
if (navTree) {
|
||||
navTree.hidden = activePreferences.navLayout === "top";
|
||||
}
|
||||
|
||||
if (sidebarResizer) {
|
||||
sidebarResizer.hidden =
|
||||
activePreferences.navLayout === "top" || !panelFeatures.allowSidebarResize;
|
||||
}
|
||||
|
||||
if (appLayout) {
|
||||
appLayout.dataset.navLayout = activePreferences.navLayout === "top" ? "top" : "sidebar";
|
||||
}
|
||||
|
||||
document.querySelectorAll("[data-feature]").forEach((element) => {
|
||||
const featureName = element.dataset.feature;
|
||||
element.hidden = featureName ? !panelFeatures[featureName] : false;
|
||||
});
|
||||
}
|
||||
|
||||
function isSettingsSectionVisible(sectionName, activePreferences) {
|
||||
switch (sectionName) {
|
||||
case "layout":
|
||||
return panelFeatures.allowNavLayoutChange;
|
||||
case "catalog":
|
||||
return panelFeatures.allowViewChange || panelFeatures.allowShowNumbers;
|
||||
case "tree":
|
||||
return panelFeatures.allowTreeOptions && activePreferences.navLayout !== "top";
|
||||
case "appearance":
|
||||
return panelFeatures.allowThemeChange || panelFeatures.allowAccentChange;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Применяет CSS-переменные акцента для пресетов или пользовательского градиента.
|
||||
|
||||
26
settings-top-bar.example.json
Normal file
26
settings-top-bar.example.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"dataUrl": "./panel-data.json",
|
||||
"data": null,
|
||||
"defaults": {
|
||||
"navLayout": "top",
|
||||
"theme": "dark",
|
||||
"view": "cards",
|
||||
"accent": "cyan",
|
||||
"showNumbers": false,
|
||||
"showTreeTags": false,
|
||||
"treeTwoColumn": false,
|
||||
"autoCollapseSearch": true,
|
||||
"sidebarWidth": 390,
|
||||
"searchPlaceholder": "Поиск сервиса..."
|
||||
},
|
||||
"features": {
|
||||
"allowNavLayoutChange": true,
|
||||
"allowSearch": true,
|
||||
"allowViewChange": true,
|
||||
"allowShowNumbers": false,
|
||||
"allowTreeOptions": false,
|
||||
"allowSidebarResize": false,
|
||||
"allowThemeChange": true,
|
||||
"allowAccentChange": true
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,29 @@
|
||||
{
|
||||
"dataUrl": "./panel-data.json",
|
||||
"data": null
|
||||
"data": null,
|
||||
"defaults": {
|
||||
"navLayout": "sidebar",
|
||||
"theme": "dark",
|
||||
"view": "cards",
|
||||
"accent": "violet",
|
||||
"customAccentFrom": "#8b5cf6",
|
||||
"customAccentTo": "#22d3ee",
|
||||
"customAccentAngle": 135,
|
||||
"showNumbers": false,
|
||||
"showTreeTags": true,
|
||||
"treeTwoColumn": false,
|
||||
"autoCollapseSearch": true,
|
||||
"sidebarWidth": 390,
|
||||
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
|
||||
},
|
||||
"features": {
|
||||
"allowNavLayoutChange": true,
|
||||
"allowSearch": true,
|
||||
"allowViewChange": true,
|
||||
"allowShowNumbers": true,
|
||||
"allowTreeOptions": true,
|
||||
"allowSidebarResize": true,
|
||||
"allowThemeChange": true,
|
||||
"allowAccentChange": true
|
||||
}
|
||||
}
|
||||
|
||||
67
styles.css
67
styles.css
@ -143,6 +143,48 @@ a {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* Компактная шапка: название + поиск, без дерева и resizer. */
|
||||
html[data-nav-layout="top"] .app-layout,
|
||||
.app-layout[data-nav-layout="top"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
html[data-nav-layout="top"] .sidebar-resizer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-nav-layout="top"] .side-panel {
|
||||
position: static;
|
||||
}
|
||||
|
||||
html[data-nav-layout="top"] .side-panel .hero-content {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px 16px;
|
||||
max-height: none;
|
||||
min-height: auto;
|
||||
overflow: visible;
|
||||
padding: 14px 58px 14px 18px;
|
||||
}
|
||||
|
||||
html[data-nav-layout="top"] .side-panel .hero-content > .eyebrow {
|
||||
margin: 0;
|
||||
flex: 0 1 auto;
|
||||
max-width: min(100%, 360px);
|
||||
}
|
||||
|
||||
html[data-nav-layout="top"] .search-field {
|
||||
margin-top: 0;
|
||||
flex: 1 1 220px;
|
||||
max-width: min(100%, 560px);
|
||||
}
|
||||
|
||||
html[data-nav-layout="top"] .nav-tree {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
04. Sidebar Resizer
|
||||
--------------------------------------------------------------------------
|
||||
@ -410,6 +452,31 @@ html[data-tree-two-column="true"] .tree-node.tree-file small {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.settings-section[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-section-title {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.settings-section-hint {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
07. Search Field And Control Inputs
|
||||
--------------------------------------------------------------------------
|
||||
|
||||
Loading…
Reference in New Issue
Block a user