// Shared props and helpers of the field controls (D-05). Kept apart from the // registry, which imports every control: a control importing the registry // would close an import cycle. import type { ControllerParams, FormField, RelationOption } from '../../api/types' /** Props every field control receives from FieldRenderer. */ export interface FieldControlProps { field: FormField modelValue: unknown /** id of the focusable control; the label points at it. */ controlId: string invalid?: boolean /** Space-separated ids of the comment and error lines. */ describedBy?: string /** Display labels of the field's current relation values (meta.labels). */ labels?: RelationOption[] /** Controller whose option endpoints serve this form; null for settings. */ source?: ControllerParams | null /** Id of the record being edited; null on create and on settings pages. */ recordId?: number | null } // Winter `attributes:` are HTML attributes on the control. Only these are // passed through; anything else (event handlers included) is dropped. const allowedAttributes = new Set([ 'readonly', 'placeholder', 'maxlength', 'minlength', 'min', 'max', 'step', 'autocomplete', 'spellcheck', 'pattern', ]) export function controlAttributes(field: FormField): Record { const out: Record = {} for (const [name, value] of Object.entries(field.attributes ?? {})) { const key = name.toLowerCase() if (!allowedAttributes.has(key) || value === null || value === false) { continue } out[key] = value } return out } /** Shared control styling: 44px, radius 10, danger border when invalid. */ export function controlClass(invalid: boolean | undefined): string { return [ 'w-full rounded-control border bg-surface px-3.5 text-text transition-colors duration-150 ease-out', invalid ? 'border-danger' : 'border-border-strong', ].join(' ') } /** Truthiness of a stored toggle value (true, 1, "1", "true"). */ export function toggleOn(value: unknown): boolean { if (typeof value === 'string') { return value !== '' && value !== '0' && value.toLowerCase() !== 'false' } return Boolean(value) } /** A toggle's next value in the stored value's own type (1/0 or boolean). */ export function toggleValue(current: unknown, on: boolean): boolean | number { return typeof current === 'number' ? (on ? 1 : 0) : on }