import type { CSSProperties } from '../utils/css-engine'; /** * Primitive variant value supported by the recipe system. */ export type VariantValue = string | number | boolean | null | undefined; export type VariantDefinitions> = { [VariantKey in keyof Variants]?: Record; }; export interface CompoundVariant> { when: Partial>; styles: CSSProperties; } export interface RecipeSelectors { [selector: string]: CSSProperties; } export interface FluentRecipe = Record> { base: CSSProperties; variants?: VariantDefinitions; compoundVariants?: Array>; selectors?: RecipeSelectors; tokens?: CSSProperties; } export interface ComputedRecipe { host: CSSProperties; selectors: RecipeSelectors; } /** * Responsible for mapping a recipe definition + runtime variants into a * concrete CSS object. The implementation is intentionally allocation-light so * that we can execute it for every host update without blowing budgets. */ export class RecipeEngine { static compute>( recipe: FluentRecipe, variants: Partial = {} ): ComputedRecipe { const hostStyles: CSSProperties = { ...recipe.base }; const selectorStyles: RecipeSelectors = { ...(recipe.selectors ?? {}) }; if (recipe.variants) { for (const [key, value] of Object.entries(variants)) { if (value === undefined || value === null) { continue; } const variantGroup = recipe.variants[key as keyof Variants]; if (!variantGroup) { continue; } const normalised = normaliseVariantValue(value); const variantStyles = variantGroup[normalised]; if (variantStyles) { Object.assign(hostStyles, variantStyles); } } } if (recipe.compoundVariants?.length) { for (const compound of recipe.compoundVariants) { const matches = Object.entries(compound.when).every(([variantName, expected]) => { const provided = variants[variantName as keyof Variants]; return normaliseVariantValue(provided) === normaliseVariantValue(expected); }); if (matches) { Object.assign(hostStyles, compound.styles); } } } if (recipe.tokens) { Object.entries(recipe.tokens).forEach(([name, value]) => { if (value !== undefined) { hostStyles[`--${name}`] = value; } }); } return { host: hostStyles, selectors: selectorStyles }; } static toStyleMap(computed: ComputedRecipe, hostSelector = ':host'): RecipeSelectors { return { [hostSelector]: computed.host, ...computed.selectors }; } } function normaliseVariantValue(value: VariantValue): string { if (value === null || value === undefined) { return ''; } if (typeof value === 'boolean') { return value ? 'true' : 'false'; } return String(value); }