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