/** * The recursive dispatcher: classifies a single schema node and renders the * matching field component for its model kind (const/number/boolean render * inline; every other kind delegates to a dedicated field component). Object, * array, and union renderers recurse back through here for their children. */ import { memo, type ReactNode } from 'react'; import { Checkbox } from '../components/checkbox'; import { Field } from '../components/field'; import { NumberInput, TextInput } from '../components/inputs'; import { ArrayField } from './array-field'; import { classifySchema } from './classify'; import { EnumField } from './enum-field'; import { FieldGroup } from './field-group'; import { JsonField } from './json-field'; import { ObjectFields } from './object-fields'; import { RecordField } from './record-field'; import { scalarLabel } from './resolve'; import { StringField } from './string-field'; import type { JsonSchema, SchemaFormErrors } from './types'; import { UnionField } from './union-field'; interface FieldNodeProps { readonly schema: JsonSchema; readonly root: JsonSchema; readonly value: unknown; readonly onChange: (value: unknown) => void; readonly path: string; /** The label to show — a property key, array-item caption, or `undefined` at root. */ readonly label: string | undefined; readonly required: boolean; /** * Append the design system's required marker (a trailing ` *`, the same * convention host forms use on their own labels) to the heading when the * field is required. Opt-in per face: the union variant surface sets it so * the ACTIVE variant's obligations read at a glance. */ readonly markRequired?: boolean; readonly errors: SchemaFormErrors | undefined; readonly idPrefix: string; } function labelText(explicit: string | undefined, label: string | undefined): string { return explicit ?? label ?? 'Value'; } /** * A single schema node, dispatched on its classified model kind. Wrapped in * `memo` so a field whose props are unchanged is skipped: the containers hand * each child a referentially-stable `onChange` (see {@link useKeyedCallbacks}) * and the controlled value keeps unedited branches identity-equal, so editing * one field re-renders only that field's subtree, not every sibling. */ function FieldNodeImpl(props: FieldNodeProps): ReactNode { const { root, value, onChange, path, required, idPrefix, errors } = props; const classified = classifySchema(props.schema, root); const base = labelText(classified.title, props.label); const heading = props.markRequired === true && required ? `${base} *` : base; const { model, description } = classified; const error = errors?.[path]; switch (model.kind) { case 'json': return ( ); case 'const': return ( ); case 'enum': return ( ); case 'string': return ( ); case 'number': return ( ); case 'boolean': return ( ); case 'array': return ( ); case 'object': return ( ); case 'record': return ( ); case 'union': return ( ); } } export const FieldNode = memo(FieldNodeImpl); /** The label/description/error chrome every scalar leaf field shares. */ interface ScalarFieldChrome { readonly heading: string; readonly description: string | undefined; readonly error: string | undefined; } /** A `const`-pinned value: shown read-only, never editable. */ function ConstField({ heading, description, error, constValue, }: ScalarFieldChrome & { constValue: unknown }): ReactNode { return ( ); } /** A number/integer field; an empty input clears the value to `undefined`. */ function NumberField({ heading, description, error, integer, value, onChange, }: ScalarFieldChrome & { integer: boolean; value: unknown; onChange: (value: unknown) => void; }): ReactNode { return ( { const raw = event.target.value; onChange(raw === '' ? undefined : Number(raw)); }} /> ); } /** A boolean field rendered as a checkbox. */ function BooleanField({ heading, description, error, value, onChange, }: ScalarFieldChrome & { value: unknown; onChange: (value: unknown) => void }): ReactNode { return ( { onChange(checked); }} /> ); }