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 {