webui: sidebar fixes, content sectioning, and theme fixes

Drop the useless 768–1024px icon-only sidebar mode; everything up to
1024px now uses the hamburger overlay and full-width sidebar.

Fix the user dropdown on touch devices: click now toggles aria-expanded
so CSS-driven visibility works without hover.  A document click listener
closes the menu when tapping outside.

Give the sidebar proper light/dark tokens instead of hardcoded dark
values. Introduce --sidebar-hover-fg (primary in light, white in dark)
so nav-link hover text is legible in both themes.

Fix dropdown hover background using --border instead of --border-subtle,
which had zero contrast against the surface in dark mode.

Keep active nav link in sync with the current URL via JS so it updates
correctly after htmx navigation instead of staying on the initial
server-rendered page.  Also, add YouTube-style progress when laoding
pages that take time, e.g., Dashboard.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-06-14 22:07:25 +02:00
parent 917eec3f25
commit f2f52f8518
5 changed files with 379 additions and 161 deletions
+119 -74
View File
@@ -42,13 +42,17 @@
--warning: var(--amber-500);
--danger: var(--red-500);
/* Sidebar (Always Dark) */
--sidebar-bg: var(--slate-800);
--sidebar-fg: var(--slate-200);
--sidebar-hover: rgba(255, 255, 255, 0.1);
--sidebar-border: rgba(255, 255, 255, 0.1);
/* Sidebar — Light mode */
--sidebar-bg: var(--slate-100);
--sidebar-fg: var(--slate-700);
--sidebar-hover: rgba(0, 0, 0, 0.07);
--sidebar-hover-fg: var(--primary);
--sidebar-border: rgba(0, 0, 0, 0.08);
--sidebar-width: 240px;
/* Login page */
--dot-color: rgba(0, 0, 0, 0.055);
/* Layout */
--radius: 8px;
--radius-sm: 4px;
@@ -70,6 +74,12 @@
--fg-muted: var(--slate-400);
--border: var(--slate-700);
--border-subtle: var(--slate-800);
--sidebar-bg: var(--slate-800);
--sidebar-fg: var(--slate-200);
--sidebar-hover: rgba(255, 255, 255, 0.1);
--sidebar-hover-fg: #fff;
--sidebar-border: rgba(255, 255, 255, 0.1);
--dot-color: rgba(255, 255, 255, 0.045);
}
}
@@ -82,6 +92,12 @@
--border: var(--slate-700);
--border-subtle: var(--slate-800);
--zebra-stripe: rgba(255,255,255,0.03);
--sidebar-bg: var(--slate-800);
--sidebar-fg: var(--slate-200);
--sidebar-hover: rgba(255, 255, 255, 0.1);
--sidebar-hover-fg: #fff;
--sidebar-border: rgba(255, 255, 255, 0.1);
--dot-color: rgba(255, 255, 255, 0.045);
}
/* Force Light Mode (override system dark preference) */
@@ -92,6 +108,12 @@
--fg-muted: var(--slate-500);
--border: var(--slate-200);
--border-subtle: var(--slate-100);
--sidebar-bg: var(--slate-100);
--sidebar-fg: var(--slate-700);
--sidebar-hover: rgba(0, 0, 0, 0.07);
--sidebar-hover-fg: var(--primary);
--sidebar-border: rgba(0, 0, 0, 0.08);
--dot-color: rgba(0, 0, 0, 0.055);
}
.light .alert-error { background: #fef2f2; color: #991b1b; border-color: #fecaca; }
.light .alert-info { background: #eff6ff; color: #1e40af; border-color: #bfdbfe; }
@@ -180,7 +202,6 @@ a:hover {
.sidebar-header {
padding: 1.5rem 1.25rem;
border-bottom: 1px solid var(--sidebar-border);
}
.sidebar-logo {
@@ -195,45 +216,58 @@ a:hover {
overflow-y: auto;
}
/* Accordion Nav Groups */
details.nav-group {
border-top: 1px solid var(--sidebar-border);
}
details.nav-group:first-child {
border-top: none;
/* Standalone nav link (Dashboard) */
.nav-standalone {
margin: 0.25rem 0.5rem 0.5rem;
}
details.nav-group > summary.nav-group-summary {
/* Top-level accordion groups (Status / Configure / Maintenance) */
details.nav-group-top {
}
details.nav-group-top > summary.nav-group-summary-top {
list-style: none;
cursor: pointer;
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.08em;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--sidebar-fg);
opacity: 0.5;
padding: 0.875rem 1rem 0.375rem;
opacity: 0.7;
padding: 0.75rem 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-top > summary.nav-group-summary-top:hover { opacity: 1; }
details.nav-group-top > summary.nav-group-summary-top::-webkit-details-marker { display: none; }
details.nav-group-top > summary.nav-group-summary-top::marker { display: none; }
details.nav-group > summary.nav-group-summary::after {
details.nav-group-top > summary.nav-group-summary-top::after {
content: '▸';
font-size: 0.7rem;
}
details[open].nav-group > summary.nav-group-summary::after {
details[open].nav-group-top > summary.nav-group-summary-top::after {
content: '▾';
}
/* Static sub-section labels within top-level accordion */
.nav-section-label {
font-size: 0.6rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--sidebar-fg);
opacity: 0.45;
padding: 0.75rem 1rem 0.2rem;
user-select: none;
}
.nav-group-items {
list-style: none;
padding-bottom: 0.5rem;
padding-bottom: 0.25rem;
}
.nav-icon {
@@ -262,13 +296,13 @@ details[open].nav-group > summary.nav-group-summary::after {
font-size: 0.875rem;
border-radius: 0.375rem;
margin: 0 0.5rem;
transition: background-color 0.15s;
transition: background-color 0.15s, color 0.15s;
font-weight: 500;
}
.nav-link:hover {
background-color: var(--sidebar-hover);
color: #fff;
color: var(--sidebar-hover-fg);
text-decoration: none;
}
@@ -278,6 +312,22 @@ details[open].nav-group > summary.nav-group-summary::after {
text-decoration: none;
}
/* Button styled as a nav-link (for actions like Reboot) */
.nav-link-btn {
width: 100%;
background: none;
border: none;
cursor: pointer;
text-align: left;
font-family: inherit;
font-size: inherit;
}
.nav-link-danger:hover {
color: var(--danger);
background-color: var(--sidebar-hover);
}
.sidebar-footer {
padding: 1.25rem;
border-top: 1px solid var(--sidebar-border);
@@ -521,12 +571,19 @@ details[open].nav-group > summary.nav-group-summary::after {
Authentication
========================================================================== */
@keyframes login-rise {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
.login-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: var(--bg);
background-color: var(--bg);
background-image: radial-gradient(var(--dot-color) 1.5px, transparent 1.5px);
background-size: 22px 22px;
padding: 1rem;
}
@@ -534,10 +591,12 @@ details[open].nav-group > summary.nav-group-summary::after {
background: var(--surface);
padding: 2.5rem;
border-radius: var(--radius);
box-shadow: var(--shadow);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.07), 0 2px 4px -2px rgb(0 0 0 / 0.07),
0 0 0 1px var(--border);
width: 100%;
max-width: 400px;
border: 1px solid var(--border);
border-top: 2px solid var(--primary);
animation: login-rise 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.login-logo {
@@ -551,6 +610,19 @@ details[open].nav-group > summary.nav-group-summary::after {
Alerts & Banners
========================================================================== */
/* Top progress bar — shown during htmx page navigations */
.page-progress {
position: fixed;
top: 0;
left: 0;
height: 2px;
width: 0%;
background: var(--primary);
z-index: 9999;
opacity: 0;
pointer-events: none;
}
.conn-banner {
background: var(--danger);
color: #fff;
@@ -1023,8 +1095,8 @@ details[open].nav-group > summary.nav-group-summary::after {
justify-content: flex-end;
padding: 0 1rem;
height: 48px;
background: var(--surface);
border-bottom: 1px solid var(--border);
background: var(--sidebar-bg);
border-bottom: 1px solid var(--sidebar-border);
flex-shrink: 0;
z-index: 100;
}
@@ -1139,7 +1211,7 @@ details[open].nav-group > summary.nav-group-summary::after {
}
.dropdown-item:hover,
.dropdown-item:focus {
background: var(--border-subtle);
background: var(--border);
color: var(--fg);
text-decoration: none;
}
@@ -1174,20 +1246,28 @@ details[open].nav-group > summary.nav-group-summary::after {
}
.login-theme-btn:hover { color: var(--fg); border-color: var(--fg-muted); }
/* Login lock icon */
.login-icon {
.login-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
gap: 0.5rem;
margin-bottom: 1.75rem;
color: var(--fg-muted);
}
.login-header h1 {
font-size: 1.25rem;
font-weight: 600;
color: var(--fg);
line-height: 1;
}
/* ==========================================================================
Responsive Layout
========================================================================== */
/* Mobile (≤768px): Sidebar hidden, hamburger visible */
@media (max-width: 768px) {
/* Narrow (≤1024px): Sidebar hidden behind hamburger overlay */
@media (max-width: 1024px) {
#sidebar {
position: fixed;
top: 0;
@@ -1221,43 +1301,8 @@ details[open].nav-group > summary.nav-group-summary::after {
}
}
/* Tablet (769px1024px): Icon-only sidebar */
@media (min-width: 769px) and (max-width: 1024px) {
:root {
--sidebar-width: 64px;
}
.nav-link span:not(.nav-icon) {
display: none;
}
/* 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;
padding: 0.6rem;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.sidebar-logo {
max-width: 32px;
}
}
/* Desktop: hamburger hidden */
@media (min-width: 769px) {
/* Desktop (>1024px): hamburger hidden, sidebar always visible */
@media (min-width: 1025px) {
.hamburger-btn {
display: none;
}
+190 -20
View File
@@ -135,6 +135,93 @@
}
})();
// Active nav link — keep in sync with the current URL after htmx navigation.
// Also opens the top-level accordion section containing the active link,
// and immediately highlights the clicked link (optimistic active state).
(function() {
function updateActiveNav() {
var path = window.location.pathname;
var activeTopGroup = null;
document.querySelectorAll('.nav-link').forEach(function(link) {
var href = link.getAttribute('href');
var active = href === path;
link.classList.toggle('active', active);
if (active) {
var topGroup = link.closest('details.nav-group-top');
if (topGroup) activeTopGroup = topGroup;
}
});
if (activeTopGroup) {
document.querySelectorAll('details.nav-group-top').forEach(function(d) {
if (d === activeTopGroup) {
d.setAttribute('open', '');
} else {
d.removeAttribute('open');
}
});
}
}
// Optimistic active state: highlight the link immediately on click so the
// sidebar doesn't lag while waiting for the server to respond.
document.addEventListener('click', function(e) {
var link = e.target.closest('a.nav-link[hx-get]');
if (!link) return;
document.querySelectorAll('.nav-link').forEach(function(l) { l.classList.remove('active'); });
link.classList.add('active');
});
document.addEventListener('DOMContentLoaded', updateActiveNav);
document.addEventListener('htmx:pushedIntoHistory', updateActiveNav);
window.addEventListener('popstate', updateActiveNav);
})();
// Top progress bar during htmx navigations
(function() {
var bar, timer;
function getBar() {
if (!bar) bar = document.getElementById('page-progress');
return bar;
}
function start() {
var b = getBar();
if (!b) return;
if (timer) { clearTimeout(timer); timer = null; }
b.style.transition = 'none';
b.style.width = '0%';
b.style.opacity = '1';
b.offsetWidth; // force reflow so the transition below fires from 0
b.style.transition = 'width 8s cubic-bezier(0.05, 0.8, 0.4, 1)';
b.style.width = '85%';
}
function finish() {
var b = getBar();
if (!b) return;
if (timer) clearTimeout(timer);
b.style.transition = 'width 0.1s ease';
b.style.width = '100%';
timer = setTimeout(function() {
b.style.transition = 'opacity 0.25s ease';
b.style.opacity = '0';
timer = setTimeout(function() {
b.style.transition = 'none';
b.style.width = '0%';
timer = null;
}, 260);
}, 120);
}
document.addEventListener('htmx:beforeSend', start);
document.addEventListener('htmx:afterSettle', finish);
document.addEventListener('htmx:responseError', finish);
document.addEventListener('htmx:sendError', finish);
})();
// Theme (auto / light / dark) — shared by main app and login page
(function() {
function getTheme() {
@@ -205,67 +292,127 @@
})();
// Accordion nav group persistence
// Top-level sections (Status / Configure / Maintenance) are mutually exclusive.
// State is persisted in localStorage under 'nav-top:Name' keys.
(function() {
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('details.nav-group').forEach(function(d) {
var label = d.querySelector('summary');
var groups = document.querySelectorAll('details.nav-group-top');
// Restore saved state (HTML default: Status open, others closed)
groups.forEach(function(d) {
var label = d.querySelector(':scope > summary');
if (!label) return;
var key = 'nav-group:' + label.textContent.trim();
var key = 'nav-top:' + label.textContent.trim();
var saved = localStorage.getItem(key);
if (saved === 'closed') {
d.removeAttribute('open');
} else if (saved === 'open') {
if (saved === 'open') {
d.setAttribute('open', '');
} else if (saved === 'closed') {
d.removeAttribute('open');
}
});
// Mutual exclusion, persistence, and auto-navigation to first page link
groups.forEach(function(d) {
var label = d.querySelector(':scope > summary');
if (!label) return;
var key = 'nav-top:' + label.textContent.trim();
d.addEventListener('toggle', function() {
localStorage.setItem(key, d.open ? 'open' : 'closed');
if (d.open) {
groups.forEach(function(other) {
if (other !== d && other.open) {
other.removeAttribute('open');
var otherLabel = other.querySelector(':scope > summary');
if (otherLabel) {
localStorage.setItem('nav-top:' + otherLabel.textContent.trim(), 'closed');
}
}
});
// Navigate to the first page link if none in this section is active
if (!d.querySelector('.nav-link.active')) {
var first = d.querySelector('a.nav-link[hx-get]');
if (first) first.click();
}
}
});
});
});
})();
// User menu — hover handled by CSS; JS manages aria-expanded and keyboard
// User menu — hover (desktop) + click-toggle (touch) + keyboard
(function() {
document.addEventListener('DOMContentLoaded', function() {
var menu = document.getElementById('user-menu');
var btn = document.getElementById('user-menu-btn');
if (!menu || !btn) return;
menu.addEventListener('mouseenter', function() {
btn.setAttribute('aria-expanded', 'true');
function setExpanded(val) {
btn.setAttribute('aria-expanded', val ? 'true' : 'false');
}
// Hover: keep aria-expanded in sync so CSS transition fires correctly
menu.addEventListener('mouseenter', function() { setExpanded(true); });
menu.addEventListener('mouseleave', function() { setExpanded(false); });
// Click/tap: toggle (primary interaction on touch devices)
btn.addEventListener('click', function(e) {
e.stopPropagation();
setExpanded(btn.getAttribute('aria-expanded') !== 'true');
});
menu.addEventListener('mouseleave', function() {
btn.setAttribute('aria-expanded', 'false');
// Close when clicking outside (touch: tap outside)
document.addEventListener('click', function(e) {
if (!menu.contains(e.target)) setExpanded(false);
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') btn.setAttribute('aria-expanded', 'false');
if (e.key === 'Escape') setExpanded(false);
});
});
})();
// Sidebar toggle (mobile)
(function() {
var MOBILE_BP = 1024;
function closeSidebar(btn) {
document.body.classList.remove('sidebar-open');
if (btn) btn.setAttribute('aria-expanded', 'false');
}
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('hamburger-btn');
if (!btn) return;
// Open/close on hamburger click
btn.addEventListener('click', function() {
var open = document.body.classList.toggle('sidebar-open');
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
});
// Close sidebar when clicking the overlay (::after pseudo)
// Close when clicking the overlay (::after pseudo-element covers body)
document.body.addEventListener('click', function(e) {
if (document.body.classList.contains('sidebar-open') && e.target === document.body) {
document.body.classList.remove('sidebar-open');
btn.setAttribute('aria-expanded', 'false');
closeSidebar(btn);
}
});
// Close sidebar when a nav link is clicked (htmx navigation)
// Close when navigating via htmx
document.addEventListener('htmx:beforeRequest', function() {
if (document.body.classList.contains('sidebar-open')) {
document.body.classList.remove('sidebar-open');
if (btn) btn.setAttribute('aria-expanded', 'false');
}
closeSidebar(btn);
});
// Close when viewport grows beyond the mobile breakpoint so the
// sidebar-open class and overlay don't linger at desktop widths.
var mq = window.matchMedia('(max-width: ' + MOBILE_BP + 'px)');
function onBreakpointChange(e) {
if (!e.matches) closeSidebar(btn);
}
if (mq.addEventListener) {
mq.addEventListener('change', onBreakpointChange);
} else {
mq.addListener(onBreakpointChange); // Safari <14 fallback
}
});
})();
@@ -295,3 +442,26 @@
}
document.addEventListener('DOMContentLoaded', focusAutofocusWhenVisible);
})();
// ─── Login page: progress bar on submit ────────────────────────────────────
// The login form is a native POST (not HTMX), so htmx:beforeSend never fires.
// Start the bar and disable the button to show the user something is happening.
(function() {
document.addEventListener('DOMContentLoaded', function() {
var form = document.querySelector('form[action="/login"]');
if (!form) return;
form.addEventListener('submit', function() {
var bar = document.getElementById('page-progress');
if (bar) {
bar.style.transition = 'none';
bar.style.width = '0%';
bar.style.opacity = '1';
bar.offsetWidth;
bar.style.transition = 'width 8s cubic-bezier(0.05, 0.8, 0.4, 1)';
bar.style.width = '85%';
}
var btn = form.querySelector('button[type="submit"]');
if (btn) { btn.disabled = true; btn.textContent = 'Logging in\u2026'; }
});
});
})();
+1 -13
View File
@@ -14,6 +14,7 @@
<script src="/assets/js/app.js"></script>
</head>
<body hx-boost="true">
<div id="page-progress" class="page-progress" aria-hidden="true"></div>
<div id="conn-banner" class="conn-banner" hidden>Device unreachable</div>
<div class="layout">
{{template "sidebar" .}}
@@ -56,19 +57,6 @@
<svg class="theme-check" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
</button>
<div class="dropdown-divider"></div>
<a href="/config" class="dropdown-item" hx-boost="false" download>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Download Config
</a>
<button class="dropdown-item dropdown-item-danger"
hx-post="/reboot"
hx-confirm="Reboot the device?"
hx-target="#content"
hx-swap="innerHTML">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
Reboot
</button>
<div class="dropdown-divider"></div>
<form method="POST" action="/logout">
<input type="hidden" name="csrf" value="{{.CsrfToken}}">
<button type="submit" class="dropdown-item dropdown-item-danger">
+63 -50
View File
@@ -5,8 +5,10 @@
</div>
<div class="sidebar-nav">
<details class="nav-group" open>
<summary class="nav-group-summary">Network</summary>
<details class="nav-group-top" open>
<summary class="nav-group-summary-top">Status</summary>
<div class="nav-section-label">System</div>
<ul class="nav-group-items">
<li>
<a href="/"
@@ -18,6 +20,32 @@
Dashboard
</a>
</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="/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>
<div class="nav-section-label">Network</div>
<ul class="nav-group-items">
<li>
<a href="/interfaces"
hx-get="/interfaces"
@@ -48,13 +76,7 @@
Firewall
</a>
</li>
</ul>
</details>
{{if and .Capabilities (.Capabilities.Has "wifi")}}
<details class="nav-group" open>
<summary class="nav-group-summary">Wireless</summary>
<ul class="nav-group-items">
{{if and .Capabilities (.Capabilities.Has "wifi")}}
<li>
<a href="/wifi"
hx-get="/wifi"
@@ -65,13 +87,7 @@
WiFi
</a>
</li>
</ul>
</details>
{{end}}
<details class="nav-group" open>
<summary class="nav-group-summary">VPN</summary>
<ul class="nav-group-items">
{{end}}
<li>
<a href="/vpn"
hx-get="/vpn"
@@ -82,11 +98,19 @@
WireGuard
</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>
<details class="nav-group" open>
<summary class="nav-group-summary">Services</summary>
<div class="nav-section-label">Services</div>
<ul class="nav-group-items">
<li>
<a href="/dhcp"
@@ -108,34 +132,17 @@
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>
<details class="nav-group" open>
<summary class="nav-group-summary">System</summary>
<details class="nav-group-top">
<summary class="nav-group-summary-top">Configure</summary>
</details>
<details class="nav-group-top">
<summary class="nav-group-summary-top">Maintenance</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"
@@ -147,15 +154,21 @@
</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 href="/config" class="nav-link" hx-boost="false" download>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex-shrink:0"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Download Config
</a>
</li>
<li>
<button class="nav-link nav-link-btn nav-link-danger"
hx-post="/reboot"
hx-confirm="Reboot the device?"
hx-target="#content"
hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex-shrink:0"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
Reboot
</button>
</li>
</ul>
</details>
+6 -4
View File
@@ -13,13 +13,15 @@
<script src="/assets/js/app.js"></script>
</head>
<body class="login-page">
<div id="page-progress" class="page-progress" aria-hidden="true"></div>
<div class="login-wrapper">
<div class="login-card">
<div class="login-icon" aria-hidden="true">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
<div class="login-header">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<h1>Login</h1>
</div>
{{if .Error}}
<div class="alert alert-error" role="alert">{{.Error}}</div>