93 lines
3.7 KiB
TypeScript
93 lines
3.7 KiB
TypeScript
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<string, string> {
|
|
const result: Record<string, string> = {};
|
|
const owners = new Map<string, string>();
|
|
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<T extends LegacyTheme>(
|
|
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<string, Record<string, string>> = {};
|
|
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,
|
|
};
|
|
}
|