From 1a907d18b0f23aa91b1b64ec8149e1c813728ef1 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Sat, 20 Jun 2026 21:17:07 +0000 Subject: [PATCH] feat: collapsible sidebar nav with localStorage state (#48) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ --- panel/assets/css/nova.css | 11 +++++++++++ panel/public/assets/css/nova.css | 11 +++++++++++ panel/public/assets/js/nova.js | 34 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/panel/assets/css/nova.css b/panel/assets/css/nova.css index 3bceb03..4063eed 100644 --- a/panel/assets/css/nova.css +++ b/panel/assets/css/nova.css @@ -139,10 +139,21 @@ input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1) opacit .sidebar-section { padding: .75rem 0; } .sidebar-section-label { + display: flex; align-items: center; justify-content: space-between; font-size: .7rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted); padding: .25rem 1.25rem .5rem; + cursor: pointer; user-select: none; + transition: color .15s; } +.sidebar-section-label:hover { color: var(--text); } +.sidebar-section-label .nav-chevron { + font-style: normal; font-size: .65rem; opacity: .5; + transition: transform .2s ease; + flex-shrink: 0; +} +.sidebar-section.collapsed .nav-chevron { transform: rotate(-90deg); } +.sidebar-section.collapsed .sidebar-link { display: none; } .sidebar-link { display: flex; align-items: center; gap: .75rem; padding: .55rem 1.25rem; text-decoration: none; diff --git a/panel/public/assets/css/nova.css b/panel/public/assets/css/nova.css index 3bceb03..4063eed 100644 --- a/panel/public/assets/css/nova.css +++ b/panel/public/assets/css/nova.css @@ -139,10 +139,21 @@ input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1) opacit .sidebar-section { padding: .75rem 0; } .sidebar-section-label { + display: flex; align-items: center; justify-content: space-between; font-size: .7rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted); padding: .25rem 1.25rem .5rem; + cursor: pointer; user-select: none; + transition: color .15s; } +.sidebar-section-label:hover { color: var(--text); } +.sidebar-section-label .nav-chevron { + font-style: normal; font-size: .65rem; opacity: .5; + transition: transform .2s ease; + flex-shrink: 0; +} +.sidebar-section.collapsed .nav-chevron { transform: rotate(-90deg); } +.sidebar-section.collapsed .sidebar-link { display: none; } .sidebar-link { display: flex; align-items: center; gap: .75rem; padding: .55rem 1.25rem; text-decoration: none; diff --git a/panel/public/assets/js/nova.js b/panel/public/assets/js/nova.js index 99ff29d..60cfa69 100644 --- a/panel/public/assets/js/nova.js +++ b/panel/public/assets/js/nova.js @@ -221,6 +221,40 @@ window.Nova = (() => { return { api, toast, modal, confirm, initNav, loadPage, progressBar, bytes, relTime, badge, serviceDot, escHtml, loading, loadingDone }; })(); +// #48 Collapsible sidebar nav — shared across all panels +document.addEventListener('DOMContentLoaded', () => { + const STORE = 'ncpx_nav_collapsed'; + const state = JSON.parse(localStorage.getItem(STORE) || '{}'); + + document.querySelectorAll('.sidebar-section').forEach(section => { + const label = section.querySelector('.sidebar-section-label'); + if (!label) return; + + // Add chevron icon + const chevron = document.createElement('i'); + chevron.className = 'nav-chevron'; + chevron.textContent = '▼'; + label.appendChild(chevron); + + const key = label.textContent.replace('▼','').trim(); + + // Restore saved state — default Overview open, others open + if (state[key]) section.classList.add('collapsed'); + + // Keep active page's section always open + const hasActive = section.querySelector('.sidebar-link.active'); + if (hasActive) section.classList.remove('collapsed'); + + label.addEventListener('click', () => { + // Don't collapse if it contains the active link + if (section.querySelector('.sidebar-link.active')) return; + section.classList.toggle('collapsed'); + state[key] = section.classList.contains('collapsed'); + localStorage.setItem(STORE, JSON.stringify(state)); + }); + }); +}); + // #26 Mobile sidebar toggle — shared across all panels document.addEventListener('DOMContentLoaded', () => { const toggle = document.getElementById('sidebar-toggle');