import type { DesignSystem, LegacyTheme, ResolveOptions, ResolvedDesignSystem } from "./types.ts"; import { resolveDesignSystem, fail } from "./resolve.ts"; import { validName } from "./validation.ts"; import { compileComponentRecipes } from "./recipes.ts"; function declarations(system: ResolvedDesignSystem, legacyOnly = false): Record { const result: Record = {}; const owners = new Map(); for (const [name, token] of Object.entries(system.tokens)) { const keys = legacyOnly ? token.legacy ? [token.legacy] : [] : [ `--wrn-ds-${name.replace(/\./g, "-")}`, ...(token.legacy ? [token.legacy === "color-scheme" ? "color-scheme" : `--wrn-${token.legacy}`] : []), ]; for (const key of keys) { const previous = owners.get(key); if (previous && previous !== name) fail("WRN-DS-COLLISION", name, `Output '${key}' is already bound to '${previous}'.`); owners.set(key, name); result[key] = String(token.value); } } return Object.fromEntries( Object.entries(result).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)), ); } /** Compile deterministic tokens and versioned core-component recipes. */ export function compileDesignSystem( system: DesignSystem, options: ResolveOptions & { scope?: string } = {}, ): { css: string; recipeCss: string; resolved: ResolvedDesignSystem } { if (options.scope !== undefined && !validName(options.scope)) fail("WRN-DS-NAME", "scope", "Invalid scope identifier."); const resolved = resolveDesignSystem(system, options); const selector = options.scope ? `[data-wrn-design="${options.scope}"]` : ":root"; const block = (target: string, value: ResolvedDesignSystem) => `${target}{${Object.entries(declarations(value)) .map(([key, val]) => `${key}:${val};`) .join("")}}`; const blocks = [block(selector, resolved)]; const defaultRecipes = compileComponentRecipes(resolved, selector, options.scope); const recipeBlocks = [defaultRecipes]; for (const mode of resolved.modes) { blocks.push( block(`${selector}[data-theme="${mode}"]`, resolveDesignSystem(system, { ...options, mode })), ); const modeResolved = resolveDesignSystem(system, { ...options, mode }); // Legacy-bound references already follow mode variables. Avoid repeating identical rules. const normalized = compileComponentRecipes(modeResolved, selector, options.scope); if (normalized !== defaultRecipes) recipeBlocks.push( compileComponentRecipes(modeResolved, `${selector}[data-theme="${mode}"]`, options.scope), ); } const recipeCss = recipeBlocks.filter(Boolean).join("\n"); return { css: blocks.join("\n") + "\n" + (recipeCss ? recipeCss + "\n" : ""), recipeCss, resolved, }; } /** Return existing ThemeConfig-compatible data, retaining explicit app overrides. */ export function adaptLegacyTheme( system: DesignSystem, legacy: T, options: ResolveOptions = {}, ): T & LegacyTheme { const resolved = resolveDesignSystem(system, options); const base = declarations(resolved, true); const names = [ ...new Set(["light", "dark", ...resolved.modes, ...Object.keys(legacy.themes ?? {})]), ].sort(); const themes: Record> = {}; for (const name of names) { const mapped = resolved.modes.includes(name) ? declarations(resolveDesignSystem(system, { ...options, mode: name }), true) : base; themes[name] = { ...mapped, ...(legacy.themes?.[name] ?? {}) }; } return { ...legacy, ...(legacy.default ? {} : resolved.mode ? { default: resolved.mode } : {}), themes, }; }