mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-03 08:46:57 +00:00
202 lines
6.9 KiB
HTML
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}}
|