import type { InputHTMLAttributes, SelectHTMLAttributes, } from "react"; import { seedValue, type SeedSource } from "../../lib/formState"; // The labelled text/number input the editor's forms are built out of. // // ONE FIELD SINCE RELEASE 16 (slice FK). Slice 3 lifted this out of // SettingsForm and left SiteForm's and ChannelForm's own copies alone on // purpose ("leave them until something actually needs them to be one"). Slice // FK is that something: all three had to learn to seed from a failed action's // values, and they were the same label → input → hint, differing only in props // (`step` here, `min` in SiteForm's, `placeholder`/`readOnly`/a controlled mode // in ChannelForm's). Those props are all here now. OverviewPanel's `Field` is a // different thing (a read-only figure, no input) and stays its own. // // `state` is the form's `useActionState` state. When the last submit failed, // the input's default is what was submitted (lib/formState.ts), so the reset // React runs after the action puts the operator's text back instead of the // stored value. `defaultValue` is the INITIAL value — the stored one. // // `value` + `onChange` make the field CONTROLLED (ChannelForm's download-filter // patterns are, so their live values can be read without touching the DOM); a // controlled field survives the reset on its own and ignores `state`. export function Field({ label, name, defaultValue, state, hint, required, type = "text", step, min, placeholder, readOnly, value, onChange, }: { label: string; name: string; defaultValue?: string; state?: SeedSource; hint?: string; required?: boolean; type?: string; // `type="number"` carries an IMPLICIT step of 1, so a decimal value fails // constraint validation and the browser blocks the submit SILENTLY — no // error, no request, the save just never happens. Any numeric field that // accepts fractions must pass a step. step?: string; min?: string; placeholder?: string; readOnly?: boolean; value?: string; onChange?: (v: string) => void; }) { const className = `rounded border border-border bg-card px-2 py-1 text-sm${readOnly ? " opacity-60" : ""}`; const shared = { type, required, step, min, placeholder, readOnly, className }; return ( ); } // An UNCONTROLLED input whose default is the failed submit's value, else // `initial`. For the inputs that are not a `Field` (their own label markup). // // A NUMBER INPUT IS RE-MOUNTED when its default changes. React does not write a // new `defaultValue` into a number input that has focus (react-dom // `setDefaultValue` skips it, so typing "1." is not rewritten under the cursor) // — and pressing Enter in one submits the form with the focus still there, so // the reset would put the OLD default back. A new element takes its default at // mount, focused or not. The key is the default itself: when it does not // change there is nothing to re-apply. export function SeededInput({ state, name, initial, type = "text", ...rest }: Omit< InputHTMLAttributes, "defaultValue" | "value" | "name" > & { state: SeedSource; name: string; initial: string; }) { const seeded = seedValue(state, name, initial); return ( ); } // An UNCONTROLLED select whose default is the failed submit's value, else // `initial`. // // RE-MOUNTED WHENEVER ITS DEFAULT CHANGES: React applies a select's // `defaultValue` only when it mounts (react-dom: an update re-applies it only // when `multiple` toggles), so a new `defaultValue` never reaches the DOM and // the reset puts back the option the select MOUNTED with — after a failure, and // after a success too (the stored value changed, the select did not). The key // is the default itself. export function SeededSelect({ state, name, initial, ...rest }: Omit< SelectHTMLAttributes, "defaultValue" | "value" | "name" > & { state: SeedSource; name: string; initial: string; }) { const seeded = seedValue(state, name, initial); return