Files
electric-cherry/src/lib/ui/components/Navigation.svelte
T

130 lines
4.4 KiB
Svelte

<script lang="ts">
import { page } from '$app/stores';
import type { UserRole } from '$lib/auth.js';
import { navigationItemActive, visibleNavigation } from '$lib/navigation.js';
import { serverHealth } from '$ui/server-health.js';
import { fleet, fleetServers } from '$ui/stores/fleet.js';
import { uiNow } from '$ui/stores/servers.js';
let { role, onNavigate }: { role: UserRole; onNavigate?: () => void } = $props();
const id = $props.id();
const groups = $derived(visibleNavigation(role));
const attention = $derived.by(() => {
if (!$fleet.receivedAt) return null;
const statuses = $fleetServers.map((server) => ({
server,
state: serverHealth(server, $uiNow).state
}));
const servers = statuses.filter(
({ server, state }) =>
!['online', 'starting', 'provisioning'].includes(state) ||
$fleet.operations.some(
(operation) =>
operation.server === server.name && operation.state === 'attention'
)
).length;
const hosts =
$fleet.hostAttention ??
new Set(
statuses
.filter(({ server, state }) => server.host && state === 'host-offline')
.map(({ server }) => server.host!.id)
).size;
const maintenance = $fleet.operations.filter(
(operation) => operation.kind === 'maintenance' && operation.state === 'attention'
).length;
return { '/': servers, '/hosts': hosts, '/maintenance': maintenance } as Record<
string,
number
>;
});
</script>
<nav aria-label="Main navigation">
{#each groups as group (group.id)}
<section aria-labelledby={`${id}-${group.id}`}>
<h2 id={`${id}-${group.id}`}>{group.label}</h2>
<ul>
{#each group.items as item (item.href)}
<li>
<a
href={item.href}
aria-current={navigationItemActive(item, $page.url.pathname)
? 'page'
: undefined}
aria-label={attention && attention[item.href] > 0
? `${item.label}, ${attention[item.href]} ${attention[item.href] === 1 ? 'needs' : 'need'} attention`
: undefined}
onclick={() => onNavigate?.()}
>
<span>{item.label}</span>
{#if attention && attention[item.href] > 0}<span
class="attention"
aria-hidden="true">{attention[item.href]}</span
>{/if}
</a>
</li>
{/each}
</ul>
</section>
{/each}
</nav>
<style>
nav {
display: grid;
gap: var(--space-4);
min-width: 0;
}
h2 {
margin: 0 0 var(--space-1);
padding: 0 var(--space-3);
color: var(--text-tertiary);
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
letter-spacing: var(--tracking-label);
text-transform: uppercase;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
a {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
min-height: 32px;
padding: 5px var(--space-3);
border-radius: var(--radius-sm);
color: var(--text-secondary);
font-size: var(--text-sm);
text-decoration: none;
}
a:hover {
color: var(--text-primary);
background: var(--surface-raised);
}
a[aria-current] {
color: var(--text-primary);
background: var(--surface-raised);
box-shadow: inset 2px 0 var(--accent-primary);
font-weight: var(--weight-semibold);
}
a:focus-visible {
outline-offset: -2px;
}
.attention {
color: var(--state-warning);
font-variant-numeric: tabular-nums;
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
}
@media (max-width: 960px) {
a {
min-height: 44px;
}
}
</style>