diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 15f332f..d524051 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -2196,18 +2196,23 @@ body.sb-collapsed .sb-collapse-btn i { font-weight: 500; white-space: nowrap; overflow: hidden; - border: 1px solid transparent; + border: none; + outline: none; background: transparent; cursor: pointer; width: 100%; text-align: left; position: relative; - transition: background 0.14s, color 0.14s, border-color 0.14s; + transition: background 0.14s, color 0.14s; } .sb-link:hover { background: rgba(255, 255, 255, 0.06); color: #e2e8f0; } +.sb-link:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 1px; +} /* Collapsed: center icon, fill the rail */ body.sb-collapsed .sb-link { justify-content: center; @@ -2245,7 +2250,6 @@ body.sb-collapsed .sb-link .sb-text { .sb-link.nav-active { background: rgba(99, 102, 241, 0.14); color: #e2e8f0; - border-color: rgba(99, 102, 241, 0.28); } .sb-link.nav-active:hover { background: rgba(99, 102, 241, 0.2); @@ -2256,7 +2260,6 @@ body.sb-collapsed .sb-link .sb-text { .sb-link--danger:hover { background: rgba(239, 68, 68, 0.1); color: #fca5a5; - border-color: rgba(239, 68, 68, 0.28); } /* ---- Notification bell badge ---- */ @@ -2374,3 +2377,226 @@ body.sb-open #sb-backdrop { display: block; } #notify-toggle.text-emerald-200 { color: #a7f3d0; } #notify-toggle.text-slate-300 { color: #cbd5e1; } #notify-toggle.border-emerald-500\/40 { border-color: rgba(16, 185, 129, 0.4); } + +/* ============================================================ + TOPBAR DROPDOWN MENUS + ============================================================ */ + +.nav-dd-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + flex: 1; + justify-content: center; +} + +.nav-dd-group-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: 8px; + background: rgba(30, 41, 59, 0.7); + border: 1px solid rgba(51, 65, 85, 0.6); + color: #94a3b8; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} +.nav-dd-group-btn:hover { + background: rgba(51, 65, 85, 0.7); + color: #e2e8f0; +} +.nav-dd-group-btn[aria-expanded="true"] { + background: rgba(51, 65, 85, 0.85); + color: #e2e8f0; + border-color: rgba(99, 102, 241, 0.3); +} +.nav-dd-group-btn i.fa-chevron-down { + font-size: 0.6rem; + transition: transform 0.15s ease; +} +.nav-dd-group-btn[aria-expanded="true"] i.fa-chevron-down { + transform: rotate(180deg); +} + +/* Dropdown wrapper */ +.nav-dd-wrapper { + position: relative; +} + +/* Dropdown menu */ +.nav-dd-menu { + position: absolute; + top: calc(100% + 6px); + left: 50%; + transform: translateX(-50%) translateY(-4px); + min-width: 200px; + padding: 6px; + border-radius: 10px; + background: rgba(15, 23, 42, 0.97); + border: 1px solid rgba(51, 65, 85, 0.7); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + gap: 2px; + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s; + z-index: 100; +} +.nav-dd-menu.nav-dd-open { + opacity: 1; + visibility: visible; + pointer-events: auto; + transform: translateX(-50%) translateY(0); +} + +/* Dropdown menu item */ +.nav-dd-item { + display: flex; + align-items: center; + gap: 10px; + padding: 9px 12px; + border-radius: 7px; + color: #94a3b8; + text-decoration: none; + font-size: 0.84rem; + font-weight: 500; + transition: background 0.12s, color 0.12s; +} +.nav-dd-item:hover { + background: rgba(99, 102, 241, 0.12); + color: #e2e8f0; +} +.nav-dd-item.nav-active { + background: rgba(99, 102, 241, 0.18); + color: #c7d2fe; + border: 1px solid rgba(99, 102, 241, 0.25); +} +.nav-dd-item-icon { + width: 18px; + text-align: center; + font-size: 0.9rem; + flex-shrink: 0; +} +.nav-dd-badge { + min-width: 18px; + height: 18px; + padding: 0 4px; + border-radius: 999px; + background: #d97706; + color: #fff; + font-size: 0.65rem; + font-weight: 700; + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: auto; +} +.nav-dd-badge:not(.hidden) { + display: inline-flex; +} + +/* ============================================================ + SIDEBAR TREE GROUPS + ============================================================ */ + +.sb-group { + display: flex; + flex-direction: column; + position: relative; +} + +.sb-group-btn { + display: flex; + align-items: center; + gap: 11px; + padding: 10px 12px; + border-radius: 9px; + color: #7c8fa3; + font-size: 0.875rem; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + border: none; + outline: none; + background: transparent; + cursor: pointer; + width: 100%; + text-align: left; + position: relative; + transition: background 0.14s, color 0.14s; +} +.sb-group-btn:hover { + background: rgba(255, 255, 255, 0.06); + color: #e2e8f0; +} +.sb-group-btn:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 1px; +} + +.sb-chevron { + margin-left: auto; + font-size: 0.6rem; + transition: transform 0.2s ease; + color: #475569; +} +.sb-chevron.sb-chevron-open { + transform: rotate(90deg); + color: #94a3b8; +} + +/* Collapsed sidebar: hide chevron, center icon */ +body.sb-collapsed .sb-chevron { + display: none; +} +body.sb-collapsed .sb-group-btn { + justify-content: center; + padding: 12px 0; + gap: 0; +} + +/* Tree children container — completely hidden when collapsed */ +.sb-group-children { + display: none; +} +.sb-group-children.sb-group-open { + display: block; +} + +/* Child links — indented */ +.sb-link-child { + padding-left: 16px !important; + font-size: 0.82rem !important; + padding-top: 6px !important; + padding-bottom: 6px !important; +} +body.sb-collapsed .sb-link-child { + padding-left: 0 !important; +} + +/* Badge in sidebar */ +.sb-badge { + min-width: 18px; + height: 18px; + padding: 0 4px; + border-radius: 999px; + background: #d97706; + color: #fff; + font-size: 0.65rem; + font-weight: 700; + display: inline-flex; + align-items: center; + justify-content: center; +} +.sb-badge:not(.hidden) { + display: inline-flex; +} diff --git a/Overlord-Server/public/assets/nav.js b/Overlord-Server/public/assets/nav.js index 4462512..1cdd904 100644 --- a/Overlord-Server/public/assets/nav.js +++ b/Overlord-Server/public/assets/nav.js @@ -32,11 +32,24 @@ if (host) { "/user-client-access": "users-link", "/notifications": "notifications-link", "/file-share": "file-share-link", + "/purgatory": "enrollment-link", }; const activeId = activeMap[path]; if (activeId) { const el = document.getElementById(activeId); - if (el) el.classList.add("nav-active"); + if (el) { + el.classList.add("nav-active"); + // Also expand the parent sidebar group if applicable + const group = el.closest(".sb-group"); + if (group) { + const btn = group.querySelector(".sb-group-btn"); + const children = group.querySelector(".sb-group-children"); + const chevron = btn?.querySelector(".sb-chevron"); + if (btn) btn.setAttribute("aria-expanded", "true"); + if (children) children.classList.add("sb-group-open"); + if (chevron) chevron.classList.add("sb-chevron-open"); + } + } } if (refs.logoutBtn && !refs.logoutBtn.dataset.boundLogout) { refs.logoutBtn.dataset.boundLogout = "true"; diff --git a/Overlord-Server/public/assets/nav/layout.js b/Overlord-Server/public/assets/nav/layout.js index 827d059..3bd0b82 100644 --- a/Overlord-Server/public/assets/nav/layout.js +++ b/Overlord-Server/public/assets/nav/layout.js @@ -3,12 +3,108 @@ import { NAV_MODE_KEY } from "./template.js"; const LS_KEY = "sb_collapsed"; const MOBILE_BP = 768; +/* ────────────────────────────────────────────── + TOPBAR DROPDOWN LOGIC + ────────────────────────────────────────────── */ + +function initDropdowns(navLinks) { + if (!navLinks) return; + + let activeDropdown = null; + + function closeAll() { + if (activeDropdown) { + const btn = activeDropdown.querySelector(".nav-dd-group-btn"); + const menu = activeDropdown.querySelector(".nav-dd-menu"); + if (btn) btn.setAttribute("aria-expanded", "false"); + if (menu) menu.classList.remove("nav-dd-open"); + activeDropdown = null; + } + } + + navLinks.addEventListener("click", (e) => { + // If clicking a dropdown menu item (actual link), let it navigate normally + const item = e.target.closest(".nav-dd-item"); + if (item) { + closeAll(); + return; // Don't prevent default — let the link navigate + } + + const wrapper = e.target.closest(".nav-dd-wrapper"); + if (!wrapper) { + closeAll(); + return; + } + const menu = wrapper.querySelector(".nav-dd-menu"); + const btn = wrapper.querySelector(".nav-dd-group-btn"); + if (!menu || !btn) return; + + e.preventDefault(); + e.stopPropagation(); + + if (activeDropdown === wrapper) { + closeAll(); + } else { + closeAll(); + btn.setAttribute("aria-expanded", "true"); + menu.classList.add("nav-dd-open"); + activeDropdown = wrapper; + } + }); + + // Close on outside click + document.addEventListener("click", (e) => { + if (!e.target.closest(".nav-dd-wrapper")) { + closeAll(); + } + }); + + // Close on Escape + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") closeAll(); + }); +} + +/* ────────────────────────────────────────────── + SIDEBAR TREE EXPAND / COLLAPSE + ────────────────────────────────────────────── */ + +function initSidebarTree(navLinks) { + if (!navLinks) return; + + navLinks.addEventListener("click", (e) => { + const groupBtn = e.target.closest(".sb-group-btn"); + if (!groupBtn) return; + + const group = groupBtn.closest(".sb-group"); + if (!group) return; + + const children = group.querySelector(".sb-group-children"); + const chevron = groupBtn.querySelector(".sb-chevron"); + if (!children) return; + + const expanded = groupBtn.getAttribute("aria-expanded") === "true"; + groupBtn.setAttribute("aria-expanded", String(!expanded)); + children.classList.toggle("sb-group-open", !expanded); + if (chevron) { + chevron.classList.toggle("sb-chevron-open", !expanded); + } + }); +} + +/* ────────────────────────────────────────────── + TOPBAR CONTROLLER (adaptive layout) + ────────────────────────────────────────────── */ + function createTopbarController(host, refs) { const { toggle, panel, navLinks, navUtility } = refs; if (!toggle || !panel || !navLinks || !navUtility) { return { applyAdaptiveNavLayout: () => {} }; } + // Init dropdowns + initDropdowns(navLinks); + const navOverflows = () => panel.scrollWidth > panel.clientWidth + 1 || host.scrollWidth > host.clientWidth + 1; @@ -104,13 +200,20 @@ function createTopbarController(host, refs) { return { applyAdaptiveNavLayout }; } +/* ────────────────────────────────────────────── + SIDEBAR CONTROLLER + ────────────────────────────────────────────── */ + function createSidebarController(host, refs) { - const { collapseBtn, toggle } = refs; + const { collapseBtn, toggle, panel } = refs; const backdrop = document.getElementById("sb-backdrop"); const navLinks = document.getElementById("nav-links"); document.body.classList.add("sb-ready"); + // Init sidebar tree + if (navLinks) initSidebarTree(navLinks); + let collapsed = localStorage.getItem(LS_KEY) === "true"; if (collapsed) document.body.classList.add("sb-collapsed"); diff --git a/Overlord-Server/public/assets/nav/role-ui.js b/Overlord-Server/public/assets/nav/role-ui.js index 4ba7a41..2575b2d 100644 --- a/Overlord-Server/public/assets/nav/role-ui.js +++ b/Overlord-Server/public/assets/nav/role-ui.js @@ -65,19 +65,25 @@ export function applyUserRoleUI(user, refs) { ); } + // Helper: unhide an element by ID in both topbar and sidebar + const unhide = (id) => { + const el = document.getElementById(id); + if (el) el.classList.remove("hidden"); + }; + if (user.role === "admin") { - usersLink?.classList.remove("hidden"); - pluginsLink?.classList.remove("hidden"); - logsLink?.classList.remove("hidden"); - solPublishLink?.classList.remove("hidden"); + unhide("users-link"); + unhide("plugins-link"); + unhide("logs-link"); + unhide("sol-publish-link"); } if (user.role === "admin" || user.role === "operator") { - buildLink?.classList.remove("hidden"); - notificationsLink?.classList.remove("hidden"); - enrollmentLink?.classList.remove("hidden"); + unhide("build-link"); + unhide("notifications-link"); + unhide("enrollment-link"); } if (user.role !== "viewer") { - scriptsLink?.classList.remove("hidden"); - fileShareLink?.classList.remove("hidden"); + unhide("scripts-link"); + unhide("file-share-link"); } } diff --git a/Overlord-Server/public/assets/nav/template.js b/Overlord-Server/public/assets/nav/template.js index 9d65212..7925504 100644 --- a/Overlord-Server/public/assets/nav/template.js +++ b/Overlord-Server/public/assets/nav/template.js @@ -1,9 +1,124 @@ export const NAV_MODE_KEY = "sb_mode"; +/* ────────────────────────────────────────────── + NAVIGATION DATA — single source of truth + ────────────────────────────────────────────── */ + +const NAV_GROUPS = [ + { + id: "clients", + label: "Clients", + icon: "fa-display", + iconColor: "text-sky-400", + href: "/", + linkId: "nav-clients", + alwaysVisible: true, + }, + { + id: "purgatory", + label: "Purgatory", + icon: "fa-user-clock", + iconColor: "text-amber-400", + href: "/purgatory", + linkId: "enrollment-link", + hasBadge: true, + }, + { + id: "system", + label: "System", + icon: "fa-gear", + iconColor: "text-slate-300", + children: [ + { href: "/logs", label: "Logs", icon: "fa-clipboard-list", iconColor: "text-amber-400", linkId: "logs-link", hidden: true }, + { href: "/file-share", label: "File Share", icon: "fa-share-nodes", iconColor: "text-rose-400", linkId: "file-share-link", hidden: true }, + { href: "/users", label: "Users", icon: "fa-users", iconColor: "text-indigo-400", linkId: "users-link", hidden: true }, + { href: "/notifications", label: "Notifications", icon: "fa-bell", iconColor: "text-yellow-400", linkId: "notifications-link", hidden: true }, + ], + }, + { + id: "management", + label: "Management", + icon: "fa-folder-open", + iconColor: "text-cyan-400", + children: [ + { href: "/scripts", label: "Scripts", icon: "fa-code", iconColor: "text-cyan-400", linkId: "scripts-link" }, + { href: "/socks5-manager", label: "Proxies", icon: "fa-network-wired", iconColor: "text-sky-400", linkId: "socks5-link" }, + ], + }, + { + id: "build", + label: "Build", + icon: "fa-wrench", + iconColor: "text-orange-400", + children: [ + { href: "/build", label: "Builder", icon: "fa-hammer", iconColor: "text-orange-400", linkId: "build-link", hidden: true }, + { href: "/plugins", label: "Plugins", icon: "fa-puzzle-piece", iconColor: "text-violet-400", linkId: "plugins-link", hidden: true }, + { href: "/sol-publish", label: "Sol Publish", icon: "fa-link-slash", iconColor: "text-purple-400", linkId: "sol-publish-link", hidden: true }, + ], + }, + { + id: "monitoring", + label: "Monitoring", + icon: "fa-chart-line", + iconColor: "text-emerald-400", + children: [ + { href: "/metrics", label: "Metrics", icon: "fa-chart-line", iconColor: "text-emerald-400", linkId: "metrics-link" }, + ], + }, +]; + +/* ────────────────────────────────────────────── + TOPBAR — dropdown menus + ────────────────────────────────────────────── */ + +function dropdownItem(child) { + const hiddenCls = child.hidden ? " hidden" : ""; + const badgeHtml = child.hasBadge + ? `` + : ""; + return ` + + ${child.label} + ${badgeHtml} + `; +} + +function dropdownGroup(group) { + if (group.href && !group.children) { + // Simple link (Clients, Purgatory) + const alwaysCls = group.alwaysVisible ? "" : " hidden"; + const badgeHtml = group.hasBadge + ? `` + : ""; + return ` + + ${group.label} + ${badgeHtml} + `; + } + // Dropdown group — wrapper always visible, children control visibility + const items = group.children.map(dropdownItem).join(""); + return ` + `; +} + function mountTopbar(host) { host.className = "sticky top-0 z-10 w-full px-5 py-3 bg-slate-950/80 backdrop-blur border-b border-slate-800"; + const groupsHtml = NAV_GROUPS.map(dropdownGroup).join(""); + host.innerHTML = `
@@ -22,47 +137,12 @@ function mountTopbar(host) {