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
92 lines
2.7 KiB
TypeScript
92 lines
2.7 KiB
TypeScript
import * as React from 'react'
|
|
import Link from 'next/link'
|
|
import { ChevronRight, Inbox } from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
/**
|
|
* Kopfzeile einer Seite: Ueberschrift, Erklaerung, Aktionen.
|
|
*
|
|
* Die Erklaerung beantwortet moeglichst die Frage "was muss ich hier als
|
|
* Naechstes tun?" - nicht "was steht hier".
|
|
*/
|
|
export function SeitenKopf({
|
|
titel,
|
|
beschreibung,
|
|
aktionen,
|
|
pfad,
|
|
className,
|
|
}: {
|
|
titel: string
|
|
beschreibung?: React.ReactNode
|
|
aktionen?: React.ReactNode
|
|
pfad?: { titel: string; href?: string }[]
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<div className={cn('space-y-3', className)}>
|
|
{pfad && pfad.length > 0 && (
|
|
<nav aria-label="Pfad">
|
|
<ol className="flex flex-wrap items-center gap-1 text-sm text-muted-foreground">
|
|
{pfad.map((eintrag, i) => (
|
|
<li key={`${eintrag.titel}-${i}`} className="flex items-center gap-1">
|
|
{i > 0 && <ChevronRight className="size-3.5" aria-hidden="true" />}
|
|
{eintrag.href ? (
|
|
<Link href={eintrag.href} className="hover:text-foreground hover:underline">
|
|
{eintrag.titel}
|
|
</Link>
|
|
) : (
|
|
<span aria-current="page">{eintrag.titel}</span>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</nav>
|
|
)}
|
|
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
<div className="min-w-0 space-y-1">
|
|
<h1 className="text-2xl font-semibold tracking-tight">{titel}</h1>
|
|
{beschreibung && (
|
|
<div className="max-w-2xl text-muted-foreground">{beschreibung}</div>
|
|
)}
|
|
</div>
|
|
{aktionen && <div className="flex shrink-0 flex-wrap gap-2">{aktionen}</div>}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Leerer Zustand. Zeigt nie nur "keine Daten", sondern immer den naechsten
|
|
* moeglichen Schritt - eine leere Liste soll nicht ratlos machen.
|
|
*/
|
|
export function Leerzustand({
|
|
titel,
|
|
beschreibung,
|
|
aktion,
|
|
icon: Icon = Inbox,
|
|
className,
|
|
}: {
|
|
titel: string
|
|
beschreibung?: React.ReactNode
|
|
aktion?: React.ReactNode
|
|
icon?: React.ComponentType<{ className?: string; 'aria-hidden'?: boolean }>
|
|
className?: string
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex flex-col items-center justify-center rounded-lg border border-dashed px-6 py-14 text-center',
|
|
className,
|
|
)}
|
|
>
|
|
<Icon className="mb-3 size-8 text-muted-foreground" aria-hidden={true} />
|
|
<h3 className="text-base font-semibold">{titel}</h3>
|
|
{beschreibung && (
|
|
<p className="mt-1 max-w-md text-sm text-muted-foreground">{beschreibung}</p>
|
|
)}
|
|
{aktion && <div className="mt-5">{aktion}</div>}
|
|
</div>
|
|
)
|
|
}
|