feat: Oberflaechenbausteine, Anwendungsrahmen und Konventionen
Basisbausteine im shadcn/ui-Stil, aber mit drei bewussten Abweichungen: - Bedienelemente sind 44px hoch statt 36px. Ich bediene die App oft am Handy im Heim oder auf dem Amt; kleiner ist dort nicht treffsicher. - Auswahlfelder benutzen das native <select>. Android und iOS oeffnen dafuer ihre eigene Liste - schneller als jeder Nachbau, und mit Screenreader und Tastatur funktioniert es ohne Zutun. - Eingabefelder haben mindestens 16px Schrift, sonst zoomt iOS-Safari beim Hineintippen das Formular heran. <Feld> verdrahtet Beschriftung, Erklaerung und Fehler ueber aria-describedby. Der Erklaerungstext ist kein Beiwerk: die Anwendung benutzt Begriffe aus dem Betreuungsrecht, die niemand nachschlagen soll. <Rechtshinweis> ist der stehende Vermerk, dass hier keine Rechtsberatung stattfindet. Er steht an jeder Stelle mit Paragraphenbezug. Der Anwendungsrahmen fuehrt drei Bedingungen zusammen, ohne die niemand an Fachdaten kommt: angemeldet, zweiter Faktor eingerichtet, mindestens eine betreute Person zugeordnet. Die Navigation zeigt Zaehler fuer ueberfaellige Fristen und neuen Posteingang - die Frage "wo muss ich hin?" wird schon im Menue beantwortet. Das Farbschema wird per Inline-Skript mit CSP-Nonce gesetzt, bevor die Seite gezeichnet wird - sonst blitzt beim Laden das helle Thema auf. docs/KONVENTIONEN.md haelt fest, was in allen weiteren Etappen gilt: Verzeichnisaufbau, Cent-Betraege, Umgang mit verschluesselten Feldern, Zugriffspruefung, Audit-Pflicht, Ampelfarben, Commit-Trailer. Etappe: 1 Phase: Fundament
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { signOut } from 'next-auth/react'
|
||||
import { LogOut, Monitor, Moon, Settings, ShieldCheck, Sun, User as UserIcon } from 'lucide-react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { initialen } from '@/lib/utils'
|
||||
|
||||
type Thema = 'hell' | 'dunkel' | 'system'
|
||||
|
||||
/**
|
||||
* Benutzermenue mit Themenwahl und Abmelden.
|
||||
*
|
||||
* Das Thema wird im localStorage gemerkt und sofort auf <html> gesetzt -
|
||||
* dieselbe Logik wie im Startskript im Layout, damit es beim naechsten
|
||||
* Laden nicht flackert.
|
||||
*/
|
||||
export function BenutzerMenue({
|
||||
name,
|
||||
email,
|
||||
rolle,
|
||||
}: {
|
||||
name: string
|
||||
email: string
|
||||
rolle: string
|
||||
}) {
|
||||
const [thema, setThema] = React.useState<Thema>('system')
|
||||
|
||||
React.useEffect(() => {
|
||||
const gespeichert = (localStorage.getItem('bc-thema') as Thema | null) ?? 'system'
|
||||
setThema(gespeichert)
|
||||
}, [])
|
||||
|
||||
function themaSetzen(neu: Thema) {
|
||||
setThema(neu)
|
||||
try {
|
||||
localStorage.setItem('bc-thema', neu)
|
||||
} catch {
|
||||
/* Speicher gesperrt - dann gilt die Wahl nur fuer diese Sitzung. */
|
||||
}
|
||||
const dunkel =
|
||||
neu === 'dunkel' ||
|
||||
(neu === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
document.documentElement.classList.toggle('dark', dunkel)
|
||||
document.documentElement.style.colorScheme = dunkel ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="w-full justify-start gap-3 px-2">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="grid size-8 shrink-0 place-items-center rounded-full bg-secondary text-xs font-semibold text-secondary-foreground"
|
||||
>
|
||||
{initialen(name)}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 text-left lg:block">
|
||||
<span className="block truncate text-sm font-medium">{name}</span>
|
||||
<span className="block truncate text-xs text-muted-foreground">{rolle}</span>
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end" className="w-64">
|
||||
<DropdownMenuLabel>
|
||||
<div className="truncate font-medium">{name}</div>
|
||||
<div className="truncate text-xs font-normal text-muted-foreground">{email}</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/konto">
|
||||
<UserIcon className="size-4" />
|
||||
Mein Konto
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/konto/zwei-faktor">
|
||||
<ShieldCheck className="size-4" />
|
||||
Zweiter Faktor
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/einstellungen">
|
||||
<Settings className="size-4" />
|
||||
Einstellungen
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
|
||||
Darstellung
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem onSelect={() => themaSetzen('hell')}>
|
||||
<Sun className="size-4" />
|
||||
Hell
|
||||
{thema === 'hell' && <span className="ml-auto text-xs">✓</span>}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => themaSetzen('dunkel')}>
|
||||
<Moon className="size-4" />
|
||||
Dunkel
|
||||
{thema === 'dunkel' && <span className="ml-auto text-xs">✓</span>}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => themaSetzen('system')}>
|
||||
<Monitor className="size-4" />
|
||||
Wie das System
|
||||
{thema === 'system' && <span className="ml-auto text-xs">✓</span>}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={() => void signOut({ callbackUrl: '/anmelden' })}>
|
||||
<LogOut className="size-4" />
|
||||
Abmelden
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user