mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-30 04:33:00 +02:00
webui: accordion sidebar and general-purpose status badges
Replace flat nav-section-header labels with collapsible <details>/<summary> accordion groups (Network, Wireless, VPN, Services, System). Groups default open; user state persists via localStorage. Tablet icon-only mode always shows all items regardless of open/closed state. Add badge-up/down/warning/info/neutral utility classes with full dark mode support, complementing the existing firewall-specific badge variants. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -182,20 +182,50 @@ a:hover {
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
flex: 1;
|
||||
padding: 1rem 0;
|
||||
padding: 0.5rem 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-section-header {
|
||||
/* Accordion Nav Groups */
|
||||
details.nav-group {
|
||||
border-top: 1px solid var(--sidebar-border);
|
||||
}
|
||||
details.nav-group:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
details.nav-group > summary.nav-group-summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--sidebar-fg);
|
||||
opacity: 0.5;
|
||||
padding: 1rem 1rem 0.25rem;
|
||||
pointer-events: none;
|
||||
padding: 0.875rem 1rem 0.375rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
details.nav-group > summary.nav-group-summary:hover { opacity: 0.8; }
|
||||
details.nav-group > summary.nav-group-summary::-webkit-details-marker { display: none; }
|
||||
details.nav-group > summary.nav-group-summary::marker { display: none; }
|
||||
|
||||
details.nav-group > summary.nav-group-summary::after {
|
||||
content: '▸';
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
details[open].nav-group > summary.nav-group-summary::after {
|
||||
content: '▾';
|
||||
}
|
||||
|
||||
.nav-group-items {
|
||||
list-style: none;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
@@ -796,6 +826,31 @@ a:hover {
|
||||
.dark .badge-drop { background: var(--slate-800); color: var(--slate-400); }
|
||||
.dark .badge-continue { background: rgba(59, 130, 246, 0.2); color: #93c5fd; }
|
||||
|
||||
/* General-purpose status badges */
|
||||
.badge-up { background: #dcfce7; color: #166534; }
|
||||
.badge-down { background: #fef2f2; color: #991b1b; }
|
||||
.badge-warning { background: #fef9c3; color: #854d0e; }
|
||||
.badge-info { background: #dbeafe; color: #1e40af; }
|
||||
.badge-neutral { background: var(--slate-100); color: var(--slate-600); }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.badge-up { background: rgba(22, 163, 74, 0.2); color: #86efac; }
|
||||
.badge-down { background: rgba(220, 38, 38, 0.2); color: #fca5a5; }
|
||||
.badge-warning { background: rgba(245, 158, 11, 0.2); color: #fcd34d; }
|
||||
.badge-info { background: rgba(59, 130, 246, 0.2); color: #93c5fd; }
|
||||
.badge-neutral { background: var(--slate-800); color: var(--slate-400); }
|
||||
}
|
||||
.dark .badge-up { background: rgba(22, 163, 74, 0.2); color: #86efac; }
|
||||
.dark .badge-down { background: rgba(220, 38, 38, 0.2); color: #fca5a5; }
|
||||
.dark .badge-warning { background: rgba(245, 158, 11, 0.2); color: #fcd34d; }
|
||||
.dark .badge-info { background: rgba(59, 130, 246, 0.2); color: #93c5fd; }
|
||||
.dark .badge-neutral { background: var(--slate-800); color: var(--slate-400); }
|
||||
.light .badge-up { background: #dcfce7; color: #166534; }
|
||||
.light .badge-down { background: #fef2f2; color: #991b1b; }
|
||||
.light .badge-warning { background: #fef9c3; color: #854d0e; }
|
||||
.light .badge-info { background: #dbeafe; color: #1e40af; }
|
||||
.light .badge-neutral { background: var(--slate-100); color: var(--slate-600); }
|
||||
|
||||
/* Interfaces */
|
||||
.iface-name { white-space: nowrap; font-weight: 500; }
|
||||
.iface-name a { color: var(--primary); text-decoration: none; }
|
||||
@@ -1047,9 +1102,13 @@ a:hover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-section-header {
|
||||
/* Accordion in icon-only mode: always show items, hide section headers */
|
||||
details.nav-group > summary.nav-group-summary {
|
||||
display: none;
|
||||
}
|
||||
details.nav-group:not([open]) > .nav-group-items {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
justify-content: center;
|
||||
|
||||
@@ -185,6 +185,26 @@
|
||||
});
|
||||
})();
|
||||
|
||||
// Accordion nav group persistence
|
||||
(function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('details.nav-group').forEach(function(d) {
|
||||
var label = d.querySelector('summary');
|
||||
if (!label) return;
|
||||
var key = 'nav-group:' + label.textContent.trim();
|
||||
var saved = localStorage.getItem(key);
|
||||
if (saved === 'closed') {
|
||||
d.removeAttribute('open');
|
||||
} else if (saved === 'open') {
|
||||
d.setAttribute('open', '');
|
||||
}
|
||||
d.addEventListener('toggle', function() {
|
||||
localStorage.setItem(key, d.open ? 'open' : 'closed');
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
// Sidebar toggle (mobile)
|
||||
(function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
@@ -3,141 +3,163 @@
|
||||
<div class="sidebar-header">
|
||||
<img src="/assets/img/logo.png" alt="Infix" class="sidebar-logo">
|
||||
</div>
|
||||
<ul class="sidebar-nav">
|
||||
<li class="nav-section-header">Network</li>
|
||||
<li>
|
||||
<a href="/"
|
||||
hx-get="/"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "dashboard"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/dashboard.svg')"></span>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/interfaces"
|
||||
hx-get="/interfaces"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "interfaces"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/interfaces.svg')"></span>
|
||||
Interfaces
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/routing"
|
||||
hx-get="/routing"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "routing"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/routing.svg')"></span>
|
||||
Routing
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/firewall"
|
||||
hx-get="/firewall"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "firewall"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/firewall.svg')"></span>
|
||||
Firewall
|
||||
</a>
|
||||
</li>
|
||||
<div class="sidebar-nav">
|
||||
|
||||
<details class="nav-group" open>
|
||||
<summary class="nav-group-summary">Network</summary>
|
||||
<ul class="nav-group-items">
|
||||
<li>
|
||||
<a href="/"
|
||||
hx-get="/"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "dashboard"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/dashboard.svg')"></span>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/interfaces"
|
||||
hx-get="/interfaces"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "interfaces"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/interfaces.svg')"></span>
|
||||
Interfaces
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/routing"
|
||||
hx-get="/routing"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "routing"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/routing.svg')"></span>
|
||||
Routing
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/firewall"
|
||||
hx-get="/firewall"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "firewall"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/firewall.svg')"></span>
|
||||
Firewall
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
{{if and .Capabilities (.Capabilities.Has "wifi")}}
|
||||
<li class="nav-section-header">Wireless</li>
|
||||
<li>
|
||||
<a href="/wifi"
|
||||
hx-get="/wifi"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "wifi"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/wifi.svg')"></span>
|
||||
WiFi
|
||||
</a>
|
||||
</li>
|
||||
<details class="nav-group" open>
|
||||
<summary class="nav-group-summary">Wireless</summary>
|
||||
<ul class="nav-group-items">
|
||||
<li>
|
||||
<a href="/wifi"
|
||||
hx-get="/wifi"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "wifi"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/wifi.svg')"></span>
|
||||
WiFi
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
{{end}}
|
||||
|
||||
<li class="nav-section-header">VPN</li>
|
||||
<li>
|
||||
<a href="/vpn"
|
||||
hx-get="/vpn"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "vpn"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/wireguard.svg')"></span>
|
||||
WireGuard
|
||||
</a>
|
||||
</li>
|
||||
<details class="nav-group" open>
|
||||
<summary class="nav-group-summary">VPN</summary>
|
||||
<ul class="nav-group-items">
|
||||
<li>
|
||||
<a href="/vpn"
|
||||
hx-get="/vpn"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "vpn"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/wireguard.svg')"></span>
|
||||
WireGuard
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<li class="nav-section-header">Services</li>
|
||||
<li>
|
||||
<a href="/dhcp"
|
||||
hx-get="/dhcp"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "dhcp"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/dhcp.svg')"></span>
|
||||
DHCP
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/ntp"
|
||||
hx-get="/ntp"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "ntp"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/ntp.svg')"></span>
|
||||
NTP
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/lldp"
|
||||
hx-get="/lldp"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "lldp"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/lldp.svg')"></span>
|
||||
LLDP
|
||||
</a>
|
||||
</li>
|
||||
<details class="nav-group" open>
|
||||
<summary class="nav-group-summary">Services</summary>
|
||||
<ul class="nav-group-items">
|
||||
<li>
|
||||
<a href="/dhcp"
|
||||
hx-get="/dhcp"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "dhcp"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/dhcp.svg')"></span>
|
||||
DHCP
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/ntp"
|
||||
hx-get="/ntp"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "ntp"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/ntp.svg')"></span>
|
||||
NTP
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/lldp"
|
||||
hx-get="/lldp"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "lldp"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/lldp.svg')"></span>
|
||||
LLDP
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<li class="nav-section-header">System</li>
|
||||
{{if and .Capabilities (.Capabilities.Has "containers")}}
|
||||
<li>
|
||||
<a href="/containers"
|
||||
hx-get="/containers"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "containers"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/containers.svg')"></span>
|
||||
Containers
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li>
|
||||
<a href="/firmware"
|
||||
hx-get="/firmware"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "firmware"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/firmware.svg')"></span>
|
||||
Firmware
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/keystore"
|
||||
hx-get="/keystore"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "keystore"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/keystore.svg')"></span>
|
||||
Keystore
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<details class="nav-group" open>
|
||||
<summary class="nav-group-summary">System</summary>
|
||||
<ul class="nav-group-items">
|
||||
{{if and .Capabilities (.Capabilities.Has "containers")}}
|
||||
<li>
|
||||
<a href="/containers"
|
||||
hx-get="/containers"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "containers"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/containers.svg')"></span>
|
||||
Containers
|
||||
</a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li>
|
||||
<a href="/firmware"
|
||||
hx-get="/firmware"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "firmware"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/firmware.svg')"></span>
|
||||
Firmware
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/keystore"
|
||||
hx-get="/keystore"
|
||||
hx-target="#content"
|
||||
hx-push-url="true"
|
||||
class="nav-link {{if eq .ActivePage "keystore"}}active{{end}}">
|
||||
<span class="nav-icon" style="--icon: url('/assets/img/icons/keystore.svg')"></span>
|
||||
Keystore
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
<div class="sidebar-footer">
|
||||
<button id="theme-toggle" class="btn btn-sidebar-action theme-toggle" aria-label="Toggle theme">
|
||||
<svg id="icon-auto" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
Reference in New Issue
Block a user