// Field renderer registry (D-05). A form field's `type` selects the control // component; any type without a renderer gets UnsupportedField, the design's // dashed box, so an unknown type never breaks the form. Phase 10.1 turns this // seam into the plugin extension point. import type { Component } from 'vue' import type { ControllerParams, FormField, RelationOption } from '../../api/types' import DropdownField from './fields/DropdownField.vue' import NumberField from './fields/NumberField.vue' import TextField from './fields/TextField.vue' import TextareaField from './fields/TextareaField.vue' import UnsupportedField from './fields/UnsupportedField.vue' /** 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 } const renderers = new Map([ ['text', TextField], ['textarea', TextareaField], ['number', NumberField], ['dropdown', DropdownField], ]) /** Types whose control shows the label itself (toggle cards). */ const selfLabelled = new Set() export function rendererFor(type: string): Component { return renderers.get(type) ?? UnsupportedField } /** Whether the SPA can edit values of this field type. */ export function isRegistered(type: string): boolean { return renderers.has(type) } export function ownsLabel(type: string): boolean { return selfLabelled.has(type) } // 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(' ') }