syncova-policies/components/defaultable-field.tsx
Weapie 265ca8ac38
All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 5m15s
Improve accessibility and access controls; verify production and database recovery
2026-09-14 11:44:41 +02:00

145 lines
4.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useEffect, useRef, useState } from "react"
import { AlertCircle, PencilLine, Undo2 } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
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)
const focusAfterChange = useRef(false)
useEffect(() => {
if (!focusAfterChange.current) return
document.getElementById(isOverride ? id : id + "-override")?.focus()
focusAfterChange.current = false
}, [isOverride, id])
/** Wechselt in den Sonderfall und übernimmt den Standard als Ausgangstext. */
const startOverride = () => {
focusAfterChange.current = true
setIsOverride(true)
if (!value.trim()) onChange(defaultValue)
}
/** Kehrt zum Standard zurück und verwirft den abweichenden Text. */
const resetToDefault = () => {
focusAfterChange.current = true
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">
<span id={id + "-label"} className="text-sm font-medium">
{label}
{required && <span className="text-destructive">*</span>}
</span>
<Badge variant="secondary">Standard</Badge>
</div>
<div
id={id}
role="group"
aria-labelledby={id + "-label"}
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"
id={id + "-override"}
onClick={startOverride}
className="text-muted-foreground"
>
<PencilLine />
Für diese Erklärung abweichen
</Button>
</div>
)
}