+
- {#if sessionReady}
-
- {#each visibleTabs as t (t.href)}
- {@const on = t.match($page.url.pathname)}
-
- {t.label}
-
- {/each}
-
- {/if}
-
- {#if sessionReady && secondary.length}
{/if}
+
+ {#if sessionReady && data.user}
{/if}
{/if}
{#if sessionReady && data.user?.role === 'viewer'}{/if}
@@ -215,7 +175,7 @@
@@ -260,157 +220,48 @@
.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); }
+ .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); }
+ .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; }
+ .header-actions { display:flex; align-items:center; gap:8px; margin-left:auto; }
.account-menu { position:relative; }
- .header-actions { order:2; display:flex; align-items:center; gap:var(--space-2); margin-left:auto; }
- .account-button { display:grid; place-items:center; width:44px; height:44px; padding:0; border:1px solid var(--border-strong); border-radius:50%; background:color-mix(in oklab, var(--accent-primary) 12%, var(--surface)); color:var(--accent-primary-hover); font:inherit; font-weight:var(--weight-semibold); cursor:pointer; transition:background-color var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard); }
- .account-button:hover,.account-button[aria-expanded='true'] { background:var(--surface-raised); border-color:var(--accent-primary-hover); }
- .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-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 { 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 { background:var(--surface-raised); }
+ .account-links a:hover,.account-links button:hover,.help-button:hover,.menu-toggle:hover { background:var(--surface-raised); }
.account-links p { padding:0 12px; font-size:var(--text-xs); color:var(--state-danger); }
-
- nav {
- position: sticky;
- top: 0;
- z-index: 20;
- display: flex;
- align-items: center;
- gap: var(--space-6);
- height: 60px;
- padding: 0 var(--space-8);
- background: color-mix(in oklab, var(--surface) 75%, transparent);
- border-bottom: 1px solid var(--border-subtle);
+ .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; }
+ .shortcuts dd { margin:0; font-size:var(--text-sm); }
+ kbd { font-family:var(--font-mono); padding:3px 8px; border:1px solid var(--border-strong); border-radius:5px; }
+ .skip-link { position:fixed; left:16px; top:-80px; z-index:300; padding:12px 16px; background:var(--surface-raised); border-radius:var(--radius-md); }
+ .skip-link:focus { top:12px; }
+ :global(input),:global(select),:global(textarea) { min-width:0; max-width:100%; }
+ :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; }
}
- @supports (backdrop-filter: blur(4px)) {
- nav {
- backdrop-filter: blur(20px) saturate(1.4);
- -webkit-backdrop-filter: blur(20px) saturate(1.4);
- background: color-mix(in oklab, var(--surface) 62%, transparent);
- }
- }
-
- .brand-link {
- flex-shrink: 0;
- display: flex;
- align-items: center;
- text-decoration: none;
- border-radius: var(--radius-md);
- }
- .brand-link:focus-visible { box-shadow: var(--focus-ring); }
-
- .tabs {
- order: 1;
- display: flex;
- align-items: center;
- gap: 2px;
- margin-left: var(--space-4);
- }
- .tabs a {
- white-space: nowrap;
- position: relative;
- color: var(--text-tertiary);
- text-decoration: none;
- padding: 8px 14px;
- border-radius: var(--radius-pill);
- font-size: var(--text-sm);
- font-weight: var(--weight-medium);
- letter-spacing: var(--tracking-normal);
- transition:
- color var(--dur-fast) var(--ease-standard),
- background-color var(--dur-fast) var(--ease-standard);
- }
- .tabs a:hover { color: var(--text-primary); }
- .tabs a:focus-visible { box-shadow: var(--focus-ring); }
- .tabs a.on {
- color: var(--text-primary);
- background: var(--surface-raised);
- /* Shared-element transition — pill slides smoothly between tabs
- when navigating. Chromium only; falls back to instant swap. */
- view-transition-name: nav-active;
- }
-
- .shortcuts { display: grid; gap: 12px; margin: 0; }
- .shortcuts > div { display: flex; gap: 20px; align-items: center; }
- .shortcuts dt { min-width: 55px; }
- .shortcuts dd { margin: 0; font-size: var(--text-sm); color: var(--text-secondary); }
- kbd { font-family: var(--font-mono); padding: 3px 8px; border: 1px solid var(--border-strong); border-radius: 5px; }
- .menu-toggle { display: none; }
-
- main {
- min-width: 0;
- overflow-wrap: anywhere;
- padding: var(--space-6) var(--space-8) var(--space-16);
- max-width: 1440px;
- margin: 0 auto;
- view-transition-name: main;
- }
-
- /* The full navigation needs more room than a phone or portrait tablet. */
- @media (max-width: 1500px) {
- nav {
- flex-wrap: wrap;
- justify-content: space-between;
- gap: 0;
- height: auto;
- min-height: 60px;
- padding: calc(8px + env(safe-area-inset-top)) max(var(--space-4), env(safe-area-inset-right)) 8px max(var(--space-4), env(safe-area-inset-left));
- }
- .header-actions { order:0; }
- .menu-toggle {
- display: inline-flex;
- align-items: center;
- gap: var(--space-2);
- min-height: 44px;
- padding: 8px 12px;
- border: 1px solid var(--border-strong);
- border-radius: var(--radius-md);
- background: var(--surface-raised);
- color: var(--text-primary);
- font: inherit;
- font-size: var(--text-sm);
- cursor: pointer;
- }
- .menu-toggle:focus-visible { box-shadow: var(--focus-ring); }
- .tabs { display: none; width: 100%; margin: 0; }
- .tabs.expanded { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); padding: var(--space-3) 0 4px; max-height:calc(100dvh - 72px - env(safe-area-inset-top)); overflow-y:auto; }
- .tabs a { display: flex; align-items: center; min-height: 44px; border-radius: var(--radius-md); }
- }
- @media (max-width: 400px) {
- .menu-label { display:none; }
- .menu-toggle { justify-content:center; width:44px; padding:8px; }
+ @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; }
}
- @media (max-width: 720px) {
- main { padding: var(--space-6) 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)); }
- /* Keep native controls readable and prevent focus zoom on phones. */
- :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; }
- }
-
- :global(input), :global(select), :global(textarea) { min-width: 0; max-width: 100%; }
- :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; }
- .skip-link { position: fixed; left: 16px; top: -80px; z-index: 300; padding: 12px 16px; background: var(--surface-raised); border-radius: var(--radius-md); }
- .skip-link:focus { top: 12px; }
- .subnav { border-bottom: 1px solid var(--border-subtle); background: var(--surface-sunken); }
- .subnav > div { display: flex; gap: var(--space-6); max-width: 1440px; margin: 0 auto; padding: 0 var(--space-8); overflow-x: auto; }
- .subnav a { text-decoration: none; white-space: nowrap; font-size: var(--text-sm); color: var(--text-secondary); padding: var(--space-3) 0; border-bottom: 2px solid transparent; }
- .subnav a[aria-current] { color: var(--text-primary); border-bottom-color: var(--accent-primary); }
- @media (max-width: 720px) { .subnav > div { padding-inline: var(--space-4); } }
-
- /* View-transition tuning — cross-fade routes, ease the shared nav pill. */
- :global(::view-transition-old(root)),
- :global(::view-transition-new(root)) {
- animation-duration: var(--dur-base);
- animation-timing-function: var(--ease-standard);
- }
- :global(::view-transition-old(main)),
- :global(::view-transition-new(main)) {
- animation-duration: var(--dur-base);
- animation-timing-function: var(--ease-standard);
- }
+ @media (max-width:360px) { .brand-link :global(.mark-sm .word) { font-size:10px; letter-spacing:0; } .header-actions { gap:4px; } }
+ :global(::view-transition-old(root)),:global(::view-transition-new(root)),:global(::view-transition-old(main)),:global(::view-transition-new(main)) { animation-duration:var(--dur-base); animation-timing-function:var(--ease-standard); }
diff --git a/tests/unit/navigation.test.ts b/tests/unit/navigation.test.ts
new file mode 100644
index 0000000..7d54bdc
--- /dev/null
+++ b/tests/unit/navigation.test.ts
@@ -0,0 +1,28 @@
+import { describe, expect, it } from 'vitest';
+import { navigationGroups, navigationItemActive, navigationLocation, visibleNavigation } from '../../src/lib/navigation.js';
+
+describe('application navigation', () => {
+ it.each([
+ ['/servers/t5_zombie_theater_1', 'fleet', '/'],
+ ['/collections/launches/42', 'fleet', '/collections'],
+ ['/hosts/42', 'fleet', '/hosts'],
+ ['/hosts/operations', 'operations', '/hosts/operations'],
+ ['/betas/new', 'operations', '/betas'],
+ ['/campaigns/42', 'operations', '/campaigns'],
+ ['/configs/42', 'library', '/configs'],
+ ['/leaderboard/players/42', 'analytics', '/leaderboard'],
+ ['/settings/users', 'administration', '/settings/users'],
+ ['/server-keys', 'administration', '/server-keys']
+ ])('gives %s exactly one canonical home', (path, group, href) => {
+ expect(navigationLocation(path, 'admin')?.group.id).toBe(group);
+ expect(navigationGroups.flatMap(group => group.items).filter(item => navigationItemActive(item, path)).map(item => item.href)).toEqual([href]);
+ });
+
+ it('keeps administration and its shortcuts out of operator and viewer navigation', () => {
+ for (const role of ['operator', 'viewer'] as const) {
+ expect(visibleNavigation(role).map(group => group.id)).not.toContain('administration');
+ expect(navigationLocation('/server-keys', role)).toBeNull();
+ expect(visibleNavigation(role).flatMap(group => group.items).filter(item => ['h', 'k'].includes(item.shortcut ?? ''))).toEqual([]);
+ }
+ });
+});