вынесение верхней панели в боковое меню

This commit is contained in:
Илья Братских 2026-06-28 14:02:39 +03:00 committed by KBAHTNET
parent 39d82f15f6
commit 64fdf719ee
3 changed files with 280 additions and 13 deletions

View File

@ -21,7 +21,8 @@
<div class="ambient ambient-two"></div> <div class="ambient ambient-two"></div>
<main class="shell"> <main class="shell">
<section class="hero" aria-labelledby="page-title"> <div class="app-layout">
<aside class="side-panel" aria-label="Навигация по экосистеме">
<div class="hero-content"> <div class="hero-content">
<button <button
class="settings-button" class="settings-button"
@ -69,8 +70,9 @@
</div> </div>
</section> </section>
<nav class="nav-tree" aria-label="Дерево навигации" data-nav-tree></nav>
</div> </div>
</section> </aside>
<div class="settings-overlay" data-settings-overlay></div> <div class="settings-overlay" data-settings-overlay></div>
<div class="control-panel" aria-label="Настройки панели" data-settings-panel> <div class="control-panel" aria-label="Настройки панели" data-settings-panel>
@ -156,6 +158,7 @@
<p>Попробуй изменить запрос или искать по названию системы, тегу или роли.</p> <p>Попробуй изменить запрос или искать по названию системы, тегу или роли.</p>
</div> </div>
</template> </template>
</div>
</main> </main>
<script src="script.js"></script> <script src="script.js"></script>

100
script.js
View File

@ -142,6 +142,7 @@ const availableViews = ["cards", "list", "compact-cards", "compact-list"];
const preferences = { ...defaultPreferences, ...readPreferences() }; const preferences = { ...defaultPreferences, ...readPreferences() };
const catalog = document.querySelector("[data-catalog-root]"); const catalog = document.querySelector("[data-catalog-root]");
const navTree = document.querySelector("[data-nav-tree]");
const searchInput = document.querySelector("[data-search-input]"); const searchInput = document.querySelector("[data-search-input]");
const settingsToggle = document.querySelector("[data-settings-toggle]"); const settingsToggle = document.querySelector("[data-settings-toggle]");
const settingsPanel = document.querySelector("[data-settings-panel]"); 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-groups]").textContent = totals.groups;
document.querySelector("[data-total-items]").textContent = totals.items; document.querySelector("[data-total-items]").textContent = totals.items;
renderNavigationTree();
applyPreferences(preferences); applyPreferences(preferences);
renderCatalog(); renderCatalog();
searchInput.addEventListener("input", 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", () => { 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);
@ -268,6 +282,8 @@ function renderCatalog() {
function createSectionElement(section) { function createSectionElement(section) {
const sectionElement = document.createElement("details"); const sectionElement = document.createElement("details");
sectionElement.className = "catalog-section"; sectionElement.className = "catalog-section";
sectionElement.id = getSectionId(section);
sectionElement.tabIndex = -1;
sectionElement.open = true; sectionElement.open = true;
sectionElement.innerHTML = ` sectionElement.innerHTML = `
<summary class="section-head"> <summary class="section-head">
@ -289,6 +305,8 @@ function createSectionElement(section) {
function createGroupElement(group, items) { function createGroupElement(group, items) {
const groupElement = document.createElement("details"); const groupElement = document.createElement("details");
groupElement.className = "catalog-group"; groupElement.className = "catalog-group";
groupElement.id = getGroupId(group);
groupElement.tabIndex = -1;
groupElement.open = true; groupElement.open = true;
const cards = items.map(createCardElement); const cards = items.map(createCardElement);
@ -319,6 +337,7 @@ function createCardElement(item) {
const link = document.createElement("a"); const link = document.createElement("a");
link.className = "nav-card"; link.className = "nav-card";
link.href = item.url; link.href = item.url;
link.id = getItemId(item);
link.dataset.type = item.type; link.dataset.type = item.type;
if (item.url === "#") { if (item.url === "#") {
@ -365,6 +384,75 @@ function matchesQuery(item, group, section, query) {
return haystack.includes(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 = `
<span>${section.title}</span>
<button type="button" data-focus-target="${getSectionId(section)}">Фокус</button>
`;
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 = `
<span>${group.title}</span>
<button type="button" data-focus-target="${getGroupId(group)}">Фокус</button>
`;
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 = `<span>${item.title}</span><small>${serviceMeta[item.type]?.label || item.type}</small>`;
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() { function attachCardInteractions() {
document.querySelectorAll(".nav-card").forEach((card) => { document.querySelectorAll(".nav-card").forEach((card) => {
let leaveTimer; let leaveTimer;
@ -442,6 +530,18 @@ function normalize(value) {
return value.toLowerCase().replaceAll("ё", "е").trim(); 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) { function clamp(value, min, max) {
return Math.min(Math.max(value, min), max); return Math.min(Math.max(value, min), max);
} }

View File

@ -107,6 +107,19 @@ a {
padding: 20px 0; 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 { .hero {
display: block; display: block;
margin-bottom: 12px; margin-bottom: 12px;
@ -123,7 +136,8 @@ a {
} }
.hero-content { .hero-content {
min-height: 190px; max-height: calc(100vh - 40px);
min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@ -134,6 +148,19 @@ a {
overflow: visible; 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 { .hero-content::before {
content: ""; content: "";
position: absolute; position: absolute;
@ -497,14 +524,124 @@ html:not([data-show-numbers="true"]) .item-number {
text-transform: uppercase; 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 { .catalog {
display: grid; display: grid;
gap: 12px; gap: 12px;
min-width: 0;
} }
.catalog-section { .catalog-section {
border-radius: 24px; border-radius: 24px;
overflow: hidden; overflow: hidden;
scroll-margin-top: 18px;
} }
.catalog-section, .catalog-section,
@ -605,6 +742,7 @@ html:not([data-show-numbers="true"]) .item-number {
border-radius: 18px; border-radius: 18px;
background: rgba(255, 255, 255, 0.035); background: rgba(255, 255, 255, 0.035);
overflow: hidden; overflow: hidden;
scroll-margin-top: 18px;
} }
.group-head { .group-head {
@ -662,6 +800,14 @@ html:not([data-show-numbers="true"]) .item-number {
border-color 220ms ease, border-color 220ms ease,
background 220ms ease, background 220ms ease,
box-shadow 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 { .nav-card::before {
@ -899,6 +1045,10 @@ html:not([data-show-numbers="true"]) .item-number {
} }
@media (max-width: 1120px) { @media (max-width: 1120px) {
.app-layout {
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
}
.items-grid { .items-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
@ -914,12 +1064,26 @@ html:not([data-show-numbers="true"]) .item-number {
padding: 14px 0; padding: 14px 0;
} }
.app-layout {
grid-template-columns: 1fr;
}
.side-panel {
position: static;
}
.hero-content { .hero-content {
min-height: auto; min-height: auto;
max-height: none;
padding: 16px 14px; padding: 16px 14px;
border-radius: 20px; border-radius: 20px;
} }
.nav-tree {
max-height: 320px;
overflow: auto;
}
.control-panel, .control-panel,
.catalog-section, .catalog-section,
.empty-state { .empty-state {