добавление комментарией к коду
This commit is contained in:
parent
c187c66737
commit
37220d7d17
245
index.html
245
index.html
@ -17,37 +17,49 @@
|
|||||||
<link rel="stylesheet" href="styles.css" />
|
<link rel="stylesheet" href="styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<!-- Декоративные фоновые пятна. Они не участвуют в логике и нужны только для глубины фона. -->
|
||||||
<div class="ambient ambient-one"></div>
|
<div class="ambient ambient-one"></div>
|
||||||
<div class="ambient ambient-two"></div>
|
<div class="ambient ambient-two"></div>
|
||||||
|
|
||||||
<main class="shell">
|
<main class="shell">
|
||||||
|
<!--
|
||||||
|
Основная сетка приложения:
|
||||||
|
слева sticky-меню с деревом навигации, справа каталог карточек.
|
||||||
|
Ширина левой колонки управляется CSS-переменной --sidebar-width и ресайзером.
|
||||||
|
-->
|
||||||
<div class="app-layout">
|
<div class="app-layout">
|
||||||
|
<!-- Боковая панель: поиск, настройки и дерево навигации по всей структуре. -->
|
||||||
<aside class="side-panel" aria-label="Навигация по экосистеме">
|
<aside class="side-panel" aria-label="Навигация по экосистеме">
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<button
|
<!-- Шестеренка открывает fixed popup с настройками внешнего вида и поведения. -->
|
||||||
class="settings-button"
|
<button
|
||||||
type="button"
|
class="settings-button"
|
||||||
aria-label="Открыть настройки внешнего вида"
|
type="button"
|
||||||
aria-expanded="false"
|
aria-label="Открыть настройки внешнего вида"
|
||||||
data-settings-toggle
|
aria-expanded="false"
|
||||||
>
|
data-settings-toggle
|
||||||
⚙
|
>
|
||||||
</button>
|
⚙
|
||||||
|
</button>
|
||||||
|
|
||||||
<p class="eyebrow" id="page-title">Панель экосистемы</p>
|
<p class="eyebrow" id="page-title">Панель экосистемы</p>
|
||||||
|
|
||||||
<label class="search-field">
|
<!-- Поиск фильтрует каталог и подсвечивает совпадения в дереве и карточках. -->
|
||||||
<input
|
<label class="search-field">
|
||||||
type="search"
|
<input
|
||||||
placeholder="Grafana, S3, журнал, сервер..."
|
type="search"
|
||||||
autocomplete="off"
|
placeholder="Grafana, S3, журнал, сервер..."
|
||||||
data-search-input
|
autocomplete="off"
|
||||||
/>
|
data-search-input
|
||||||
</label>
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<nav class="nav-tree" aria-label="Дерево навигации" data-nav-tree></nav>
|
<!-- Дерево генерируется из ecosystemSections в script.js. -->
|
||||||
|
<nav class="nav-tree" aria-label="Дерево навигации" data-nav-tree></nav>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<!-- Вертикальная ручка для изменения ширины бокового меню мышью/указателем. -->
|
||||||
<div
|
<div
|
||||||
class="sidebar-resizer"
|
class="sidebar-resizer"
|
||||||
role="separator"
|
role="separator"
|
||||||
@ -56,99 +68,118 @@
|
|||||||
data-sidebar-resizer
|
data-sidebar-resizer
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div class="settings-overlay" data-settings-overlay></div>
|
<!-- Затемнение страницы за popup-настройками. -->
|
||||||
<div class="control-panel" aria-label="Настройки панели" data-settings-panel>
|
<div class="settings-overlay" data-settings-overlay></div>
|
||||||
<div class="settings-panel-head">
|
|
||||||
<div>
|
<!--
|
||||||
<p class="eyebrow">View settings</p>
|
Popup настроек:
|
||||||
<h2>Настройки</h2>
|
- тема;
|
||||||
|
- режим отображения карточек;
|
||||||
|
- видимость нумерации;
|
||||||
|
- видимость тегов в дереве;
|
||||||
|
- авто-раскрытие дерева при поиске;
|
||||||
|
- цветовой акцент.
|
||||||
|
-->
|
||||||
|
<div class="control-panel" aria-label="Настройки панели" data-settings-panel>
|
||||||
|
<div class="settings-panel-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">View settings</p>
|
||||||
|
<h2>Настройки</h2>
|
||||||
|
</div>
|
||||||
|
<button class="settings-close" type="button" aria-label="Закрыть настройки" data-settings-close>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Переключение светлой/темной темы. Значение сохраняется в localStorage. -->
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Режимы каталога: обычные/компактные плитки и списки. -->
|
||||||
|
<div class="control-group view-group">
|
||||||
|
<span>Вид</span>
|
||||||
|
<button class="segmented is-active" type="button" data-view="cards">
|
||||||
|
Плитки
|
||||||
|
</button>
|
||||||
|
<button class="segmented" type="button" data-view="list">
|
||||||
|
Список
|
||||||
|
</button>
|
||||||
|
<button class="segmented" type="button" data-view="compact-cards">
|
||||||
|
Компакт плитки
|
||||||
|
</button>
|
||||||
|
<button class="segmented" type="button" data-view="compact-list">
|
||||||
|
Компакт список
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Показывать номера разделов, подразделов и элементов. -->
|
||||||
|
<label class="toggle-row">
|
||||||
|
<span>
|
||||||
|
<strong>Показывать нумерацию</strong>
|
||||||
|
<small>Разделы, подразделы и элементы</small>
|
||||||
|
</span>
|
||||||
|
<input type="checkbox" data-show-numbers />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Показывать поисковые теги рядом с файлами в дереве навигации. -->
|
||||||
|
<label class="toggle-row">
|
||||||
|
<span>
|
||||||
|
<strong>Показывать теги дерева</strong>
|
||||||
|
<small>Поисковые теги рядом с элементом</small>
|
||||||
|
</span>
|
||||||
|
<input type="checkbox" data-show-tree-tags />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Управляет тем, раскрывает ли поиск только ветки с совпадениями. -->
|
||||||
|
<label class="toggle-row">
|
||||||
|
<span>
|
||||||
|
<strong>Авто раскрывать поиск</strong>
|
||||||
|
<small>Сворачивать ветки без совпадений</small>
|
||||||
|
</span>
|
||||||
|
<input type="checkbox" data-auto-collapse-search />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Выбор акцентного цвета интерфейса. -->
|
||||||
|
<div class="control-group accent-group">
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<button class="settings-close" type="button" aria-label="Закрыть настройки" data-settings-close>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-group">
|
<!-- Каталог карточек генерируется из ecosystemSections в script.js. -->
|
||||||
<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 view-group">
|
|
||||||
<span>Вид</span>
|
|
||||||
<button class="segmented is-active" type="button" data-view="cards">
|
|
||||||
Плитки
|
|
||||||
</button>
|
|
||||||
<button class="segmented" type="button" data-view="list">
|
|
||||||
Список
|
|
||||||
</button>
|
|
||||||
<button class="segmented" type="button" data-view="compact-cards">
|
|
||||||
Компакт плитки
|
|
||||||
</button>
|
|
||||||
<button class="segmented" type="button" data-view="compact-list">
|
|
||||||
Компакт список
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label class="toggle-row">
|
|
||||||
<span>
|
|
||||||
<strong>Показывать нумерацию</strong>
|
|
||||||
<small>Разделы, подразделы и элементы</small>
|
|
||||||
</span>
|
|
||||||
<input type="checkbox" data-show-numbers />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="toggle-row">
|
|
||||||
<span>
|
|
||||||
<strong>Показывать теги дерева</strong>
|
|
||||||
<small>Поисковые теги рядом с элементом</small>
|
|
||||||
</span>
|
|
||||||
<input type="checkbox" data-show-tree-tags />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="toggle-row">
|
|
||||||
<span>
|
|
||||||
<strong>Авто раскрывать поиск</strong>
|
|
||||||
<small>Сворачивать ветки без совпадений</small>
|
|
||||||
</span>
|
|
||||||
<input type="checkbox" data-auto-collapse-search />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="control-group accent-group">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="catalog cards-view" aria-live="polite" data-catalog-root></section>
|
<section class="catalog cards-view" aria-live="polite" data-catalog-root></section>
|
||||||
|
|
||||||
|
<!-- Шаблон состояния, когда поиск не нашел ни одного элемента. -->
|
||||||
<template id="empty-template">
|
<template id="empty-template">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<img src="assets/search.svg" alt="" />
|
<img src="assets/search.svg" alt="" />
|
||||||
|
|||||||
145
script.js
145
script.js
@ -1,8 +1,31 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// Persistent storage keys
|
||||||
|
// ============================================================================
|
||||||
|
// STORAGE_KEY хранит пользовательские настройки интерфейса:
|
||||||
|
// тему, акцент, режим отображения, ширину сайдбара и toggle-настройки.
|
||||||
const STORAGE_KEY = "ecosystem-panel-preferences";
|
const STORAGE_KEY = "ecosystem-panel-preferences";
|
||||||
|
|
||||||
|
// COLLAPSE_STORAGE_KEY хранит ручные состояния раскрытия details:
|
||||||
|
// отдельно для дерева навигации и отдельно для контентных секций.
|
||||||
const COLLAPSE_STORAGE_KEY = "ecosystem-panel-collapse-state";
|
const COLLAPSE_STORAGE_KEY = "ecosystem-panel-collapse-state";
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Navigation data model
|
||||||
|
// ============================================================================
|
||||||
// Меняй навигацию здесь: разделы -> подразделы -> элементы.
|
// Меняй навигацию здесь: разделы -> подразделы -> элементы.
|
||||||
// url можно заменить на реальные адреса, tags помогают поиску.
|
// url можно заменить на реальные адреса, tags помогают поиску.
|
||||||
|
//
|
||||||
|
// Структура:
|
||||||
|
// - section: крупный раздел панели;
|
||||||
|
// - group: подраздел внутри section;
|
||||||
|
// - item: конечный кликабельный элемент.
|
||||||
|
//
|
||||||
|
// Каждый item имеет:
|
||||||
|
// - number: человекочитаемый номер;
|
||||||
|
// - title: название карточки;
|
||||||
|
// - type: ключ из serviceMeta, который задает иконку и тип сервиса;
|
||||||
|
// - tags: поисковые теги, которые также можно показывать в дереве;
|
||||||
|
// - url: ссылка перехода. По умолчанию "#", чтобы заглушки не дергали страницу.
|
||||||
const ecosystemSections = [
|
const ecosystemSections = [
|
||||||
{
|
{
|
||||||
id: "user",
|
id: "user",
|
||||||
@ -113,6 +136,8 @@ const ecosystemSections = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Метаданные типов сервисов.
|
||||||
|
// item.type ссылается на ключ здесь: так карточки получают label и icon.
|
||||||
const serviceMeta = {
|
const serviceMeta = {
|
||||||
ceph: { label: "Ceph", icon: "assets/ceph.svg" },
|
ceph: { label: "Ceph", icon: "assets/ceph.svg" },
|
||||||
cloudbeaver: { label: "CloudBeaver", icon: "assets/database.svg" },
|
cloudbeaver: { label: "CloudBeaver", icon: "assets/database.svg" },
|
||||||
@ -133,6 +158,10 @@ const serviceMeta = {
|
|||||||
xmpp: { label: "XMPP", icon: "assets/xmpp.svg" },
|
xmpp: { label: "XMPP", icon: "assets/xmpp.svg" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Default UI preferences
|
||||||
|
// ============================================================================
|
||||||
|
// Значения используются при первом открытии или если localStorage пуст/битый.
|
||||||
const defaultPreferences = {
|
const defaultPreferences = {
|
||||||
theme: window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark",
|
theme: window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark",
|
||||||
view: "cards",
|
view: "cards",
|
||||||
@ -144,8 +173,16 @@ const defaultPreferences = {
|
|||||||
};
|
};
|
||||||
const availableViews = ["cards", "list", "compact-cards", "compact-list"];
|
const availableViews = ["cards", "list", "compact-cards", "compact-list"];
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Runtime state and DOM references
|
||||||
|
// ============================================================================
|
||||||
|
// preferences - текущие настройки интерфейса, объединенные с localStorage.
|
||||||
const preferences = { ...defaultPreferences, ...readPreferences() };
|
const preferences = { ...defaultPreferences, ...readPreferences() };
|
||||||
|
|
||||||
|
// collapseState - объект вида { [collapseKey]: boolean } для details.
|
||||||
const collapseState = readCollapseState();
|
const collapseState = readCollapseState();
|
||||||
|
|
||||||
|
// Корневые DOM-узлы, которые нужны для рендера и событий.
|
||||||
const catalog = document.querySelector("[data-catalog-root]");
|
const catalog = document.querySelector("[data-catalog-root]");
|
||||||
const navTree = document.querySelector("[data-nav-tree]");
|
const navTree = document.querySelector("[data-nav-tree]");
|
||||||
const sidebarResizer = document.querySelector("[data-sidebar-resizer]");
|
const sidebarResizer = document.querySelector("[data-sidebar-resizer]");
|
||||||
@ -162,15 +199,29 @@ const showTreeTagsInput = document.querySelector("[data-show-tree-tags]");
|
|||||||
const autoCollapseSearchInput = document.querySelector("[data-auto-collapse-search]");
|
const autoCollapseSearchInput = document.querySelector("[data-auto-collapse-search]");
|
||||||
const emptyTemplate = document.querySelector("#empty-template");
|
const emptyTemplate = document.querySelector("#empty-template");
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Initial render
|
||||||
|
// ============================================================================
|
||||||
|
// Сначала строим дерево, затем применяем настройки, затем рендерим каталог.
|
||||||
|
// Такой порядок нужен, потому что applyPreferences выставляет data-атрибуты
|
||||||
|
// и CSS-переменные, от которых зависит внешний вид уже созданного DOM.
|
||||||
renderNavigationTree();
|
renderNavigationTree();
|
||||||
applyPreferences(preferences);
|
applyPreferences(preferences);
|
||||||
renderCatalog();
|
renderCatalog();
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Event bindings
|
||||||
|
// ============================================================================
|
||||||
|
// Поиск перерисовывает и дерево, и каталог:
|
||||||
|
// - дерево получает подсветку совпадений и авто-раскрытие веток;
|
||||||
|
// - каталог фильтруется и тоже подсвечивает найденный текст.
|
||||||
searchInput.addEventListener("input", () => {
|
searchInput.addEventListener("input", () => {
|
||||||
renderNavigationTree();
|
renderNavigationTree();
|
||||||
renderCatalog();
|
renderCatalog();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Клик по файлу/папке в дереве фокусирует соответствующую секцию/карточку.
|
||||||
|
// Само раскрытие details остается на нативном summary/caret.
|
||||||
navTree.addEventListener("click", (event) => {
|
navTree.addEventListener("click", (event) => {
|
||||||
const button = event.target.closest("[data-focus-target]");
|
const button = event.target.closest("[data-focus-target]");
|
||||||
|
|
||||||
@ -183,15 +234,19 @@ navTree.addEventListener("click", (event) => {
|
|||||||
focusCatalogTarget(button.dataset.focusTarget);
|
focusCatalogTarget(button.dataset.focusTarget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Сохраняем ручное раскрытие/сворачивание дерева и контента.
|
||||||
|
// Используем capture=true, потому что toggle не всплывает как обычный click.
|
||||||
navTree.addEventListener("toggle", handleDetailsToggle, true);
|
navTree.addEventListener("toggle", handleDetailsToggle, true);
|
||||||
catalog.addEventListener("toggle", handleDetailsToggle, true);
|
catalog.addEventListener("toggle", handleDetailsToggle, true);
|
||||||
|
|
||||||
|
// Открытие popup-настроек по шестеренке.
|
||||||
settingsToggle.addEventListener("click", () => {
|
settingsToggle.addEventListener("click", () => {
|
||||||
const isOpen = settingsPanel.classList.toggle("is-open");
|
const isOpen = settingsPanel.classList.toggle("is-open");
|
||||||
settingsOverlay.classList.toggle("is-open", isOpen);
|
settingsOverlay.classList.toggle("is-open", isOpen);
|
||||||
settingsToggle.setAttribute("aria-expanded", String(isOpen));
|
settingsToggle.setAttribute("aria-expanded", String(isOpen));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Закрытие popup-настроек всеми привычными способами.
|
||||||
settingsClose.addEventListener("click", closeSettingsPanel);
|
settingsClose.addEventListener("click", closeSettingsPanel);
|
||||||
settingsOverlay.addEventListener("click", closeSettingsPanel);
|
settingsOverlay.addEventListener("click", closeSettingsPanel);
|
||||||
|
|
||||||
@ -221,6 +276,7 @@ themeButtons.forEach((button) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Переключение режима отображения каталога.
|
||||||
viewButtons.forEach((button) => {
|
viewButtons.forEach((button) => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
preferences.view = button.dataset.view;
|
preferences.view = button.dataset.view;
|
||||||
@ -229,6 +285,7 @@ viewButtons.forEach((button) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Выбор цветового акцента.
|
||||||
accentButtons.forEach((button) => {
|
accentButtons.forEach((button) => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
preferences.accent = button.dataset.accent;
|
preferences.accent = button.dataset.accent;
|
||||||
@ -237,18 +294,21 @@ accentButtons.forEach((button) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Показывает/скрывает номера в секциях, группах и карточках.
|
||||||
showNumbersInput.addEventListener("change", () => {
|
showNumbersInput.addEventListener("change", () => {
|
||||||
preferences.showNumbers = showNumbersInput.checked;
|
preferences.showNumbers = showNumbersInput.checked;
|
||||||
applyPreferences(preferences);
|
applyPreferences(preferences);
|
||||||
savePreferences(preferences);
|
savePreferences(preferences);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Показывает/скрывает поисковые теги в файловом дереве.
|
||||||
showTreeTagsInput.addEventListener("change", () => {
|
showTreeTagsInput.addEventListener("change", () => {
|
||||||
preferences.showTreeTags = showTreeTagsInput.checked;
|
preferences.showTreeTags = showTreeTagsInput.checked;
|
||||||
applyPreferences(preferences);
|
applyPreferences(preferences);
|
||||||
savePreferences(preferences);
|
savePreferences(preferences);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Включает/выключает авто-раскрытие веток дерева при поиске.
|
||||||
autoCollapseSearchInput.addEventListener("change", () => {
|
autoCollapseSearchInput.addEventListener("change", () => {
|
||||||
preferences.autoCollapseSearch = autoCollapseSearchInput.checked;
|
preferences.autoCollapseSearch = autoCollapseSearchInput.checked;
|
||||||
applyPreferences(preferences);
|
applyPreferences(preferences);
|
||||||
@ -256,6 +316,8 @@ autoCollapseSearchInput.addEventListener("change", () => {
|
|||||||
savePreferences(preferences);
|
savePreferences(preferences);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Drag-resize бокового меню.
|
||||||
|
// Ширина сохраняется в preferences.sidebarWidth и применяется через CSS-переменную.
|
||||||
sidebarResizer.addEventListener("pointerdown", (event) => {
|
sidebarResizer.addEventListener("pointerdown", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
document.body.classList.add("is-resizing-sidebar");
|
document.body.classList.add("is-resizing-sidebar");
|
||||||
@ -280,10 +342,22 @@ sidebarResizer.addEventListener("pointerdown", (event) => {
|
|||||||
sidebarResizer.addEventListener("pointercancel", handlePointerUp);
|
sidebarResizer.addEventListener("pointercancel", handlePointerUp);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Data helpers
|
||||||
|
// ============================================================================
|
||||||
|
// Упрощает запись item-объектов в ecosystemSections.
|
||||||
function createItem(number, title, type, tags = [], url = "#") {
|
function createItem(number, title, type, tags = [], url = "#") {
|
||||||
return { number, title, type, tags, url };
|
return { number, title, type, tags, url };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Catalog rendering
|
||||||
|
// ============================================================================
|
||||||
|
// Рендерит правую часть: details-разделы, details-подразделы и карточки.
|
||||||
|
// При активном поиске:
|
||||||
|
// - оставляет только группы с найденными элементами;
|
||||||
|
// - принудительно раскрывает найденные ветки;
|
||||||
|
// - подсвечивает совпадения через highlightText().
|
||||||
function renderCatalog() {
|
function renderCatalog() {
|
||||||
const query = normalize(searchInput.value);
|
const query = normalize(searchInput.value);
|
||||||
const forceOpen = Boolean(query);
|
const forceOpen = Boolean(query);
|
||||||
@ -325,6 +399,9 @@ function renderCatalog() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Создает корневой details для раздела контента.
|
||||||
|
// forceOpen нужен для поиска: найденные ветки должны быть видимы независимо
|
||||||
|
// от сохраненного collapsed-состояния.
|
||||||
function createSectionElement(section, forceOpen = false, query = "") {
|
function createSectionElement(section, forceOpen = false, query = "") {
|
||||||
const sectionElement = document.createElement("details");
|
const sectionElement = document.createElement("details");
|
||||||
sectionElement.className = "catalog-section";
|
sectionElement.className = "catalog-section";
|
||||||
@ -349,6 +426,8 @@ function createSectionElement(section, forceOpen = false, query = "") {
|
|||||||
return sectionElement;
|
return sectionElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Создает details-подраздел внутри раздела.
|
||||||
|
// items уже отфильтрованы на уровне renderCatalog().
|
||||||
function createGroupElement(group, items, forceOpen = false, query = "") {
|
function createGroupElement(group, items, forceOpen = false, query = "") {
|
||||||
const groupElement = document.createElement("details");
|
const groupElement = document.createElement("details");
|
||||||
groupElement.className = "catalog-group";
|
groupElement.className = "catalog-group";
|
||||||
@ -380,6 +459,8 @@ function createGroupElement(group, items, forceOpen = false, query = "") {
|
|||||||
return groupElement;
|
return groupElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Создает конечную карточку навигации.
|
||||||
|
// Если url === "#", карточка считается заглушкой и не меняет текущий hash/scroll.
|
||||||
function createCardElement(item, query = "") {
|
function createCardElement(item, query = "") {
|
||||||
const meta = serviceMeta[item.type] || serviceMeta.config;
|
const meta = serviceMeta[item.type] || serviceMeta.config;
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
@ -411,6 +492,11 @@ function createCardElement(item, query = "") {
|
|||||||
return link;
|
return link;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Search matching and highlighting
|
||||||
|
// ============================================================================
|
||||||
|
// Проверяет, подходит ли конкретный item под строку поиска.
|
||||||
|
// Поиск идет не только по title, но и по номеру, type, label, tags и родителям.
|
||||||
function matchesQuery(item, group, section, query) {
|
function matchesQuery(item, group, section, query) {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return true;
|
return true;
|
||||||
@ -432,6 +518,8 @@ function matchesQuery(item, group, section, query) {
|
|||||||
return haystack.includes(query);
|
return haystack.includes(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяет, есть ли совпадение на уровне группы или ее элементов.
|
||||||
|
// Используется для авто-раскрытия дерева при поиске.
|
||||||
function groupMatchesQuery(group, section, query) {
|
function groupMatchesQuery(group, section, query) {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return true;
|
return true;
|
||||||
@ -443,6 +531,7 @@ function groupMatchesQuery(group, section, query) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяет произвольный набор строк на наличие нормализованного query.
|
||||||
function matchesText(values, query) {
|
function matchesText(values, query) {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return true;
|
return true;
|
||||||
@ -451,6 +540,11 @@ function matchesText(values, query) {
|
|||||||
return normalize(values.join(" ")).includes(query);
|
return normalize(values.join(" ")).includes(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Sidebar tree rendering
|
||||||
|
// ============================================================================
|
||||||
|
// Строит левое файловое дерево из той же структуры данных, что и каталог.
|
||||||
|
// Это важно: дерево и карточки всегда остаются синхронизированными.
|
||||||
function renderNavigationTree() {
|
function renderNavigationTree() {
|
||||||
const query = normalize(searchInput.value);
|
const query = normalize(searchInput.value);
|
||||||
const shouldAutoCollapse = Boolean(query && preferences.autoCollapseSearch);
|
const shouldAutoCollapse = Boolean(query && preferences.autoCollapseSearch);
|
||||||
@ -511,6 +605,14 @@ function renderNavigationTree() {
|
|||||||
navTree.replaceChildren(fragment);
|
navTree.replaceChildren(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Focusing from tree to content
|
||||||
|
// ============================================================================
|
||||||
|
// Переход из дерева к правому контенту:
|
||||||
|
// - если текущий поиск скрывает цель, поиск очищается;
|
||||||
|
// - родительские details раскрываются;
|
||||||
|
// - состояние раскрытия сохраняется;
|
||||||
|
// - целевой элемент скроллится в центр и подсвечивается.
|
||||||
function focusCatalogTarget(targetId) {
|
function focusCatalogTarget(targetId) {
|
||||||
let target = document.getElementById(targetId);
|
let target = document.getElementById(targetId);
|
||||||
|
|
||||||
@ -534,6 +636,10 @@ function focusCatalogTarget(targetId) {
|
|||||||
window.setTimeout(() => target.classList.remove("is-focused"), 1200);
|
window.setTimeout(() => target.classList.remove("is-focused"), 1200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Collapse state handling
|
||||||
|
// ============================================================================
|
||||||
|
// Сохраняет состояние каждого details, у которого есть data-collapse-key.
|
||||||
function handleDetailsToggle(event) {
|
function handleDetailsToggle(event) {
|
||||||
const details = event.target;
|
const details = event.target;
|
||||||
|
|
||||||
@ -545,6 +651,8 @@ function handleDetailsToggle(event) {
|
|||||||
saveCollapseState();
|
saveCollapseState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Принудительно сохраняет родительский details как раскрытый.
|
||||||
|
// Используется, когда пользователь прыгает к элементу из дерева.
|
||||||
function persistOpenedAncestor(details) {
|
function persistOpenedAncestor(details) {
|
||||||
if (!details?.dataset.collapseKey) {
|
if (!details?.dataset.collapseKey) {
|
||||||
return;
|
return;
|
||||||
@ -554,6 +662,12 @@ function persistOpenedAncestor(details) {
|
|||||||
saveCollapseState();
|
saveCollapseState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Card pointer interaction
|
||||||
|
// ============================================================================
|
||||||
|
// Добавляет мягкий tilt/highlight эффект для карточек.
|
||||||
|
// После каждого renderCatalog() обработчики нужно навешивать заново,
|
||||||
|
// потому что карточки пересоздаются.
|
||||||
function attachCardInteractions() {
|
function attachCardInteractions() {
|
||||||
document.querySelectorAll(".nav-card").forEach((card) => {
|
document.querySelectorAll(".nav-card").forEach((card) => {
|
||||||
let leaveTimer;
|
let leaveTimer;
|
||||||
@ -591,6 +705,14 @@ function attachCardInteractions() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Preferences application
|
||||||
|
// ============================================================================
|
||||||
|
// Применяет preferences к DOM:
|
||||||
|
// - data-theme / data-accent для CSS;
|
||||||
|
// - CSS-переменную ширины сайдбара;
|
||||||
|
// - класс режима отображения каталога;
|
||||||
|
// - состояния toggle-контролов в настройках.
|
||||||
function applyPreferences(nextPreferences) {
|
function applyPreferences(nextPreferences) {
|
||||||
if (!availableViews.includes(nextPreferences.view)) {
|
if (!availableViews.includes(nextPreferences.view)) {
|
||||||
nextPreferences.view = defaultPreferences.view;
|
nextPreferences.view = defaultPreferences.view;
|
||||||
@ -614,6 +736,7 @@ function applyPreferences(nextPreferences) {
|
|||||||
setActiveButton(accentButtons, "accent", nextPreferences.accent);
|
setActiveButton(accentButtons, "accent", nextPreferences.accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляет визуальное состояние группы кнопок.
|
||||||
function setActiveButton(buttons, key, value) {
|
function setActiveButton(buttons, key, value) {
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => {
|
||||||
const isActive = button.dataset[key] === value;
|
const isActive = button.dataset[key] === value;
|
||||||
@ -622,10 +745,16 @@ function setActiveButton(buttons, key, value) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Generic helpers
|
||||||
|
// ============================================================================
|
||||||
|
// Нормализует строки для поиска: регистр + ё/е.
|
||||||
function normalize(value) {
|
function normalize(value) {
|
||||||
return value.toLowerCase().replaceAll("ё", "е").trim();
|
return value.toLowerCase().replaceAll("ё", "е").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Подсвечивает все совпадения query внутри value.
|
||||||
|
// Возвращает HTML-строку, поэтому весь обычный текст пропускается через escapeHTML().
|
||||||
function highlightText(value, query) {
|
function highlightText(value, query) {
|
||||||
const text = String(value);
|
const text = String(value);
|
||||||
const normalizedQuery = normalize(query);
|
const normalizedQuery = normalize(query);
|
||||||
@ -649,6 +778,7 @@ function highlightText(value, query) {
|
|||||||
return result + escapeHTML(text.slice(cursor));
|
return result + escapeHTML(text.slice(cursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Минимальная HTML-экранизация для безопасной вставки строк в innerHTML.
|
||||||
function escapeHTML(value) {
|
function escapeHTML(value) {
|
||||||
return String(value)
|
return String(value)
|
||||||
.replaceAll("&", "&")
|
.replaceAll("&", "&")
|
||||||
@ -658,6 +788,8 @@ function escapeHTML(value) {
|
|||||||
.replaceAll("'", "'");
|
.replaceAll("'", "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Стабильные id для секций, групп и элементов.
|
||||||
|
// Они используются одновременно как DOM id и как цель tree focus.
|
||||||
function getSectionId(section) {
|
function getSectionId(section) {
|
||||||
return `section-${section.id}`;
|
return `section-${section.id}`;
|
||||||
}
|
}
|
||||||
@ -666,6 +798,8 @@ function getGroupId(group) {
|
|||||||
return `group-${group.id}`;
|
return `group-${group.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Отдельные ключи для дерева, чтобы collapse-состояния дерева
|
||||||
|
// не конфликтовали с collapse-состояниями контента.
|
||||||
function getTreeSectionId(section) {
|
function getTreeSectionId(section) {
|
||||||
return `tree-${getSectionId(section)}`;
|
return `tree-${getSectionId(section)}`;
|
||||||
}
|
}
|
||||||
@ -678,10 +812,15 @@ function getItemId(item) {
|
|||||||
return `item-${item.number.replaceAll(".", "-")}`;
|
return `item-${item.number.replaceAll(".", "-")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ограничивает число диапазоном.
|
||||||
function clamp(value, min, max) {
|
function clamp(value, min, max) {
|
||||||
return Math.min(Math.max(value, min), max);
|
return Math.min(Math.max(value, min), max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// localStorage helpers
|
||||||
|
// ============================================================================
|
||||||
|
// Читает настройки интерфейса.
|
||||||
function readPreferences() {
|
function readPreferences() {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
|
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
|
||||||
@ -690,14 +829,18 @@ function readPreferences() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сохраняет настройки интерфейса.
|
||||||
function savePreferences(nextPreferences) {
|
function savePreferences(nextPreferences) {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(nextPreferences));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(nextPreferences));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Возвращает сохраненное состояние details.
|
||||||
|
// По умолчанию все ветки открыты, чтобы первый вход был максимально понятным.
|
||||||
function getSavedOpenState(key) {
|
function getSavedOpenState(key) {
|
||||||
return collapseState[key] ?? true;
|
return collapseState[key] ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Читает состояния раскрытия дерева/контента.
|
||||||
function readCollapseState() {
|
function readCollapseState() {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(localStorage.getItem(COLLAPSE_STORAGE_KEY)) || {};
|
return JSON.parse(localStorage.getItem(COLLAPSE_STORAGE_KEY)) || {};
|
||||||
@ -706,10 +849,12 @@ function readCollapseState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сохраняет состояния раскрытия дерева/контента.
|
||||||
function saveCollapseState() {
|
function saveCollapseState() {
|
||||||
localStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(collapseState));
|
localStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(collapseState));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Закрывает popup-настройки и синхронизирует aria-expanded.
|
||||||
function closeSettingsPanel() {
|
function closeSettingsPanel() {
|
||||||
settingsPanel.classList.remove("is-open");
|
settingsPanel.classList.remove("is-open");
|
||||||
settingsOverlay.classList.remove("is-open");
|
settingsOverlay.classList.remove("is-open");
|
||||||
|
|||||||
147
styles.css
147
styles.css
@ -1,3 +1,11 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
01. Theme Tokens
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
Все ключевые цвета и размеры вынесены в CSS-переменные. JavaScript меняет
|
||||||
|
data-theme/data-accent и --sidebar-width, а остальная верстка реагирует
|
||||||
|
без прямого изменения inline-стилей у отдельных элементов.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--bg: #080a12;
|
--bg: #080a12;
|
||||||
@ -17,6 +25,7 @@
|
|||||||
--sidebar-width: 390px;
|
--sidebar-width: 390px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Светлая тема переопределяет только токены, структура стилей остается общей. */
|
||||||
html[data-theme="light"] {
|
html[data-theme="light"] {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
--bg: #f5f7fb;
|
--bg: #f5f7fb;
|
||||||
@ -30,6 +39,7 @@ html[data-theme="light"] {
|
|||||||
--shadow: 0 24px 70px rgba(56, 65, 91, 0.18);
|
--shadow: 0 24px 70px rgba(56, 65, 91, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Наборы акцентов. --accent-rgb нужен для прозрачных градиентов через rgba(). */
|
||||||
html[data-accent="cyan"] {
|
html[data-accent="cyan"] {
|
||||||
--accent: #06b6d4;
|
--accent: #06b6d4;
|
||||||
--accent-2: #3b82f6;
|
--accent-2: #3b82f6;
|
||||||
@ -48,6 +58,12 @@ html[data-accent="amber"] {
|
|||||||
--accent-rgb: 245, 158, 11;
|
--accent-rgb: 245, 158, 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
02. Base Reset And Page Background
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
Базовые правила страницы, плавная смена темы и фоновые декоративные слои.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@ -79,6 +95,7 @@ a {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Фоновые "ambient" пятна создают глубину, но не перехватывают клики. */
|
||||||
.ambient {
|
.ambient {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
@ -102,12 +119,20 @@ a {
|
|||||||
background: rgba(34, 211, 238, 0.3);
|
background: rgba(34, 211, 238, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
03. Application Layout
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
.shell ограничивает общую ширину, .app-layout делит экран на:
|
||||||
|
sidebar -> resizer -> catalog.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
.shell {
|
.shell {
|
||||||
width: min(1560px, calc(100% - 28px));
|
width: min(1560px, calc(100% - 28px));
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Левая колонка получает ширину из --sidebar-width, которую меняет JS. */
|
||||||
.app-layout {
|
.app-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--sidebar-width) 8px minmax(0, 1fr);
|
grid-template-columns: var(--sidebar-width) 8px minmax(0, 1fr);
|
||||||
@ -115,6 +140,13 @@ a {
|
|||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
04. Sidebar Resizer
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
Узкая колонка между меню и каталогом. Pointer Events в script.js двигают
|
||||||
|
CSS-переменную --sidebar-width и сохраняют значение в localStorage.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
.sidebar-resizer {
|
.sidebar-resizer {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
@ -141,22 +173,31 @@ body.is-resizing-sidebar .sidebar-resizer::before {
|
|||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Во время drag меняем курсор по всей странице и запрещаем выделение текста. */
|
||||||
body.is-resizing-sidebar {
|
body.is-resizing-sidebar {
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
05. Sidebar Panel
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
Sticky-панель слева: заголовок, шестеренка, поиск и дерево навигации.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
.side-panel {
|
.side-panel {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* .hero остался как совместимый контейнерный класс после уплотнения шапки. */
|
||||||
.hero {
|
.hero {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Общая "стеклянная" поверхность для главных панелей. */
|
||||||
.hero-content,
|
.hero-content,
|
||||||
.control-panel,
|
.control-panel,
|
||||||
.catalog-section,
|
.catalog-section,
|
||||||
@ -185,6 +226,7 @@ body.is-resizing-sidebar {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Внутренний фон sidebar: градиент + hexagon pattern. */
|
||||||
.hero-content::before {
|
.hero-content::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -201,6 +243,7 @@ body.is-resizing-sidebar {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Общий стиль маленьких uppercase-меток и номеров. */
|
||||||
.eyebrow,
|
.eyebrow,
|
||||||
.section-number,
|
.section-number,
|
||||||
.group-number,
|
.group-number,
|
||||||
@ -213,16 +256,19 @@ body.is-resizing-sidebar {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Нумерация управляется toggle-настройкой "Показывать нумерацию". */
|
||||||
html:not([data-show-numbers="true"]) .section-number,
|
html:not([data-show-numbers="true"]) .section-number,
|
||||||
html:not([data-show-numbers="true"]) .group-number,
|
html:not([data-show-numbers="true"]) .group-number,
|
||||||
html:not([data-show-numbers="true"]) .item-number {
|
html:not([data-show-numbers="true"]) .item-number {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Теги в дереве управляются toggle-настройкой "Показывать теги дерева". */
|
||||||
html[data-show-tree-tags="false"] .tree-node small {
|
html[data-show-tree-tags="false"] .tree-node small {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Подсветка совпадений поиска в дереве и в карточках. */
|
||||||
.search-mark {
|
.search-mark {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
@ -230,6 +276,13 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
06. Settings Popup
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
Настройки вынесены из sidebar в fixed popup, чтобы на мобильных устройствах
|
||||||
|
они не перекрывались потоком контента и всегда были поверх страницы.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
.control-panel {
|
.control-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
@ -253,6 +306,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
transform 180ms ease;
|
transform 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Затемнение и blur-подложка за открытыми настройками. */
|
||||||
.settings-overlay {
|
.settings-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@ -265,6 +319,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
transition: opacity 180ms ease, visibility 180ms ease;
|
transition: opacity 180ms ease, visibility 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Состояния открытия popup и overlay. */
|
||||||
.settings-overlay.is-open {
|
.settings-overlay.is-open {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
@ -278,6 +333,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
transform: translateY(0) scale(1);
|
transform: translateY(0) scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Кнопки шестеренки и закрытия имеют один визуальный язык. */
|
||||||
.settings-button,
|
.settings-button,
|
||||||
.settings-close {
|
.settings-close {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@ -307,6 +363,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
background: rgba(var(--accent-rgb), 0.12);
|
background: rgba(var(--accent-rgb), 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Заголовок popup-настроек. */
|
||||||
.settings-panel-head {
|
.settings-panel-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -329,6 +386,13 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
07. Search Field And Control Inputs
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
Поиск живет в sidebar, а остальные контролы в popup. Здесь собраны общие
|
||||||
|
стили input, toggle-row, segmented buttons и accent swatches.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
.search-field {
|
.search-field {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
|
|
||||||
@ -344,6 +408,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Иконка поиска рисуется CSS-ом, чтобы не добавлять лишний DOM. */
|
||||||
.search-field::after {
|
.search-field::after {
|
||||||
content: "⌕";
|
content: "⌕";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -374,6 +439,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
background: var(--panel-strong);
|
background: var(--panel-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Toggle-row используется для checkbox-настроек с описанием. */
|
||||||
.toggle-row {
|
.toggle-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -400,6 +466,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Кастомный checkbox выглядит как компактный switch. */
|
||||||
.toggle-row input {
|
.toggle-row input {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
@ -434,6 +501,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
transform: translateX(20px);
|
transform: translateX(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Контейнеры групп кнопок настроек. */
|
||||||
.control-group {
|
.control-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -450,6 +518,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Для четырех режимов отображения кнопки складываются в компактную сетку. */
|
||||||
.view-group {
|
.view-group {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
@ -462,6 +531,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
flex-basis: calc(50% - 3px);
|
flex-basis: calc(50% - 3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Segmented buttons и color swatches имеют общий focus/hover baseline. */
|
||||||
.segmented,
|
.segmented,
|
||||||
.accent-swatch {
|
.accent-swatch {
|
||||||
border: 0;
|
border: 0;
|
||||||
@ -502,6 +572,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
box-shadow: inset 0 0 0 4px rgba(255, 255, 255, 0.18);
|
box-shadow: inset 0 0 0 4px rgba(255, 255, 255, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Акцентные кружки используют те же цвета, что и html[data-accent]. */
|
||||||
.accent-swatch[data-accent="violet"] {
|
.accent-swatch[data-accent="violet"] {
|
||||||
background: linear-gradient(135deg, #8b5cf6, #22d3ee);
|
background: linear-gradient(135deg, #8b5cf6, #22d3ee);
|
||||||
}
|
}
|
||||||
@ -523,6 +594,14 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
08. File-Tree Navigation
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
Левая навигация визуально имитирует дерево файлов:
|
||||||
|
details/summary дают нативное раскрытие, а CSS добавляет линии, caret,
|
||||||
|
"folder" для разделов/групп и "file" для конечных элементов.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
.nav-tree {
|
.nav-tree {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
@ -531,6 +610,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
font-size: 0.86rem;
|
font-size: 0.86rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Details в дереве intentionally выглядят не как карточки, а как вложенный список. */
|
||||||
.tree-section,
|
.tree-section,
|
||||||
.tree-group {
|
.tree-group {
|
||||||
border: 0;
|
border: 0;
|
||||||
@ -546,6 +626,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
border-left: 1px solid var(--border);
|
border-left: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Summary получает caret, иконку папки и кликабельный текст. */
|
||||||
.tree-section > summary,
|
.tree-section > summary,
|
||||||
.tree-group > summary {
|
.tree-group > summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -559,11 +640,13 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Нативный маркер details скрываем, так как рисуем свой caret. */
|
||||||
.tree-section > summary::-webkit-details-marker,
|
.tree-section > summary::-webkit-details-marker,
|
||||||
.tree-group > summary::-webkit-details-marker {
|
.tree-group > summary::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Caret поворачивается через состояние [open] у details. */
|
||||||
.tree-caret {
|
.tree-caret {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
@ -583,6 +666,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Конечный элемент дерева: иконка файла, название и optional теги. */
|
||||||
.tree-node {
|
.tree-node {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -608,11 +692,13 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Вертикальная линия вложенности дерева. */
|
||||||
.tree-node::before {
|
.tree-node::before {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
opacity: 0.92;
|
opacity: 0.92;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Папки и файлы нарисованы CSS-иконками, чтобы не тянуть отдельные SVG. */
|
||||||
.tree-folder {
|
.tree-folder {
|
||||||
grid-template-columns: 18px minmax(0, 1fr);
|
grid-template-columns: 18px minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
@ -631,6 +717,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
font-size: 0.86rem;
|
font-size: 0.86rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Контейнер элементов внутри группы дерева. */
|
||||||
.tree-items {
|
.tree-items {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -646,6 +733,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
background: rgba(var(--accent-rgb), 0.14);
|
background: rgba(var(--accent-rgb), 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Теги дерева усечены ellipsis, чтобы длинные списки не ломали sidebar. */
|
||||||
.tree-node small {
|
.tree-node small {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -656,12 +744,20 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
09. Catalog Structure
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
Правая часть страницы. Разделы и подразделы тоже построены на details,
|
||||||
|
поэтому пользователь может сворачивать не только дерево, но и контент.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
.catalog {
|
.catalog {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Главные glass-карточки разделов. */
|
||||||
.catalog-section {
|
.catalog-section {
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -673,6 +769,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Summary у разделов/групп заменяет заголовок и одновременно управляет collapse. */
|
||||||
.catalog-section > summary,
|
.catalog-section > summary,
|
||||||
.catalog-group > summary {
|
.catalog-group > summary {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@ -684,6 +781,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Шапка раздела: номер, название, описание, счетчик и индикатор collapse. */
|
||||||
.section-head {
|
.section-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -726,6 +824,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Счетчики помогают понять размер раздела без чтения всех карточек. */
|
||||||
.section-count {
|
.section-count {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@ -737,6 +836,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стрелка сворачивания поворачивается через [open]. */
|
||||||
.collapse-indicator {
|
.collapse-indicator {
|
||||||
width: 34px;
|
width: 34px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
@ -755,6 +855,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Подразделы внутри секции. */
|
||||||
.section-groups {
|
.section-groups {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@ -794,6 +895,13 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
10. Navigation Cards
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
Конечные кликабельные элементы каталога. Карточки используют CSS-переменные
|
||||||
|
--card-x/--card-y, которые JS обновляет при движении мыши для shine-эффекта.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
.items-grid {
|
.items-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
@ -801,6 +909,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Базовая карточка с иконкой, контентом, тегами, стрелкой и shine-слоем. */
|
||||||
.nav-card {
|
.nav-card {
|
||||||
--tilt-x: 0deg;
|
--tilt-x: 0deg;
|
||||||
--tilt-y: 0deg;
|
--tilt-y: 0deg;
|
||||||
@ -827,6 +936,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
scroll-margin-top: 18px;
|
scroll-margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Временная подсветка цели после клика в дереве. */
|
||||||
.catalog-section.is-focused,
|
.catalog-section.is-focused,
|
||||||
.catalog-group.is-focused,
|
.catalog-group.is-focused,
|
||||||
.nav-card.is-focused {
|
.nav-card.is-focused {
|
||||||
@ -834,6 +944,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.16), var(--shadow);
|
box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.16), var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Градиентный бордер/подсветка под курсором. */
|
||||||
.nav-card::before {
|
.nav-card::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -849,6 +960,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hover задается классом .is-hovered из JS, чтобы убрать скачки на границах. */
|
||||||
.nav-card.is-hovered,
|
.nav-card.is-hovered,
|
||||||
.nav-card:focus-visible {
|
.nav-card:focus-visible {
|
||||||
border-color: rgba(var(--accent-rgb), 0.52);
|
border-color: rgba(var(--accent-rgb), 0.52);
|
||||||
@ -862,6 +974,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Иконка сервиса берется из serviceMeta. */
|
||||||
.nav-card img {
|
.nav-card img {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
@ -880,6 +993,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Тип сервиса выделен акцентом и помогает быстро считывать назначение. */
|
||||||
.item-type {
|
.item-type {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
@ -887,6 +1001,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
font-weight: 750;
|
font-weight: 750;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Теги показывают, по каким словам карточку удобно искать. */
|
||||||
.tags {
|
.tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -904,6 +1019,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стрелка справа подчеркивает, что карточка ведет по ссылке. */
|
||||||
.arrow {
|
.arrow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
@ -926,6 +1042,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Декоративный shine слой, положение которого обновляется pointermove. */
|
||||||
.shine {
|
.shine {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@ -945,10 +1062,18 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
transform: translateX(130%);
|
transform: translateX(130%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
11. View Modes
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
Режимы отображения переключаются классами на .catalog:
|
||||||
|
cards-view, list-view, compact-cards-view, compact-list-view.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
.catalog.list-view .items-grid {
|
.catalog.list-view .items-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* В list-режимах карточка вытягивается в строку. */
|
||||||
.catalog.list-view .nav-card,
|
.catalog.list-view .nav-card,
|
||||||
.catalog.compact-list-view .nav-card {
|
.catalog.compact-list-view .nav-card {
|
||||||
min-height: 96px;
|
min-height: 96px;
|
||||||
@ -964,10 +1089,12 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
justify-self: end;
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Compact modes скрывают второстепенную информацию и уменьшают отступы. */
|
||||||
.catalog.compact-cards-view .items-grid {
|
.catalog.compact-cards-view .items-grid {
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Compact list на широких экранах использует 3 колонки. */
|
||||||
.catalog.compact-list-view .items-grid {
|
.catalog.compact-list-view .items-grid {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
@ -977,6 +1104,7 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* В compact-режимах описания и теги скрыты ради плотности. */
|
||||||
.catalog.compact-cards-view .section-head p:not(.section-number),
|
.catalog.compact-cards-view .section-head p:not(.section-number),
|
||||||
.catalog.compact-list-view .section-head p:not(.section-number),
|
.catalog.compact-list-view .section-head p:not(.section-number),
|
||||||
.catalog.compact-cards-view .group-head p:not(.group-number),
|
.catalog.compact-cards-view .group-head p:not(.group-number),
|
||||||
@ -1047,6 +1175,12 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
12. Empty State
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
Показывается, когда фильтр поиска не оставил ни одной карточки.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
min-height: 280px;
|
min-height: 280px;
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -1069,6 +1203,13 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
13. Responsive Layout
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
На планшетах sidebar перестает быть отдельной колонкой, а на телефонах
|
||||||
|
уменьшаются панели, кнопки и размеры карточек.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
@media (max-width: 1120px) {
|
@media (max-width: 1120px) {
|
||||||
.app-layout {
|
.app-layout {
|
||||||
grid-template-columns: minmax(240px, min(var(--sidebar-width), 320px)) 8px minmax(0, 1fr);
|
grid-template-columns: minmax(240px, min(var(--sidebar-width), 320px)) 8px minmax(0, 1fr);
|
||||||
@ -1194,6 +1335,12 @@ html[data-show-tree-tags="false"] .tree-node small {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
14. Reduced Motion
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
Уважает системную настройку пользователя и отключает анимации/плавный скролл.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user