diff --git a/src/lib/ui/components/Navigation.svelte b/src/lib/ui/components/Navigation.svelte index 2a0d3de..d014077 100644 --- a/src/lib/ui/components/Navigation.svelte +++ b/src/lib/ui/components/Navigation.svelte @@ -1,81 +1,61 @@ -{#snippet links(prefix: string)} - +{#if variant === 'primary'} + {#each groups as group (group.id)} - - toggle(group.id)}> - {group.label}{collapsed[group.id] ? '+' : '−'} - - - {#each group.items as item (item.href)} - {item.label} - {/each} - - + {group.label} {/each} -{/snippet} - - - open = false} - onclick={event => { if (event.target === drawer) { const box = drawer!.getBoundingClientRect(); if (event.clientX > box.right || event.clientX < box.left || event.clientY < box.top || event.clientY > box.bottom) open = false; } }}> - Navigation open = false}>× - {@render links('mobile-navigation')} - +{:else if location} + + {#each location.group.items as item (item.href)} + {item.label} + {/each} + +{/if} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index fe12308..3a4161f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -7,7 +7,7 @@ import { startFleetMonitor, hydrateFleet } from '$ui/stores/fleet.js'; import { page } from '$app/stores'; import Navigation from '$ui/components/Navigation.svelte'; - import { navigationLocation, visibleNavigation } from '$lib/navigation.js'; + import { visibleNavigation } from '$lib/navigation.js'; import BrandMark from '$ui/components/BrandMark.svelte'; import Confirmation from '$ui/components/Confirmation.svelte'; import Dialog from '$ui/components/primitives/Dialog.svelte'; @@ -23,12 +23,10 @@ let { children, data }: Props = $props(); const publicPage = $derived(['/login', '/setup'].includes($page.url.pathname)); const sessionReady = $derived(!!data.user && !data.user.mustChangePassword && !publicPage); - let menuOpen = $state(false); let accountOpen = $state(false); let signingOut = $state(false); let signOutError = $state(''); let helpOpen = $state(false); - let menuButton = $state(); let accountButton = $state(); const accountName = $derived(data.user?.displayName || data.user?.username || 'Account'); const accountInitial = $derived(Array.from(accountName.trim())[0]?.toLocaleUpperCase() || '?'); @@ -38,20 +36,15 @@ } function openHelp() { - if (menuOpen) { menuButton?.focus(); menuOpen = false; } helpOpen = true; } beforeNavigate(() => performance.mark('ec:navigation-start')); afterNavigate(() => { if (performance.getEntriesByName('ec:navigation-start').length) { performance.measure('ec:navigation', 'ec:navigation-start'); performance.clearMarks('ec:navigation-start'); } }); - afterNavigate(() => { menuOpen = false; accountOpen = false; resolveConfirmation(false); }); + afterNavigate(() => { accountOpen = false; resolveConfirmation(false); }); function closeMenu(event: KeyboardEvent) { if (event.key === 'Escape' && accountOpen) { accountOpen = false; accountButton?.focus(); } - if (event.key === 'Escape' && menuOpen) { - menuOpen = false; - menuButton?.focus(); - } } async function signOut() { @@ -122,13 +115,12 @@ // Non-Chromium browsers fall through to plain navigation. onNavigate(enableViewTransitions); - const location = $derived(navigationLocation($page.url.pathname, data.user?.role ?? 'viewer')); const shortcuts = $derived(visibleNavigation(data.user?.role ?? 'viewer').flatMap(group => group.items).filter(item => item.shortcut)); - + Skip to content {#if publicPage} @@ -137,21 +129,14 @@ - {#if sessionReady && location}{location.group.label} / {location.item.label}{/if} + {#if sessionReady && data.user}{/if} - {#if sessionReady}?{/if} - {#if sessionReady} { menuOpen = !menuOpen; accountOpen = false; }}> - {menuOpen ? 'Close' : 'Menu'} - {menuOpen ? '×' : '☰'} - {/if} {#if data.user} { accountOpen = !accountOpen; menuOpen = false; }}> + onclick={() => accountOpen = !accountOpen}> {accountInitial} {#if accountOpen} @@ -167,9 +152,9 @@ {/if} - {#if sessionReady && data.user}{/if} {/if} + {#if sessionReady && data.user}{/if} {#if sessionReady && data.user?.role === 'viewer'}{/if} {@render children()} @@ -221,24 +206,21 @@ .shell { min-height: 100vh; } .auth-brand { display:flex; justify-content:center; padding:48px 20px 20px; } main { min-width:0; overflow-wrap:anywhere; padding:var(--space-6) var(--space-8) var(--space-16); max-width:1600px; margin:0 auto; view-transition-name:main; } - .with-navigation main { margin-left:232px; } main.auth-main { max-width:520px; padding-top:16px; } .viewer-note { display:flex; gap:8px 16px; flex-wrap:wrap; padding:14px 18px; margin-bottom:24px; border:1px solid var(--border-subtle); border-radius:var(--radius-md); background:var(--surface); font-size:var(--text-sm); color:var(--text-secondary); } .viewer-note strong { color:var(--text-primary); } - .app-header { position:sticky; top:0; z-index:20; display:flex; align-items:center; gap:24px; height:60px; padding:0 20px; border-bottom:1px solid var(--border-subtle); background:var(--surface); } + .app-header { position:sticky; top:0; z-index:20; display:flex; align-items:center; gap:24px; min-height:60px; padding:0 20px; border-bottom:1px solid var(--border-subtle); background:var(--surface); } .brand-link { flex-shrink:0; display:flex; align-items:center; text-decoration:none; } - .location { font-size:var(--text-sm); color:var(--text-secondary); min-width:0; } - .location strong { color:var(--text-primary); font-weight:500; } + .primary-navigation { flex:1; min-width:0; } .header-actions { display:flex; align-items:center; gap:8px; margin-left:auto; } .account-menu { position:relative; } - .account-button,.help-button,.menu-toggle { display:inline-flex; align-items:center; justify-content:center; min-width:44px; height:44px; padding:8px 12px; border:1px solid var(--border-strong); border-radius:var(--radius-md); background:var(--surface); color:var(--text-primary); font:inherit; cursor:pointer; } + .account-button { display:inline-flex; align-items:center; justify-content:center; min-width:44px; height:44px; padding:8px 12px; border:1px solid var(--border-strong); border-radius:var(--radius-md); background:var(--surface); color:var(--text-primary); font:inherit; cursor:pointer; } .account-button { border-radius:50%; color:var(--accent-primary-hover); background:color-mix(in oklab,var(--accent-primary) 12%,var(--surface)); font-weight:600; } .account-links { position:absolute; right:0; top:calc(100% + 8px); width:220px; max-width:calc(100vw - 32px); max-height:calc(100dvh - 84px); overflow-y:auto; z-index:30; display:grid; padding:8px; background:var(--surface); border:1px solid var(--border-strong); border-radius:var(--radius-md); box-shadow:0 12px 32px #0005; } .account-name { padding:8px 12px 12px; margin-bottom:4px; border-bottom:1px solid var(--border-subtle); color:var(--text-secondary); font-size:var(--text-sm); overflow-wrap:anywhere; } .account-links a,.account-links button { text-align:left; padding:10px 12px; color:var(--text-primary); background:none; border:0; border-radius:var(--radius-sm); font:inherit; font-size:var(--text-sm); cursor:pointer; text-decoration:none; } - .account-links a:hover,.account-links button:hover,.help-button:hover,.menu-toggle:hover { background:var(--surface-raised); } + .account-links a:hover,.account-links button:hover { background:var(--surface-raised); } .account-links p { padding:0 12px; font-size:var(--text-xs); color:var(--state-danger); } - .menu-toggle { display:none; gap:8px; } .shortcuts { display:grid; gap:12px; margin:0; } .shortcuts > div { display:flex; gap:20px; align-items:center; } .shortcuts dt { min-width:55px; } @@ -250,14 +232,12 @@ :global(input),:global(select),:global(textarea),:global(button),:global(a) { scroll-margin-block:90px 100px; } :global(dialog) { max-height:calc(100dvh - 32px); overflow-y:auto; overscroll-behavior:contain; } @media (max-width:999px) { - .with-navigation main { margin-inline:auto; } - .menu-toggle { display:inline-flex; } - .location { display:none; } + .app-header { flex-wrap:wrap; gap:0 16px; padding-top:8px; } + .primary-navigation { order:3; flex-basis:100%; } } @media (max-width:720px) { .app-header { padding-inline:12px; gap:8px; } .brand-link :global(.mark-sm .word) { font-size:12px; letter-spacing:1px; } - .menu-label { display:none; } main { padding:var(--space-5) max(var(--space-4),env(safe-area-inset-right)) calc(var(--space-16) + env(safe-area-inset-bottom)) max(var(--space-4),env(safe-area-inset-left)); } :global(input:not([type='checkbox']):not([type='radio'])),:global(select),:global(textarea) { font-size:16px !important; min-height:44px; } :global(button),:global(.btn) { min-height:44px; }