improve navbar
This commit is contained in:
@@ -3342,6 +3342,150 @@ body.sb-open #sb-backdrop { display: block; }
|
||||
#notify-toggle.text-slate-300 { color: #cbd5e1; }
|
||||
#notify-toggle.border-emerald-500\/40 { border-color: rgba(16, 185, 129, 0.4); }
|
||||
|
||||
/* ============================================================
|
||||
TOPBAR — 3-column grid (brand · centered nav · utility)
|
||||
============================================================ */
|
||||
|
||||
.topbar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
grid-template-areas: "left center right";
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
.topbar-left {
|
||||
grid-area: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
justify-self: start;
|
||||
min-width: 0;
|
||||
}
|
||||
.topbar-center {
|
||||
grid-area: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-self: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.topbar-right {
|
||||
grid-area: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-self: end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.topbar-grid {
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-areas:
|
||||
"left right"
|
||||
"center center";
|
||||
}
|
||||
.topbar-center { justify-self: stretch; }
|
||||
.topbar-center.hidden { display: none; }
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
USER ACTIONS DROPDOWN (notify · hide · logout)
|
||||
============================================================ */
|
||||
|
||||
.user-actions-trigger {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: rgba(30, 41, 59, 0.7);
|
||||
border: 1px solid rgba(51, 65, 85, 0.6);
|
||||
color: #cbd5e1;
|
||||
cursor: pointer;
|
||||
transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
|
||||
}
|
||||
.user-actions-trigger:hover {
|
||||
background: rgba(51, 65, 85, 0.7);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.user-actions-trigger[aria-expanded="true"] {
|
||||
background: rgba(51, 65, 85, 0.85);
|
||||
border-color: rgba(99, 102, 241, 0.3);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.user-actions-dot {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #f43f5e;
|
||||
box-shadow: 0 0 0 2px rgba(8, 12, 24, 1);
|
||||
}
|
||||
.user-actions-dot.hidden { display: none; }
|
||||
|
||||
.user-actions-menu {
|
||||
min-width: 220px;
|
||||
/* Anchor down-right of the trigger; override base centering */
|
||||
left: auto !important;
|
||||
right: 0 !important;
|
||||
transform: translateY(-4px) !important;
|
||||
}
|
||||
.user-actions-menu.nav-dd-open {
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
.user-actions-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 9px 12px;
|
||||
border-radius: 7px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #cbd5e1;
|
||||
font-size: 0.84rem;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, color 120ms ease;
|
||||
}
|
||||
.user-actions-item:hover {
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.user-actions-item--danger:hover {
|
||||
background: rgba(244, 63, 94, 0.12);
|
||||
color: #fecaca;
|
||||
}
|
||||
.user-actions-icon {
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.user-actions-label { flex: 1; }
|
||||
.user-actions-shortcut {
|
||||
margin-left: auto;
|
||||
font-size: 0.7rem;
|
||||
color: var(--cv-text-dim, #6b7488);
|
||||
font-family: ui-monospace, "SFMono-Regular", monospace;
|
||||
}
|
||||
.user-actions-divider {
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
margin: 4px 6px;
|
||||
}
|
||||
|
||||
/* The notify-badge inside the dropdown should stack right next to the label */
|
||||
.user-actions-item #notify-badge { margin-left: auto; }
|
||||
|
||||
/* ============================================================
|
||||
TOPBAR DROPDOWN MENUS
|
||||
============================================================ */
|
||||
|
||||
@@ -58,10 +58,11 @@ function initDropdowns(navLinks) {
|
||||
|
||||
function closeAll() {
|
||||
if (activeDropdown) {
|
||||
const btn = activeDropdown.querySelector(".nav-dd-group-btn");
|
||||
const btn = activeDropdown._activeBtn || activeDropdown.querySelector(".nav-dd-group-btn, .user-actions-trigger");
|
||||
const menu = activeDropdown.querySelector(".nav-dd-menu");
|
||||
if (btn) btn.setAttribute("aria-expanded", "false");
|
||||
if (menu) menu.classList.remove("nav-dd-open");
|
||||
activeDropdown._activeBtn = null;
|
||||
activeDropdown = null;
|
||||
}
|
||||
}
|
||||
@@ -80,7 +81,7 @@ function initDropdowns(navLinks) {
|
||||
return;
|
||||
}
|
||||
const menu = wrapper.querySelector(".nav-dd-menu");
|
||||
const btn = wrapper.querySelector(".nav-dd-group-btn");
|
||||
const btn = wrapper.querySelector(".nav-dd-group-btn, .user-actions-trigger");
|
||||
if (!menu || !btn) return;
|
||||
|
||||
e.preventDefault();
|
||||
@@ -93,6 +94,7 @@ function initDropdowns(navLinks) {
|
||||
btn.setAttribute("aria-expanded", "true");
|
||||
menu.classList.add("nav-dd-open");
|
||||
activeDropdown = wrapper;
|
||||
activeDropdown._activeBtn = btn;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -109,6 +111,28 @@ function initDropdowns(navLinks) {
|
||||
});
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────
|
||||
NOTIFY BADGE MIRROR (utility dropdown)
|
||||
────────────────────────────────────────────── */
|
||||
|
||||
function initNotifyBadgeMirror(scope) {
|
||||
if (!scope) return;
|
||||
const badge = scope.querySelector("#notify-badge");
|
||||
const dot = scope.querySelector("#user-actions-dot");
|
||||
if (!badge || !dot) return;
|
||||
|
||||
const sync = () => {
|
||||
const visible = !badge.classList.contains("hidden");
|
||||
dot.classList.toggle("hidden", !visible);
|
||||
};
|
||||
|
||||
sync();
|
||||
new MutationObserver(sync).observe(badge, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────
|
||||
SIDEBAR TREE EXPAND / COLLAPSE
|
||||
────────────────────────────────────────────── */
|
||||
@@ -141,107 +165,18 @@ function initSidebarTree(navLinks) {
|
||||
────────────────────────────────────────────── */
|
||||
|
||||
function createTopbarController(host, refs) {
|
||||
const { toggle, panel, navLinks, navUtility } = refs;
|
||||
if (!toggle || !panel || !navLinks || !navUtility) {
|
||||
const { panel, navLinks, navUtility } = refs;
|
||||
if (!panel || !navLinks || !navUtility) {
|
||||
return { applyAdaptiveNavLayout: () => {} };
|
||||
}
|
||||
|
||||
// Init dropdowns
|
||||
// Dropdowns + notify-badge mirror; layout itself is pure CSS now.
|
||||
initDropdowns(navLinks);
|
||||
initDropdowns(navUtility);
|
||||
initNotifyBadgeMirror(navUtility);
|
||||
|
||||
const navOverflows = () =>
|
||||
panel.scrollWidth > panel.clientWidth + 1 || host.scrollWidth > host.clientWidth + 1;
|
||||
|
||||
function resetInlineStyles() {
|
||||
panel.style.display = "";
|
||||
panel.style.flexDirection = "";
|
||||
panel.style.alignItems = "";
|
||||
panel.style.gap = "";
|
||||
navLinks.style.flexDirection = "";
|
||||
navLinks.style.flexWrap = "";
|
||||
navLinks.style.alignItems = "";
|
||||
navLinks.style.justifyContent = "";
|
||||
navUtility.style.display = "";
|
||||
navUtility.style.width = "";
|
||||
navUtility.style.justifyContent = "";
|
||||
navUtility.style.flexWrap = "";
|
||||
}
|
||||
|
||||
function applyAdaptiveNavLayout() {
|
||||
if (window.innerWidth < MOBILE_BP) {
|
||||
host.dataset.navMode = "mobile";
|
||||
panel.classList.add("hidden");
|
||||
resetInlineStyles();
|
||||
panel.dataset.open = "false";
|
||||
toggle.style.display = "";
|
||||
toggle.setAttribute("aria-expanded", "false");
|
||||
return;
|
||||
}
|
||||
|
||||
host.dataset.navMode = "desktop";
|
||||
panel.classList.remove("hidden");
|
||||
panel.style.display = "flex";
|
||||
panel.dataset.open = "true";
|
||||
navUtility.style.display = "flex";
|
||||
toggle.style.display = "none";
|
||||
toggle.setAttribute("aria-expanded", "false");
|
||||
|
||||
if (navOverflows()) {
|
||||
host.dataset.navMode = "desktop-compact";
|
||||
navUtility.style.display = "none";
|
||||
if (navOverflows()) {
|
||||
host.dataset.navMode = "compact";
|
||||
panel.style.display = "none";
|
||||
panel.dataset.open = "false";
|
||||
toggle.style.display = "inline-flex";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openCompactPanel() {
|
||||
panel.dataset.open = "true";
|
||||
panel.classList.remove("hidden");
|
||||
panel.style.display = "flex";
|
||||
panel.style.flexDirection = "column";
|
||||
panel.style.alignItems = "stretch";
|
||||
panel.style.gap = "10px";
|
||||
navLinks.style.flexDirection = "row";
|
||||
navLinks.style.flexWrap = "wrap";
|
||||
navLinks.style.alignItems = "center";
|
||||
navLinks.style.justifyContent = "flex-start";
|
||||
navUtility.style.display = "flex";
|
||||
navUtility.style.width = "100%";
|
||||
navUtility.style.justifyContent = "space-between";
|
||||
navUtility.style.flexWrap = "wrap";
|
||||
toggle.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
|
||||
function closeCompactPanel() {
|
||||
panel.dataset.open = "false";
|
||||
panel.style.display = "none";
|
||||
if (host.dataset.navMode === "mobile") panel.classList.add("hidden");
|
||||
toggle.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
toggle.addEventListener("click", () => {
|
||||
const compact =
|
||||
host.dataset.navMode === "compact" || host.dataset.navMode === "mobile";
|
||||
if (!compact) return;
|
||||
if (panel.dataset.open === "true") {
|
||||
closeCompactPanel();
|
||||
} else {
|
||||
openCompactPanel();
|
||||
}
|
||||
});
|
||||
|
||||
let resizeRaf = null;
|
||||
window.addEventListener("resize", () => {
|
||||
if (resizeRaf) cancelAnimationFrame(resizeRaf);
|
||||
resizeRaf = requestAnimationFrame(applyAdaptiveNavLayout);
|
||||
});
|
||||
|
||||
applyAdaptiveNavLayout();
|
||||
return { applyAdaptiveNavLayout };
|
||||
host.dataset.navMode = "desktop";
|
||||
return { applyAdaptiveNavLayout: () => {} };
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────
|
||||
|
||||
@@ -129,60 +129,61 @@ function mountTopbar(host) {
|
||||
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">
|
||||
<div class="topbar-grid">
|
||||
<div class="topbar-left">
|
||||
<a href="/" class="flex items-center gap-2 font-semibold tracking-wide">
|
||||
<i class="fa-solid fa-crown header-crown"></i> Overlord
|
||||
</a>
|
||||
<button
|
||||
id="nav-toggle"
|
||||
class="md:hidden inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-800/70 border border-slate-700"
|
||||
aria-expanded="false"
|
||||
aria-controls="nav-panel"
|
||||
>
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
<span>Menu</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="nav-panel"
|
||||
class="hidden md:flex md:flex-1 md:items-center md:justify-center gap-3"
|
||||
class="topbar-center"
|
||||
>
|
||||
<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 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">
|
||||
<i class="fa-solid fa-bell"></i>
|
||||
<span id="notify-badge" class="hidden min-w-[20px] h-5 px-1 rounded-full bg-rose-500 text-white text-xs flex items-center justify-center"></span>
|
||||
</button>
|
||||
<button id="account-settings-btn"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-full bg-slate-800 text-slate-100 min-w-0 max-w-full md:max-w-none border border-slate-700/70 hover:bg-slate-700 transition-colors"
|
||||
title="Open settings" aria-label="Open settings" type="button">
|
||||
<i class="fa-solid fa-user-shield text-sky-300"></i>
|
||||
<span id="username-display" class="truncate max-w-[110px] sm:max-w-[180px] md:max-w-none">Loading...</span>
|
||||
<span id="role-badge" class="text-sm px-2 py-0.5 rounded-full bg-slate-700 shrink-0"></span>
|
||||
</button>
|
||||
<button id="logout-btn"
|
||||
class="group inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-red-900/40 hover:bg-red-800/60 text-red-100 border border-red-700/60 transition-colors"
|
||||
title="Logout" aria-label="Logout">
|
||||
<i class="fa-solid fa-right-from-bracket text-rose-300 group-hover:text-rose-200 transition-colors"></i>
|
||||
</button>
|
||||
<button id="nav-hide-btn"
|
||||
class="inline-flex items-center justify-center w-9 h-9 rounded-lg bg-slate-900/50 border border-slate-800 text-slate-500 hover:text-slate-200 hover:bg-slate-800 transition-colors"
|
||||
title="Hide navigation (Ctrl+\\)" aria-label="Hide navigation">
|
||||
<i class="fa-solid fa-eye-slash" style="font-size:0.75rem"></i>
|
||||
</div>
|
||||
<div id="nav-utility" class="topbar-right">
|
||||
<div class="nav-dd-wrapper" data-group="user-actions">
|
||||
<button id="user-actions-btn" class="user-actions-trigger" aria-haspopup="true" aria-expanded="false" title="Account actions" aria-label="Account actions">
|
||||
<i class="fa-solid fa-bars-staggered"></i>
|
||||
<span id="user-actions-dot" class="user-actions-dot hidden"></span>
|
||||
</button>
|
||||
<div class="nav-dd-menu user-actions-menu">
|
||||
<button id="notify-toggle" class="user-actions-item"
|
||||
title="Toggle notifications" aria-label="Toggle notifications">
|
||||
<i class="fa-solid fa-bell user-actions-icon"></i>
|
||||
<span class="user-actions-label">Notifications</span>
|
||||
<span id="notify-badge" class="hidden min-w-[20px] h-5 px-1 rounded-full bg-rose-500 text-white text-xs flex items-center justify-center"></span>
|
||||
</button>
|
||||
<button id="nav-hide-btn" class="user-actions-item"
|
||||
title="Hide navigation (Ctrl+\\)" aria-label="Hide navigation">
|
||||
<i class="fa-solid fa-eye-slash user-actions-icon"></i>
|
||||
<span class="user-actions-label">Hide nav</span>
|
||||
<span class="user-actions-shortcut">Ctrl+\\</span>
|
||||
</button>
|
||||
<div class="user-actions-divider"></div>
|
||||
<button id="logout-btn" class="user-actions-item user-actions-item--danger"
|
||||
title="Logout" aria-label="Logout">
|
||||
<i class="fa-solid fa-right-from-bracket user-actions-icon"></i>
|
||||
<span class="user-actions-label">Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button id="account-settings-btn"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-full bg-slate-800 text-slate-100 min-w-0 max-w-full md:max-w-none border border-slate-700/70 hover:bg-slate-700 transition-colors"
|
||||
title="Open settings" aria-label="Open settings" type="button">
|
||||
<i class="fa-solid fa-user-shield text-sky-300"></i>
|
||||
<span id="username-display" class="truncate max-w-[110px] sm:max-w-[180px] md:max-w-none">Loading...</span>
|
||||
<span id="role-badge" class="text-sm px-2 py-0.5 rounded-full bg-slate-700 shrink-0"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Collect all link refs
|
||||
const refs = {
|
||||
toggle: document.getElementById("nav-toggle"),
|
||||
toggle: null,
|
||||
panel: document.getElementById("nav-panel"),
|
||||
collapseBtn: null,
|
||||
navLinks: document.getElementById("nav-links"),
|
||||
|
||||
Reference in New Issue
Block a user