feat: Team wird zum Menuepunkt mit Submenue
Das Teamdropdown stand als eigenes Bedienelement neben dem Namen — ein zweiter Schalter fuer eine Sache, die schon einen Menuepunkt hat. Jetzt klappt der Menuepunkt Team beim Ueberfahren auf: die eigenen Teams, ein Klick wechselt und oeffnet die Teamseite, darunter "Team erstellen". Die Zeiger- und Tastenhandler von bits-ui werden am Trigger bewusst ueberschrieben. Deren onclick ruft preventDefault und macht den Link tot, ihr pointerdown klappt zu, was der Hover gerade aufgeklappt hat — zwei Herren ueber dasselbe open, das Ergebnis war Flackern statt Navigation. Das Menue verzichtet zudem auf Fokusfang und Scrollsperre; beides gehoert zu einem Dialog, nicht zu etwas, ueber das der Zeiger nur streift. Der Anlegedialog liegt jetzt als CreateTeamDialog vor: Es gibt zwei Einstiege, und ein neues Team muss an beiden dasselbe bedeuten — Team anlegen, Standardsatz Flag-Typen dazu, ab dann ist es das aktive. Das Logo ragt mit -my-3 leicht ueber die Kopfleiste hinaus, verlinkt aufs Dashboard und teilt sich den Hover mit dessen Menuepunkt. Der aktive Menuepunkt traegt das Primary-Design. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
This commit is contained in:
parent
45835cd45c
commit
66e351e54f
4 changed files with 313 additions and 98 deletions
76
frontend/src/lib/components/CreateTeamDialog.svelte
Normal file
76
frontend/src/lib/components/CreateTeamDialog.svelte
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Dialog „Neues Team". Liegt als eigene Komponente vor, weil es zwei
|
||||||
|
* Einstiege gibt — den Teamwechsler im Kopf und die Teamseite — und ein
|
||||||
|
* neues Team an beiden Stellen dasselbe bedeuten muss: Team anlegen, den
|
||||||
|
* Standardsatz Flag-Typen dazu, und ab dann ist es das aktive Team.
|
||||||
|
*
|
||||||
|
* Ein Team ohne Flag-Typen kann zwar Marker aufnehmen, aber nichts
|
||||||
|
* einordnen. Deshalb kommt der Standardsatz gleich mit — änderbar und,
|
||||||
|
* wie alle Flag-Typen, allein Sache dieses Teams.
|
||||||
|
*/
|
||||||
|
import { getTeamContext } from '$lib/stores/teams.svelte'
|
||||||
|
import { getTrailFlagContext } from '$lib/stores/trailFlags.svelte'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import * as Dialog from '@/components/ui/dialog'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
|
||||||
|
let { open = $bindable(false) }: { open?: boolean } = $props()
|
||||||
|
|
||||||
|
const teams = getTeamContext()
|
||||||
|
const flags = getTrailFlagContext()
|
||||||
|
|
||||||
|
let name = $state('')
|
||||||
|
let error = $state<string | null>(null)
|
||||||
|
let busy = $state(false)
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!name.trim() || busy) return
|
||||||
|
|
||||||
|
busy = true
|
||||||
|
error = null
|
||||||
|
try {
|
||||||
|
const created = await teams.create(name.trim())
|
||||||
|
|
||||||
|
try {
|
||||||
|
await flags.seedDefaults(created.id)
|
||||||
|
} catch (e: any) {
|
||||||
|
error =
|
||||||
|
'Das Team wurde angelegt, die Standard-Flag-Typen aber nicht: ' +
|
||||||
|
(e.message ?? 'unbekannter Fehler') +
|
||||||
|
' Du kannst sie unter „Flag-Typen" nachholen.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
open = false
|
||||||
|
name = ''
|
||||||
|
} catch (e: any) {
|
||||||
|
error = e.message ?? 'Das Team konnte nicht angelegt werden.'
|
||||||
|
} finally {
|
||||||
|
busy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog.Root bind:open>
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Neues Team</Dialog.Title>
|
||||||
|
<Dialog.Description>Du wirst automatisch Owner.</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="team-name">Name</Label>
|
||||||
|
<Input id="team-name" bind:value={name} placeholder="Mein Team" onkeydown={(e) => e.key === 'Enter' && submit()} />
|
||||||
|
</div>
|
||||||
|
{#if error}
|
||||||
|
<p class="text-sm text-destructive">{error}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button variant="ghost" onclick={() => (open = false)}>Abbrechen</Button>
|
||||||
|
<Button onclick={submit} disabled={!name.trim() || busy}>Erstellen</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
@ -18,11 +18,11 @@
|
||||||
const url = $derived(team?.logo ? getFileURL(team, team.logo) : null)
|
const url = $derived(team?.logo ? getFileURL(team, team.logo) : null)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative {className}" id="logo">
|
<div class="relative {className} hover:scale-110 hover:rotate-3 transition-transform" id="logo">
|
||||||
{#if url}
|
{#if url}
|
||||||
<img alt="" class="absolute inset-0 h-full w-full object-contain" src={url} />
|
<img alt="" class="absolute h-full w-full object-contain" src={url} />
|
||||||
{:else}
|
{:else}
|
||||||
<img alt="" class="absolute inset-0 h-full w-full object-contain" src="/schildl.svg">
|
<img alt="" class="absolute h-full w-full object-contain" src="/schildl.svg">
|
||||||
<img alt="" class="absolute inset-0 h-full w-full scale-110 object-contain" src="/radl.svg">
|
<img alt="" class="absolute h-full w-full scale-110 object-contain" src="/radl.svg">
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,110 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Der Menuepunkt „Team" in der Kopfleiste — ein Menuepunkt, der sich beim
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Auf dem Touchscreen gibt es kein Ueberfahren: Dort fuehrt der Tipp auf
|
||||||
|
* die Teamseite, und gewechselt wird in deren Liste „Meine Teams".
|
||||||
|
*/
|
||||||
|
import { goto } from '$app/navigation'
|
||||||
import { getTeamContext } from '$lib/stores/teams.svelte'
|
import { getTeamContext } from '$lib/stores/teams.svelte'
|
||||||
import { Button } from '@/components/ui/button'
|
import CreateTeamDialog from '$lib/components/CreateTeamDialog.svelte'
|
||||||
import * as DropdownMenu from '@/components/ui/dropdown-menu'
|
import * as DropdownMenu from '@/components/ui/dropdown-menu'
|
||||||
import { ChevronDown, Plus, Users } from 'lucide-svelte'
|
import { ChevronDown, Plus, Users } from 'lucide-svelte'
|
||||||
import { goto } from '$app/navigation'
|
|
||||||
|
let {
|
||||||
|
class: className = '',
|
||||||
|
onnavigate,
|
||||||
|
}: { class?: string; onnavigate?: () => void } = $props()
|
||||||
|
|
||||||
const teams = getTeamContext()
|
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
|
||||||
|
* bloss still, was jede Liste zeigt — die Teamseite macht sichtbar,
|
||||||
|
* worauf man gewechselt hat.
|
||||||
|
*/
|
||||||
|
function switchTo(id: string) {
|
||||||
|
teams.setActive(id)
|
||||||
|
goto('/dashboard/team')
|
||||||
|
onnavigate?.()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root bind:open={menuOpen}>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<Button {...props} variant="outline" size="sm" class="gap-2">
|
<!--
|
||||||
|
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?.()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Users class="h-4 w-4" />
|
<Users class="h-4 w-4" />
|
||||||
<span class="font-medium">{teams.active?.name ?? 'Kein Team'}</span>
|
<span>Team</span>
|
||||||
<ChevronDown class="h-3 w-3 opacity-50" />
|
<ChevronDown class="h-3 w-3 opacity-50" />
|
||||||
</Button>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content align="end" class="min-w-56">
|
|
||||||
<DropdownMenu.Label>Teams wechseln</DropdownMenu.Label>
|
<!--
|
||||||
{#each teams.records as t}
|
Aufgeklappt wird beim Ueberfahren, nicht auf Ansage: Das Menue darf
|
||||||
{@const isMe = teams.isOwner(t) || teams.isAdmin(t)}
|
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.
|
||||||
|
-->
|
||||||
|
<DropdownMenu.Content
|
||||||
|
align="start"
|
||||||
|
class="min-w-56"
|
||||||
|
trapFocus={false}
|
||||||
|
preventScroll={false}
|
||||||
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
|
onmouseenter={enter}
|
||||||
|
onmouseleave={leave}
|
||||||
|
>
|
||||||
|
{#each teams.records as t (t.id)}
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onclick={() => teams.setActive(t.id)}
|
onclick={() => switchTo(t.id)}
|
||||||
class={t.id === teams.activeId ? 'bg-accent' : ''}
|
class={t.id === teams.activeId ? 'bg-accent' : ''}
|
||||||
>
|
>
|
||||||
<span class="flex-1">{t.name}</span>
|
<span class="flex-1">{t.name}</span>
|
||||||
|
|
@ -33,11 +114,18 @@
|
||||||
<span class="text-xs text-muted-foreground">Admin</span>
|
<span class="text-xs text-muted-foreground">Admin</span>
|
||||||
{/if}
|
{/if}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
{:else}
|
||||||
|
<DropdownMenu.Item disabled>Noch in keinem Team</DropdownMenu.Item>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<DropdownMenu.Item onclick={() => goto('/dashboard/settings/team')}>
|
<DropdownMenu.Item onclick={() => (createDialog = true)}>
|
||||||
<Users class="h-4 w-4 mr-2" />
|
<Plus class="h-4 w-4 mr-2" />
|
||||||
Teams verwalten
|
Team erstellen
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
|
<!-- Ausserhalb des Menues: mit dem Klick schliesst sich das Dropdown, und ein
|
||||||
|
darin gerendeter Dialog verschwaende mit ihm. -->
|
||||||
|
<CreateTeamDialog bind:open={createDialog} />
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { auth } from '$lib/stores/pocketbase.svelte'
|
import { auth } from '$lib/stores/pocketbase.svelte'
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { app, tooltip } from '$lib/stores/app.svelte'
|
import { page } from '$app/state'
|
||||||
|
import { tooltip } from '$lib/stores/app.svelte'
|
||||||
import { setTeamContext } from '$lib/stores/teams.svelte'
|
import { setTeamContext } from '$lib/stores/teams.svelte'
|
||||||
|
import { setTeamInviteContext } from '$lib/stores/teamInvites.svelte'
|
||||||
import { setEventContext } from '$lib/stores/events.svelte'
|
import { setEventContext } from '$lib/stores/events.svelte'
|
||||||
import { setRunContext } from '$lib/stores/runs.svelte'
|
import { setRunContext } from '$lib/stores/runs.svelte'
|
||||||
import { setRiderContext } from '$lib/stores/riders.svelte'
|
import { setRiderContext } from '$lib/stores/riders.svelte'
|
||||||
|
|
@ -17,10 +19,10 @@
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import Logo from '$lib/components/Logo.svelte'
|
import Logo from '$lib/components/Logo.svelte'
|
||||||
import TeamSwitcher from '$lib/components/TeamSwitcher.svelte'
|
import TeamMenu from '$lib/components/TeamMenu.svelte'
|
||||||
import Avatar from '$lib/components/Avatar.svelte'
|
import Avatar from '$lib/components/Avatar.svelte'
|
||||||
import RunningTimesToast from '$lib/components/RunningTimesToast.svelte'
|
import RunningTimesToast from '$lib/components/RunningTimesToast.svelte'
|
||||||
import { Home, Calendar, LogOut, Menu, X, Moon, Sun, Route, Settings, Bike } from 'lucide-svelte'
|
import { Home, Calendar, LogOut, Menu, X, Moon, Sun, Route, Settings, Users } from 'lucide-svelte'
|
||||||
import { toggleMode, mode } from 'mode-watcher'
|
import { toggleMode, mode } from 'mode-watcher'
|
||||||
import { onDestroy, onMount } from 'svelte'
|
import { onDestroy, onMount } from 'svelte'
|
||||||
|
|
||||||
|
|
@ -29,6 +31,7 @@
|
||||||
// Teams MUSS vor den anderen Stores stehen, weil die anderen Stores
|
// Teams MUSS vor den anderen Stores stehen, weil die anderen Stores
|
||||||
// getTeamContext() im Constructor aufrufen
|
// getTeamContext() im Constructor aufrufen
|
||||||
const teams = setTeamContext()
|
const teams = setTeamContext()
|
||||||
|
const invites = setTeamInviteContext()
|
||||||
const events = setEventContext()
|
const events = setEventContext()
|
||||||
const runs = setRunContext()
|
const runs = setRunContext()
|
||||||
const riders = setRiderContext()
|
const riders = setRiderContext()
|
||||||
|
|
@ -54,9 +57,11 @@
|
||||||
teams.subscribe()
|
teams.subscribe()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
events.load(), runs.load(), riders.load(), times.load(), participants.load(), series.load(),
|
events.load(), runs.load(), riders.load(), times.load(), participants.load(), series.load(),
|
||||||
|
invites.load(),
|
||||||
trails.load(), trailVersions.load(), trailFlags.load(), trailMarkers.load(),
|
trails.load(), trailVersions.load(), trailFlags.load(), trailMarkers.load(),
|
||||||
trailComments.load(),
|
trailComments.load(),
|
||||||
])
|
])
|
||||||
|
invites.subscribe()
|
||||||
events.subscribe()
|
events.subscribe()
|
||||||
runs.subscribe()
|
runs.subscribe()
|
||||||
riders.subscribe()
|
riders.subscribe()
|
||||||
|
|
@ -72,6 +77,7 @@
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
teams.destroy()
|
teams.destroy()
|
||||||
|
invites.destroy()
|
||||||
events.destroy()
|
events.destroy()
|
||||||
runs.destroy()
|
runs.destroy()
|
||||||
riders.destroy()
|
riders.destroy()
|
||||||
|
|
@ -85,25 +91,54 @@
|
||||||
trailComments.destroy()
|
trailComments.destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleLogout() {
|
// Ohne Rückfrage: Abmelden löscht nichts und ist mit einem Login wieder
|
||||||
app.confirm.request({
|
// rückgängig gemacht — eine Bestätigung wäre nur ein zusätzlicher Klick.
|
||||||
title: 'Abmelden?',
|
async function handleLogout() {
|
||||||
text: 'Möchten Sie sich wirklich abmelden?',
|
|
||||||
yes: async () => {
|
|
||||||
await auth.logout()
|
await auth.logout()
|
||||||
app.confirm.close()
|
|
||||||
goto('/login')
|
goto('/login')
|
||||||
},
|
|
||||||
no: () => app.confirm.close(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ein Menuepunkt sieht aus wie der andere, ob Link oder aufklappbar.
|
||||||
|
const navItemClass =
|
||||||
|
'inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors'
|
||||||
|
const navIdleClass = 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||||
|
const navHoverClass = 'text-foreground bg-accent'
|
||||||
|
const navActiveClass = 'bg-primary text-primary-foreground hover:bg-primary/90'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wo man gerade steht. „/dashboard" nur genau dort — sonst waere das
|
||||||
|
* Dashboard auf jeder Unterseite mit-aktiv, weil alle Pfade damit
|
||||||
|
* anfangen.
|
||||||
|
*/
|
||||||
|
function isActive(href: string) {
|
||||||
|
const path = page.url.pathname
|
||||||
|
return href === '/dashboard' ? path === '/dashboard' : path.startsWith(href)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die vollstaendige Klassenliste eines Menuepunkts. Bewusst eine
|
||||||
|
* Funktion und kein zusaetzliches `class:`: `text-foreground` und
|
||||||
|
* `text-muted-foreground` sind dieselbe Utility-Gruppe — stuenden beide
|
||||||
|
* am Element, entschiede die Reihenfolge im Stylesheet und nicht die im
|
||||||
|
* Attribut.
|
||||||
|
*/
|
||||||
|
function navClass(href: string, hovered = false) {
|
||||||
|
if (isActive(href)) return `${navItemClass} ${navActiveClass}`
|
||||||
|
return `${navItemClass} ${hovered ? navHoverClass : navIdleClass}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logo und Menuepunkt „Dashboard" fuehren an dieselbe Stelle, also
|
||||||
|
// reagieren sie gemeinsam — egal, welches von beiden man ueberfaehrt.
|
||||||
|
// Ueber CSS ginge das nicht: Der Menuepunkt steckt in der <nav>, das Logo
|
||||||
|
// daneben, und peer-hover reicht nur an Geschwister, nicht in sie hinein.
|
||||||
|
let dashHover = $state(false)
|
||||||
|
|
||||||
// Zeitnahme und Ergebnisse haben keine eigenen Seiten mehr: Beides
|
// Zeitnahme und Ergebnisse haben keine eigenen Seiten mehr: Beides
|
||||||
// passiert dort, wo es hingehört — am Run innerhalb seines Events.
|
// passiert dort, wo es hingehört — am Run innerhalb seines Events.
|
||||||
const nav = [
|
const nav = [
|
||||||
{ href: '/dashboard', icon: Home, label: 'Dashboard' },
|
{ href: '/dashboard', icon: Home, label: 'Dashboard' },
|
||||||
{ href: '/dashboard/events', icon: Calendar, label: 'Events' },
|
{ href: '/dashboard/events', icon: Calendar, label: 'Events' },
|
||||||
{ href: '/dashboard/riders', icon: Bike, label: 'Fahrer' },
|
{ href: '/dashboard/team', icon: Users, label: 'Team', submenu: true },
|
||||||
{ href: '/dashboard/trails', icon: Route, label: 'Trails' },
|
{ href: '/dashboard/trails', icon: Route, label: 'Trails' },
|
||||||
{ href: '/dashboard/settings', icon: Settings, label: 'Einstellungen' },
|
{ href: '/dashboard/settings', icon: Settings, label: 'Einstellungen' },
|
||||||
]
|
]
|
||||||
|
|
@ -121,38 +156,53 @@
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="pointer-events-none fixed -bottom-24 -right-24 z-0 select-none opacity-[0.04] dark:opacity-[0.07]"
|
class="pointer-events-none fixed -bottom-24 -right-24 z-0 select-none opacity-[0.04] dark:opacity-[0.07]"
|
||||||
>
|
>
|
||||||
|
|
||||||
<Logo team={teams.active} class="size-[32rem] max-w-[70vw] rotate-12" />
|
<Logo team={teams.active} class="size-[32rem] max-w-[70vw] rotate-12" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<header class="relative z-10 border-b bg-card">
|
<header class="relative z-10 border-b bg-card">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="flex min-h-16 items-center justify-between gap-4 py-2">
|
<div class="flex min-h-16 items-center justify-between gap-4 py-2">
|
||||||
<!-- Links: Logo und Menü. Das Logo fuellt seine Box, in
|
<!-- Links: Logo und Menü. Das negative -my-3 frisst das
|
||||||
voller Groesse wuerde es die 64 px hohe Leiste
|
py-2 der Leiste auf und noch etwas mehr: Das Logo
|
||||||
ueberragen und beschnitten wirken. -->
|
steht dadurch 72 px hoch in einer 64 px hohen Leiste,
|
||||||
|
ragt oben und unten leicht darueber hinaus und wirkt
|
||||||
|
praesenter, ohne den Header hoeher zu machen. -->
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<Logo team={teams.active} class="h-11 w-11 shrink-0" />
|
<a
|
||||||
|
href="/dashboard"
|
||||||
|
class="shrink-0 transition-transform relative duration-200 top-2"
|
||||||
|
class:scale-105={dashHover}
|
||||||
|
onmouseenter={() => (dashHover = true)}
|
||||||
|
onmouseleave={() => (dashHover = false)}
|
||||||
|
>
|
||||||
|
<Logo team={teams.active} class="size-[4.5rem] -my-3" />
|
||||||
|
</a>
|
||||||
|
|
||||||
<nav class="hidden md:flex items-center gap-1">
|
<nav class="hidden md:flex items-center gap-2">
|
||||||
{#each nav as item}
|
{#each nav as item}
|
||||||
{@const Icon = item.icon}
|
{@const Icon = item.icon}
|
||||||
|
{#if item.submenu}
|
||||||
|
<TeamMenu class={navClass(item.href)} />
|
||||||
|
{:else}
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
href={item.href}
|
||||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
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" />
|
<Icon class="h-4 w-4" />
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</a>
|
</a>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rechts eine Spalte: Name samt Design und Logout in einer
|
<!-- Rechts: wer man ist, und was man mit sich tun kann.
|
||||||
Zeile, das Team-Dropdown darunter. -->
|
Das Team steht jetzt im Menue links. -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Wer man ist, steht links; was man tun kann, rechts. -->
|
<div class="hidden md:flex min-w-0 items-center gap-3">
|
||||||
<div class="hidden md:flex min-w-56 flex-col gap-1">
|
|
||||||
<div class="flex items-center justify-between gap-3">
|
|
||||||
<a
|
<a
|
||||||
href="/dashboard/settings"
|
href="/dashboard/settings"
|
||||||
class="flex min-w-0 items-center gap-2"
|
class="flex min-w-0 items-center gap-2"
|
||||||
|
|
@ -190,11 +240,6 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex">
|
|
||||||
<TeamSwitcher />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
|
@ -217,14 +262,21 @@
|
||||||
<nav class="px-2 py-2 space-y-1">
|
<nav class="px-2 py-2 space-y-1">
|
||||||
{#each nav as item}
|
{#each nav as item}
|
||||||
{@const Icon = item.icon}
|
{@const Icon = item.icon}
|
||||||
|
{#if item.submenu}
|
||||||
|
<TeamMenu
|
||||||
|
class="w-full {navClass(item.href)}"
|
||||||
|
onnavigate={() => (isMobileMenuOpen = false)}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onclick={() => (isMobileMenuOpen = false)}
|
onclick={() => (isMobileMenuOpen = false)}
|
||||||
class="flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent"
|
class="w-full {navClass(item.href)}"
|
||||||
>
|
>
|
||||||
<Icon class="h-4 w-4" />
|
<Icon class="h-4 w-4" />
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</a>
|
</a>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
<Separator class="my-2" />
|
<Separator class="my-2" />
|
||||||
<div class="flex items-center justify-between gap-2 px-3 py-2">
|
<div class="flex items-center justify-between gap-2 px-3 py-2">
|
||||||
|
|
@ -235,7 +287,6 @@
|
||||||
{auth.user.name || auth.user.email}
|
{auth.user.name || auth.user.email}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<TeamSwitcher />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
<Button variant="ghost" size="icon" onclick={toggleMode} tooltip="Design wechseln">
|
<Button variant="ghost" size="icon" onclick={toggleMode} tooltip="Design wechseln">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue