diff --git a/index.html b/index.html index 7daa68a..02b0605 100644 --- a/index.html +++ b/index.html @@ -21,8 +21,9 @@
-
-
+
+
- - + + +
@@ -147,15 +149,16 @@
-
+
- + +
diff --git a/script.js b/script.js index 5d0141a..1b9c076 100644 --- a/script.js +++ b/script.js @@ -142,6 +142,7 @@ const availableViews = ["cards", "list", "compact-cards", "compact-list"]; const preferences = { ...defaultPreferences, ...readPreferences() }; const catalog = document.querySelector("[data-catalog-root]"); +const navTree = document.querySelector("[data-nav-tree]"); const searchInput = document.querySelector("[data-search-input]"); const settingsToggle = document.querySelector("[data-settings-toggle]"); const settingsPanel = document.querySelector("[data-settings-panel]"); @@ -158,11 +159,24 @@ document.querySelector("[data-total-sections]").textContent = totals.sections; document.querySelector("[data-total-groups]").textContent = totals.groups; document.querySelector("[data-total-items]").textContent = totals.items; +renderNavigationTree(); applyPreferences(preferences); renderCatalog(); searchInput.addEventListener("input", renderCatalog); +navTree.addEventListener("click", (event) => { + const button = event.target.closest("[data-focus-target]"); + + if (!button) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + focusCatalogTarget(button.dataset.focusTarget); +}); + settingsToggle.addEventListener("click", () => { const isOpen = settingsPanel.classList.toggle("is-open"); settingsOverlay.classList.toggle("is-open", isOpen); @@ -268,6 +282,8 @@ function renderCatalog() { function createSectionElement(section) { const sectionElement = document.createElement("details"); sectionElement.className = "catalog-section"; + sectionElement.id = getSectionId(section); + sectionElement.tabIndex = -1; sectionElement.open = true; sectionElement.innerHTML = ` @@ -289,6 +305,8 @@ function createSectionElement(section) { function createGroupElement(group, items) { const groupElement = document.createElement("details"); groupElement.className = "catalog-group"; + groupElement.id = getGroupId(group); + groupElement.tabIndex = -1; groupElement.open = true; const cards = items.map(createCardElement); @@ -319,6 +337,7 @@ function createCardElement(item) { const link = document.createElement("a"); link.className = "nav-card"; link.href = item.url; + link.id = getItemId(item); link.dataset.type = item.type; if (item.url === "#") { @@ -365,6 +384,75 @@ function matchesQuery(item, group, section, query) { return haystack.includes(query); } +function renderNavigationTree() { + const fragment = document.createDocumentFragment(); + + ecosystemSections.forEach((section) => { + const sectionNode = document.createElement("details"); + sectionNode.className = "tree-section"; + sectionNode.open = true; + + const sectionSummary = document.createElement("summary"); + sectionSummary.innerHTML = ` + ${section.title} + + `; + sectionNode.append(sectionSummary); + + section.groups.forEach((group) => { + const groupNode = document.createElement("details"); + groupNode.className = "tree-group"; + groupNode.open = true; + + const groupSummary = document.createElement("summary"); + groupSummary.innerHTML = ` + ${group.title} + + `; + groupNode.append(groupSummary); + + const itemList = document.createElement("div"); + itemList.className = "tree-items"; + + group.items.forEach((item) => { + const itemButton = document.createElement("button"); + itemButton.type = "button"; + itemButton.dataset.focusTarget = getItemId(item); + itemButton.innerHTML = `${item.title}${serviceMeta[item.type]?.label || item.type}`; + itemList.append(itemButton); + }); + + groupNode.append(itemList); + sectionNode.append(groupNode); + }); + + fragment.append(sectionNode); + }); + + navTree.replaceChildren(fragment); +} + +function focusCatalogTarget(targetId) { + let target = document.getElementById(targetId); + + if (!target) { + searchInput.value = ""; + renderCatalog(); + target = document.getElementById(targetId); + } + + if (!target) { + return; + } + + target.closest(".catalog-section")?.setAttribute("open", ""); + target.closest(".catalog-group")?.setAttribute("open", ""); + target.scrollIntoView({ behavior: "smooth", block: "center" }); + target.focus({ preventScroll: true }); + target.classList.add("is-focused"); + window.setTimeout(() => target.classList.remove("is-focused"), 1200); +} + function attachCardInteractions() { document.querySelectorAll(".nav-card").forEach((card) => { let leaveTimer; @@ -442,6 +530,18 @@ function normalize(value) { return value.toLowerCase().replaceAll("ё", "е").trim(); } +function getSectionId(section) { + return `section-${section.id}`; +} + +function getGroupId(group) { + return `group-${group.id}`; +} + +function getItemId(item) { + return `item-${item.number.replaceAll(".", "-")}`; +} + function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } diff --git a/styles.css b/styles.css index 9680a20..0024123 100644 --- a/styles.css +++ b/styles.css @@ -107,6 +107,19 @@ a { padding: 20px 0; } +.app-layout { + display: grid; + grid-template-columns: minmax(280px, 340px) minmax(0, 1fr); + gap: 12px; + align-items: start; +} + +.side-panel { + position: sticky; + top: 20px; + min-width: 0; +} + .hero { display: block; margin-bottom: 12px; @@ -123,7 +136,8 @@ a { } .hero-content { - min-height: 190px; + max-height: calc(100vh - 40px); + min-height: 0; display: flex; flex-direction: column; justify-content: center; @@ -134,6 +148,19 @@ a { overflow: visible; } +.side-panel .hero-content { + justify-content: flex-start; + overflow: auto; +} + +.side-panel h1 { + font-size: clamp(1.65rem, 2.4vw, 2.45rem); +} + +.side-panel .lead { + font-size: 0.9rem; +} + .hero-content::before { content: ""; position: absolute; @@ -497,14 +524,124 @@ html:not([data-show-numbers="true"]) .item-number { text-transform: uppercase; } +.nav-tree { + display: grid; + gap: 8px; + min-height: 0; + padding-top: 4px; +} + +.tree-section, +.tree-group { + border: 1px solid var(--border); + border-radius: 16px; + background: rgba(255, 255, 255, 0.035); + overflow: hidden; +} + +.tree-group { + margin: 6px 8px 8px; + border-radius: 14px; +} + +.tree-section > summary, +.tree-group > summary { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + align-items: center; + list-style: none; + cursor: pointer; + padding: 9px 10px; +} + +.tree-section > summary::-webkit-details-marker, +.tree-group > summary::-webkit-details-marker { + display: none; +} + +.tree-section summary span, +.tree-group summary span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text); + font-size: 0.88rem; + font-weight: 850; +} + +.tree-group summary span { + font-size: 0.82rem; + color: var(--muted); +} + +.tree-section summary button, +.tree-group summary button, +.tree-items button { + border: 1px solid var(--border); + border-radius: 999px; + cursor: pointer; + color: var(--accent); + background: rgba(var(--accent-rgb), 0.1); + font: inherit; + font-size: 0.72rem; + font-weight: 850; +} + +.tree-section summary button, +.tree-group summary button { + padding: 4px 7px; +} + +.tree-items { + display: grid; + gap: 4px; + padding: 0 8px 8px; +} + +.tree-items button { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + align-items: center; + width: 100%; + padding: 7px 9px; + border-radius: 12px; + text-align: left; + background: transparent; +} + +.tree-items button:hover, +.tree-section summary button:hover, +.tree-group summary button:hover { + border-color: rgba(var(--accent-rgb), 0.5); + background: rgba(var(--accent-rgb), 0.14); +} + +.tree-items span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text); +} + +.tree-items small { + color: var(--muted); + font-weight: 800; +} + .catalog { display: grid; gap: 12px; + min-width: 0; } .catalog-section { border-radius: 24px; overflow: hidden; + scroll-margin-top: 18px; } .catalog-section, @@ -605,6 +742,7 @@ html:not([data-show-numbers="true"]) .item-number { border-radius: 18px; background: rgba(255, 255, 255, 0.035); overflow: hidden; + scroll-margin-top: 18px; } .group-head { @@ -662,6 +800,14 @@ html:not([data-show-numbers="true"]) .item-number { border-color 220ms ease, background 220ms ease, box-shadow 220ms ease; + scroll-margin-top: 18px; +} + +.catalog-section.is-focused, +.catalog-group.is-focused, +.nav-card.is-focused { + border-color: rgba(var(--accent-rgb), 0.75); + box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.16), var(--shadow); } .nav-card::before { @@ -899,6 +1045,10 @@ html:not([data-show-numbers="true"]) .item-number { } @media (max-width: 1120px) { + .app-layout { + grid-template-columns: minmax(240px, 300px) minmax(0, 1fr); + } + .items-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -914,12 +1064,26 @@ html:not([data-show-numbers="true"]) .item-number { padding: 14px 0; } + .app-layout { + grid-template-columns: 1fr; + } + + .side-panel { + position: static; + } + .hero-content { min-height: auto; + max-height: none; padding: 16px 14px; border-radius: 20px; } + .nav-tree { + max-height: 320px; + overflow: auto; + } + .control-panel, .catalog-section, .empty-state {