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

62 lines
3.0 KiB
Svelte

<script lang="ts">
import { tick } from 'svelte';
import { page } from '$app/stores';
import type { UserRole } from '$lib/auth.js';
import { navigationItemActive, navigationLocation, visibleNavigation } from '$lib/navigation.js';
let { role, variant = 'primary' }: { role: UserRole; variant?: 'primary' | 'secondary' } = $props();
let navigation = $state<HTMLElement>();
const groups = $derived(visibleNavigation(role));
const location = $derived(navigationLocation($page.url.pathname, role));
// Keep the current section or page visible in horizontally scrolling rows.
$effect(() => {
const current = location;
const node = navigation;
if (!current || !node) return;
let cancelled = false;
let observer: ResizeObserver | undefined;
void tick().then(() => {
if (cancelled) return;
const selected = node.querySelector<HTMLElement>('[aria-current]');
if (!selected) return;
const reveal = () => {
const bounds = node.getBoundingClientRect();
const target = selected.getBoundingClientRect();
if (target.left < bounds.left) node.scrollLeft -= bounds.left - target.left;
else if (target.right > bounds.right) node.scrollLeft += target.right - bounds.right;
};
reveal();
observer = new ResizeObserver(reveal);
observer.observe(node);
});
return () => { cancelled = true; observer?.disconnect(); };
});
</script>
{#if variant === 'primary'}
<nav class="primary" aria-label="Primary" bind:this={navigation}>
{#each groups as group (group.id)}
<a href={group.items[0].href} aria-current={location?.group.id === group.id ? 'location' : undefined}>{group.label}</a>
{/each}
</nav>
{:else if location}
<nav class="secondary" aria-label={`${location.group.label} pages`} bind:this={navigation}>
{#each location.group.items as item (item.href)}
<a href={item.href} aria-current={navigationItemActive(item, $page.url.pathname) ? 'page' : undefined}>{item.label}</a>
{/each}
</nav>
{/if}
<style>
nav { display:flex; align-items:stretch; gap:4px; min-width:0; max-width:100%; overflow-x:auto; overscroll-behavior-x:contain; scrollbar-width:thin; }
a { display:flex; align-items:center; flex-shrink:0; min-height:44px; padding:10px 16px; border-bottom:2px solid transparent; color:var(--text-secondary); text-decoration:none; white-space:nowrap; font-size:var(--text-sm); font-weight:500; }
a:hover { color:var(--text-primary); background:var(--surface-raised); }
a:focus-visible { outline-offset:-3px; }
a[aria-current] { color:var(--accent-primary-hover); border-bottom-color:var(--accent-primary); }
.primary a { min-height:59px; }
.secondary { margin-bottom:var(--space-6); border-bottom:1px solid var(--border-subtle); }
@media (max-width:999px) { .primary a { min-height:48px; } }
@media (max-width:720px) { a { padding-inline:12px; } }
</style>