From 65bb7f1f3f428f7d495f4128c3f2db1f13ea1cb4 Mon Sep 17 00:00:00 2001 From: Daniel Michelberger Date: Mon, 7 Sep 2026 22:23:10 +0200 Subject: [PATCH] refactor: Kopfnavigation auf bits-uis NavigationMenu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr --- frontend/package-lock.json | 21 +-- frontend/package.json | 4 +- frontend/src/lib/components/TeamMenu.svelte | 122 ++++++------------ .../components/ui/navigation-menu/index.ts | 28 ++++ .../navigation-menu-content.svelte | 20 +++ .../navigation-menu-indicator.svelte | 22 ++++ .../navigation-menu-item.svelte | 17 +++ .../navigation-menu-link.svelte | 17 +++ .../navigation-menu-list.svelte | 20 +++ .../navigation-menu-trigger.svelte | 29 +++++ .../navigation-menu-viewport.svelte | 22 ++++ .../ui/navigation-menu/navigation-menu.svelte | 31 +++++ frontend/src/routes/dashboard/+layout.svelte | 116 ++++++++++++----- 13 files changed, 344 insertions(+), 125 deletions(-) create mode 100644 frontend/src/lib/components/ui/navigation-menu/index.ts create mode 100644 frontend/src/lib/components/ui/navigation-menu/navigation-menu-content.svelte create mode 100644 frontend/src/lib/components/ui/navigation-menu/navigation-menu-indicator.svelte create mode 100644 frontend/src/lib/components/ui/navigation-menu/navigation-menu-item.svelte create mode 100644 frontend/src/lib/components/ui/navigation-menu/navigation-menu-link.svelte create mode 100644 frontend/src/lib/components/ui/navigation-menu/navigation-menu-list.svelte create mode 100644 frontend/src/lib/components/ui/navigation-menu/navigation-menu-trigger.svelte create mode 100644 frontend/src/lib/components/ui/navigation-menu/navigation-menu-viewport.svelte create mode 100644 frontend/src/lib/components/ui/navigation-menu/navigation-menu.svelte diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 215fb91..0846f82 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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 } } }, diff --git a/frontend/package.json b/frontend/package.json index 2c27b66..8b768e0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/lib/components/TeamMenu.svelte b/frontend/src/lib/components/TeamMenu.svelte index 72ff5e9..6bb86a4 100644 --- a/frontend/src/lib/components/TeamMenu.svelte +++ b/frontend/src/lib/components/TeamMenu.svelte @@ -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 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 | 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' - - + + {#snippet child({ props })} - - { - menuOpen = false - onnavigate?.() - }} - > + onnavigate?.()}> Team {/snippet} - + - - e.preventDefault()} - onCloseAutoFocus={(e) => e.preventDefault()} - onmouseenter={enter} - onmouseleave={leave} - > + {#each teams.records as t (t.id)} - switchTo(t.id)} - class={t.id === teams.activeId ? 'bg-accent' : ''} + class="{itemClass} {t.id === teams.activeId ? 'bg-accent' : ''}" > - {t.name} + {t.name} {#if teams.isOwner(t)} Owner {:else if teams.isAdmin(t)} Admin {/if} - + {:else} - Noch in keinem Team +

Noch in keinem Team

{/each} - - (createDialog = true)}> - - Team erstellen - -
- +
- + + + + + diff --git a/frontend/src/lib/components/ui/navigation-menu/index.ts b/frontend/src/lib/components/ui/navigation-menu/index.ts new file mode 100644 index 0000000..208cb93 --- /dev/null +++ b/frontend/src/lib/components/ui/navigation-menu/index.ts @@ -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, +}; diff --git a/frontend/src/lib/components/ui/navigation-menu/navigation-menu-content.svelte b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-content.svelte new file mode 100644 index 0000000..0b6b6c5 --- /dev/null +++ b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/frontend/src/lib/components/ui/navigation-menu/navigation-menu-indicator.svelte b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-indicator.svelte new file mode 100644 index 0000000..f4cf060 --- /dev/null +++ b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-indicator.svelte @@ -0,0 +1,22 @@ + + + +
+
diff --git a/frontend/src/lib/components/ui/navigation-menu/navigation-menu-item.svelte b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-item.svelte new file mode 100644 index 0000000..4760458 --- /dev/null +++ b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-item.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/navigation-menu/navigation-menu-link.svelte b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-link.svelte new file mode 100644 index 0000000..742d9bd --- /dev/null +++ b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-link.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/navigation-menu/navigation-menu-list.svelte b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-list.svelte new file mode 100644 index 0000000..6f36137 --- /dev/null +++ b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-list.svelte @@ -0,0 +1,20 @@ + + + diff --git a/frontend/src/lib/components/ui/navigation-menu/navigation-menu-trigger.svelte b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-trigger.svelte new file mode 100644 index 0000000..64e269a --- /dev/null +++ b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-trigger.svelte @@ -0,0 +1,29 @@ + + + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/navigation-menu/navigation-menu-viewport.svelte b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-viewport.svelte new file mode 100644 index 0000000..b2ce064 --- /dev/null +++ b/frontend/src/lib/components/ui/navigation-menu/navigation-menu-viewport.svelte @@ -0,0 +1,22 @@ + + +
+ +
diff --git a/frontend/src/lib/components/ui/navigation-menu/navigation-menu.svelte b/frontend/src/lib/components/ui/navigation-menu/navigation-menu.svelte new file mode 100644 index 0000000..27cd0d6 --- /dev/null +++ b/frontend/src/lib/components/ui/navigation-menu/navigation-menu.svelte @@ -0,0 +1,31 @@ + + + + {@render children?.()} + {#if viewport} + + {/if} + diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 8a350e1..1d48acf 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -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 @@ - + + {#if item.submenu} - (isMobileMenuOpen = false)} - /> - {:else} - (isMobileMenuOpen = false)} - class="w-full {navClass(item.href)}" - > - - {item.label} - +
+ {#each teams.records as t (t.id)} + + {/each} + +
{/if} {/each} @@ -313,5 +363,7 @@ + + {/if}