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