"use client"; import { Button } from "@prototype/components/ui/button"; import { cn } from "@prototype/lib/utils"; import { Plus, X } from "lucide-react"; import { useEffect, useRef, useState, type ReactNode } from "react"; export type RepeatableFieldItem = { id: string }; export type RepeatableFieldListProps = { items: TItem[]; onChange: (items: TItem[]) => void; /** Factory for a fresh, empty item appended when the user clicks add. */ createItem: () => TItem; /** Label shown for the add-new button (e.g. "Add doc"). */ addLabel: string; /** Per-item header label. Receives the item's index and the total count. */ itemLabel: (index: number, total: number) => string; /** Render the fields for one item. `update` patches that item in place. */ renderItem: (item: TItem, update: (patch: Partial) => void) => ReactNode; }; const EXIT_MS = 200; function prefersReducedMotion(): boolean { if (typeof window === "undefined") return false; return window.matchMedia("(prefers-reduced-motion: reduce)").matches; } type RepeatableFieldItemCardProps = { label: string; isLast: boolean; onRemove: () => void; children: ReactNode; }; function RepeatableFieldItemCard({ label, isLast, onRemove, children, }: RepeatableFieldItemCardProps) { const [exiting, setExiting] = useState(false); const onRemoveRef = useRef(onRemove); onRemoveRef.current = onRemove; useEffect(() => { if (!exiting) return; const timer = window.setTimeout(() => onRemoveRef.current(), EXIT_MS); return () => window.clearTimeout(timer); }, [exiting]); const handleRemove = () => { if (exiting) return; if (prefersReducedMotion()) { onRemove(); return; } setExiting(true); }; return (
{label}
{children}
); } /** * Bordered add/remove list chrome shared by the gallery create modals. Owns the * card wrapper, per-item header + remove button, and the add-new button; callers * supply the item fields via `renderItem` and patch state through `update`. */ export function RepeatableFieldList({ items, onChange, createItem, addLabel, itemLabel, renderItem, }: RepeatableFieldListProps) { const itemsRef = useRef(items); itemsRef.current = items; return (
{items.map((item, index) => { const label = itemLabel(index, items.length); const update = (patch: Partial) => onChange( itemsRef.current.map((entry) => entry.id === item.id ? { ...entry, ...patch } : entry, ), ); return ( onChange( itemsRef.current.filter((entry) => entry.id !== item.id), ) } > {renderItem(item, update)} ); })}
); }