diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 813aa39..475ed22 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -4485,6 +4485,26 @@ body.sb-open #sb-backdrop { display: block; } justify-self: start; min-width: 0; } +.topbar-nav-toggle { + display: none; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 999px; + background: rgba(14, 165, 233, 0.12); + border: 1px solid rgba(56, 189, 248, 0.28); + color: #7dd3fc; + cursor: pointer; + flex: 0 0 auto; + transition: background 140ms ease, color 140ms ease, border-color 140ms ease; +} +.topbar-nav-toggle:hover, +.topbar-nav-toggle[aria-expanded="true"] { + background: rgba(14, 165, 233, 0.2); + color: #bae6fd; + border-color: rgba(125, 211, 252, 0.48); +} .topbar-center { grid-area: center; display: flex; @@ -4502,6 +4522,99 @@ body.sb-open #sb-backdrop { display: block; } min-width: 0; } +body.topbar-compact #top-nav { + position: sticky; + overflow: visible; +} +body.topbar-compact .topbar-grid { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-areas: "left right"; + position: relative; +} +body.topbar-compact .topbar-left { + max-width: 100%; +} +body.topbar-compact .topbar-right { + flex: 0 0 auto; + min-width: max-content; +} +body.topbar-compact .topbar-nav-toggle { + display: inline-flex; +} +body.topbar-compact .topbar-center { + position: absolute; + top: calc(100% + 10px); + left: 0; + right: 0; + display: none; + justify-self: stretch; + width: 100%; + min-width: 0; + padding: 10px; + border-radius: 10px; + background: rgba(8, 13, 28, 0.98); + border: 1px solid rgba(51, 65, 85, 0.82); + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.5); + z-index: 230; + max-height: min(68vh, 520px); + overflow-y: auto; +} +body.topbar-compact.topbar-menu-open .topbar-center { + display: flex; +} +body.topbar-compact .nav-dd-bar { + justify-content: flex-start; + flex-wrap: nowrap; + align-items: stretch; + gap: 6px; + width: 100%; + padding-top: 0; + flex-direction: column; +} +body.topbar-compact .nav-dd-wrapper { + width: 100%; +} +body.topbar-compact .nav-dd-group-btn { + width: 100%; + justify-content: flex-start; +} +body.topbar-compact .nav-dd-group-btn i.fa-chevron-down { + margin-left: auto; +} +body.topbar-compact .nav-dd-menu { + position: static; + display: none; + min-width: 0; + width: 100%; + margin-top: 6px; + transform: none; + box-shadow: none; + border-radius: 8px; + background: rgba(2, 8, 22, 0.55); + border-color: rgba(51, 65, 85, 0.45); +} +body.topbar-compact .nav-dd-menu.nav-dd-open { + display: flex; + opacity: 1; + visibility: visible; + pointer-events: auto; + transform: none; +} +body.topbar-compact #account-settings-btn { + flex: 0 0 auto; + max-width: none; + min-width: max-content; +} +body.topbar-compact #username-display { + max-width: none; + overflow: visible; + text-overflow: clip; + white-space: nowrap; +} +body.topbar-compact #role-badge { + white-space: nowrap; +} + @media (max-width: 767px) { .topbar-grid { grid-template-columns: 1fr auto; diff --git a/Overlord-Server/public/assets/nav/layout.js b/Overlord-Server/public/assets/nav/layout.js index 0c7e78f..901f6c3 100644 --- a/Overlord-Server/public/assets/nav/layout.js +++ b/Overlord-Server/public/assets/nav/layout.js @@ -165,7 +165,7 @@ function initSidebarTree(navLinks) { ────────────────────────────────────────────── */ function createTopbarController(host, refs) { - const { panel, navLinks, navUtility } = refs; + const { panel, navLinks, navUtility, topbarToggle } = refs; if (!panel || !navLinks || !navUtility) { return { applyAdaptiveNavLayout: () => {} }; } @@ -175,8 +175,105 @@ function createTopbarController(host, refs) { initDropdowns(navUtility); initNotifyBadgeMirror(navUtility); + const grid = host.querySelector(".topbar-grid"); + const left = host.querySelector(".topbar-left"); + let compact = false; + let layoutFrame = 0; + + const closeCompactMenu = () => { + document.body.classList.remove("topbar-menu-open"); + topbarToggle?.setAttribute("aria-expanded", "false"); + }; + + const setCompact = (next) => { + if (compact === next) return; + compact = next; + document.body.classList.toggle("topbar-compact", next); + host.dataset.navMode = next ? "compact" : "desktop"; + if (!next) closeCompactMenu(); + }; + + const measureDesktopWidth = () => { + const wasCompact = document.body.classList.contains("topbar-compact"); + const wasOpen = document.body.classList.contains("topbar-menu-open"); + if (wasCompact) { + document.body.classList.remove("topbar-compact", "topbar-menu-open"); + } + + const style = getComputedStyle(host); + const horizontalPadding = parseFloat(style.paddingLeft || "0") + parseFloat(style.paddingRight || "0"); + const gridGap = grid ? parseFloat(getComputedStyle(grid).columnGap || "0") || 0 : 0; + const navGap = parseFloat(getComputedStyle(navLinks).columnGap || getComputedStyle(navLinks).gap || "0") || 0; + const visibleNavItems = Array.from(navLinks.children).filter((el) => { + const itemStyle = getComputedStyle(el); + return itemStyle.display !== "none" && !el.classList.contains("hidden"); + }); + const navWidth = visibleNavItems.reduce((sum, el) => sum + el.getBoundingClientRect().width, 0) + + Math.max(0, visibleNavItems.length - 1) * navGap; + const requiredWidth = + (left?.scrollWidth || 0) + + navWidth + + (navUtility?.scrollWidth || 0) + + (gridGap * 2) + + horizontalPadding + + 24; + + if (wasCompact) { + document.body.classList.add("topbar-compact"); + document.body.classList.toggle("topbar-menu-open", wasOpen); + } + return requiredWidth; + }; + + const applyAdaptiveNavLayout = () => { + const availableWidth = host.clientWidth || window.innerWidth; + const requiredWidth = measureDesktopWidth(); + const shouldCompact = requiredWidth > availableWidth; + setCompact(shouldCompact); + }; + + const scheduleAdaptiveNavLayout = () => { + if (layoutFrame) return; + layoutFrame = requestAnimationFrame(() => { + layoutFrame = 0; + applyAdaptiveNavLayout(); + }); + }; + + topbarToggle?.addEventListener("click", (event) => { + event.stopPropagation(); + const nextOpen = !document.body.classList.contains("topbar-menu-open"); + document.body.classList.toggle("topbar-menu-open", nextOpen); + topbarToggle.setAttribute("aria-expanded", String(nextOpen)); + }); + + document.addEventListener("click", (event) => { + if (!compact) return; + if (event.target.closest("#top-nav")) return; + closeCompactMenu(); + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") closeCompactMenu(); + }); + + navLinks.addEventListener("click", (event) => { + if (event.target.closest("a[href]")) closeCompactMenu(); + }); + + window.addEventListener("resize", scheduleAdaptiveNavLayout); + + if ("ResizeObserver" in window) { + const resizeObserver = new ResizeObserver(scheduleAdaptiveNavLayout); + resizeObserver.observe(host); + resizeObserver.observe(navLinks); + resizeObserver.observe(navUtility); + } + + scheduleAdaptiveNavLayout(); + host.dataset.navMode = "desktop"; - return { applyAdaptiveNavLayout: () => {} }; + return { applyAdaptiveNavLayout }; } /* ────────────────────────────────────────────── diff --git a/Overlord-Server/public/assets/nav/template.js b/Overlord-Server/public/assets/nav/template.js index 77aa4d9..5ce63dd 100644 --- a/Overlord-Server/public/assets/nav/template.js +++ b/Overlord-Server/public/assets/nav/template.js @@ -136,6 +136,9 @@ function mountTopbar(host) { Overlord +