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,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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user