diff --git a/README.md b/README.md
index 945d38f..fbc183a 100644
--- a/README.md
+++ b/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`) |
+| **Оформление** | Тема, акцент, свой градиент |
+
+### Режим «Сверху»
+
+Компактная шапка над каталогом: заголовок панели, поиск и ⚙. Дерево навигации скрыто — переход только через карточки каталога. Подходит для простых порталов и когда дерево избыточно.
### Свой градиент
diff --git a/examples/README.md b/examples/README.md
index 1610df3..14d0986 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -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/
diff --git a/examples/flat/index.html b/examples/flat/index.html
index 3554015..2a6f59d 100644
--- a/examples/flat/index.html
+++ b/examples/flat/index.html
@@ -1,4 +1,4 @@
-
+
@@ -17,13 +17,21 @@
+
+
+
+
+
+
@@ -70,28 +91,57 @@
-
- Тема
-
-
-
+
+
+ Макет
+
+ Панель навигации
+
+
+
+
+ Режим «Сверху» — компактная шапка с названием и поиском, без дерева.
+
+
-
+
+
-
+
+ Оформление
+
+
+ Тема
+
+
+
+
+
+
Акцент
-
-
-
-
-
-
+
+

@@ -156,6 +264,7 @@
+

@@ -166,11 +275,6 @@
-
+
diff --git a/examples/flat/settings.json b/examples/flat/settings.json
index 342d127..6c10759 100644
--- a/examples/flat/settings.json
+++ b/examples/flat/settings.json
@@ -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
+ }
}
diff --git a/examples/full/index.html b/examples/full/index.html
index a9e3a62..355559b 100644
--- a/examples/full/index.html
+++ b/examples/full/index.html
@@ -1,4 +1,4 @@
-
+
@@ -17,13 +17,21 @@
+
+
+
+
+
+
@@ -70,28 +91,57 @@
-
- Тема
- Темная
- Светлая
-
+
+
+ Макет
+
+ Панель навигации
+
+ Сбоку
+
+
+ Сверху
+
+
+
+ Режим «Сверху» — компактная шапка с названием и поиском, без дерева.
+
+
-
+
+
-
+
+ Оформление
+
+
+ Тема
+
+ Темная
+
+
+ Светлая
+
+
+
+
+
Акцент
-
-
-
-
-
+
+
+
+
+
Свой
-
+
+

@@ -156,6 +264,7 @@
+

@@ -166,11 +275,6 @@
-
+
diff --git a/examples/full/settings.json b/examples/full/settings.json
index 342d127..6c10759 100644
--- a/examples/full/settings.json
+++ b/examples/full/settings.json
@@ -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
+ }
}
diff --git a/examples/mixed/index.html b/examples/mixed/index.html
index 8651d34..db4d045 100644
--- a/examples/mixed/index.html
+++ b/examples/mixed/index.html
@@ -1,4 +1,4 @@
-
+
@@ -17,13 +17,21 @@
+
+
+
+
+
+
@@ -70,28 +91,57 @@
-
- Тема
- Темная
- Светлая
-
+
+
+ Макет
+
+ Панель навигации
+
+ Сбоку
+
+
+ Сверху
+
+
+
+ Режим «Сверху» — компактная шапка с названием и поиском, без дерева.
+
+
-
+
+
-
+
+ Оформление
+
+
+ Тема
+
+ Темная
+
+
+ Светлая
+
+
+
+
+
Акцент
-
-
-
-
-
+
+
+
+
+
Свой
-
+
+

@@ -156,6 +264,7 @@
+

@@ -166,11 +275,6 @@
-
+
diff --git a/examples/mixed/settings.json b/examples/mixed/settings.json
index 342d127..6c10759 100644
--- a/examples/mixed/settings.json
+++ b/examples/mixed/settings.json
@@ -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
+ }
}
diff --git a/examples/section-items/index.html b/examples/section-items/index.html
index a79c4c0..e11267f 100644
--- a/examples/section-items/index.html
+++ b/examples/section-items/index.html
@@ -1,4 +1,4 @@
-
+
@@ -17,13 +17,21 @@
+
+
+
+
+
+
@@ -70,28 +91,57 @@
-
- Тема
- Темная
- Светлая
-
+
+
+ Макет
+
+ Панель навигации
+
+ Сбоку
+
+
+ Сверху
+
+
+
+ Режим «Сверху» — компактная шапка с названием и поиском, без дерева.
+
+
-
+
+
-
+
+ Оформление
+
+
+ Тема
+
+ Темная
+
+
+ Светлая
+
+
+
+
+
Акцент
-
-
-
-
-
+
+
+
+
+
Свой
-
+
+

@@ -156,6 +264,7 @@
+

@@ -166,11 +275,6 @@
-
+
diff --git a/examples/section-items/settings.json b/examples/section-items/settings.json
index 342d127..6c10759 100644
--- a/examples/section-items/settings.json
+++ b/examples/section-items/settings.json
@@ -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
+ }
}
diff --git a/examples/shared/script.js b/examples/shared/script.js
index 1f6ff40..411d0f3 100644
--- a/examples/shared/script.js
+++ b/examples/shared/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-переменные акцента для пресетов или пользовательского градиента.
diff --git a/examples/shared/styles.css b/examples/shared/styles.css
index 3f26ecf..597afb4 100644
--- a/examples/shared/styles.css
+++ b/examples/shared/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
--------------------------------------------------------------------------
diff --git a/index.html b/index.html
index 137e831..6c47656 100644
--- a/index.html
+++ b/index.html
@@ -45,7 +45,7 @@
Панель экосистемы
-