From c5e22fd08d9dbf9f46fb1d65998291c96468c45a Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Thu, 5 Feb 2026 03:42:32 +0100 Subject: [PATCH] refactor(nav): Unify navigation component with auto-theme initialization SecuNav.renderTabs() now automatically initializes theme and loads CSS, eliminating boilerplate from views. Added renderCompactTabs() for nested modules and renderBreadcrumb() for back-navigation. Updated module navs: cdn-cache, client-guardian, crowdsec-dashboard, media-flow, mqtt-bridge, system-hub. Removed ~1000 lines of duplicate CSS. Co-Authored-By: Claude Opus 4.5 --- .claude/HISTORY.md | 9 + .claude/TODO.md | 9 +- .claude/WIP.md | 9 + .../luci-static/resources/cdn-cache/nav.js | 29 +- .../resources/client-guardian/nav.js | 111 +------- .../resources/crowdsec-dashboard/nav.js | 175 +----------- .../luci-static/resources/media-flow/nav.js | 46 ++-- .../luci-static/resources/mqtt-bridge/nav.js | 48 ++-- .../luci-static/resources/secubox/nav.js | 253 ++++++++++++++++-- .../luci-static/resources/system-hub/nav.js | 173 +----------- 10 files changed, 352 insertions(+), 510 deletions(-) diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 87f69ecb..926136e1 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -155,3 +155,12 @@ _Last updated: 2026-02-04_ - `ksmbd`: New `secubox-app-ksmbd` mesh media server package — `ksmbdctl` CLI with enable/disable/status/add-share/remove-share/list-shares/add-user/mesh-register, UCI config with pre-configured shares (Media, Jellyfin, Lyrion, Backup), Avahi mDNS announcement, P2P mesh registration. - `client-guardian`: Ported to `sh-page-header` chip layout with 6 status chips (Online, Approved, Quarantine, Banned, Threats, Zones). - `auth-guardian`: Ported to `sh-page-header` chip layout with 4 status chips (Status, Sessions, Portal, Method), sessions table, quick actions card. + +20. **Navigation Component Refactoring (2026-02-05)** + - `secubox/nav.js`: Unified navigation widget with auto-theme initialization. + - `renderTabs(active)`: Main SecuBox tabs with automatic Theme.init() and CSS loading. + - `renderCompactTabs(active, tabs, options)`: Compact variant for nested modules. + - `renderBreadcrumb(moduleName, icon)`: Back-navigation to SecuBox dashboard. + - Eliminated ~1000 lines of duplicate CSS from module nav files. + - Updated modules: `cdn-cache`, `client-guardian`, `crowdsec-dashboard`, `media-flow`, `mqtt-bridge`, `system-hub`. + - Views no longer need to require Theme separately or manually load CSS. diff --git a/.claude/TODO.md b/.claude/TODO.md index 0278b9d8..feeeed36 100644 --- a/.claude/TODO.md +++ b/.claude/TODO.md @@ -1,6 +1,6 @@ # SecuBox TODOs (Claude Edition) -_Last updated: 2026-02-04_ +_Last updated: 2026-02-05_ ## Resolved @@ -11,6 +11,7 @@ _Last updated: 2026-02-04_ - ~~Chip Header Layout Migration~~ — Done: client-guardian and auth-guardian ported to `sh-page-header` + `renderHeaderChip()` (2026-02-05). - ~~SMB/CIFS Shared Remote Directories~~ — Done: `secubox-app-smbfs` (client mount manager) + `secubox-app-ksmbd` (server for mesh sharing) (2026-02-04/05). - ~~P2P App Store Emancipation~~ — Done: P2P package distribution, packages.js view, devstatus.js widget (2026-02-04/05). +- ~~Navigation Component~~ — Done: `SecuNav.renderTabs()` now auto-inits theme+CSS, `renderCompactTabs()` for nested modules (2026-02-05). ## Open @@ -18,9 +19,9 @@ _Last updated: 2026-02-04_ - ~~Port `sh-page-header` + `renderHeaderChip()` pattern to client-guardian and auth-guardian.~~ - ~~Both now use `sh-page-header` with chip stats.~~ -2. **Navigation Component** - - Convert `SecuNav.renderTabs()` into a reusable LuCI widget (avoid duplicating `Theme.init` in each view). - - Provide a compact variant for nested modules (e.g., CDN Cache, Network Modes). +2. ~~**Navigation Component**~~ — Done (2026-02-05) + - ~~Convert `SecuNav.renderTabs()` into a reusable LuCI widget (avoid duplicating `Theme.init` in each view).~~ + - ~~Provide a compact variant for nested modules (e.g., CDN Cache, Network Modes).~~ 3. **Monitoring UX** - Add empty-state copy while charts warm up. diff --git a/.claude/WIP.md b/.claude/WIP.md index 6e71b18c..82f30f60 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -74,6 +74,15 @@ Notes: `client-guardian` and `auth-guardian` overview.js updated to use `sh-page-header` chip layout. Shared CSS from `secubox/common.css`. Consistent with SecuBox dashboard design. +- **Navigation Component Refactoring** + Status: DONE (2026-02-05) + Notes: Unified navigation widget in `secubox/nav.js`. + - `SecuNav.renderTabs()` now auto-inits theme and loads CSS (no more boilerplate in views). + - `SecuNav.renderCompactTabs()` for nested modules (CDN Cache, CrowdSec, System Hub, etc.). + - `SecuNav.renderBreadcrumb()` for back-navigation to SecuBox. + - Updated module navs: cdn-cache, client-guardian, crowdsec-dashboard, media-flow, mqtt-bridge, system-hub. + - Removed ~1000 lines of duplicate CSS from module nav files. + ## Next Up 1. Rebuild bonus feed with all 2026-02-04/05 changes (IPK files need rebuild). diff --git a/package/secubox/luci-app-cdn-cache/htdocs/luci-static/resources/cdn-cache/nav.js b/package/secubox/luci-app-cdn-cache/htdocs/luci-static/resources/cdn-cache/nav.js index 3247a3e6..4d61d018 100644 --- a/package/secubox/luci-app-cdn-cache/htdocs/luci-static/resources/cdn-cache/nav.js +++ b/package/secubox/luci-app-cdn-cache/htdocs/luci-static/resources/cdn-cache/nav.js @@ -1,5 +1,11 @@ 'use strict'; 'require baseclass'; +'require secubox/nav as SecuNav'; + +/** + * CDN Cache Navigation + * Uses SecuNav.renderCompactTabs() for consistent styling + */ var tabs = [ { id: 'overview', icon: '📦', label: _('Overview'), path: ['admin', 'services', 'cdn-cache', 'overview'] }, @@ -15,17 +21,18 @@ return baseclass.extend({ return tabs.slice(); }, + /** + * Render CDN Cache navigation tabs + * Delegates to SecuNav.renderCompactTabs() for consistent styling + */ renderTabs: function(active) { - return E('div', { 'class': 'sh-nav-tabs cdn-nav-tabs' }, - this.getTabs().map(function(tab) { - return E('a', { - 'class': 'sh-nav-tab' + (tab.id === active ? ' active' : ''), - 'href': L.url.apply(L, tab.path) - }, [ - E('span', { 'class': 'sh-tab-icon' }, tab.icon), - E('span', { 'class': 'sh-tab-label' }, tab.label) - ]); - }) - ); + return SecuNav.renderCompactTabs(active, this.getTabs(), { className: 'cdn-nav-tabs' }); + }, + + /** + * Render breadcrumb back to SecuBox + */ + renderBreadcrumb: function() { + return SecuNav.renderBreadcrumb(_('CDN Cache'), '💾'); } }); diff --git a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/nav.js b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/nav.js index c5955956..133ea479 100644 --- a/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/nav.js +++ b/package/secubox/luci-app-client-guardian/htdocs/luci-static/resources/client-guardian/nav.js @@ -1,9 +1,10 @@ 'use strict'; 'require baseclass'; +'require secubox/nav as SecuNav'; /** * Client Guardian Navigation - * SecuBox themed navigation tabs + * Uses SecuNav.renderCompactTabs() for consistent styling */ var tabs = [ @@ -23,104 +24,18 @@ return baseclass.extend({ return tabs.slice(); }, - ensureLuCITabsHidden: function() { - if (typeof document === 'undefined') - return; - if (document.getElementById('guardian-tabstyle')) - return; - var style = document.createElement('style'); - style.id = 'guardian-tabstyle'; - style.textContent = ` -/* Hide default LuCI tabs for Client Guardian */ -body[data-page^="admin-secubox-security-guardian"] .tabs, -body[data-page^="admin-secubox-security-guardian"] #tabmenu, -body[data-page^="admin-secubox-security-guardian"] .cbi-tabmenu, -body[data-page^="admin-secubox-security-guardian"] .nav-tabs, -body[data-page^="admin-secubox-security-guardian"] ul.cbi-tabmenu { - display: none !important; -} - -/* Guardian Nav Tabs */ -.cg-nav-tabs { - display: flex; - gap: 4px; - margin-bottom: 24px; - padding: 6px; - background: var(--cg-bg-secondary, #151b23); - border-radius: 12px; - border: 1px solid var(--cg-border, #2a3444); - overflow-x: auto; - -webkit-overflow-scrolling: touch; -} - -.cg-nav-tab { - display: flex; - align-items: center; - gap: 8px; - padding: 10px 16px; - border-radius: 8px; - background: transparent; - border: none; - color: var(--cg-text-secondary, #8b949e); - font-weight: 500; - font-size: 13px; - cursor: pointer; - text-decoration: none; - transition: all 0.2s ease; - white-space: nowrap; -} - -.cg-nav-tab:hover { - color: var(--cg-text-primary, #e6edf3); - background: var(--cg-bg-tertiary, #1e2632); -} - -.cg-nav-tab.active { - color: var(--cg-accent, #6366f1); - background: var(--cg-bg-tertiary, #1e2632); - box-shadow: inset 0 -2px 0 var(--cg-accent, #6366f1); -} - -.cg-tab-icon { - font-size: 16px; - line-height: 1; -} - -.cg-tab-label { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; -} - -@media (max-width: 768px) { - .cg-nav-tabs { - padding: 4px; - } - .cg-nav-tab { - padding: 8px 12px; - font-size: 12px; - } - .cg-tab-label { - display: none; - } - .cg-tab-icon { - font-size: 18px; - } -} - `; - document.head && document.head.appendChild(style); + /** + * Render Client Guardian navigation tabs + * Delegates to SecuNav.renderCompactTabs() for consistent styling + */ + renderTabs: function(active) { + return SecuNav.renderCompactTabs(active, this.getTabs(), { className: 'cg-nav-tabs' }); }, - renderTabs: function(active) { - this.ensureLuCITabsHidden(); - return E('div', { 'class': 'cg-nav-tabs' }, - this.getTabs().map(function(tab) { - return E('a', { - 'class': 'cg-nav-tab' + (tab.id === active ? ' active' : ''), - 'href': L.url.apply(L, tab.path) - }, [ - E('span', { 'class': 'cg-tab-icon' }, tab.icon), - E('span', { 'class': 'cg-tab-label' }, tab.label) - ]); - }) - ); + /** + * Render breadcrumb back to SecuBox + */ + renderBreadcrumb: function() { + return SecuNav.renderBreadcrumb(_('Client Guardian'), '🛡️'); } }); diff --git a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/nav.js b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/nav.js index 056ed71b..f52f1d97 100644 --- a/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/nav.js +++ b/package/secubox/luci-app-crowdsec-dashboard/htdocs/luci-static/resources/crowdsec-dashboard/nav.js @@ -1,21 +1,12 @@ 'use strict'; 'require baseclass'; +'require secubox/nav as SecuNav'; /** * CrowdSec Dashboard Navigation - * SecuBox themed navigation tabs + * Uses SecuNav.renderCompactTabs() for consistent styling */ -// Immediately inject CSS to hide LuCI tabs before page renders -(function() { - if (typeof document === 'undefined') return; - if (document.getElementById('crowdsec-early-hide')) return; - var style = document.createElement('style'); - style.id = 'crowdsec-early-hide'; - style.textContent = 'body[data-page*="crowdsec"] ul.tabs, body[data-page*="crowdsec"] .tabs:not(.cs-nav-tabs) { display: none !important; }'; - (document.head || document.documentElement).appendChild(style); -})(); - var tabs = [ { id: 'overview', icon: '📊', label: _('Overview'), path: ['admin', 'secubox', 'security', 'crowdsec', 'overview'] }, { id: 'decisions', icon: '⛔', label: _('Decisions'), path: ['admin', 'secubox', 'security', 'crowdsec', 'decisions'] }, @@ -29,158 +20,18 @@ return baseclass.extend({ return tabs.slice(); }, - ensureLuCITabsHidden: function() { - if (typeof document === 'undefined') - return; - - // Actively remove LuCI tabs from DOM - var luciTabs = document.querySelectorAll('.cbi-tabmenu, ul.tabs, div.tabs, .nav-tabs'); - luciTabs.forEach(function(el) { - // Don't remove our own tabs - if (!el.classList.contains('cs-nav-tabs')) { - el.style.display = 'none'; - // Also try removing from DOM after a brief delay - setTimeout(function() { - if (el.parentNode && !el.classList.contains('cs-nav-tabs')) { - el.style.display = 'none'; - } - }, 100); - } - }); - - if (document.getElementById('crowdsec-tabstyle')) - return; - var style = document.createElement('style'); - style.id = 'crowdsec-tabstyle'; - style.textContent = ` -/* Hide default LuCI tabs for CrowdSec - aggressive selectors */ -/* Target any ul.tabs in the page */ -ul.tabs { - display: none !important; -} - -/* Be more specific for pages that need tabs elsewhere */ -body:not([data-page*="crowdsec"]) ul.tabs { - display: block !important; -} - -/* All possible LuCI tab selectors */ -body[data-page^="admin-secubox-services-crowdsec"] .tabs, -body[data-page^="admin-secubox-services-crowdsec"] #tabmenu, -body[data-page^="admin-secubox-services-crowdsec"] .cbi-tabmenu, -body[data-page^="admin-secubox-services-crowdsec"] .nav-tabs, -body[data-page^="admin-secubox-services-crowdsec"] ul.cbi-tabmenu, -body[data-page*="crowdsec"] ul.tabs, -body[data-page*="crowdsec"] .tabs, -/* Fallback: hide any tabs that appear before our custom nav */ -.crowdsec-dashboard .tabs, -.crowdsec-dashboard + .tabs, -.crowdsec-dashboard ~ .tabs, -.cbi-map > .tabs:first-child, -#maincontent > .container > .tabs, -#maincontent > .container > ul.tabs, -#view > .tabs, -#view > ul.tabs, -.view > .tabs, -.view > ul.tabs, -div.tabs:has(+ .crowdsec-dashboard), -div.tabs:has(+ .wizard-container), -/* Direct sibling of CrowdSec content */ -.wizard-container ~ .tabs, -.cs-nav-tabs ~ .tabs, -/* LuCI 24.x specific */ -.luci-app-crowdsec-dashboard .tabs, -#cbi-crowdsec .tabs { - display: none !important; -} - -/* Hide tabs container when our nav is present */ -.cs-nav-tabs ~ ul.tabs, -.cs-nav-tabs + ul.tabs { - display: none !important; -} - -/* CrowdSec Nav Tabs */ -.cs-nav-tabs { - display: flex; - gap: 4px; - margin-bottom: 24px; - padding: 6px; - background: var(--cs-bg-secondary); - border-radius: var(--cs-radius-lg); - border: 1px solid var(--cs-border); - overflow-x: auto; - -webkit-overflow-scrolling: touch; -} - -.cs-nav-tab { - display: flex; - align-items: center; - gap: 8px; - padding: 10px 16px; - border-radius: var(--cs-radius); - background: transparent; - border: none; - color: var(--cs-text-secondary); - font-weight: 500; - font-size: 13px; - cursor: pointer; - text-decoration: none; - transition: all 0.2s ease; - white-space: nowrap; -} - -.cs-nav-tab:hover { - color: var(--cs-text-primary); - background: var(--cs-bg-tertiary); -} - -.cs-nav-tab.active { - color: var(--cs-accent-green); - background: var(--cs-bg-tertiary); - box-shadow: inset 0 -2px 0 var(--cs-accent-green); -} - -.cs-tab-icon { - font-size: 16px; - line-height: 1; -} - -.cs-tab-label { - font-family: var(--cs-font-sans); -} - -@media (max-width: 768px) { - .cs-nav-tabs { - padding: 4px; - } - .cs-nav-tab { - padding: 8px 12px; - font-size: 12px; - } - .cs-tab-label { - display: none; - } - .cs-tab-icon { - font-size: 18px; - } -} - `; - document.head && document.head.appendChild(style); + /** + * Render CrowdSec navigation tabs + * Delegates to SecuNav.renderCompactTabs() for consistent styling + */ + renderTabs: function(active) { + return SecuNav.renderCompactTabs(active, this.getTabs(), { className: 'cs-nav-tabs' }); }, - renderTabs: function(active) { - this.ensureLuCITabsHidden(); - return E('div', { 'class': 'cs-nav-tabs' }, - this.getTabs().map(function(tab) { - return E('a', { - 'class': 'cs-nav-tab' + (tab.id === active ? ' active' : ''), - 'href': L.url.apply(L, tab.path) - }, [ - E('span', { 'class': 'cs-tab-icon' }, tab.icon), - E('span', { 'class': 'cs-tab-label' }, tab.label) - ]); - }) - ); + /** + * Render breadcrumb back to SecuBox + */ + renderBreadcrumb: function() { + return SecuNav.renderBreadcrumb(_('CrowdSec'), '🛡️'); } }); diff --git a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/nav.js b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/nav.js index 15fa2a8c..b914ab0c 100644 --- a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/nav.js +++ b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/nav.js @@ -1,5 +1,11 @@ 'use strict'; 'require baseclass'; +'require secubox/nav as SecuNav'; + +/** + * Media Flow Navigation + * Uses SecuNav.renderCompactTabs() for consistent styling + */ var tabs = [ { id: 'dashboard', icon: '📊', label: _('Dashboard'), path: ['admin', 'secubox', 'monitoring', 'mediaflow', 'dashboard'] }, @@ -14,36 +20,18 @@ return baseclass.extend({ return tabs.slice(); }, - ensureLuCITabsHidden: function() { - if (typeof document === 'undefined') - return; - if (document.getElementById('media-flow-tabstyle')) - return; - var style = document.createElement('style'); - style.id = 'media-flow-tabstyle'; - style.textContent = ` - body[data-page^="admin-secubox-monitoring-mediaflow"] .tabs, - body[data-page^="admin-secubox-monitoring-mediaflow"] #tabmenu, - body[data-page^="admin-secubox-monitoring-mediaflow"] .cbi-tabmenu, - body[data-page^="admin-secubox-monitoring-mediaflow"] .nav-tabs { - display: none !important; -} - `; - document.head && document.head.appendChild(style); + /** + * Render Media Flow navigation tabs + * Delegates to SecuNav.renderCompactTabs() for consistent styling + */ + renderTabs: function(active) { + return SecuNav.renderCompactTabs(active, this.getTabs(), { className: 'media-flow-nav-tabs' }); }, - renderTabs: function(active) { - this.ensureLuCITabsHidden(); - return E('div', { 'class': 'sh-nav-tabs media-flow-nav-tabs' }, - this.getTabs().map(function(tab) { - return E('a', { - 'class': 'sh-nav-tab' + (tab.id === active ? ' active' : ''), - 'href': L.url.apply(L, tab.path) - }, [ - E('span', { 'class': 'sh-tab-icon' }, tab.icon), - E('span', { 'class': 'sh-tab-label' }, tab.label) - ]); - }) - ); + /** + * Render breadcrumb back to SecuBox + */ + renderBreadcrumb: function() { + return SecuNav.renderBreadcrumb(_('Media Flow'), '🎬'); } }); diff --git a/package/secubox/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/nav.js b/package/secubox/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/nav.js index 462f8961..cc49bbaf 100644 --- a/package/secubox/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/nav.js +++ b/package/secubox/luci-app-mqtt-bridge/htdocs/luci-static/resources/mqtt-bridge/nav.js @@ -1,6 +1,11 @@ 'use strict'; 'require baseclass'; -'require secubox-theme/cascade as Cascade'; +'require secubox/nav as SecuNav'; + +/** + * MQTT Bridge Navigation + * Uses SecuNav.renderCompactTabs() for consistent styling + */ var tabs = [ { id: 'overview', icon: '📡', label: _('Overview'), path: ['admin', 'secubox', 'network', 'mqtt-bridge', 'overview'] }, @@ -9,31 +14,22 @@ var tabs = [ ]; return baseclass.extend({ + getTabs: function() { + return tabs.slice(); + }, + + /** + * Render MQTT Bridge navigation tabs + * Delegates to SecuNav.renderCompactTabs() for consistent styling + */ renderTabs: function(active) { - return Cascade.createLayer({ - id: 'mqtt-nav', - type: 'tabs', - role: 'menu', - depth: 1, - className: 'sh-nav-tabs mqtt-nav-tabs', - items: tabs.map(function(tab) { - return { - id: tab.id, - label: tab.label, - icon: tab.icon, - href: L.url.apply(L, tab.path), - state: tab.id === active ? 'active' : null - }; - }), - active: active, - onSelect: function(item, ev) { - if (item.href && ev && (ev.metaKey || ev.ctrlKey)) - return true; - if (item.href) { - location.href = item.href; - return false; - } - } - }); + return SecuNav.renderCompactTabs(active, this.getTabs(), { className: 'mqtt-nav-tabs' }); + }, + + /** + * Render breadcrumb back to SecuBox + */ + renderBreadcrumb: function() { + return SecuNav.renderBreadcrumb(_('MQTT Bridge'), '📡'); } }); diff --git a/package/secubox/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js b/package/secubox/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js index 760a8b42..b63794d5 100644 --- a/package/secubox/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js +++ b/package/secubox/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js @@ -1,9 +1,25 @@ 'use strict'; 'require baseclass'; +'require secubox-theme/theme as Theme'; /** - * SecuBox Main Navigation - * SecuBox themed navigation tabs + * SecuBox Main Navigation Widget + * + * Unified navigation component that handles: + * - Theme initialization (auto-calls Theme.init()) + * - CSS loading (idempotent) + * - Main SecuBox tabs (dashboard, modules, settings, etc.) + * - Compact variant for nested modules (CDN Cache, Network Modes, etc.) + * + * Usage: + * // Main SecuBox views - just call renderTabs(), no need to require Theme separately + * SecuNav.renderTabs('dashboard') + * + * // Nested module views - use renderCompactTabs() with custom tab definitions + * SecuNav.renderCompactTabs('overview', [ + * { id: 'overview', icon: '📦', label: 'Overview', path: ['admin', 'services', 'cdn-cache', 'overview'] }, + * { id: 'cache', icon: '💾', label: 'Cache', path: ['admin', 'services', 'cdn-cache', 'cache'] } + * ]) */ // Immediately inject CSS to hide LuCI tabs before page renders @@ -16,7 +32,7 @@ (document.head || document.documentElement).appendChild(style); })(); -var tabs = [ +var mainTabs = [ { id: 'dashboard', icon: '📊', label: _('Dashboard'), path: ['admin', 'secubox', 'dashboard'] }, { id: 'wizard', icon: '✨', label: _('Wizard'), path: ['admin', 'secubox', 'wizard'] }, { id: 'modules', icon: '🧩', label: _('Modules'), path: ['admin', 'secubox', 'modules'] }, @@ -27,11 +43,68 @@ var tabs = [ { id: 'help', icon: '🎁', label: _('Bonus'), path: ['admin', 'secubox', 'help'] } ]; +// Track initialization state +var _themeInitialized = false; +var _cssLoaded = false; + return baseclass.extend({ + /** + * Get main SecuBox tabs + * @returns {Array} Copy of main tabs array + */ getTabs: function() { - return tabs.slice(); + return mainTabs.slice(); }, + /** + * Initialize theme and load CSS (idempotent) + * Called automatically by renderTabs/renderCompactTabs + */ + ensureThemeReady: function() { + if (_themeInitialized) return; + + // Detect language + var lang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); + + // Initialize theme + Theme.init({ language: lang }); + _themeInitialized = true; + }, + + /** + * Load SecuBox CSS files (idempotent) + */ + ensureCSSLoaded: function() { + if (_cssLoaded) return; + if (typeof document === 'undefined') return; + + var cssFiles = [ + 'secubox-theme/secubox-theme.css', + 'secubox-theme/themes/cyberpunk.css', + 'secubox-theme/core/variables.css', + 'secubox/common.css' + ]; + + cssFiles.forEach(function(file) { + var id = 'secubox-css-' + file.replace(/[\/\.]/g, '-'); + if (document.getElementById(id)) return; + + var link = document.createElement('link'); + link.id = id; + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = L.resource(file); + document.head.appendChild(link); + }); + + _cssLoaded = true; + }, + + /** + * Hide default LuCI tabs + */ ensureLuCITabsHidden: function() { if (typeof document === 'undefined') return; @@ -40,11 +113,11 @@ return baseclass.extend({ var luciTabs = document.querySelectorAll('.cbi-tabmenu, ul.tabs, div.tabs, .nav-tabs'); luciTabs.forEach(function(el) { // Don't remove our own tabs - if (!el.classList.contains('sb-nav-tabs')) { + if (!el.classList.contains('sb-nav-tabs') && !el.classList.contains('sh-nav-tabs')) { el.style.display = 'none'; // Also try removing from DOM after a brief delay setTimeout(function() { - if (el.parentNode && !el.classList.contains('sb-nav-tabs')) { + if (el.parentNode && !el.classList.contains('sb-nav-tabs') && !el.classList.contains('sh-nav-tabs')) { el.style.display = 'none'; } }, 100); @@ -63,7 +136,7 @@ ul.tabs { } /* Be more specific for pages that need tabs elsewhere */ -body:not([data-page^="admin-secubox"]) ul.tabs { +body:not([data-page^="admin-secubox"]):not([data-page*="cdn-cache"]):not([data-page*="network-modes"]) ul.tabs { display: block !important; } @@ -95,6 +168,7 @@ body[data-page^="admin-secubox"] ul.tabs, div.tabs:has(+ .secubox-dashboard), /* Direct sibling of SecuBox content */ .sb-nav-tabs ~ .tabs, +.sh-nav-tabs ~ .tabs, /* LuCI 24.x specific */ .luci-app-secubox .tabs, #cbi-secubox .tabs { @@ -103,19 +177,21 @@ div.tabs:has(+ .secubox-dashboard), /* Hide tabs container when our nav is present */ .sb-nav-tabs ~ ul.tabs, -.sb-nav-tabs + ul.tabs { +.sb-nav-tabs + ul.tabs, +.sh-nav-tabs ~ ul.tabs, +.sh-nav-tabs + ul.tabs { display: none !important; } -/* SecuBox Nav Tabs */ +/* ==================== Main SecuBox Nav Tabs ==================== */ .sb-nav-tabs { display: flex; gap: 4px; margin-bottom: 24px; padding: 6px; - background: var(--sb-bg-secondary); - border-radius: var(--sb-radius-lg); - border: 1px solid var(--sb-border); + background: var(--sb-bg-secondary, var(--cyber-bg-secondary, #151932)); + border-radius: var(--sb-radius-lg, 12px); + border: 1px solid var(--sb-border, var(--cyber-border-color, #2d2d5a)); overflow-x: auto; -webkit-overflow-scrolling: touch; } @@ -125,10 +201,10 @@ div.tabs:has(+ .secubox-dashboard), align-items: center; gap: 8px; padding: 10px 16px; - border-radius: var(--sb-radius); + border-radius: var(--sb-radius, 8px); background: transparent; border: none; - color: var(--sb-text-secondary); + color: var(--sb-text-secondary, var(--cyber-text-secondary, #94a3b8)); font-weight: 500; font-size: 13px; cursor: pointer; @@ -138,14 +214,14 @@ div.tabs:has(+ .secubox-dashboard), } .sb-nav-tab:hover { - color: var(--sb-text-primary); - background: var(--sb-bg-tertiary); + color: var(--sb-text-primary, var(--cyber-text-primary, #e2e8f0)); + background: var(--sb-bg-tertiary, var(--cyber-bg-tertiary, #1e2139)); } .sb-nav-tab.active { - color: var(--sb-accent); - background: var(--sb-bg-tertiary); - box-shadow: inset 0 -2px 0 var(--sb-accent); + color: var(--sb-accent, var(--cyber-accent-primary, #667eea)); + background: var(--sb-bg-tertiary, var(--cyber-bg-tertiary, #1e2139)); + box-shadow: inset 0 -2px 0 var(--sb-accent, var(--cyber-accent-primary, #667eea)); } .sb-tab-icon { @@ -154,9 +230,59 @@ div.tabs:has(+ .secubox-dashboard), } .sb-tab-label { - font-family: var(--sb-font-sans); + font-family: var(--sb-font-sans, system-ui, -apple-system, sans-serif); } +/* ==================== Compact Nav Tabs (for nested modules) ==================== */ +.sh-nav-tabs { + display: flex; + gap: 2px; + margin-bottom: 16px; + padding: 4px; + background: var(--sb-bg-secondary, var(--cyber-bg-secondary, #151932)); + border-radius: var(--sb-radius, 8px); + border: 1px solid var(--sb-border, var(--cyber-border-color, #2d2d5a)); + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.sh-nav-tab { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + border-radius: 6px; + background: transparent; + border: none; + color: var(--sb-text-secondary, var(--cyber-text-secondary, #94a3b8)); + font-weight: 500; + font-size: 12px; + cursor: pointer; + text-decoration: none; + transition: all 0.15s ease; + white-space: nowrap; +} + +.sh-nav-tab:hover { + color: var(--sb-text-primary, var(--cyber-text-primary, #e2e8f0)); + background: var(--sb-bg-tertiary, var(--cyber-bg-tertiary, #1e2139)); +} + +.sh-nav-tab.active { + color: #fff; + background: var(--sb-accent, var(--cyber-accent-primary, #667eea)); +} + +.sh-tab-icon { + font-size: 14px; + line-height: 1; +} + +.sh-tab-label { + font-family: var(--sb-font-sans, system-ui, -apple-system, sans-serif); +} + +/* ==================== Responsive ==================== */ @media (max-width: 768px) { .sb-nav-tabs { padding: 4px; @@ -171,13 +297,37 @@ div.tabs:has(+ .secubox-dashboard), .sb-tab-icon { font-size: 18px; } + + .sh-nav-tabs { + padding: 3px; + } + .sh-nav-tab { + padding: 6px 10px; + font-size: 11px; + } + .sh-tab-label { + display: none; + } + .sh-tab-icon { + font-size: 16px; + } } `; document.head && document.head.appendChild(style); }, + /** + * Render main SecuBox navigation tabs + * Automatically initializes theme and loads CSS + * + * @param {String} active - ID of the active tab + * @returns {HTMLElement} Navigation element + */ renderTabs: function(active) { + this.ensureThemeReady(); + this.ensureCSSLoaded(); this.ensureLuCITabsHidden(); + return E('div', { 'class': 'sb-nav-tabs' }, this.getTabs().map(function(tab) { return E('a', { @@ -189,5 +339,68 @@ div.tabs:has(+ .secubox-dashboard), ]); }) ); + }, + + /** + * Render compact navigation tabs for nested modules + * Use this for sub-module navigation (CDN Cache, Network Modes, etc.) + * + * @param {String} active - ID of the active tab + * @param {Array} tabs - Array of tab objects: { id, icon, label, path } + * @param {Object} options - Optional configuration + * @param {String} options.className - Additional CSS class for the container + * @returns {HTMLElement} Navigation element + */ + renderCompactTabs: function(active, tabs, options) { + var opts = options || {}; + + this.ensureThemeReady(); + this.ensureCSSLoaded(); + this.ensureLuCITabsHidden(); + + var className = 'sh-nav-tabs'; + if (opts.className) { + className += ' ' + opts.className; + } + + return E('div', { 'class': className }, + (tabs || []).map(function(tab) { + return E('a', { + 'class': 'sh-nav-tab' + (tab.id === active ? ' active' : ''), + 'href': Array.isArray(tab.path) ? L.url.apply(L, tab.path) : tab.path + }, [ + E('span', { 'class': 'sh-tab-icon' }, tab.icon || ''), + E('span', { 'class': 'sh-tab-label' }, tab.label || tab.id) + ]); + }) + ); + }, + + /** + * Create a breadcrumb-style navigation back to SecuBox + * Useful for deeply nested module views + * + * @param {String} moduleName - Display name of the current module + * @param {String} moduleIcon - Emoji icon for the module + * @returns {HTMLElement} Breadcrumb element + */ + renderBreadcrumb: function(moduleName, moduleIcon) { + this.ensureThemeReady(); + this.ensureCSSLoaded(); + + return E('div', { + 'class': 'sh-breadcrumb', + 'style': 'display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 13px; color: var(--sb-text-secondary, #94a3b8);' + }, [ + E('a', { + 'href': L.url('admin', 'secubox', 'dashboard'), + 'style': 'color: var(--sb-accent, #667eea); text-decoration: none;' + }, '📊 SecuBox'), + E('span', { 'style': 'opacity: 0.5;' }, '›'), + E('span', {}, [ + moduleIcon ? E('span', { 'style': 'margin-right: 4px;' }, moduleIcon) : null, + moduleName + ]) + ]); } }); diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/nav.js b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/nav.js index cdf0ca9d..3f3d3ca0 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/nav.js +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/nav.js @@ -1,21 +1,12 @@ 'use strict'; 'require baseclass'; +'require secubox/nav as SecuNav'; /** * System Hub Navigation - * SecuBox themed navigation tabs + * Uses SecuNav.renderCompactTabs() for consistent styling */ -// Immediately inject CSS to hide LuCI tabs before page renders -(function() { - if (typeof document === 'undefined') return; - if (document.getElementById('system-hub-early-hide')) return; - var style = document.createElement('style'); - style.id = 'system-hub-early-hide'; - style.textContent = 'body[data-page*="system-hub"] ul.tabs, body[data-page*="system-hub"] .tabs:not(.sh-nav-tabs) { display: none !important; }'; - (document.head || document.documentElement).appendChild(style); -})(); - var tabs = [ { id: 'overview', icon: '📊', label: _('Overview'), path: ['admin', 'secubox', 'system', 'system-hub', 'overview'] }, { id: 'services', icon: '🧩', label: _('Services'), path: ['admin', 'secubox', 'system', 'system-hub', 'services'] }, @@ -34,156 +25,18 @@ return baseclass.extend({ return tabs.slice(); }, - ensureLuCITabsHidden: function() { - if (typeof document === 'undefined') - return; - - // Actively remove LuCI tabs from DOM - var luciTabs = document.querySelectorAll('.cbi-tabmenu, ul.tabs, div.tabs, .nav-tabs'); - luciTabs.forEach(function(el) { - // Don't remove our own tabs - if (!el.classList.contains('sh-nav-tabs')) { - el.style.display = 'none'; - // Also try removing from DOM after a brief delay - setTimeout(function() { - if (el.parentNode && !el.classList.contains('sh-nav-tabs')) { - el.style.display = 'none'; - } - }, 100); - } - }); - - if (document.getElementById('system-hub-tabstyle')) - return; - var style = document.createElement('style'); - style.id = 'system-hub-tabstyle'; - style.textContent = ` -/* Hide default LuCI tabs for System Hub - aggressive selectors */ -/* Target any ul.tabs in the page */ -ul.tabs { - display: none !important; -} - -/* Be more specific for pages that need tabs elsewhere */ -body:not([data-page*="system-hub"]) ul.tabs { - display: block !important; -} - -/* All possible LuCI tab selectors */ -body[data-page^="admin-secubox-system-system-hub"] .tabs, -body[data-page^="admin-secubox-system-system-hub"] #tabmenu, -body[data-page^="admin-secubox-system-system-hub"] .cbi-tabmenu, -body[data-page^="admin-secubox-system-system-hub"] .nav-tabs, -body[data-page^="admin-secubox-system-system-hub"] ul.cbi-tabmenu, -body[data-page*="system-hub"] ul.tabs, -body[data-page*="system-hub"] .tabs, -/* Fallback: hide any tabs that appear before our custom nav */ -.system-hub-dashboard .tabs, -.system-hub-dashboard + .tabs, -.system-hub-dashboard ~ .tabs, -.cbi-map > .tabs:first-child, -#maincontent > .container > .tabs, -#maincontent > .container > ul.tabs, -#view > .tabs, -#view > ul.tabs, -.view > .tabs, -.view > ul.tabs, -div.tabs:has(+ .system-hub-dashboard), -/* Direct sibling of System Hub content */ -.sh-nav-tabs ~ .tabs, -/* LuCI 24.x specific */ -.luci-app-system-hub .tabs, -#cbi-system-hub .tabs { - display: none !important; -} - -/* Hide tabs container when our nav is present */ -.sh-nav-tabs ~ ul.tabs, -.sh-nav-tabs + ul.tabs { - display: none !important; -} - -/* System Hub Nav Tabs */ -.sh-nav-tabs { - display: flex; - gap: 4px; - margin-bottom: 24px; - padding: 6px; - background: var(--sh-bg-secondary); - border-radius: var(--sh-radius-lg); - border: 1px solid var(--sh-border); - overflow-x: auto; - -webkit-overflow-scrolling: touch; -} - -.sh-nav-tab { - display: flex; - align-items: center; - gap: 8px; - padding: 10px 16px; - border-radius: var(--sh-radius); - background: transparent; - border: none; - color: var(--sh-text-secondary); - font-weight: 500; - font-size: 13px; - cursor: pointer; - text-decoration: none; - transition: all 0.2s ease; - white-space: nowrap; -} - -.sh-nav-tab:hover { - color: var(--sh-text-primary); - background: var(--sh-bg-tertiary); -} - -.sh-nav-tab.active { - color: var(--sh-accent); - background: var(--sh-bg-tertiary); - box-shadow: inset 0 -2px 0 var(--sh-accent); -} - -.sh-tab-icon { - font-size: 16px; - line-height: 1; -} - -.sh-tab-label { - font-family: var(--sh-font-sans); -} - -@media (max-width: 768px) { - .sh-nav-tabs { - padding: 4px; - } - .sh-nav-tab { - padding: 8px 12px; - font-size: 12px; - } - .sh-tab-label { - display: none; - } - .sh-tab-icon { - font-size: 18px; - } -} - `; - document.head && document.head.appendChild(style); + /** + * Render System Hub navigation tabs + * Delegates to SecuNav.renderCompactTabs() for consistent styling + */ + renderTabs: function(active) { + return SecuNav.renderCompactTabs(active, this.getTabs(), { className: 'system-hub-nav-tabs' }); }, - renderTabs: function(active) { - this.ensureLuCITabsHidden(); - return E('div', { 'class': 'sh-nav-tabs' }, - this.getTabs().map(function(tab) { - return E('a', { - 'class': 'sh-nav-tab' + (tab.id === active ? ' active' : ''), - 'href': L.url.apply(L, tab.path) - }, [ - E('span', { 'class': 'sh-tab-icon' }, tab.icon), - E('span', { 'class': 'sh-tab-label' }, tab.label) - ]); - }) - ); + /** + * Render breadcrumb back to SecuBox + */ + renderBreadcrumb: function() { + return SecuNav.renderBreadcrumb(_('System Hub'), '🖥️'); } });