Files

202 lines
6.9 KiB
HTML

{{define "page-header"}}{{end}}
{{define "page-content"}}
<!-- Dashboard (loads immediately) -->
<div id="tab-dashboard" class="admin-panel"
role="tabpanel"
aria-labelledby="tab-label-dashboard"
tabindex="0"
hx-get="/admin/api/tab/dashboard"
hx-trigger="load"
hx-swap="innerHTML"
aria-live="polite"
aria-busy="true">
<p class="text-base-content/50 italic">Loading...</p>
</div>
<!-- Crew (loads on first activation) -->
<div id="tab-crew" class="admin-panel hidden"
role="tabpanel"
aria-labelledby="tab-label-crew"
tabindex="0"
hx-get="/admin/api/tab/crew"
hx-trigger="tab:crew from:body once"
hx-swap="innerHTML"
aria-live="polite">
</div>
<!-- Settings (loads on first activation) -->
<div id="tab-settings" class="admin-panel hidden"
role="tabpanel"
aria-labelledby="tab-label-settings"
tabindex="0"
hx-get="/admin/api/tab/settings"
hx-trigger="tab:settings from:body once"
hx-swap="innerHTML"
aria-live="polite">
</div>
<!-- Relays (loads on first activation) -->
<div id="tab-relays" class="admin-panel hidden"
role="tabpanel"
aria-labelledby="tab-label-relays"
tabindex="0"
hx-get="/admin/api/tab/relays"
hx-trigger="tab:relays from:body once"
hx-swap="innerHTML"
aria-live="polite">
</div>
<!-- Storage / GC (loads on first activation) -->
<div id="tab-storage" class="admin-panel hidden"
role="tabpanel"
aria-labelledby="tab-label-storage"
tabindex="0"
hx-get="/admin/api/tab/storage"
hx-trigger="tab:storage from:body once"
hx-swap="innerHTML"
aria-live="polite">
</div>
<script>
(function() {
var validTabs = ['dashboard', 'crew', 'settings', 'relays', 'storage'];
var tabTitles = {
dashboard: 'Dashboard',
crew: 'Crew',
settings: 'Settings',
relays: 'Relays',
storage: 'Storage'
};
function switchAdminTab(tabId) {
if (validTabs.indexOf(tabId) === -1) tabId = 'dashboard';
// Toggle panel visibility
document.querySelectorAll('.admin-panel').forEach(function(p) {
var isActive = p.id === 'tab-' + tabId;
p.classList.toggle('hidden', !isActive);
});
// Desktop sidebar: toggle menu-active + aria-selected + aria-current
document.querySelectorAll('.menu li[data-tab]').forEach(function(li) {
var isActive = li.dataset.tab === tabId;
li.classList.toggle('menu-active', isActive);
var link = li.querySelector('a, button');
if (link) {
link.setAttribute('aria-selected', isActive ? 'true' : 'false');
if (isActive) {
link.setAttribute('aria-current', 'page');
} else {
link.removeAttribute('aria-current');
}
link.setAttribute('tabindex', isActive ? '0' : '-1');
}
});
// Mobile: toggle btn-secondary / btn-ghost + aria-selected
document.querySelectorAll('.admin-tab-mobile').forEach(function(a) {
var isActive = a.dataset.tab === tabId;
a.classList.toggle('btn-secondary', isActive);
a.classList.toggle('btn-ghost', !isActive);
a.setAttribute('aria-selected', isActive ? 'true' : 'false');
if (isActive) {
a.setAttribute('aria-current', 'page');
} else {
a.removeAttribute('aria-current');
}
a.setAttribute('tabindex', isActive ? '0' : '-1');
});
// Update document.title so AT users navigating by page title see the
// active tab (WCAG 2.4.2). The server already renders "{{.Title}} -
// Hold Admin" so we keep that suffix.
document.title = tabTitles[tabId] + ' — Hold Admin';
history.replaceState(null, '', '#' + tabId);
document.body.dispatchEvent(new CustomEvent('tab:' + tabId));
}
// Arrow-key navigation inside a tablist. Left/Right move focus and
// activate the next tab; Home/End jump to the first/last tab. Only
// enabled when the event target is actually a tab.
function handleTabKey(e, tabs) {
var idx = tabs.indexOf(e.target);
if (idx === -1) return;
var next = -1;
switch (e.key) {
case 'ArrowLeft':
case 'ArrowUp':
next = (idx - 1 + tabs.length) % tabs.length;
break;
case 'ArrowRight':
case 'ArrowDown':
next = (idx + 1) % tabs.length;
break;
case 'Home':
next = 0;
break;
case 'End':
next = tabs.length - 1;
break;
default:
return;
}
e.preventDefault();
var target = tabs[next];
switchAdminTab(target.dataset.tab);
target.focus();
}
document.addEventListener('DOMContentLoaded', function() {
// Mobile tab click handlers
var mobileTabs = Array.from(document.querySelectorAll('.admin-tab-mobile'));
mobileTabs.forEach(function(a) {
a.addEventListener('click', function(e) {
e.preventDefault();
switchAdminTab(this.dataset.tab);
});
a.addEventListener('keydown', function(e) { handleTabKey(e, mobileTabs); });
});
// Desktop sidebar click handlers
var desktopTabs = Array.from(document.querySelectorAll('.menu li[data-tab] a'));
desktopTabs.forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
switchAdminTab(this.parentElement.dataset.tab);
});
link.addEventListener('keydown', function(e) { handleTabKey(e, desktopTabs); });
});
// Activate tab from hash (default: dashboard). rAF deferral keeps
// the initial state update outside the first paint.
var rafId = requestAnimationFrame(function() {
rafId = 0;
var hash = window.location.hash.replace('#', '') || 'dashboard';
switchAdminTab(hash);
});
// Cancel on bfcache/pageshow restore so the callback doesn't run
// against a stale DOM.
window.addEventListener('pagehide', function() {
if (rafId) cancelAnimationFrame(rafId);
}, { once: true });
});
// Handle browser back/forward
window.addEventListener('hashchange', function() {
var hash = window.location.hash.replace('#', '') || 'dashboard';
switchAdminTab(hash);
});
// Clear aria-busy when the dashboard panel finishes loading.
document.body.addEventListener('htmx:afterSettle', function(evt) {
var elt = evt.detail && evt.detail.elt;
if (elt && elt.classList && elt.classList.contains('admin-panel')) {
elt.removeAttribute('aria-busy');
}
});
})();
</script>
{{end}}