revamp sidebar/menu navigation
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
? `<span id="enrollment-badge" class="nav-dd-badge hidden"></span>`
|
||||
: "";
|
||||
return `<a href="${child.href}" id="${child.linkId}"
|
||||
class="nav-dd-item${hiddenCls}" data-link-id="${child.linkId}">
|
||||
<i class="fa-solid ${child.icon} ${child.iconColor} nav-dd-item-icon"></i>
|
||||
<span>${child.label}</span>
|
||||
${badgeHtml}
|
||||
</a>`;
|
||||
}
|
||||
|
||||
function dropdownGroup(group) {
|
||||
if (group.href && !group.children) {
|
||||
// Simple link (Clients, Purgatory)
|
||||
const alwaysCls = group.alwaysVisible ? "" : " hidden";
|
||||
const badgeHtml = group.hasBadge
|
||||
? `<span id="enrollment-badge" class="nav-dd-badge hidden"></span>`
|
||||
: "";
|
||||
return `<a href="${group.href}" id="${group.linkId}"
|
||||
class="nav-dd-group-btn${alwaysCls}" data-group="${group.id}">
|
||||
<i class="fa-solid ${group.icon} ${group.iconColor}"></i>
|
||||
<span>${group.label}</span>
|
||||
${badgeHtml}
|
||||
</a>`;
|
||||
}
|
||||
// Dropdown group — wrapper always visible, children control visibility
|
||||
const items = group.children.map(dropdownItem).join("");
|
||||
return `
|
||||
<div class="nav-dd-wrapper" data-group="${group.id}">
|
||||
<button class="nav-dd-group-btn" data-group="${group.id}" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa-solid ${group.icon} ${group.iconColor}"></i>
|
||||
<span>${group.label}</span>
|
||||
<i class="fa-solid fa-chevron-down nav-dd-chevron"></i>
|
||||
</button>
|
||||
<div class="nav-dd-menu" role="menu">
|
||||
${items}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3 w-full">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
@@ -22,47 +137,12 @@ function mountTopbar(host) {
|
||||
</div>
|
||||
<div
|
||||
id="nav-panel"
|
||||
class="hidden md:flex md:flex-1 md:items-center md:justify-between gap-3"
|
||||
class="hidden md:flex md:flex-1 md:items-center md:justify-center gap-3"
|
||||
>
|
||||
<nav id="nav-links" class="flex flex-wrap items-center gap-2 md:flex-1 md:justify-center">
|
||||
<a href="/" id="nav-clients"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
|
||||
><i class="fa-solid fa-display text-sky-400"></i> Clients</a>
|
||||
<a href="/metrics" id="metrics-link"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
|
||||
><i class="fa-solid fa-chart-line text-emerald-400"></i> Metrics</a>
|
||||
<a href="/logs" id="logs-link"
|
||||
class="hidden inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
|
||||
><i class="fa-solid fa-clipboard-list text-amber-400"></i> Logs</a>
|
||||
<a href="/scripts" id="scripts-link"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
|
||||
><i class="fa-solid fa-code text-cyan-400"></i> Scripts</a>
|
||||
<a href="/socks5-manager" id="socks5-link"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
|
||||
><i class="fa-solid fa-network-wired text-sky-400"></i> Proxies</a>
|
||||
<a href="/file-share" id="file-share-link"
|
||||
class="hidden inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
|
||||
><i class="fa-solid fa-share-nodes text-rose-400"></i> File Share</a>
|
||||
<a href="/plugins" id="plugins-link"
|
||||
class="hidden inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
|
||||
><i class="fa-solid fa-puzzle-piece text-violet-400"></i> Plugins</a>
|
||||
<a href="/build" id="build-link"
|
||||
class="hidden inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
|
||||
><i class="fa-solid fa-hammer text-orange-400"></i> Builder</a>
|
||||
<a href="/sol-publish" id="sol-publish-link"
|
||||
class="hidden inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
|
||||
><i class="fa-solid fa-link-slash text-purple-400"></i> Sol Publish</a>
|
||||
<a href="/notifications" id="notifications-link"
|
||||
class="hidden inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
|
||||
><i class="fa-solid fa-bell text-yellow-400"></i> Notifications</a>
|
||||
<a href="/users" id="users-link"
|
||||
class="hidden inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
|
||||
><i class="fa-solid fa-users text-indigo-400"></i> Users</a>
|
||||
<a href="/purgatory" id="enrollment-link"
|
||||
class="hidden inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 hover:bg-slate-800 text-slate-300 transition-colors"
|
||||
><i class="fa-solid fa-user-clock text-amber-400"></i> Purgatory<span id="enrollment-badge" class="hidden min-w-[20px] h-5 px-1 rounded-full bg-amber-500 text-white text-xs flex items-center justify-center"></span></a>
|
||||
<nav id="nav-links" class="nav-dd-bar">
|
||||
${groupsHtml}
|
||||
</nav>
|
||||
<div id="nav-utility" class="flex flex-wrap items-center gap-2 md:w-auto md:justify-end md:shrink-0">
|
||||
<div id="nav-utility" class="flex flex-wrap items-center gap-2 md:w-auto md:justify-end md:shrink-0 md:ml-auto">
|
||||
<button id="notify-toggle"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-900/70 border border-slate-800 text-slate-300 hover:bg-slate-800"
|
||||
title="Toggle notifications" aria-label="Toggle notifications">
|
||||
@@ -86,7 +166,8 @@ function mountTopbar(host) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
return {
|
||||
// Collect all link refs
|
||||
const refs = {
|
||||
toggle: document.getElementById("nav-toggle"),
|
||||
panel: document.getElementById("nav-panel"),
|
||||
collapseBtn: null,
|
||||
@@ -98,21 +179,83 @@ function mountTopbar(host) {
|
||||
accountSettingsBtn: document.getElementById("account-settings-btn"),
|
||||
usernameDisplay: document.getElementById("username-display"),
|
||||
roleBadge: document.getElementById("role-badge"),
|
||||
usersLink: document.getElementById("users-link"),
|
||||
buildLink: document.getElementById("build-link"),
|
||||
solPublishLink: document.getElementById("sol-publish-link"),
|
||||
pluginsLink: document.getElementById("plugins-link"),
|
||||
scriptsLink: document.getElementById("scripts-link"),
|
||||
logsLink: document.getElementById("logs-link"),
|
||||
notificationsLink: document.getElementById("notifications-link"),
|
||||
enrollmentLink: document.getElementById("enrollment-link"),
|
||||
enrollmentBadge: document.getElementById("enrollment-badge"),
|
||||
fileShareLink: document.getElementById("file-share-link"),
|
||||
};
|
||||
|
||||
// Add individual link refs
|
||||
NAV_GROUPS.forEach((g) => {
|
||||
if (g.linkId) refs[g.linkId.replace(/-/g, "") + "Ref"] = document.getElementById(g.linkId);
|
||||
if (g.children) {
|
||||
g.children.forEach((c) => {
|
||||
const key = c.linkId.replace(/-/g, "") + "Ref";
|
||||
refs[key] = document.getElementById(c.linkId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Convenience refs expected by nav.js
|
||||
refs.usersLink = document.getElementById("users-link");
|
||||
refs.buildLink = document.getElementById("build-link");
|
||||
refs.solPublishLink = document.getElementById("sol-publish-link");
|
||||
refs.pluginsLink = document.getElementById("plugins-link");
|
||||
refs.scriptsLink = document.getElementById("scripts-link");
|
||||
refs.logsLink = document.getElementById("logs-link");
|
||||
refs.notificationsLink = document.getElementById("notifications-link");
|
||||
refs.enrollmentLink = document.getElementById("enrollment-link");
|
||||
refs.fileShareLink = document.getElementById("file-share-link");
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────
|
||||
SIDEBAR — tree-style expandable
|
||||
────────────────────────────────────────────── */
|
||||
|
||||
function sidebarChild(child) {
|
||||
const hiddenCls = child.hidden ? " hidden" : "";
|
||||
const badgeHtml = child.hasBadge
|
||||
? `<span id="enrollment-badge" class="sb-badge hidden"></span>`
|
||||
: "";
|
||||
return `<a href="${child.href}" id="${child.linkId}"
|
||||
class="sb-link sb-link-child${hiddenCls}" data-link-id="${child.linkId}" title="${child.label}">
|
||||
<i class="fa-solid ${child.icon} ${child.iconColor} sb-icon"></i>
|
||||
<span class="sb-text">${child.label}</span>
|
||||
${badgeHtml}
|
||||
</a>`;
|
||||
}
|
||||
|
||||
function sidebarGroup(group) {
|
||||
if (group.href && !group.children) {
|
||||
// Simple link (Clients, Purgatory)
|
||||
const alwaysCls = group.alwaysVisible ? "" : " hidden";
|
||||
const badgeHtml = group.hasBadge
|
||||
? `<span id="enrollment-badge" class="sb-badge hidden"></span>`
|
||||
: "";
|
||||
return `<a href="${group.href}" id="${group.linkId}"
|
||||
class="sb-link${alwaysCls}" data-link-id="${group.linkId}" title="${group.label}">
|
||||
<i class="fa-solid ${group.icon} ${group.iconColor} sb-icon"></i>
|
||||
<span class="sb-text">${group.label}</span>
|
||||
${badgeHtml}
|
||||
</a>`;
|
||||
}
|
||||
// Expandable group — wrapper always visible, children control visibility
|
||||
const childrenHtml = group.children.map(sidebarChild).join("");
|
||||
return `
|
||||
<div class="sb-group" data-group="${group.id}">
|
||||
<button class="sb-group-btn" data-group="${group.id}" aria-expanded="false">
|
||||
<i class="fa-solid ${group.icon} ${group.iconColor} sb-icon"></i>
|
||||
<span class="sb-text">${group.label}</span>
|
||||
<i class="fa-solid fa-chevron-right sb-chevron"></i>
|
||||
</button>
|
||||
<div class="sb-group-children" role="group">
|
||||
${childrenHtml}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function mountSidebar(host) {
|
||||
// Sidebar HTML injected into the fixed #top-nav element
|
||||
const groupsHtml = NAV_GROUPS.map(sidebarGroup).join("");
|
||||
|
||||
host.innerHTML = `
|
||||
<div class="sb-header">
|
||||
<a href="/" class="sb-logo">
|
||||
@@ -125,57 +268,7 @@ function mountSidebar(host) {
|
||||
</div>
|
||||
|
||||
<nav id="nav-links" class="sb-nav">
|
||||
<a href="/" id="nav-clients" class="sb-link" title="Clients">
|
||||
<i class="fa-solid fa-display text-sky-400 sb-icon"></i>
|
||||
<span class="sb-text">Clients</span>
|
||||
</a>
|
||||
<a href="/metrics" id="metrics-link" class="sb-link" title="Metrics">
|
||||
<i class="fa-solid fa-chart-line text-emerald-400 sb-icon"></i>
|
||||
<span class="sb-text">Metrics</span>
|
||||
</a>
|
||||
<a href="/logs" id="logs-link" class="sb-link hidden" title="Logs">
|
||||
<i class="fa-solid fa-clipboard-list text-amber-400 sb-icon"></i>
|
||||
<span class="sb-text">Logs</span>
|
||||
</a>
|
||||
<a href="/scripts" id="scripts-link" class="sb-link" title="Scripts">
|
||||
<i class="fa-solid fa-code text-cyan-400 sb-icon"></i>
|
||||
<span class="sb-text">Scripts</span>
|
||||
</a>
|
||||
<a href="/socks5-manager" id="socks5-link" class="sb-link" title="Proxies">
|
||||
<i class="fa-solid fa-network-wired text-sky-400 sb-icon"></i>
|
||||
<span class="sb-text">Proxies</span>
|
||||
</a>
|
||||
<a href="/file-share" id="file-share-link" class="sb-link hidden" title="File Share">
|
||||
<i class="fa-solid fa-share-nodes text-rose-400 sb-icon"></i>
|
||||
<span class="sb-text">File Share</span>
|
||||
</a>
|
||||
<a href="/plugins" id="plugins-link" class="sb-link hidden" title="Plugins">
|
||||
<i class="fa-solid fa-puzzle-piece text-violet-400 sb-icon"></i>
|
||||
<span class="sb-text">Plugins</span>
|
||||
</a>
|
||||
<a href="/build" id="build-link" class="sb-link hidden" title="Builder">
|
||||
<i class="fa-solid fa-hammer text-orange-400 sb-icon"></i>
|
||||
<span class="sb-text">Builder</span>
|
||||
</a>
|
||||
<a href="/sol-publish" id="sol-publish-link" class="sb-link hidden" title="Sol Publish">
|
||||
<i class="fa-solid fa-link-slash text-purple-400 sb-icon"></i>
|
||||
<span class="sb-text">Sol Publish</span>
|
||||
</a>
|
||||
<a href="/notifications" id="notifications-link" class="sb-link hidden" title="Notifications">
|
||||
<i class="fa-solid fa-bell text-yellow-400 sb-icon"></i>
|
||||
<span class="sb-text">Notifications</span>
|
||||
</a>
|
||||
<a href="/users" id="users-link" class="sb-link hidden" title="Users">
|
||||
<i class="fa-solid fa-users text-indigo-400 sb-icon"></i>
|
||||
<span class="sb-text">Users</span>
|
||||
</a>
|
||||
<a href="/purgatory" id="enrollment-link" class="sb-link hidden" title="Purgatory">
|
||||
<i class="fa-solid fa-user-clock text-amber-400 sb-icon"></i>
|
||||
<span class="sb-text">
|
||||
Purgatory
|
||||
<span id="enrollment-badge" class="sb-inline-badge hidden"></span>
|
||||
</span>
|
||||
</a>
|
||||
${groupsHtml}
|
||||
</nav>
|
||||
|
||||
<div id="nav-utility" class="sb-utility">
|
||||
@@ -222,7 +315,8 @@ function mountSidebar(host) {
|
||||
backdrop.id = "sb-backdrop";
|
||||
document.body.appendChild(backdrop);
|
||||
|
||||
return {
|
||||
// Collect refs
|
||||
const refs = {
|
||||
toggle: document.getElementById("nav-toggle"),
|
||||
collapseBtn: document.getElementById("sb-collapse-btn"),
|
||||
panel: host,
|
||||
@@ -234,17 +328,21 @@ function mountSidebar(host) {
|
||||
accountSettingsBtn: document.getElementById("account-settings-btn"),
|
||||
usernameDisplay: document.getElementById("username-display"),
|
||||
roleBadge: document.getElementById("role-badge"),
|
||||
usersLink: document.getElementById("users-link"),
|
||||
buildLink: document.getElementById("build-link"),
|
||||
solPublishLink: document.getElementById("sol-publish-link"),
|
||||
pluginsLink: document.getElementById("plugins-link"),
|
||||
scriptsLink: document.getElementById("scripts-link"),
|
||||
logsLink: document.getElementById("logs-link"),
|
||||
notificationsLink: document.getElementById("notifications-link"),
|
||||
enrollmentLink: document.getElementById("enrollment-link"),
|
||||
enrollmentBadge: document.getElementById("enrollment-badge"),
|
||||
fileShareLink: document.getElementById("file-share-link"),
|
||||
};
|
||||
|
||||
// Convenience refs expected by nav.js
|
||||
refs.usersLink = document.getElementById("users-link");
|
||||
refs.buildLink = document.getElementById("build-link");
|
||||
refs.solPublishLink = document.getElementById("sol-publish-link");
|
||||
refs.pluginsLink = document.getElementById("plugins-link");
|
||||
refs.scriptsLink = document.getElementById("scripts-link");
|
||||
refs.logsLink = document.getElementById("logs-link");
|
||||
refs.notificationsLink = document.getElementById("notifications-link");
|
||||
refs.enrollmentLink = document.getElementById("enrollment-link");
|
||||
refs.fileShareLink = document.getElementById("file-share-link");
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
export function mountNav(host) {
|
||||
|
||||
Reference in New Issue
Block a user