import type { ReactNode } from "react"; /** * WHO OWNS THE DRAFT — the one axis every data-entry control in the kit turns on. * A control is either CONTROLLED (a form holds the value and commits in its * footer) or SELF-COMMITTING (the field holds the draft and writes it itself); * the mode is read off which callback the caller passed, and `?: never` on the * opposite arm is what keeps a caller from passing both. * * A caller must not FLIP a control's mode on one instance: the branch picks a * different component and React remounts the field under the reader. */ /** How a self-committing field leaves its draft. */ export type CommitControls = "blur" | "buttons"; /** A FORM owns the draft and commits in its footer. */ export interface Controlled { value: T; onValueChange: (next: T) => void; onSave?: never; controls?: never; actions?: never; } /** The FIELD owns the draft: async save, saving + error in place, Escape reverts, * blur or Enter commits, pending-commit tracking. */ export interface SelfCommitting { value: T; /** Throwing surfaces the message under the field and KEEPS the draft. */ onSave: (next: T) => void | Promise; /** "blur" (default): blur or Enter commits, Escape reverts. * "buttons": ✓/✕ own the exit and a stray blur is inert. */ controls?: CommitControls; /** Verbs ON the field's surface, rendered in BOTH states so the field never * changes width. Siblings of the control, never nested in it. */ actions?: ReactNode; onValueChange?: never; } export type Commit = Controlled | SelfCommitting;