import type { ComponentType } from 'react'; import { Button, FormControl, FormHelperText, InputLabel, MenuItem, Select } from '@mui/material'; import type { InputId, OverrideDiagnostic, Preset, SelectionId, ThemeOverrideEditorDraft, ThemeSelections, } from '../override/types'; import { diagnosticsForControl, type ControlDefinition, type EditorControlType, type ResolvedControlDefinition, } from '../override/controls'; import { validateHex } from '../validation'; import { ColorControl } from './ColorControl'; import { FontControl } from './FontControl'; import { PresetControl } from './PresetControl'; type SelectionValue = NonNullable; export interface ControlWidgetProps { readonly definition: ResolvedControlDefinition; readonly draft: ThemeOverrideEditorDraft; readonly diagnostics: readonly OverrideDiagnostic[]; readonly fontOptions: Readonly>; readonly onPresetChange: (preset: Preset) => void; readonly onInputChange: (inputId: InputId, value: string) => void; readonly onInputClear: (inputId: InputId) => void; readonly onSelectionChange: ( definition: ResolvedControlDefinition, value: SelectionValue, ) => void; readonly onSelectionClear: (selectionId: SelectionId) => void; } function bindingError(definition: ControlDefinition, expected: string): never { throw new Error( `Editor control "${definition.id}" uses "${definition.binding.kind}" storage with a ${expected} widget.`, ); } export function encodeSelectionReference( definition: ResolvedControlDefinition, reference: string, ): SelectionValue { if (definition.binding.kind !== 'selection') bindingError(definition, 'selection'); const selection = definition.selectionDefinition; if (!selection) { throw new Error(`Editor control "${definition.id}" is missing resolved selection metadata.`); } if (selection.valueShape === 'invariant-reference') { if (selection.modePolicy !== 'invariant' || definition.modeExposure !== 'none') { throw new Error( `Editor control "${definition.id}" has incompatible invariant selection metadata.`, ); } return { source: { ref: reference } }; } if (selection.valueShape === 'linked-reference') { if (selection.modePolicy !== 'mode-capable' || definition.modeExposure === 'none') { throw new Error( `Editor control "${definition.id}" has incompatible linked selection metadata.`, ); } return { mode: 'linked', source: { ref: reference }, }; } throw new Error( `Editor control "${definition.id}" cannot encode selection shape "${selection.valueShape}".`, ); } function selectedReference(value: SelectionValue | undefined): string { return value && 'source' in value ? value.source.ref : ''; } function PresetWidget({ definition, draft, onPresetChange }: Readonly) { if (definition.binding.kind !== 'metadata') bindingError(definition, 'preset'); return ( ); } function FontWidget({ definition, draft, fontOptions, onInputChange, }: Readonly) { if (definition.binding.kind !== 'input') bindingError(definition, 'font'); const inputId = definition.binding.key; const field = draft.inputs[inputId]; return ( onInputChange(inputId, value)} /> ); } function HexInputWidget({ definition, draft, diagnostics, onInputChange, onInputClear, }: Readonly) { if (definition.binding.kind !== 'input') bindingError(definition, 'input'); const inputId = definition.binding.key; const field = draft.inputs[inputId]; const relationshipLabel = definition.intentBehavior === 'pin-unpin' && field.relationship ? `${definition.label} (${field.relationship})` : definition.label; const diagnostic = diagnosticsForControl(definition, diagnostics).find( ({ severity }) => severity === 'error', ); return ( <> onInputChange(inputId, value)} /> {definition.intentBehavior === 'pin-unpin' && field.relationship === 'pinned' ? ( ) : null} ); } function SelectionWidget({ definition, draft, diagnostics, onSelectionChange, onSelectionClear, }: Readonly) { if (definition.binding.kind !== 'selection') bindingError(definition, 'selection'); if (!definition.selectionDefinition || !definition.sourceSet) { throw new Error(`Editor control "${definition.id}" is missing resolved catalog metadata.`); } const { selectionId } = definition.binding; const value = selectedReference(draft.selections[selectionId]?.displayValue); const controlDiagnostics = diagnosticsForControl(definition, diagnostics); const error = controlDiagnostics.find(({ severity }) => severity === 'error'); const warning = controlDiagnostics.find(({ severity }) => severity === 'warning'); return ( {definition.label} {error?.message ?? warning?.message ?? definition.description} ); } type ControlWidget = ComponentType>; const CONTROL_WIDGET_REGISTRY: Partial> = { 'preset-select': PresetWidget, 'hex-input': HexInputWidget, 'font-select': FontWidget, 'token-ref': SelectionWidget, 'scale-ref': SelectionWidget, }; export function resolveControlWidget(type: EditorControlType): ControlWidget { const widget = CONTROL_WIDGET_REGISTRY[type]; if (!widget) { throw new Error(`No Theme Editor widget is registered for control type "${type}".`); } return widget; } export function validateControlWidgetRegistry(definitions: readonly ControlDefinition[]): void { for (const definition of definitions) { resolveControlWidget(definition.type); } } export function ControlRenderer(props: Readonly) { const Widget = resolveControlWidget(props.definition.type); return ; }