refactor: Kopfnavigation auf bits-uis NavigationMenu

Das Team-Submenue stand auf einem DropdownMenu, und alles, was ein
Menuepunkt braucht und ein Dropdown nicht kann, stand hier als
Handarbeit daneben: ein Zeitgeber fuers Aufklappen, ueberschriebene
Zeigerhandler gegen das Flackern beim Klick, unterdrueckter Fokus gegen
den Ring nach dem Zeigerbesuch.

NavigationMenu kann das von sich aus. Es hat openOnHover samt
delayDuration, es haelt seinen eigenen onclick zurueck, wenn per Zeiger
aufgeklappt wurde ("if opened via pointer move, we prevent the click
event"), es faengt keinen Fokus und sperrt kein Scrollen. Der Trigger
bleibt ueber das child-Snippet ein Link auf die Teamseite.

viewport={false}: Das Panel haengt unter seinem Menuepunkt, statt als
animierte Flaeche ueber die ganze Leiste zu fahren. Bei einem einzigen
aufklappbaren Punkt waere das grosses Kino ohne Stueck.

Das mobile Menue bekommt die Teams stattdessen eingerueckt und
aufgeklappt untergestellt. Auf dem Touchscreen gibt es kein
Ueberfahren — mit dem Menue allein waere seit dem Wegfall der Liste
"Meine Teams" gar kein Wechsel mehr moeglich gewesen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
This commit is contained in:
Daniel Michelberger 2026-09-07 22:23:10 +02:00
parent 0f4b532773
commit 65bb7f1f3f
13 changed files with 344 additions and 125 deletions

View file

@ -18,7 +18,7 @@
},
"devDependencies": {
"@internationalized/date": "^3.12.1",
"@lucide/svelte": "^1.16.0",
"@lucide/svelte": "^1.42.0",
"@sveltejs/adapter-node": "^5.5.7",
"@sveltejs/kit": "^2.43.2",
"@sveltejs/vite-plugin-svelte": "^6.2.0",
@ -32,7 +32,7 @@
"svelte": "^5.39.5",
"svelte-check": "^4.3.2",
"tailwind-merge": "^3.3.1",
"tailwind-variants": "^3.2.2",
"tailwind-variants": "^3.3.1",
"tailwindcss": "^4.1.13",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.2",
@ -812,9 +812,9 @@
}
},
"node_modules/@lucide/svelte": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.16.0.tgz",
"integrity": "sha512-AvvPJnaWxeiNkAljI5MsSEc84yHPLMaWQIAJOcbX7k9au/f9ITS7cxTTQiautDiOFKVOXiYdZ+d6mtl88J+Kbg==",
"version": "1.42.0",
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.42.0.tgz",
"integrity": "sha512-9vOi3EdzJht5aD7F5OraoxrK2GyyL1Y+kVXEtIg4+IAdKmDBrwYK2d/x7akC89uA+cvkfjcw+IuzH0vfd/TuzA==",
"dev": true,
"license": "ISC",
"peerDependencies": {
@ -5737,13 +5737,13 @@
}
},
"node_modules/tailwind-variants": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz",
"integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.3.1.tgz",
"integrity": "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16.x",
"node": ">=16.9.x",
"pnpm": ">=7.x"
},
"peerDependencies": {
@ -5753,6 +5753,9 @@
"peerDependenciesMeta": {
"tailwind-merge": {
"optional": true
},
"tailwindcss": {
"optional": true
}
}
},

View file

@ -19,7 +19,7 @@
},
"devDependencies": {
"@internationalized/date": "^3.12.1",
"@lucide/svelte": "^1.16.0",
"@lucide/svelte": "^1.42.0",
"@sveltejs/adapter-node": "^5.5.7",
"@sveltejs/kit": "^2.43.2",
"@sveltejs/vite-plugin-svelte": "^6.2.0",
@ -33,7 +33,7 @@
"svelte": "^5.39.5",
"svelte-check": "^4.3.2",
"tailwind-merge": "^3.3.1",
"tailwind-variants": "^3.2.2",
"tailwind-variants": "^3.3.1",
"tailwindcss": "^4.1.13",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.2",

View file

@ -4,18 +4,28 @@
* Ueberfahren aufklappt: darunter der Wechsel zwischen den eigenen Teams
* und der Weg zu einem neuen.
*
* Der Punkt selbst ist ein Link auf die Teamseite. Ein eigener Eintrag
* „Uebersicht" waere die zweite Tuer zu derselben Seite, und eine
* Ueberschrift ueber drei Teamnamen sagt nichts, was die Namen nicht
* schon sagen.
* Traegt bits-uis NavigationMenu und nicht das DropdownMenu. Ein Dropdown
* ist ein Menue, das auf Ansage aufgeht; eine Navigationsleiste ist etwas
* anderes, und die Unterschiede sind genau die, die hier gebraucht werden:
* Aufklappen beim Ueberfahren samt Verzoegerung, ein Klick, der nach dem
* Hover-Aufklappen nicht wieder zuklappt, kein Fokusfang, keine
* Scrollsperre, kein Fokusring nach dem Zeigerbesuch. Das alles stand hier
* vorher als Handarbeit — Zeitgeber, ueberschriebene Zeigerhandler,
* unterdrueckter Fokus. Die Komponente kann es von sich aus.
*
* Der Punkt selbst bleibt ein Link auf die Teamseite: Der Trigger wird
* ueber das child-Snippet als <a> gezeichnet. bits-ui haelt seinen eigenen
* onclick zurueck, wenn per Zeiger aufgeklappt wurde („if opened via
* pointer move, we prevent the click event"), der Klick landet also beim
* Link.
*
* Auf dem Touchscreen gibt es kein Ueberfahren: Dort fuehrt der Tipp auf
* die Teamseite, und gewechselt wird in deren Liste „Meine Teams".
* die Teamseite, und gewechselt wird in deren Liste.
*/
import { goto } from '$app/navigation'
import { getTeamContext } from '$lib/stores/teams.svelte'
import CreateTeamDialog from '$lib/components/CreateTeamDialog.svelte'
import * as DropdownMenu from '@/components/ui/dropdown-menu'
import * as NavigationMenu from '@/components/ui/navigation-menu'
import { ChevronDown, Plus, Users } from 'lucide-svelte'
let {
@ -26,21 +36,6 @@
const teams = getTeamContext()
let createDialog = $state(false)
let menuOpen = $state(false)
// Zwischen Knopf und aufgeklapptem Menue liegt ein Spalt. Ohne die kurze
// Frist fiele das Menue zu, sobald der Zeiger ihn ueberquert.
let closeTimer: ReturnType<typeof setTimeout> | null = null
function enter() {
if (closeTimer) clearTimeout(closeTimer)
menuOpen = true
}
function leave() {
if (closeTimer) clearTimeout(closeTimer)
closeTimer = setTimeout(() => (menuOpen = false), 150)
}
/**
* Team wechseln heisst hier auch: hinsehen. Der Wechsel allein aendert
@ -52,86 +47,49 @@
goto('/dashboard/team')
onnavigate?.()
}
const itemClass =
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-left transition-colors hover:bg-accent'
</script>
<DropdownMenu.Root bind:open={menuOpen}>
<DropdownMenu.Trigger>
<NavigationMenu.Item value="team">
<NavigationMenu.Trigger>
{#snippet child({ props })}
<!--
Die Zeiger- und Tastenhandler von bits-ui werden bewusst
ueberschrieben: Deren onclick ruft preventDefault und macht
den Link damit tot, und ihr pointerdown klappt das Menue zu,
das der Hover gerade aufgeklappt hat — zwei Herren ueber
dasselbe `open`, das Ergebnis war Flackern statt Navigation.
Hier klappt allein der Hover auf, und der Klick folgt dem
Link wie bei jedem anderen Menuepunkt.
-->
<a
{...props}
href="/dashboard/team"
class={className}
onmouseenter={enter}
onmouseleave={leave}
onpointerdown={undefined}
onpointerup={undefined}
onkeydown={undefined}
onclick={() => {
menuOpen = false
onnavigate?.()
}}
>
<a {...props} href="/dashboard/team" class={className} onclick={() => onnavigate?.()}>
<Users class="h-4 w-4" />
<span>Team</span>
<ChevronDown class="h-3 w-3 opacity-50" />
</a>
{/snippet}
</DropdownMenu.Trigger>
</NavigationMenu.Trigger>
<!--
Aufgeklappt wird beim Ueberfahren, nicht auf Ansage: Das Menue darf
deshalb weder den Fokus an sich ziehen noch das Scrollen sperren —
beides gehoert zu einem Dialog, nicht zu einem Menuepunkt, ueber den
der Zeiger nur streift.
Auch der Fokus beim Schliessen bleibt aus. bits-ui gibt ihn sonst an
den Trigger zurueck, und weil hier der Zeiger schliesst und nicht die
Tastatur, stuende danach ein Fokusring auf dem Menuepunkt — fuer
jemanden, der nur darueber gefahren ist.
-->
<DropdownMenu.Content
align="start"
class="min-w-56"
trapFocus={false}
preventScroll={false}
onOpenAutoFocus={(e) => e.preventDefault()}
onCloseAutoFocus={(e) => e.preventDefault()}
onmouseenter={enter}
onmouseleave={leave}
>
<NavigationMenu.Content class="min-w-56">
{#each teams.records as t (t.id)}
<DropdownMenu.Item
<button
type="button"
onclick={() => switchTo(t.id)}
class={t.id === teams.activeId ? 'bg-accent' : ''}
class="{itemClass} {t.id === teams.activeId ? 'bg-accent' : ''}"
>
<span class="flex-1">{t.name}</span>
<span class="flex-1 truncate">{t.name}</span>
{#if teams.isOwner(t)}
<span class="text-xs text-muted-foreground">Owner</span>
{:else if teams.isAdmin(t)}
<span class="text-xs text-muted-foreground">Admin</span>
{/if}
</DropdownMenu.Item>
</button>
{:else}
<DropdownMenu.Item disabled>Noch in keinem Team</DropdownMenu.Item>
<p class="px-2 py-1.5 text-sm text-muted-foreground">Noch in keinem Team</p>
{/each}
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={() => (createDialog = true)}>
<Plus class="h-4 w-4 mr-2" />
Team erstellen
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<div class="my-1 h-px bg-border"></div>
<!-- Ausserhalb des Menues: mit dem Klick schliesst sich das Dropdown, und ein
darin gerendeter Dialog verschwaende mit ihm. -->
<button type="button" onclick={() => (createDialog = true)} class={itemClass}>
<Plus class="h-4 w-4" />
Team erstellen
</button>
</NavigationMenu.Content>
</NavigationMenu.Item>
<!-- Ausserhalb des Menues: mit dem Klick klappt es zu, und ein darin
gerendeter Dialog verschwaende mit ihm. -->
<CreateTeamDialog bind:open={createDialog} />

View file

@ -0,0 +1,28 @@
import Content from "./navigation-menu-content.svelte";
import Indicator from "./navigation-menu-indicator.svelte";
import Item from "./navigation-menu-item.svelte";
import Link from "./navigation-menu-link.svelte";
import List from "./navigation-menu-list.svelte";
import Trigger from "./navigation-menu-trigger.svelte";
import Viewport from "./navigation-menu-viewport.svelte";
import Root from "./navigation-menu.svelte";
export {
Root,
Content,
Indicator,
Item,
Link,
List,
Trigger,
Viewport,
//
Root as NavigationMenuRoot,
Content as NavigationMenuContent,
Indicator as NavigationMenuIndicator,
Item as NavigationMenuItem,
Link as NavigationMenuLink,
List as NavigationMenuList,
Trigger as NavigationMenuTrigger,
Viewport as NavigationMenuViewport,
};

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.ContentProps = $props();
</script>
<NavigationMenuPrimitive.Content
bind:ref
data-slot="navigation-menu-content"
class={cn(
"p-1 ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:ring-foreground/10 group-data-[viewport=false]/navigation-menu:duration-300 data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95 top-0 left-0 w-full group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none md:absolute md:w-auto",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.IndicatorProps = $props();
</script>
<NavigationMenuPrimitive.Indicator
bind:ref
data-slot="navigation-menu-indicator"
class={cn(
"data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:animate-in data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className
)}
{...restProps}
>
<div class="rounded-tl-sm bg-border shadow-md relative top-[60%] h-2 w-2 rotate-45"></div>
</NavigationMenuPrimitive.Indicator>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.ItemProps = $props();
</script>
<NavigationMenuPrimitive.Item
bind:ref
data-slot="navigation-menu-item"
class={cn("cn-navigation-menu-item relative", className)}
{...restProps}
/>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.LinkProps = $props();
</script>
<NavigationMenuPrimitive.Link
bind:ref
data-slot="navigation-menu-link"
class={cn("flex items-center gap-2 rounded-lg p-2 text-sm transition-all outline-none hover:bg-muted focus:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-1 in-data-[slot=navigation-menu-content]:rounded-md data-active:bg-muted/50 data-active:hover:bg-muted data-active:focus:bg-muted [&_svg:not([class*='size-'])]:size-4", className)}
{...restProps}
/>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.ListProps = $props();
</script>
<NavigationMenuPrimitive.List
bind:ref
data-slot="navigation-menu-list"
class={cn(
"gap-0 group flex flex-1 list-none items-center justify-center",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,29 @@
<script lang="ts" module>
import { tv } from "tailwind-variants";
import { cn } from "$lib/utils.js";
export const navigationMenuTriggerStyle = tv({
base: "rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all hover:bg-muted focus:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-1 disabled:opacity-50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted data-open:bg-muted/50 data-open:hover:bg-muted data-open:focus:bg-muted group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center outline-none disabled:pointer-events-none",
});
</script>
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: NavigationMenuPrimitive.TriggerProps = $props();
</script>
<NavigationMenuPrimitive.Trigger
bind:ref
data-slot="navigation-menu-trigger"
class={cn(navigationMenuTriggerStyle(), "group", className)}
{...restProps}
>
{@render children?.()}
<ChevronDownIcon class="relative top-px ml-1 size-3 transition duration-300 group-data-popup-open/navigation-menu-trigger:rotate-180 group-data-open/navigation-menu-trigger:rotate-180" aria-hidden="true" />
</NavigationMenuPrimitive.Trigger>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.ViewportProps = $props();
</script>
<div class={cn("absolute start-0 top-full isolate z-50 flex justify-center")}>
<NavigationMenuPrimitive.Viewport
bind:ref
data-slot="navigation-menu-viewport"
class={cn(
"rounded-lg bg-popover text-popover-foreground shadow ring-1 ring-foreground/10 duration-100 data-open:animate-in data-open:zoom-in-90 data-closed:animate-out data-closed:zoom-out-90 origin-top-center relative mt-1.5 h-[calc(var(--bits-navigation-menu-viewport-height)+1rem)] w-full overflow-hidden md:w-[calc(var(--bits-navigation-menu-viewport-width)+1rem)]",
className
)}
{...restProps}
/>
</div>

View file

@ -0,0 +1,31 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import NavigationMenuViewport from "./navigation-menu-viewport.svelte";
let {
ref = $bindable(null),
class: className,
viewport = true,
children,
...restProps
}: NavigationMenuPrimitive.RootProps & {
viewport?: boolean;
} = $props();
</script>
<NavigationMenuPrimitive.Root
bind:ref
data-slot="navigation-menu"
data-viewport={viewport}
class={cn(
"max-w-max group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
)}
{...restProps}
>
{@render children?.()}
{#if viewport}
<NavigationMenuViewport />
{/if}
</NavigationMenuPrimitive.Root>

View file

@ -20,9 +20,11 @@
import { Separator } from '@/components/ui/separator'
import Logo from '$lib/components/Logo.svelte'
import TeamMenu from '$lib/components/TeamMenu.svelte'
import CreateTeamDialog from '$lib/components/CreateTeamDialog.svelte'
import * as NavigationMenu from '@/components/ui/navigation-menu'
import Avatar from '$lib/components/Avatar.svelte'
import RunningTimesToast from '$lib/components/RunningTimesToast.svelte'
import { Home, Calendar, LogOut, Menu, X, Moon, Sun, Route, Settings, Users } from 'lucide-svelte'
import { Home, Calendar, LogOut, Menu, X, Moon, Sun, Plus, Route, Settings, Users } from 'lucide-svelte'
import { toggleMode, mode } from 'mode-watcher'
import { onDestroy, onMount } from 'svelte'
@ -47,6 +49,10 @@
let isMobileMenuOpen = $state(false)
// Der Dialog gehoert nicht ins mobile Menue: Das schliesst sich beim
// Klick, und der Dialog verschwaende mit ihm.
let createTeamDialog = $state(false)
onMount(async () => {
if (!auth.user) {
goto('/login', { replaceState: true })
@ -180,24 +186,34 @@
<Logo team={teams.active} class="size-[4.5rem] -my-3" />
</a>
<nav class="hidden md:flex items-center gap-2">
{#each nav as item}
{@const Icon = item.icon}
{#if item.submenu}
<TeamMenu class={navClass(item.href)} />
{:else}
<a
href={item.href}
class={navClass(item.href, dashHover && item.href === '/dashboard')}
onmouseenter={() => item.href === '/dashboard' && (dashHover = true)}
onmouseleave={() => item.href === '/dashboard' && (dashHover = false)}
>
<Icon class="h-4 w-4" />
<span>{item.label}</span>
</a>
{/if}
{/each}
</nav>
<!--
viewport={false}: Das Panel haengt unter seinem
Menuepunkt, statt als animierte Flaeche ueber die
ganze Leiste zu fahren. Bei einem einzigen
aufklappbaren Punkt waere das grosse Kino ohne
Stueck. -->
<NavigationMenu.Root viewport={false} class="hidden md:flex">
<NavigationMenu.List class="items-center gap-2">
{#each nav as item}
{@const Icon = item.icon}
{#if item.submenu}
<TeamMenu class={navClass(item.href)} />
{:else}
<NavigationMenu.Item>
<NavigationMenu.Link
href={item.href}
class={navClass(item.href, dashHover && item.href === '/dashboard')}
onmouseenter={() => item.href === '/dashboard' && (dashHover = true)}
onmouseleave={() => item.href === '/dashboard' && (dashHover = false)}
>
<Icon class="h-4 w-4" />
<span>{item.label}</span>
</NavigationMenu.Link>
</NavigationMenu.Item>
{/if}
{/each}
</NavigationMenu.List>
</NavigationMenu.Root>
</div>
<!-- Rechts: wer man ist, und was man mit sich tun kann.
@ -263,20 +279,54 @@
<nav class="px-2 py-2 space-y-1">
{#each nav as item}
{@const Icon = item.icon}
<a
href={item.href}
onclick={() => (isMobileMenuOpen = false)}
class="w-full {navClass(item.href)}"
>
<Icon class="h-4 w-4" />
<span>{item.label}</span>
</a>
<!--
Auf dem Touchscreen gibt es kein Ueberfahren,
also auch kein Aufklappen. Die Teams stehen
hier eingerueckt unter ihrem Menuepunkt —
sichtbar statt versteckt, und der einzige Weg,
auf dem sich ohne Zeiger wechseln laesst.
-->
{#if item.submenu}
<TeamMenu
class="w-full {navClass(item.href)}"
onnavigate={() => (isMobileMenuOpen = false)}
/>
{:else}
<a
href={item.href}
onclick={() => (isMobileMenuOpen = false)}
class="w-full {navClass(item.href)}"
>
<Icon class="h-4 w-4" />
<span>{item.label}</span>
</a>
<div class="ml-6 space-y-1 border-l pl-3">
{#each teams.records as t (t.id)}
<button
type="button"
onclick={() => {
teams.setActive(t.id)
isMobileMenuOpen = false
}}
class="flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-accent {t.id ===
teams.activeId
? 'font-medium text-foreground'
: 'text-muted-foreground'}"
>
<span class="flex-1 truncate">{t.name}</span>
{#if t.id === teams.activeId}
<span class="text-xs text-muted-foreground">aktiv</span>
{/if}
</button>
{/each}
<button
type="button"
onclick={() => {
isMobileMenuOpen = false
createTeamDialog = true
}}
class="flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-accent"
>
<Plus class="h-4 w-4" />
Team erstellen
</button>
</div>
{/if}
{/each}
<Separator class="my-2" />
@ -313,5 +363,7 @@
<!-- Laufende Zeiten bleiben auf jeder Dashboard-Seite im Blick. -->
<RunningTimesToast />
<CreateTeamDialog bind:open={createTeamDialog} />
</div>
{/if}