Files
minabot/src/components/InlineItemForm.tsx
2026-09-04 18:33:18 +02:00

79 lines
3.4 KiB
TypeScript

import { useEffect, useRef, useState, type ReactNode } from "react";
import { Button } from "./design-system/primitives";
import { Field } from "./design-system/Field";
export type ItemMode = "edit" | "delete";
/** In-place editing and confirmation, shared by habit headings and task rows. */
export function InlineItemForm({ name, kind, mode, disabled, children, onSave, onDelete, onClose, submitLabel, formLabel }: {
name: string;
kind: "habit" | "task";
mode: ItemMode;
disabled?: boolean;
children?: ReactNode;
submitLabel?: string;
formLabel?: string;
onSave: (name: string) => Promise<void>;
onDelete: () => Promise<void>;
onClose: () => void;
}) {
const form = useRef<HTMLFormElement>(null);
const saving = useRef(false);
const [draft, setDraft] = useState(name);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
const previous = document.activeElement as HTMLElement | null;
form.current?.querySelector<HTMLElement>(mode === "edit" ? "input" : "button")?.focus();
return () => { if (previous?.isConnected) previous.focus(); };
}, [mode]);
return (
<form ref={form} className={`ds-inline-item-form ds-inline-item-form--${kind}`} aria-label={formLabel ?? `${mode === "edit" ? "Edit" : kind === "habit" ? "Archive" : "Delete"} ${kind} ${name}`}
onKeyDown={(event) => {
if (event.key === "Escape" && !saving.current) { event.preventDefault(); onClose(); }
}}
onSubmit={async (event) => {
event.preventDefault();
if (saving.current || disabled) return;
if (mode === "edit" && !draft.trim()) { setError("Enter a name."); return; }
saving.current = true;
setBusy(true);
setError("");
try {
if (mode === "delete") await onDelete();
else await onSave(draft.trim());
onClose();
} catch (error) {
setError(error instanceof Error ? error.message : "Could not save your change. Try again.");
} finally {
saving.current = false;
setBusy(false);
}
}}>
{mode === "edit" ? (
<fieldset className="ds-inline-item-fields" disabled={busy || disabled}>
<Field label={kind === "habit" ? "Habit name" : "Task name"}>
{(id) => <input id={id} value={draft} required maxLength={200} onChange={(event) => setDraft(event.target.value)} />}
</Field>
{children}
</fieldset>
) : (
<p className="ds-inline-delete-copy">
{kind === "habit" ? "Archive" : "Delete"} <strong>{name}</strong>? {kind === "habit"
? "This removes the habit from your dashboard starting today. Earlier history is kept. You can restore it from Archived habits."
: "This removes the task from today and future check-ins. Earlier history is kept."}
</p>
)}
{error && <p className="ds-form-feedback" role="alert">{error}</p>}
<div className="ds-inline-form-actions">
{mode === "delete" && <Button variant="text" disabled={busy} onClick={onClose}>Cancel</Button>}
<Button type="submit" disabled={busy || disabled}>
{busy ? "Saving…" : mode === "delete" ? `${kind === "habit" ? "Archive" : "Delete"} ${kind}` : submitLabel ?? "Save changes"}
</Button>
{mode === "edit" && <Button variant="text" disabled={busy} onClick={onClose}>Cancel</Button>}
</div>
</form>
);
}