import React, { useRef, useState } from "react"; import { format, parseISO } from "date-fns"; import { Pencil } from "lucide-react"; import { cn } from "@/lib/utils"; import { Input } from "./input"; import { DatePicker } from "./date-picker"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./select"; /** * EditableTextField — shadcn/WealthX * * A single inline-editable value. Display mode shows the formatted value with a pencil affordance * (revealed on hover); activating it switches to an editor. Committing (blur / Enter for text, * selection for select/date) calls `onSave`; Escape cancels. * * Atom. Pure & controlled — no auto-save timers, no API. The parent owns the value and persistence. * Three editors via `variant`: "text" (Input), "select" (Select), "date" (DatePicker). * * NOTE: distinct from `EditableMoneyItem`, which is a money-specific row (swatch + currency + slider). */ export interface EditableTextFieldOption { value: string; label: string; } export interface EditableTextFieldProps { /** Accessible name — required, the field renders no visible label of its own. */ ariaLabel: string; /** Current committed value, in string form (ISO `yyyy-MM-dd` for variant="date"). */ value: string; /** Called with the new value when an edit is committed. */ onSave: (value: string) => void; /** Editor kind. Default "text". */ variant?: "text" | "select" | "date"; /** Options for variant="select". */ options?: EditableTextFieldOption[]; /** Input type for variant="text" (e.g. "email", "tel"). */ inputType?: React.HTMLInputTypeAttribute; placeholder?: string; /** Format the committed value for display (e.g. a date, a masked number). */ formatDisplay?: (value: string) => string; /** Shown in display mode when `value` is empty. Default "—". */ emptyLabel?: string; disabled?: boolean; className?: string; } export function EditableTextField({ ariaLabel, value, onSave, variant = "text", options = [], inputType = "text", placeholder, formatDisplay, emptyLabel = "—", disabled, className, }: EditableTextFieldProps) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(value); const inputRef = useRef(null); const startEditing = () => { if (disabled) return; setDraft(value); setEditing(true); setTimeout(() => inputRef.current?.select(), 0); }; const commit = (next: string) => { onSave(next); setEditing(false); }; const displayText = value ? variant === "select" ? (options.find((o) => o.value === value)?.label ?? value) : (formatDisplay?.(value) ?? value) : ""; // ── Edit mode ─────────────────────────────────────────────────────────────── if (editing && !disabled) { if (variant === "select") { return ( ); } if (variant === "date") { return ( { // Local-date formatting (not toISOString, which is UTC and can shift the day). commit(date ? format(date, "yyyy-MM-dd") : ""); }} /> ); } return ( setDraft(e.target.value)} onBlur={() => commit(draft)} onKeyDown={(e) => { if (e.key === "Enter") commit(draft); if (e.key === "Escape") setEditing(false); }} className={cn("h-9", className)} /> ); } // ── Display mode ────────────────────────────────────────────────────────────── return (
); }