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
+96
View File
@@ -0,0 +1,96 @@
import * as React from 'react'
import { AlertCircle } from 'lucide-react'
import { Label } from '@/components/ui/label'
import { cn } from '@/lib/utils'
/**
* Ein Formularfeld mit Beschriftung, Erklaerung und Fehlermeldung.
*
* Der Erklaerungstext ist kein Beiwerk: Diese Anwendung benutzt an vielen
* Stellen Begriffe aus dem Betreuungs- und Sozialrecht. Wer sie nicht
* kennt, soll sie hier erklaert bekommen, statt nachschlagen zu muessen.
*
* Beschriftung, Erklaerung und Fehler sind ueber aria-describedby mit dem
* Eingabefeld verbunden - Screenreader lesen sie mit vor.
*/
export function Feld({
id,
beschriftung,
erklaerung,
fehler,
pflicht,
className,
children,
}: {
id: string
beschriftung: string
erklaerung?: React.ReactNode
fehler?: string | string[] | null
pflicht?: boolean
className?: string
children: React.ReactNode
}) {
const fehlertext = Array.isArray(fehler) ? fehler[0] : fehler
const erklaerungId = erklaerung ? `${id}-erklaerung` : undefined
const fehlerId = fehlertext ? `${id}-fehler` : undefined
const beschriftungVon = [erklaerungId, fehlerId].filter(Boolean).join(' ') || undefined
return (
<div className={cn('space-y-1.5', className)}>
<Label htmlFor={id}>
{beschriftung}
{pflicht && (
<span className="ml-1 text-destructive" aria-label="Pflichtfeld">
*
</span>
)}
</Label>
{React.isValidElement(children)
? React.cloneElement(children as React.ReactElement<Record<string, unknown>>, {
id,
'aria-describedby': beschriftungVon,
'aria-invalid': fehlertext ? true : undefined,
'aria-required': pflicht || undefined,
})
: children}
{erklaerung && (
<p id={erklaerungId} className="text-sm text-muted-foreground">
{erklaerung}
</p>
)}
{fehlertext && (
<p id={fehlerId} className="flex items-start gap-1.5 text-sm font-medium text-destructive">
<AlertCircle className="mt-0.5 size-4 shrink-0" aria-hidden="true" />
{fehlertext}
</p>
)}
</div>
)
}
/** Gruppiert zusammengehoerige Felder mit einer Ueberschrift. */
export function Feldgruppe({
titel,
beschreibung,
children,
className,
}: {
titel: string
beschreibung?: React.ReactNode
children: React.ReactNode
className?: string
}) {
return (
<fieldset className={cn('space-y-4', className)}>
<legend className="sr-only">{titel}</legend>
<div>
<h2 className="text-base font-semibold">{titel}</h2>
{beschreibung && <p className="mt-0.5 text-sm text-muted-foreground">{beschreibung}</p>}
</div>
<div className="grid gap-4 sm:grid-cols-2">{children}</div>
</fieldset>
)
}