diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index f83dc50..d01b43b 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -1992,3 +1992,356 @@ tbody tr:hover { transform: none !important; } } + +/* ============================================================ + SIDEBAR LAYOUT + ============================================================ */ + +:root { + --sb-w: 224px; + --sb-w-collapsed: 64px; + --sb-speed: 240ms; + --sb-ease: cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Offset body content so it sits beside the sidebar */ +body.sb-ready { + padding-left: var(--sb-w); + transition: padding-left var(--sb-speed) var(--sb-ease); +} +body.sb-ready.sb-collapsed { + padding-left: var(--sb-w-collapsed); +} + +/* On mobile the sidebar becomes a drawer, not a persistent rail */ +@media (max-width: 767px) { + body.sb-ready, + body.sb-ready.sb-collapsed { + padding-left: 0; + padding-top: 52px; + } +} + +/* ---- The sidebar element (#top-nav) ---- */ +body.sb-ready #top-nav { + position: fixed; + top: 0; + left: 0; + height: 100dvh; + width: var(--sb-w); + background: rgba(2, 8, 22, 0.97); + border-right: 1px solid rgba(255, 255, 255, 0.07); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + display: flex; + flex-direction: column; + z-index: 50; + overflow: hidden; + padding: 0; + transition: width var(--sb-speed) var(--sb-ease); +} +body.sb-collapsed #top-nav { + width: var(--sb-w-collapsed); +} +@media (max-width: 767px) { + body.sb-ready #top-nav { + width: var(--sb-w) !important; + transform: translateX(calc(-100% - 2px)); + box-shadow: 4px 0 32px rgba(0, 0, 0, 0.55); + z-index: 51; + transition: transform var(--sb-speed) var(--sb-ease); + } + body.sb-open #top-nav { + transform: translateX(0); + } +} + +/* ---- Sidebar header (logo row) ---- */ +.sb-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 16px 12px 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + flex-shrink: 0; + min-width: 0; +} +body.sb-collapsed .sb-header { + justify-content: center; + padding: 16px 0 12px; +} +.sb-logo { + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; + font-size: 0.95rem; + color: #f1f5f9; + text-decoration: none; + white-space: nowrap; + overflow: hidden; + flex: 1; + min-width: 0; + opacity: 1; + transition: opacity var(--sb-speed) var(--sb-ease); +} +body.sb-collapsed .sb-logo { + opacity: 0; + pointer-events: none; + width: 0; + flex: 0; + overflow: hidden; +} +.sb-logo .sb-text { + overflow: hidden; + opacity: 1; + max-width: 160px; + transition: opacity var(--sb-speed) var(--sb-ease), max-width var(--sb-speed) var(--sb-ease); +} +body.sb-collapsed .sb-logo .sb-text { + max-width: 0; + opacity: 0; + pointer-events: none; +} + +/* ---- Collapse toggle button (desktop only) ---- */ +.sb-collapse-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex-shrink: 0; + border-radius: 7px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.08); + color: #64748b; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} +.sb-collapse-btn:hover { + background: rgba(255, 255, 255, 0.1); + color: #cbd5e1; +} +.sb-collapse-btn i { + display: block; + font-size: 0.7rem; + transition: transform var(--sb-speed) var(--sb-ease); +} +body.sb-collapsed .sb-collapse-btn i { + transform: rotate(180deg); +} +@media (max-width: 767px) { + .sb-collapse-btn { display: none; } +} + +/* ---- Nav section ---- */ +.sb-nav { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 8px; + display: flex; + flex-direction: column; + gap: 3px; + scrollbar-width: thin; + scrollbar-color: rgba(100, 116, 139, 0.25) transparent; +} +.sb-nav::-webkit-scrollbar { width: 4px; } +.sb-nav::-webkit-scrollbar-track { background: transparent; } +.sb-nav::-webkit-scrollbar-thumb { + background: rgba(100, 116, 139, 0.3); + border-radius: 4px; +} + +/* ---- Shared sidebar link style ( and - - - + + `; @@ -133,6 +86,7 @@ export function mountNav(host) { return { toggle: document.getElementById("nav-toggle"), panel: document.getElementById("nav-panel"), + collapseBtn: null, navLinks: document.getElementById("nav-links"), navUtility: document.getElementById("nav-utility"), logoutBtn: document.getElementById("logout-btn"), @@ -152,3 +106,143 @@ export function mountNav(host) { enrollmentBadge: document.getElementById("enrollment-badge"), }; } + +function mountSidebar(host) { + // Sidebar HTML injected into the fixed #top-nav element + host.innerHTML = ` +
+ + +
+ + + + + `; + + // Mobile topbar + const mobileBar = document.createElement("div"); + mobileBar.id = "sb-mobile-bar"; + mobileBar.innerHTML = ` + + + + Overlord + + `; + host.insertAdjacentElement("afterend", mobileBar); + + // Mobile backdrop + const backdrop = document.createElement("div"); + backdrop.id = "sb-backdrop"; + document.body.appendChild(backdrop); + + return { + toggle: document.getElementById("nav-toggle"), + collapseBtn: document.getElementById("sb-collapse-btn"), + panel: host, + navLinks: document.getElementById("nav-links"), + navUtility: document.getElementById("nav-utility"), + logoutBtn: document.getElementById("logout-btn"), + notifyToggle: document.getElementById("notify-toggle"), + notifyBadge: document.getElementById("notify-badge"), + 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"), + }; +} + +export function mountNav(host) { + const mode = localStorage.getItem(NAV_MODE_KEY); + if (mode === "sidebar") { + return mountSidebar(host); + } + return mountTopbar(host); +} + diff --git a/Overlord-Server/public/assets/settings.js b/Overlord-Server/public/assets/settings.js index f1490f9..a1b9cf4 100644 --- a/Overlord-Server/public/assets/settings.js +++ b/Overlord-Server/public/assets/settings.js @@ -7,6 +7,7 @@ import { } from "./notify-client.js"; const PREF_REFRESH_KEY = "overlord_refresh_interval_seconds"; +const NAV_MODE_KEY = "sb_mode"; const usernameEl = document.getElementById("settings-username"); const roleEl = document.getElementById("settings-role"); @@ -276,6 +277,17 @@ async function loadTlsSettings() { setTlsFormDisabled(false); } +function updateNavLayoutButtons(mode, sidebarBtn, topbarBtn) { + const active = ["bg-indigo-600/80", "border-indigo-500", "text-white"]; + const inactive = ["bg-slate-800", "border-slate-700", "text-slate-400", "hover:bg-slate-700", "hover:text-slate-200"]; + const base = ["nav-layout-btn", "flex-1", "flex", "items-center", "justify-center", "gap-2", "px-3", "py-2", "rounded-lg", "border", "text-sm", "font-medium", "transition-colors"]; + + sidebarBtn.className = [...base, ...(mode === "sidebar" ? active : inactive)].join(" "); + topbarBtn.className = [...base, ...(mode === "topbar" ? active : inactive)].join(" "); + sidebarBtn.dataset.selected = mode === "sidebar" ? "true" : "false"; + topbarBtn.dataset.selected = mode === "topbar" ? "true" : "false"; +} + function loadPrefs() { prefNotificationsInput.checked = getNotificationsEnabled(); if (prefDesktopNotificationsInput) { @@ -283,6 +295,13 @@ function loadPrefs() { } const refreshSeconds = Number(localStorage.getItem(PREF_REFRESH_KEY) || 8); prefRefreshSecondsInput.value = String(Math.min(120, Math.max(3, refreshSeconds))); + + const navMode = localStorage.getItem(NAV_MODE_KEY) || "topbar"; + const sidebarBtn = document.getElementById("pref-nav-sidebar"); + const topbarBtn = document.getElementById("pref-nav-topbar"); + if (sidebarBtn && topbarBtn) { + updateNavLayoutButtons(navMode === "topbar" ? "topbar" : "sidebar", sidebarBtn, topbarBtn); + } } async function loadCurrentUser() { @@ -369,6 +388,7 @@ function savePrefs(event) { if (prefDesktopNotificationsHint) prefDesktopNotificationsHint.classList.add("hidden"); showMessage("Preferences saved."); } + } async function saveSecurityPolicy(event) { @@ -827,6 +847,22 @@ async function init() { passwordForm.addEventListener("submit", updatePassword); prefsForm.addEventListener("submit", savePrefs); + + const sidebarBtn = document.getElementById("pref-nav-sidebar"); + const topbarBtn = document.getElementById("pref-nav-topbar"); + if (sidebarBtn && topbarBtn) { + sidebarBtn.addEventListener("click", () => { + if (localStorage.getItem(NAV_MODE_KEY) === "sidebar") return; + localStorage.setItem(NAV_MODE_KEY, "sidebar"); + window.location.reload(); + }); + topbarBtn.addEventListener("click", () => { + if (localStorage.getItem(NAV_MODE_KEY) === "topbar") return; + localStorage.setItem(NAV_MODE_KEY, "topbar"); + window.location.reload(); + }); + } + securityForm.addEventListener("submit", saveSecurityPolicy); tlsForm.addEventListener("submit", saveTlsSettings); tlsCertbotAutoBtn.addEventListener("click", runCertbotAutoSetup); diff --git a/Overlord-Server/public/settings.html b/Overlord-Server/public/settings.html index a078c68..6c5c495 100644 --- a/Overlord-Server/public/settings.html +++ b/Overlord-Server/public/settings.html @@ -209,6 +209,20 @@ class="mt-1 w-full px-3 py-2 rounded-lg bg-slate-900 border border-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500" /> +
+ Navigation layout +
+ + +
+

Changes apply immediately on save.

+