/** * Theme CSS Compiler -- `ThemeDef` to `html[data-theme]` selector blocks + transitions. * * Emits per-variant CSS blocks overriding `--czap-*` custom properties, * and optional transition declarations for animated theme switching. * * @module */ import type { Theme } from '@czap/core'; import { THEME_TRANSITION_DURATION_MS, THEME_TRANSITION_EASING } from '@czap/core'; import { stringifyCSSValue } from './css-utils.js'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- /** * Output of {@link ThemeCSSCompiler.compile}. * * `selectors` is the concatenated `html[data-theme="variant"]` rule block, * one per theme variant that has at least one token override. `transitions` * is the optional `:root { transition-*: … }` block emitted when the theme * carries metadata indicating animated switching is desired. */ export interface ThemeCSSResult { /** Per-variant `html[data-theme]` selector blocks. */ readonly selectors: string; /** Optional root transition declarations for animated theme swaps. */ readonly transitions: string; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** * Derive the CSS custom property name from a token name within a theme. */ function fxProp(tokenName: string): string { return `--czap-${tokenName}`; } // --------------------------------------------------------------------------- // ThemeCSSCompiler // --------------------------------------------------------------------------- /** * Compile a {@link Theme.Shape} into per-variant selector blocks and optional * root transitions. */ function compile(theme: Theme.Shape): ThemeCSSResult { const selectorBlocks: string[] = []; const transitionProps = new Set(); for (const variant of theme.variants) { const declarations: string[] = []; for (const [tokenName, variantMap] of Object.entries(theme.tokens)) { const value = variantMap[variant]; if (value === undefined) continue; const prop = fxProp(tokenName); declarations.push(` ${prop}: ${stringifyCSSValue(value)};`); transitionProps.add(prop); } if (declarations.length === 0) continue; selectorBlocks.push([`html[data-theme="${variant}"] {`, ...declarations, `}`].join('\n')); } const selectors = selectorBlocks.join('\n\n'); // Emit transition rules if theme has meta (indicates animated switching is desired) let transitions = ''; if (theme.meta && transitionProps.size > 0) { const propList = Array.from(transitionProps).join(', '); transitions = [ `:root {`, ` transition-property: ${propList};`, ` transition-duration: ${THEME_TRANSITION_DURATION_MS}ms;`, ` transition-timing-function: ${THEME_TRANSITION_EASING};`, `}`, ].join('\n'); } return { selectors, transitions }; } /** * Theme CSS compiler namespace. * * Serializes a {@link Theme.Shape} into `html[data-theme="…"]` selector * overrides of `--czap-*` custom properties and, when theme metadata * requests it, a `:root` transition block that animates all theme * property changes. */ export const ThemeCSSCompiler = { /** Compile a theme definition into per-variant selector blocks. */ compile, } as const;