'use client'; import { useEffect, useRef, useState } from 'react'; import { Loader2, Pencil } from 'lucide-react'; import { toast } from 'sonner'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { cn } from '@/lib/utils'; interface SelectOption { value: string; label: string; } interface BaseProps { value: string | null | undefined; onSave: (next: string | null) => Promise; placeholder?: string; emptyText?: string; className?: string; disabled?: boolean; } interface TextProps extends BaseProps { variant?: 'text'; } interface SelectFieldProps extends BaseProps { variant: 'select'; options: SelectOption[]; } interface TextareaProps extends BaseProps { variant: 'textarea'; rows?: number; } export type InlineEditableFieldProps = TextProps | SelectFieldProps | TextareaProps; /** * Click-to-edit field used in detail panels. Shows the value as plain text * with a pencil affordance on hover; clicking swaps to an input that saves on * Enter/blur and cancels on Escape. */ export function InlineEditableField(props: InlineEditableFieldProps) { const { value, onSave, placeholder, emptyText = '—', className, disabled } = props; const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(value ?? ''); const [saving, setSaving] = useState(false); const inputRef = useRef(null); const textareaRef = useRef(null); useEffect(() => { setDraft(value ?? ''); }, [value]); useEffect(() => { if (editing) { if (inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } else if (textareaRef.current) { textareaRef.current.focus(); textareaRef.current.select(); } } }, [editing]); async function commit(nextRaw: string) { const trimmed = nextRaw.trim(); if (trimmed === (value ?? '')) { setEditing(false); return; } setSaving(true); try { await onSave(trimmed === '' ? null : trimmed); setEditing(false); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to save'); setDraft(value ?? ''); } finally { setSaving(false); } } function cancel() { setDraft(value ?? ''); setEditing(false); } if (props.variant === 'select') { const labelFor = (v: string | null | undefined) => v ? (props.options.find((o) => o.value === v)?.label ?? v) : null; if (!editing) { return ( setEditing(true)} className={className} /> ); } return (
{saving && }
); } if (props.variant === 'textarea') { if (!editing) { return ( setEditing(true)} multiline className={className} /> ); } return (