All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 1m33s
Rechtsrahmen: § 13 Abs. 3 LBGG SH i. V. m. §§ 3 und 4 BITV 2.0. Eine Erklaerung "nicht barrierefrei" ersetzt die Herstellungspflicht nicht, sie dokumentiert den Stand nur voruebergehend. Farben, alle Werte gegen die WCAG-Formel nachgerechnet: - Sekundaertext 4.40:1 auf muted-Flaechen -> 4.55:1 - Rahmen von Eingabefeldern 1.24:1 -> 3:1 (WCAG 1.4.11); dekorative Trenner bleiben unveraendert - Badge-Farben scheiterten auf der eigenen 10-Prozent-Flaeche (brand 4.20, success 4.18, destructive 3.88) -> jeweils ueber 4.5 - Warnfarbe im Admin 3.11:1 -> amber-700 mit 4.90:1 Fokus war faktisch unsichtbar: Der Ring wurde durchgaengig mit halber Deckkraft gezeichnet (ring-ring/50), effektiv rund 1.5:1. Jetzt volle Deckkraft in Markenfarbe, 12 Dateien betroffen. Weiter: Sprungmarke zum Inhalt auf allen Seiten, Suchfelder beschriftet, Druck-Schaltflaechen hatten als reine Icon-Buttons gar keinen zugaenglichen Namen, Liste als echte Liste ausgezeichnet, doppelter Link je Zeile entfernt, Trefferzahl als Live-Region. Responsiv: Aussenabstaende und Kopfzeilen ab 320 Pixel, Listenzeilen brechen um. Neu nach § 4 BITV 2.0: Seiten fuer Leichte Sprache und Gebaerdensprache, im Admin pflegbar. Leer gelassen und damit noch nicht online - die Inhalte kommen vom Amt. In der Erklaerung als bekannte Luecke benannt. Geprueft mit axe-core (WCAG 2.1 A und AA) ueber alle oeffentlichen Seiten und den Adminbereich, hell und dunkel, dazu Tastaturbedienung, 200 Prozent Vergroesserung und 320 Pixel Breite: keine Verstoesse, kein waagerechter Ueberlauf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
134 lines
3.9 KiB
TypeScript
134 lines
3.9 KiB
TypeScript
"use client"
|
||
|
||
import { useState } from "react"
|
||
import { AlertCircle, PencilLine, Undo2 } from "lucide-react"
|
||
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Label } from "@/components/ui/label"
|
||
import { MarkdownInput } from "@/components/markdown-input"
|
||
import { MarkdownRenderer } from "@/components/markdown-renderer"
|
||
|
||
interface DefaultableFieldProps {
|
||
id: string
|
||
label: string
|
||
/** Der zentral gepflegte Standardtext für dieses Feld. */
|
||
defaultValue: string
|
||
/** Abweichender Text dieser Erklärung; leer bedeutet „Standard gilt“. */
|
||
value: string
|
||
onChange: (value: string) => void
|
||
/** Ohne Standardtext ist ein Pflichtfeld unvollständig – dann Warnung zeigen. */
|
||
required?: boolean
|
||
rows?: number
|
||
invalid?: boolean
|
||
}
|
||
|
||
/**
|
||
* Ein Baustein, der normalerweise aus den zentralen Standardtexten kommt.
|
||
*
|
||
* Im Regelfall zeigt das Feld den Standardtext nur an. Erst „Abweichen“ macht
|
||
* ihn für genau diese Erklärung bearbeitbar; „Standard übernehmen“ verwirft die
|
||
* Abweichung wieder. Gespeichert wird ausschließlich der abweichende Text –
|
||
* ein leerer Wert heißt, dass die Erklärung dem Standard folgt.
|
||
*/
|
||
export function DefaultableField({
|
||
id,
|
||
label,
|
||
defaultValue,
|
||
value,
|
||
onChange,
|
||
required = false,
|
||
rows = 6,
|
||
invalid = false,
|
||
}: DefaultableFieldProps) {
|
||
// Beim Mounten entscheidet der gespeicherte Wert über den Modus; danach
|
||
// steuert ihn der Benutzer über die beiden Schaltflächen.
|
||
const [isOverride, setIsOverride] = useState(() => value.trim().length > 0)
|
||
|
||
/** Wechselt in den Sonderfall und übernimmt den Standard als Ausgangstext. */
|
||
const startOverride = () => {
|
||
setIsOverride(true)
|
||
if (!value.trim()) onChange(defaultValue)
|
||
}
|
||
|
||
/** Kehrt zum Standard zurück und verwirft den abweichenden Text. */
|
||
const resetToDefault = () => {
|
||
setIsOverride(false)
|
||
onChange("")
|
||
}
|
||
|
||
if (isOverride) {
|
||
return (
|
||
<div className="space-y-2">
|
||
<MarkdownInput
|
||
id={id}
|
||
label={label}
|
||
value={value}
|
||
onChange={onChange}
|
||
rows={rows}
|
||
required={required}
|
||
invalid={invalid}
|
||
/>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<Badge variant="outline" className="text-amber-700 dark:text-amber-400">
|
||
Weicht vom Standard ab
|
||
</Badge>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={resetToDefault}
|
||
className="text-muted-foreground"
|
||
>
|
||
<Undo2 />
|
||
Standard übernehmen
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<Label htmlFor={id} className="gap-1">
|
||
{label}
|
||
{required && <span className="text-destructive">*</span>}
|
||
</Label>
|
||
<Badge variant="secondary">Standard</Badge>
|
||
</div>
|
||
|
||
<div
|
||
id={id}
|
||
className="rounded-md border border-dashed bg-muted/30 p-4"
|
||
style={{ minHeight: `${rows * 1.5 + 1}rem` }}
|
||
>
|
||
{defaultValue.trim() ? (
|
||
<MarkdownRenderer content={defaultValue} />
|
||
) : required ? (
|
||
<p className="flex gap-2 text-sm text-destructive">
|
||
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
||
Noch kein Standardtext hinterlegt – im Admin unter „Standardtexte“
|
||
pflegen oder hier abweichen.
|
||
</p>
|
||
) : (
|
||
<p className="text-sm text-muted-foreground italic">
|
||
Kein Standardtext hinterlegt – dieser Abschnitt bleibt leer.
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={startOverride}
|
||
className="text-muted-foreground"
|
||
>
|
||
<PencilLine />
|
||
Für diese Erklärung abweichen
|
||
</Button>
|
||
</div>
|
||
)
|
||
}
|