import type { InputId, InputStrategy, OverrideDiagnostic, SelectionId, SelectionValueShape, } from './types'; export type EditorControlType = | 'preset-select' | 'hex-input' | 'font-select' | 'token-ref' | 'scale-ref' | 'palette-select'; export type EditorStorageBinding = | { readonly kind: 'metadata'; readonly key: 'basePreset'; } | { readonly kind: 'input'; readonly key: InputId; } | { readonly kind: 'selection'; readonly selectionId: SelectionId; }; export interface ControlDefinition { readonly id: string; readonly type: EditorControlType; readonly label: string; readonly description: string; readonly binding: EditorStorageBinding; readonly modeExposure: 'none' | 'linked-only' | 'linked-and-unlinked'; readonly intentBehavior?: 'pin-unpin'; } export interface ResolvedControlDefinition extends ControlDefinition { readonly inputDefinition?: EditorInputCatalogEntry; readonly selectionDefinition?: EditorSelectionCatalogEntry; readonly sourceSet?: EditorSourceSetCatalogEntry; } export interface EditorLayoutSection { readonly id: string; readonly tier: 'default' | 'advanced'; readonly heading?: string; readonly controlIds: readonly string[]; } export interface EditorInputCatalogEntry { readonly id: InputId; readonly valueType: 'color-seed' | 'font-family'; readonly strategy: InputStrategy; } export interface EditorSelectionCatalogEntry { readonly id: SelectionId; readonly sourceSetId?: string; readonly valueShape: SelectionValueShape; readonly modePolicy: 'invariant' | 'mode-capable'; } export interface EditorSourceSetCatalogEntry { readonly id: string; readonly references: readonly string[]; } export interface EditorControlCatalogQueries { readonly getInput: (id: string) => EditorInputCatalogEntry | undefined; readonly getSelection: (id: string) => EditorSelectionCatalogEntry | undefined; readonly getSourceSet: (id: string) => EditorSourceSetCatalogEntry | undefined; } export const TARGET_CONTROL_DEFINITIONS = [ { id: 'preset', type: 'preset-select', label: 'Preset', description: 'Select the Forge theme personality.', binding: { kind: 'metadata', key: 'basePreset' }, modeExposure: 'none', }, { id: 'brand-primary', type: 'hex-input', label: 'Brand color', description: 'Generate the primary Brand scale.', binding: { kind: 'input', key: 'brandPrimary' }, modeExposure: 'none', }, { id: 'accent-primary', type: 'hex-input', label: 'Accent color', description: 'Generate the supporting Accent scale.', binding: { kind: 'input', key: 'accentPrimary' }, modeExposure: 'none', }, { id: 'display-font', type: 'font-select', label: 'Display font', description: 'Choose an allowed display family for the selected preset.', binding: { kind: 'input', key: 'displayFont' }, modeExposure: 'none', }, { id: 'base-primary', type: 'hex-input', label: 'Base color', description: 'Keep Base derived from Brand or pin an independent neutral seed.', binding: { kind: 'input', key: 'basePrimary' }, modeExposure: 'none', intentBehavior: 'pin-unpin', }, { id: 'border-radius', type: 'scale-ref', label: 'Border radius', description: 'Choose the default component radius scale.', binding: { kind: 'selection', selectionId: 'borderRadius' }, modeExposure: 'none', }, { id: 'button-border-radius', type: 'scale-ref', label: 'Button border radius', description: 'Choose the button radius independently of the default component radius.', binding: { kind: 'selection', selectionId: 'buttonBorderRadius' }, modeExposure: 'none', }, { id: 'canvas-background', type: 'token-ref', label: 'Canvas background', description: 'Choose the linked background family source.', binding: { kind: 'selection', selectionId: 'surfaceBackground' }, modeExposure: 'linked-only', }, { id: 'primary-surface', type: 'token-ref', label: 'Primary surface', description: 'Choose the linked primary surface family source.', binding: { kind: 'selection', selectionId: 'surfacePrimary' }, modeExposure: 'linked-only', }, { id: 'action-surface', type: 'token-ref', label: 'Action', description: 'Choose the linked Action family source, including Focus.', binding: { kind: 'selection', selectionId: 'actionSurface' }, modeExposure: 'linked-only', }, { id: 'link-color', type: 'token-ref', label: 'Link', description: 'Choose the linked Link family source.', binding: { kind: 'selection', selectionId: 'linkColor' }, modeExposure: 'linked-only', }, { id: 'status-danger', type: 'token-ref', label: 'Danger', description: 'Choose the linked Danger family source.', binding: { kind: 'selection', selectionId: 'danger' }, modeExposure: 'linked-only', }, { id: 'status-success', type: 'token-ref', label: 'Success', description: 'Choose the linked Success family source.', binding: { kind: 'selection', selectionId: 'success' }, modeExposure: 'linked-only', }, { id: 'status-warning', type: 'token-ref', label: 'Warning', description: 'Choose the linked Warning family source.', binding: { kind: 'selection', selectionId: 'warning' }, modeExposure: 'linked-only', }, { id: 'status-info', type: 'token-ref', label: 'Info', description: 'Choose the linked Info family source.', binding: { kind: 'selection', selectionId: 'info' }, modeExposure: 'linked-only', }, ] as const satisfies readonly ControlDefinition[]; export const TARGET_EDITOR_LAYOUT = [ { id: 'theme-foundations', tier: 'default', heading: undefined, controlIds: [ 'preset', 'brand-primary', 'accent-primary', 'display-font', 'base-primary', 'border-radius', 'button-border-radius', ], }, { id: 'semantic-families', tier: 'advanced', heading: 'Advanced customization', controlIds: [ 'canvas-background', 'primary-surface', 'action-surface', 'link-color', 'status-danger', 'status-success', 'status-warning', 'status-info', ], }, ] as const satisfies readonly EditorLayoutSection[]; export function resolveControlDefinitions( definitions: readonly ControlDefinition[], catalogs: EditorControlCatalogQueries, ): readonly ResolvedControlDefinition[] { return definitions.map((definition) => { if (definition.binding.kind === 'metadata') { if (definition.type !== 'preset-select' || definition.modeExposure !== 'none') { throw new Error( `Editor control "${definition.id}" requires a preset-select widget with no mode exposure.`, ); } return definition; } if (definition.binding.kind === 'input') { if (definition.type !== 'hex-input' && definition.type !== 'font-select') { throw new Error( `Editor control "${definition.id}" uses an incompatible "${definition.type}" input widget.`, ); } const input = catalogs.getInput(definition.binding.key); if (!input) { throw new Error( `Editor control "${definition.id}" references unknown input "${definition.binding.key}".`, ); } if (definition.type === 'hex-input' && input.valueType !== 'color-seed') { throw new Error( `Editor control "${definition.id}" requires a color-seed input, received "${input.valueType}".`, ); } if (definition.type === 'font-select' && input.valueType !== 'font-family') { throw new Error( `Editor control "${definition.id}" requires a font-family input, received "${input.valueType}".`, ); } if ( definition.intentBehavior === 'pin-unpin' && (input.strategy.kind !== 'palette' || !input.strategy.fallback) ) { throw new Error( `Editor control "${definition.id}" exposes pin/unpin for an input without a fallback strategy.`, ); } return { ...definition, inputDefinition: input, }; } if ( definition.type !== 'token-ref' && definition.type !== 'scale-ref' && definition.type !== 'palette-select' ) { throw new Error( `Editor control "${definition.id}" uses an incompatible "${definition.type}" selection widget.`, ); } const selection = catalogs.getSelection(definition.binding.selectionId); if (!selection) { throw new Error( `Editor control "${definition.id}" references unknown selection "${definition.binding.selectionId}".`, ); } const sourceSet = selection.sourceSetId ? catalogs.getSourceSet(selection.sourceSetId) : undefined; if (selection.sourceSetId && !sourceSet) { throw new Error( `Editor control "${definition.id}" references unavailable source set "${selection.sourceSetId}".`, ); } if (definition.modeExposure === 'none' && selection.modePolicy === 'mode-capable') { throw new Error( `Editor control "${definition.id}" hides a mode-capable selection without linked intent.`, ); } if (selection.valueShape === 'invariant-reference' && definition.modeExposure !== 'none') { throw new Error( `Editor control "${definition.id}" exposes modes for an invariant selection.`, ); } if (definition.type === 'scale-ref' && selection.valueShape !== 'invariant-reference') { throw new Error( `Editor control "${definition.id}" requires an invariant-reference selection.`, ); } if (definition.type === 'token-ref' && selection.valueShape !== 'linked-reference') { throw new Error(`Editor control "${definition.id}" requires a linked-reference selection.`); } if (definition.modeExposure === 'linked-and-unlinked') { throw new Error( `Editor control "${definition.id}" requests unlinked mode, but its widget does not support it.`, ); } return { ...definition, selectionDefinition: selection, sourceSet, }; }); } export function validateEditorLayout( definitions: readonly ControlDefinition[], layout: readonly EditorLayoutSection[], ): void { const definitionIds = new Set(definitions.map(({ id }) => id)); const placed = new Set(); if (definitionIds.size !== definitions.length) { throw new Error('Theme Editor control IDs must be unique.'); } for (const section of layout) { for (const controlId of section.controlIds) { if (!definitionIds.has(controlId)) { throw new Error( `Theme Editor layout section "${section.id}" references unknown control "${controlId}".`, ); } if (placed.has(controlId)) { throw new Error(`Theme Editor control "${controlId}" is placed more than once.`); } placed.add(controlId); } } const unplaced = [...definitionIds].filter((controlId) => !placed.has(controlId)); if (unplaced.length > 0) { throw new Error(`Theme Editor controls are not placed in layout: ${unplaced.join(', ')}.`); } } export function diagnosticsForControl( definition: ControlDefinition, diagnostics: readonly OverrideDiagnostic[], ): readonly OverrideDiagnostic[] { const intentPath = definition.binding.kind === 'metadata' ? '$extensions.com.forge.ui.themeOverride.basePreset' : definition.binding.kind === 'input' ? `inputs.${definition.binding.key}` : `selections.${definition.binding.selectionId}`; return diagnostics.filter((diagnostic) => { if ( definition.binding.kind === 'selection' && diagnostic.provenance?.selectionId === definition.binding.selectionId ) { return true; } return diagnostic.intentPath === intentPath; }); }