62 lines
3.0 KiB
Svelte
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>
|