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:
Florian Wilde
2026-08-22 01:54:56 +02:00
parent bd1c7b801f
commit dcd3a8ce71
30 changed files with 2151 additions and 12 deletions
+91
View File
@@ -0,0 +1,91 @@
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>
)
}