Files
sshkeyvault/src/components/ui/Feld.tsx
T
florian.wildeandClaude Opus 5.5 90a41dc55d feat(hosts): Seiten fuer Hosts und Hostgruppen (#008)
Ersetzt die Platzhalter: Hostliste mit Suche und Gruppenfilter per GET
(teilbare Links), Formulare fuer Hosts und Gruppen, Hostdetail als Geruest
und Gruppendetail mit Mitgliederauswahl. Bearbeiten-Knoepfe entscheidet der
Server per darf(); jede Seite prueft die Sitzung selbst.

Die Detailseite rendert HostKeyAktionen (#009) und BootstrapAktionen (#010)
als feste Platzhalter mit fertigen Props, damit die Folge-Issues nur ihre
eigene Datei ersetzen. Formulare senden per onSubmit, weil React ein
<form action> nach jeder Antwort zuruecksetzt und Eingaben bei Feldfehlern
sonst verloren gingen. Unter md zeigt die Hostliste Karten statt Tabelle,
damit Status und Host-Key auf 390 px ohne Wischen sichtbar bleiben.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:50:34 +02:00

186 lines
5.4 KiB
TypeScript
Raw 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.
import type { ComponentProps, ReactNode } from "react";
import { clsx } from "clsx";
const EINGABE_KLASSEN = clsx(
"bg-bg border-border-strong text-text rounded-feld min-h-11 w-full min-w-0 border px-3 text-[15px]",
"placeholder:text-muted/70 focus-visible:border-accent",
"aria-[invalid=true]:border-danger-border",
);
/** Hinweis und Fehlermeldung unter einem Feld; die Ids verknuepft `aria-describedby` (#008). */
function FeldFuss({ feldId, hinweis, fehler }: { feldId: string; hinweis?: ReactNode; fehler?: string }) {
return (
<>
{/* Bei einem Fehler ersetzt die Meldung den Hinweis – meist sagt sie dasselbe genauer. */}
{hinweis && !fehler && (
<p id={`${feldId}-hinweis`} className="text-muted text-[12.5px]">
{hinweis}
</p>
)}
{fehler && (
<p id={`${feldId}-fehler`} className="text-danger text-[13px]">
{fehler}
</p>
)}
</>
);
}
function beschreibtDurch(feldId: string, hinweis?: ReactNode, fehler?: string): string | undefined {
if (fehler) return `${feldId}-fehler`;
return hinweis ? `${feldId}-hinweis` : undefined;
}
/** Eingabefeld mit Beschriftung (#004). Mindesthoehe 44 px wie alle Klickflaechen. */
export function Feld({
beschriftung,
hinweis,
fehler,
mono = false,
className,
id,
name,
...rest
}: ComponentProps<"input"> & { beschriftung: string; hinweis?: ReactNode; fehler?: string; mono?: boolean }) {
const feldId = id ?? `feld-${name}`;
return (
<div className={clsx("flex min-w-0 flex-col gap-1.5", className)}>
<label htmlFor={feldId} className="text-muted text-[13px] font-medium">
{beschriftung}
</label>
<input
id={feldId}
name={name}
aria-invalid={fehler ? true : undefined}
aria-describedby={beschreibtDurch(feldId, hinweis, fehler)}
{...rest}
className={clsx(EINGABE_KLASSEN, mono && "font-mono tracking-wide")}
/>
<FeldFuss feldId={feldId} hinweis={hinweis} fehler={fehler} />
</div>
);
}
/** Mehrzeiliges Textfeld, z. B. fuer Beschreibungen (#008). */
export function Textbereich({
beschriftung,
hinweis,
fehler,
className,
id,
name,
...rest
}: ComponentProps<"textarea"> & { beschriftung: string; hinweis?: ReactNode; fehler?: string }) {
const feldId = id ?? `feld-${name}`;
return (
<div className={clsx("flex min-w-0 flex-col gap-1.5", className)}>
<label htmlFor={feldId} className="text-muted text-[13px] font-medium">
{beschriftung}
</label>
<textarea
id={feldId}
name={name}
rows={3}
aria-invalid={fehler ? true : undefined}
aria-describedby={beschreibtDurch(feldId, hinweis, fehler)}
{...rest}
className={clsx(EINGABE_KLASSEN, "py-2.5 leading-normal")}
/>
<FeldFuss feldId={feldId} hinweis={hinweis} fehler={fehler} />
</div>
);
}
/** Auswahlliste im Stil der Eingabefelder (#008). */
export function Auswahlliste({
beschriftung,
className,
id,
name,
children,
...rest
}: ComponentProps<"select"> & { beschriftung: string }) {
const feldId = id ?? `feld-${name}`;
return (
<div className={clsx("flex min-w-0 flex-col gap-1.5", className)}>
<label htmlFor={feldId} className="text-muted text-[13px] font-medium">
{beschriftung}
</label>
<select id={feldId} name={name} {...rest} className={EINGABE_KLASSEN}>
{children}
</select>
</div>
);
}
/**
* Checkbox oder Radio mit Beschriftung; die ganze Zeile ist Klickflaeche (44 px), weil das
* Kaestchen allein auf dem Telefon zu klein ist (#008).
*/
export function Ankreuzfeld({
beschriftung,
erklaerung,
className,
...rest
}: Omit<ComponentProps<"input">, "type"> & {
type?: "checkbox" | "radio";
beschriftung: ReactNode;
erklaerung?: ReactNode;
}) {
return (
<label
className={clsx(
"hover:bg-raised/60 rounded-box flex min-h-11 min-w-0 cursor-pointer items-start gap-3 px-2 py-2.5 transition-colors",
className,
)}
>
<input type="checkbox" {...rest} className="accent-accent mt-0.5 size-[18px] shrink-0 cursor-pointer" />
<span className="min-w-0 text-[14.5px] leading-snug">
<span className="text-text block break-words">{beschriftung}</span>
{erklaerung && <span className="text-muted mt-0.5 block text-[13px]">{erklaerung}</span>}
</span>
</label>
);
}
/** Gruppe von Ankreuzfeldern mit Legende und Fehler (#008). */
export function Feldgruppe({
legende,
hinweis,
fehler,
children,
className,
}: {
legende: string;
hinweis?: ReactNode;
fehler?: string;
children: ReactNode;
className?: string;
}) {
return (
<fieldset className={clsx("flex min-w-0 flex-col gap-1.5", className)}>
<legend className="text-muted mb-1.5 text-[13px] font-medium">{legende}</legend>
{hinweis && <p className="text-muted -mt-1 text-[12.5px]">{hinweis}</p>}
{children}
{fehler && (
<p role="alert" className="text-danger text-[13px]">
{fehler}
</p>
)}
</fieldset>
);
}
/** Fehlermeldung eines Formulars – mit role=alert, damit Screenreader sie vorlesen. */
export function Formularfehler({ text }: { text?: string }) {
if (!text) return null;
return (
<p
role="alert"
className="border-danger-border bg-danger-soft text-danger rounded-box border px-3 py-2.5 text-[14px]"
>
{text}
</p>
);
}