feat: release design system engine, studio and component contracts

This commit is contained in:
Clintchiz committed 2026-10-19 13:09:03 +05:30
1 parent ff1065fb27
commit 3943b946d6
693 files changed
+175546 -3894

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,45 @@
import { CATALOG_COMPONENT_CONTRACTS } from "./catalog-contracts.ts";
import type { ComponentRecipe, RecipeStyles } from "./types.ts";
/** Family defaults change styling only: no display, dimensions, positioning, clipping or graph geometry. */
export function catalogRecipes(): Record<string, ComponentRecipe> {
const recipes: Record<string, ComponentRecipe> = {};
const ref = (name: string) => ({ ref: name });
for (const [name, contract] of Object.entries(CATALOG_COMPONENT_CONTRACTS)) {
const base: Record<string, RecipeStyles> = {};
const axes: Record<string, { default: string }> = contract.axes;
for (const [part, descriptor] of Object.entries(contract.parts)) {
if (descriptor.ownership !== "framework" || descriptor.profile === "managed") continue;
const styles: RecipeStyles = {};
if (
descriptor.properties.includes("fontFamily") &&
descriptor.role !== "code" &&
name !== "Kbd"
)
styles.fontFamily = ref(
/^title|heading|headline/i.test(part) ? "type.heading" : "type.body",
);
const roundedFalse = axes.rounded?.default === "false";
const circle = ["circle", "round"].includes(axes.shape?.default ?? "");
const semanticGeometry =
/thumb|track|spinner|progress|marker|port$|status|dot|star|srOnly/i.test(part);
if (
descriptor.properties.includes("borderRadius") &&
!roundedFalse &&
!circle &&
!semanticGeometry
) {
styles.borderRadius = ref(
descriptor.profile === "control" ? "shape.control" : "shape.panel",
);
}
// Only existing surfaces receive elevation. Layout wrappers and provider hosts are untouched.
if (descriptor.profile === "surface" && descriptor.properties.includes("boxShadow"))
styles.boxShadow = ref("elevation.panel");
if (Object.keys(styles).length) base[part] = styles;
}
// Every catalog component has a deliberate recipe, even if only its text wrapper is styled.
recipes[name] = { contractVersion: 1, base };
}
return recipes;
}
+92
View File
@@ -0,0 +1,92 @@
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,
};
}
@@ -0,0 +1,36 @@
/** Shared immutable property profiles keep catalog metadata small and explicit. */
export const CATALOG_PART_PROPERTIES = {
text: ["color", "fontFamily", "fontSize", "fontWeight", "letterSpacing"],
layout: ["color", "fontFamily", "gap", "paddingBlock", "paddingInline"],
surface: [
"backgroundColor",
"borderColor",
"borderRadius",
"borderStyle",
"borderWidth",
"boxShadow",
"color",
"fontFamily",
"outlineColor",
],
control: [
"backgroundColor",
"backgroundImage",
"borderColor",
"borderRadius",
"borderStyle",
"borderWidth",
"boxShadow",
"color",
"fontFamily",
"fontSize",
"fontWeight",
"letterSpacing",
"outlineColor",
"paddingBlock",
"paddingInline",
],
decoration: ["color", "opacity"],
media: ["borderColor", "borderRadius", "borderStyle", "borderWidth", "boxShadow"],
managed: [],
} as const;
+9
View File
@@ -0,0 +1,9 @@
import { CORE_COMPONENT_CONTRACTS } from "./core-contracts.ts";
import { CATALOG_COMPONENT_CONTRACTS } from "./catalog-contracts.ts";
/** Entire first-party catalog. CORE_COMPONENT_CONTRACTS remains the original stable subset. */
export const COMPONENT_CONTRACTS = {
...CORE_COMPONENT_CONTRACTS,
...CATALOG_COMPONENT_CONTRACTS,
} as const;
export { CATALOG_COMPONENT_CONTRACTS } from "./catalog-contracts.ts";
File diff suppressed because it is too large. Load diff
+41
View File
@@ -0,0 +1,41 @@
export type {
TokenType,
TokenReference,
DesignToken,
TokenMap,
DesignSystem,
DesignDiagnostic,
ResolveOptions,
DesignSystemInput,
ResolvedToken,
ResolvedDesignSystem,
LegacyTheme,
} from "./types.ts";
export {
DESIGN_SYSTEM_SCHEMA_VERSION,
TOKEN_TYPES,
DesignSystemError,
assertDesignSystem,
defineDesignSystem,
} from "./validation.ts";
export { validateDesignSystem } from "./validate.ts";
export { resolveDesignSystem } from "./resolve.ts";
export { compileDesignSystem, adaptLegacyTheme } from "./compile.ts";
export { CORE_COMPONENT_CONTRACTS } from "./core-contracts.ts";
export { COMPONENT_CONTRACTS, CATALOG_COMPONENT_CONTRACTS } from "./contracts.ts";
export { componentDesignContract } from "./recipes.ts";
export type { ComponentDesignContract } from "./recipes.ts";
export type {
RecipeValue,
RecipeStyles,
RecipeParts,
RecipeRule,
RecipeStateRule,
ComponentRecipe,
} from "./types.ts";
export {
CORE_DESIGN_SYSTEMS,
SQUARE_DESIGN_SYSTEM,
SOFT_DESIGN_SYSTEM,
ANGULAR_DESIGN_SYSTEM,
} from "./presets.ts";
+96
View File
@@ -0,0 +1,96 @@
import { validateDesignSystem } from "./validate.ts";
import type { DesignSystem } from "./types.ts";
export const DESIGN_PACKAGE_VERSION = 1;
export interface DesignPackage {
formatVersion: 1;
contractVersion: 1;
system: DesignSystem;
notes?: string;
integrity: { algorithm: "SHA-256"; digest: string };
}
function stable(value: unknown): string {
if (Array.isArray(value)) return "[" + value.map(stable).join(",") + "]";
if (value && typeof value === "object")
return (
"{" +
Object.keys(value)
.filter((key) => (value as Record<string, unknown>)[key] !== undefined)
.sort()
.map((key) => JSON.stringify(key) + ":" + stable((value as Record<string, unknown>)[key]))
.join(",") +
"}"
);
return JSON.stringify(value);
}
async function digest(body: unknown) {
return [
...new Uint8Array(
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(stable(body))),
),
]
.map((n) => n.toString(16).padStart(2, "0"))
.join("");
}
function check(system: unknown): asserts system is DesignSystem {
const errors = validateDesignSystem(system);
if (errors.length)
throw Error(
"WRN-DESIGN-PACKAGE: " + errors.map((error) => error.path + ": " + error.message).join("; "),
);
if ((system as DesignSystem).extends?.length)
throw Error(
"WRN-DESIGN-PACKAGE: shared packages must be self-contained; flatten inheritance before packaging.",
);
if (!/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test((system as DesignSystem).version))
throw Error("WRN-DESIGN-PACKAGE: release version must be a semantic version.");
}
/** Portable self-contained data. The digest detects corruption; it is not a publisher signature. */
export async function createDesignPackage(
system: DesignSystem,
notes?: string,
): Promise<DesignPackage> {
check(system);
if (notes !== undefined && (typeof notes !== "string" || notes.length > 2000))
throw Error("WRN-DESIGN-PACKAGE: release notes exceed 2,000 characters.");
const body = {
formatVersion: 1 as const,
contractVersion: 1 as const,
system: structuredClone(system),
...(notes === undefined ? {} : { notes }),
};
if (JSON.stringify(body).length > 2_000_000)
throw Error("WRN-DESIGN-PACKAGE: package exceeds 2 MB.");
return { ...body, integrity: { algorithm: "SHA-256", digest: await digest(body) } };
}
/** Explicit migration: schema-v1 canonical JSON becomes a verified package; future formats fail closed. */
export async function readDesignPackage(
source: string,
): Promise<{ package: DesignPackage; migrations: string[] }> {
if (typeof source !== "string" || source.length > 2_000_000)
throw Error("WRN-DESIGN-PACKAGE: source exceeds 2 MB.");
const input = JSON.parse(source);
if (input?.formatVersion === undefined && input?.schemaVersion === 1)
return {
package: await createDesignPackage(input),
migrations: ["canonical-v1-to-package-v1"],
};
if (input?.formatVersion !== 1 || input?.contractVersion !== 1)
throw Error(
"WRN-DESIGN-PACKAGE-VERSION: unsupported format or contract version; keep the original source.",
);
if (
Object.keys(input).some(
(key) => !["formatVersion", "contractVersion", "system", "notes", "integrity"].includes(key),
)
)
throw Error("WRN-DESIGN-PACKAGE: unknown envelope fields.");
const expected = await createDesignPackage(input.system, input.notes);
if (
input.integrity?.algorithm !== "SHA-256" ||
input.integrity?.digest !== expected.integrity.digest
)
throw Error(
"WRN-DESIGN-PACKAGE-INTEGRITY: package content changed; obtain the reviewed original.",
);
return { package: expected, migrations: [] };
}
+255
View File
@@ -0,0 +1,255 @@
import type { ComponentRecipe, DesignSystem, DesignToken, RecipeParts, TokenMap } from "./types.ts";
import { catalogRecipes } from "./catalog-recipes.ts";
const ref = (name: string) => ({ ref: name });
const token = (
type: DesignToken["type"],
value: DesignToken["value"],
legacy?: string,
): DesignToken => ({ type, value, ...(legacy ? { legacy } : {}) });
function colors(values: Record<string, string>): TokenMap {
const expanded = {
...values,
"surface-raised": values.surface!,
"surface-muted": values["surface-2"]!,
"surface-soft": values["surface-2"]!,
"text-subtle": values["text-muted"]!,
"border-strong": values.border!,
"primary-muted": values["primary-soft"]!,
};
return Object.fromEntries(
Object.entries(expanded).map(([key, value]) => [
`color.${key}`,
token("color", value, `color-${key}`),
]),
);
}
function recipes(angular: boolean, outlined: boolean): Record<string, ComponentRecipe> {
const recipe = (base: RecipeParts): ComponentRecipe => ({ contractVersion: 1, base });
const surface = {
borderRadius: ref("shape.panel"),
borderWidth: "1px",
borderStyle: "solid",
borderColor: ref("color.border"),
boxShadow: ref("elevation.panel"),
};
const field = {
borderRadius: ref("shape.control"),
borderWidth: "1px",
borderStyle: "solid",
borderColor: ref("color.border"),
backgroundColor: ref("color.surface"),
color: ref("color.text"),
fontFamily: ref("type.body"),
};
const result: Record<string, ComponentRecipe> = {
Button: {
contractVersion: 1,
base: {
control: {
borderRadius: ref("shape.control"),
borderWidth: "1px",
borderStyle: "solid",
paddingInline: ref("space.control"),
boxShadow: ref("elevation.panel"),
fontFamily: ref("type.body"),
...(angular ? { rotate: "-2deg" } : {}),
},
},
variants: [
{
when: { variant: "default" },
parts: {
control: {
backgroundImage: "none",
backgroundColor: outlined ? "transparent" : ref("color.primary"),
color: outlined ? ref("color.primary") : ref("color.primary-contrast"),
borderColor: ref("color.primary"),
},
},
},
],
states: [
{ state: "focusVisible", parts: { control: { outlineColor: ref("color.primary") } } },
{
state: "disabled",
parts: { control: { opacity: 0.55, ...(angular ? { rotate: "0deg" } : {}) } },
},
],
},
Input: recipe({ control: field }),
Textarea: recipe({ control: field }),
Select: recipe({ control: field }),
Checkbox: recipe({ control: { borderRadius: ref("shape.control"), borderWidth: "1px" } }),
Radio: recipe({ control: { borderWidth: "1px" } }),
Switch: recipe({ control: { borderRadius: ref("shape.control") } }),
Card: recipe({
panel: { ...surface, ...(angular ? { rotate: "1deg" } : {}) },
body: { paddingInline: ref("space.panel"), paddingBlock: ref("space.panel") },
heading: { fontFamily: ref("type.heading") },
}),
Alert: recipe({
root: { borderRadius: ref("shape.panel"), boxShadow: ref("elevation.panel") },
}),
Modal: recipe({
panel: { ...surface, borderRadius: ref("shape.dialog") },
heading: { fontFamily: ref("type.heading") },
}),
ContextMenu: recipe({
panel: { ...surface, borderRadius: ref("shape.dialog") },
item: { borderRadius: ref("shape.control"), fontFamily: ref("type.body") },
}),
Nav: {
contractVersion: 1,
base: { link: { borderRadius: ref("shape.control"), fontFamily: ref("type.body") } },
states: [{ state: "selected", parts: { link: { fontWeight: 700 } } }],
},
PageHeader: recipe({ title: { fontFamily: ref("type.heading"), letterSpacing: "-0.025em" } }),
Container: recipe({ root: { paddingInline: ref("space.page") } }),
};
return { ...catalogRecipes(), ...result };
}
function preset(id: string, name: string, soft: boolean, angular: boolean): DesignSystem {
const mono = '"IBM Plex Mono", "Courier New", monospace';
const body = soft ? "Georgia, serif" : angular ? "Arial, sans-serif" : mono;
const heading = angular ? '"Times New Roman", serif' : body;
const light = soft
? {
bg: "#E8DCC7",
background: "#E8DCC7",
surface: "#D4B895",
"surface-2": "#E8DCC7",
text: "#292D24",
foreground: "#292D24",
"text-muted": "#4B5141",
border: "#606C38",
primary: "#606C38",
"primary-hover": "#4B542C",
"primary-active": "#3D4424",
"primary-contrast": "#FFFFFF",
"primary-text": "#3D4424",
"primary-soft": "#D4B895",
}
: angular
? {
bg: "#E8E0C0",
background: "#E8E0C0",
surface: "#EDE4CF",
"surface-2": "#E8E0C0",
text: "#232320",
foreground: "#232320",
"text-muted": "#535146",
border: "#535146",
primary: "#8F2448",
"primary-hover": "#701C39",
"primary-active": "#59172D",
"primary-contrast": "#FFFFFF",
"primary-text": "#701C39",
"primary-soft": "#E8E0C0",
}
: {
bg: "#FFFFFF",
background: "#FFFFFF",
surface: "#F7F7F8",
"surface-2": "#FFFFFF",
text: "#0B0C0A",
foreground: "#0B0C0A",
"text-muted": "#454741",
border: "#696C63",
primary: "#705000",
"primary-hover": "#594000",
"primary-active": "#443100",
"primary-contrast": "#FFFFFF",
"primary-text": "#594000",
"primary-soft": "#F7F7F8",
};
const dark = soft
? {
bg: "#292D24",
background: "#292D24",
surface: "#363D2F",
"surface-2": "#414B37",
text: "#E8DCC7",
foreground: "#E8DCC7",
"text-muted": "#C9C0AA",
border: "#8B9D83",
primary: "#D4B895",
"primary-hover": "#E8DCC7",
"primary-active": "#E8DCC7",
"primary-contrast": "#292D24",
"primary-text": "#E8DCC7",
"primary-soft": "#414B37",
}
: angular
? {
bg: "#25221E",
background: "#25221E",
surface: "#353128",
"surface-2": "#454032",
text: "#EDE4CF",
foreground: "#EDE4CF",
"text-muted": "#D0C7B2",
border: "#B0A78D",
primary: "#FF9FC6",
"primary-hover": "#FFC0DA",
"primary-active": "#FFD2E5",
"primary-contrast": "#25221E",
"primary-text": "#FF9FC6",
"primary-soft": "#454032",
}
: {
bg: "#0B0C0A",
background: "#0B0C0A",
surface: "#000000",
"surface-2": "#0B0C0A",
text: "#F7F7F8",
foreground: "#F7F7F8",
"text-muted": "#BEC0B9",
border: "#696C63",
primary: "#FFB800",
"primary-hover": "#FFD15C",
"primary-active": "#FFE099",
"primary-contrast": "#000000",
"primary-text": "#FFB800",
"primary-soft": "#0B0C0A",
};
const tokens: TokenMap = {
"shape.control": token("dimension", soft ? "16px" : "0", "radius-sm"),
"shape.panel": token("dimension", soft ? "24px" : "0", "radius-md"),
"shape.dialog": token("dimension", soft ? "32px" : "0", "radius-lg"),
"shape.base": token("dimension", soft ? "24px" : "0", "radius"),
"space.control": token("dimension", soft ? "1.25rem" : "0.75rem"),
"space.panel": token("dimension", soft ? "1.5rem" : "1rem", "space-panel"),
"space.page": token("dimension", soft ? "1.5rem" : "1rem"),
"control.height": token("dimension", soft ? "2.75rem" : "2.5rem", "control-height"),
"type.body": token("fontFamily", body, "font-sans"),
"type.heading": token("fontFamily", heading),
"elevation.panel": token("shadow", angular ? "3px 3px 0 #535146" : "none", "shadow-1"),
...colors(dark),
};
return {
schemaVersion: 1,
id,
name,
version: "1.0.0",
defaultMode: soft || angular ? "light" : "dark",
tokens,
modes: {
light: { ...colors(light), scheme: token("colorScheme", "light", "color-scheme") },
dark: { ...colors(dark), scheme: token("colorScheme", "dark", "color-scheme") },
},
recipes: recipes(angular, !soft),
};
}
/** Industrial: black/amber, monospace, square outlined controls and flat panels. */
export const SQUARE_DESIGN_SYSTEM = preset("square", "Square / Industrial", false, false);
/** Organic: sand/sage surfaces, warm serif typography, generous rounded controls. */
export const SOFT_DESIGN_SYSTEM = preset("soft", "Soft / Organic", true, false);
/** Lo-Fi: paper-yellow, mixed system typography, rotated actions and hard shadows. */
export const ANGULAR_DESIGN_SYSTEM = preset("angular", "Angular / Lo-Fi", false, true);
export const CORE_DESIGN_SYSTEMS = {
square: SQUARE_DESIGN_SYSTEM,
soft: SOFT_DESIGN_SYSTEM,
angular: ANGULAR_DESIGN_SYSTEM,
} as const;
+312
View File
@@ -0,0 +1,312 @@
import { COMPONENT_CONTRACTS } from "./contracts.ts";
import { CORE_COMPONENT_CONTRACTS } from "./core-contracts.ts";
import { isRecord, validTokenValue } from "./validation.ts";
import type {
ComponentRecipe,
DesignDiagnostic,
RecipeParts,
ResolvedDesignSystem,
TokenType,
} from "./types.ts";
import { fail } from "./resolve.ts";
export interface ComponentDesignContract {
contractVersion: number;
component: string;
source: string;
family: string;
support: string;
axes: Record<string, { default: string; values: readonly string[] }>;
parts: Record<
string,
{
states: Record<string, string>;
elements: readonly string[];
properties: readonly string[];
profile?: string;
role?: string;
ownership?: "framework" | "provider" | "native" | "content";
}
>;
slots: readonly string[];
tokens: readonly string[];
constraints: readonly string[];
fixtures: readonly string[];
compatibility: { engine: number; componentContract: number };
regions?: Record<string, { ownership: string; parts: readonly string[]; limitations: string }>;
}
export function componentDesignContract(name: string): ComponentDesignContract | undefined {
return Object.hasOwn(COMPONENT_CONTRACTS, name)
? (COMPONENT_CONTRACTS[name as keyof typeof COMPONENT_CONTRACTS] as ComponentDesignContract)
: undefined;
}
export const RECIPE_PROPERTIES: Record<string, TokenType | "borderStyle" | "angle" | "imageReset"> =
{
backgroundImage: "imageReset",
borderRadius: "dimension",
borderWidth: "dimension",
borderColor: "color",
borderStyle: "borderStyle",
boxShadow: "shadow",
paddingInline: "dimension",
paddingBlock: "dimension",
gap: "dimension",
minHeight: "dimension",
color: "color",
backgroundColor: "color",
fontFamily: "fontFamily",
fontSize: "dimension",
fontWeight: "fontWeight",
letterSpacing: "dimension",
outlineColor: "color",
opacity: "number",
rotate: "angle",
};
function literal(property: string, value: unknown): boolean {
const type = RECIPE_PROPERTIES[property];
if (type === "imageReset") return value === "none";
if (type === "borderStyle")
return ["solid", "dashed", "dotted", "double", "none"].includes(value as string);
if (type === "angle")
return (
typeof value === "string" &&
/^-?\d+(?:\.\d+)?deg$/.test(value) &&
Math.abs(parseFloat(value)) <= 8
);
if (!type || !validTokenValue(type, value) || isRecord(value)) return false;
if (property === "opacity") return Number(value) >= 0 && Number(value) <= 1;
if (
[
"borderRadius",
"borderWidth",
"fontSize",
"minHeight",
"paddingInline",
"paddingBlock",
"gap",
].includes(property)
)
return parseFloat(String(value)) >= 0;
return true;
}
export function validateRecipes(input: unknown): DesignDiagnostic[] {
if (input === undefined) return [];
const issues: DesignDiagnostic[] = [];
const error = (path: string, message: string) =>
issues.push({ code: "WRN-DS-RECIPE", path, message });
if (!isRecord(input))
return [
{ code: "WRN-DS-RECIPE", path: "recipes", message: "Expected a component recipe map." },
];
for (const [name, recipe] of Object.entries(input)) {
const path = `recipes.${name}`;
const contract = componentDesignContract(name);
if (!contract) {
error(path, "Unknown component contract.");
continue;
}
if (!isRecord(recipe)) {
error(path, "Expected recipe object.");
continue;
}
if (recipe.contractVersion !== 1)
error(`${path}.contractVersion`, "Only contract version 1 is supported.");
for (const key of Object.keys(recipe))
if (!["contractVersion", "base", "variants", "states"].includes(key))
error(`${path}.${key}`, "Unknown recipe field.");
const parts = (value: unknown, at: string, state?: string) => {
if (!isRecord(value)) {
error(at, "Expected a part map.");
return;
}
for (const [part, styles] of Object.entries(value)) {
const descriptor = contract.parts[part];
if (!Object.hasOwn(contract.parts, part) || !descriptor) {
error(`${at}.${part}`, "Unknown component part.");
continue;
}
if (state && !Object.hasOwn(descriptor.states, state))
error(`${at}.${part}`, `State '${state}' is unsupported on this part.`);
if (!isRecord(styles)) {
error(`${at}.${part}`, "Expected style property map.");
continue;
}
for (const [property, value] of Object.entries(styles)) {
if (
!Object.hasOwn(RECIPE_PROPERTIES, property) ||
!descriptor.properties.includes(property)
) {
error(`${at}.${part}.${property}`, "Unsupported property.");
continue;
}
if (
property === "rotate" &&
!((name === "Button" && part === "control") || (name === "Card" && part === "panel"))
) {
error(
`${at}.${part}.${property}`,
"Rotation is supported only on Button control and Card panel.",
);
continue;
}
if (isRecord(value)) {
if (
Object.keys(value).length !== 1 ||
typeof value.ref !== "string" ||
!validTokenValue("color", value)
)
error(`${at}.${part}.${property}`, "Expected a typed token reference.");
} else if (!literal(property, value))
error(`${at}.${part}.${property}`, "Invalid property value.");
}
}
};
if (recipe.base !== undefined) parts(recipe.base, `${path}.base`);
for (const key of ["variants", "states"])
if (recipe[key] !== undefined) {
if (!Array.isArray(recipe[key])) {
error(`${path}.${key}`, "Expected ordered rule array.");
continue;
}
for (const [index, rule] of recipe[key].entries()) {
const at = `${path}.${key}.${index}`;
if (!isRecord(rule)) {
error(at, "Expected rule object.");
continue;
}
for (const field of Object.keys(rule))
if (!["parts", key === "variants" ? "when" : "state"].includes(field))
error(`${at}.${field}`, "Unknown rule field.");
if (key === "variants") {
if (!isRecord(rule.when) || !Object.keys(rule.when).length)
error(`${at}.when`, "Expected nonempty axis conditions.");
else
for (const [axis, value] of Object.entries(rule.when))
if (
!Object.hasOwn(contract.axes, axis) ||
!contract.axes[axis]?.values.includes(value as string)
)
error(`${at}.when.${axis}`, "Unknown axis or unsupported axis value.");
} else if (
typeof rule.state !== "string" ||
!Object.values(contract.parts).some((part) =>
Object.hasOwn(part.states, rule.state as string),
)
)
error(`${at}.state`, "Unknown state name.");
parts(rule.parts, `${at}.parts`, key === "states" ? String(rule.state) : undefined);
}
}
}
return issues;
}
/** Direct owner+part selectors avoid ancestor variant leakage into nested components. */
export function compileComponentRecipes(
system: ResolvedDesignSystem,
scope = ":root",
scopeId?: string,
): string {
const blocks: string[] = [];
for (const name of Object.keys(system.recipes).sort()) {
const recipe = system.recipes[name]!;
const contract = componentDesignContract(name)!;
const emit = (parts: RecipeParts, when: Record<string, string> = {}, state?: string) => {
for (const part of Object.keys(parts).sort())
for (const property of Object.keys(parts[part]!).sort()) {
let value = parts[part]![property]!;
if (isRecord(value)) {
const token = system.tokens[value.ref as string];
if (!token)
fail(
"WRN-DS-REFERENCE-MISSING",
`recipes.${name}.${part}.${property}`,
`Unknown token '${value.ref}'.`,
);
const expected = RECIPE_PROPERTIES[property];
if (token.type !== expected || !literal(property, token.value))
fail(
"WRN-DS-REFERENCE-TYPE",
`recipes.${name}.${part}.${property}`,
"Recipe reference type/value does not match property.",
);
value =
token.legacy && token.legacy !== "color-scheme"
? `var(--wrn-${token.legacy})`
: token.value;
}
let target = `[data-wrn-owner="${name}"][data-wrn-part="${part}"][data-wrn-owner]`;
for (const [axis, choice] of Object.entries(when).sort())
target += `:where([data-wrn-axis-${axis}="${choice}"])`;
// Preserve explicit geometry/size/color choices instead of erasing instance APIs.
const guard = (axis: string) => {
if (contract.axes[axis] && !Object.hasOwn(when, axis))
target += `:where([data-wrn-axis-${axis}="${contract.axes[axis]!.default}"])`;
};
if (property === "borderRadius") {
if (name === "Button") guard("pill");
guard("radius");
guard("shape");
guard("rounded");
guard("flush");
}
if (
["Input", "Textarea", "Select"].includes(name) &&
["borderRadius", "paddingInline", "paddingBlock", "minHeight"].includes(property)
)
guard("variant");
if (
!Object.hasOwn(CORE_COMPONENT_CONTRACTS, name) &&
["borderRadius", "boxShadow", "paddingInline", "paddingBlock"].includes(property)
)
guard("variant");
if (property === "boxShadow") guard("shadow");
if (property === "borderWidth") {
guard("bordered");
guard("flush");
}
if (["paddingInline", "paddingBlock", "gap", "minHeight", "fontSize"].includes(property))
guard("size");
if (["paddingInline", "paddingBlock", "gap"].includes(property)) {
guard("compact");
guard("dense");
}
if (
["color", "backgroundColor", "backgroundImage", "borderColor", "outlineColor"].includes(
property,
)
)
guard("color");
if (state) target += `:where(${contract.parts[part]!.states[state]})`;
// Scope identifiers name unique islands. Stop at a nested design owner.
target += scopeId
? `:not([data-wrn-design="${scopeId}"] [data-wrn-design],[data-wrn-design="${scopeId}"] [data-wrn-design] *)`
: `:not([data-wrn-design],[data-wrn-design] *)`;
const cssName = property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
blocks.push(`${scope}${target},${scope} ${target}{${cssName}:${value};}`);
}
};
if (recipe.base) emit(recipe.base);
for (const rule of recipe.variants ?? []) emit(rule.parts, rule.when);
for (const rule of recipe.states ?? []) emit(rule.parts, {}, rule.state);
}
return blocks.join("\n");
}
export function mergeRecipes(
base: Record<string, ComponentRecipe>,
next: Record<string, ComponentRecipe> = {},
): void {
for (const [name, recipe] of Object.entries(next)) {
const previous = base[name];
const parts: RecipeParts = { ...previous?.base };
for (const [part, styles] of Object.entries(recipe.base ?? {}))
parts[part] = { ...parts[part], ...styles };
base[name] = {
contractVersion: 1,
base: parts,
variants: [...(previous?.variants ?? []), ...(recipe.variants ?? [])],
states: [...(previous?.states ?? []), ...(recipe.states ?? [])],
};
}
}
+117
View File
@@ -0,0 +1,117 @@
import type {
DesignSystem,
ResolveOptions,
ResolvedDesignSystem,
ResolvedToken,
TokenMap,
DesignToken,
} from "./types.ts";
import { assertDesignSystem, DesignSystemError, isReference } from "./validation.ts";
import { mergeRecipes } from "./recipes.ts";
import type { ComponentRecipe } from "./types.ts";
export function fail(code: string, path: string, message: string): never {
throw new DesignSystemError([{ code, path, message }]);
}
export function resolveDesignSystem(
system: DesignSystem,
options: ResolveOptions = {},
): ResolvedDesignSystem {
const layers: DesignSystem[] = [];
const visiting = new Set<string>();
const visit = (current: DesignSystem) => {
assertDesignSystem(current);
if (visiting.has(current.id))
fail("WRN-DS-EXTENDS-CYCLE", current.id, "System extension cycle.");
if (visiting.size >= 64)
fail("WRN-DS-LIMIT", current.id, "System inheritance exceeds 64 levels.");
visiting.add(current.id);
for (const id of current.extends ?? []) {
const parent =
options.registry && Object.hasOwn(options.registry, id) ? options.registry[id] : undefined;
if (!parent)
fail("WRN-DS-EXTENDS-MISSING", `${current.id}.extends`, `Unknown system '${id}'.`);
if (parent.id !== id) fail("WRN-DS-EXTENDS-ID", id, "Registry key must match system id.");
visit(parent);
}
visiting.delete(current.id);
layers.push(current);
if (layers.length > 256) fail("WRN-DS-LIMIT", current.id, "Too many inherited layers.");
};
visit(system);
const modes = [...new Set(layers.flatMap((layer) => Object.keys(layer.modes ?? {})))].sort();
const mode =
options.mode ?? [...layers].reverse().find((layer) => layer.defaultMode)?.defaultMode;
if (mode !== undefined && !modes.includes(mode))
fail("WRN-DS-MODE", "mode", `Unknown mode '${mode}'.`);
if (
options.density !== undefined &&
!layers.some((layer) => Object.hasOwn(layer.densities ?? {}, options.density!))
)
fail("WRN-DS-DENSITY", "density", `Unknown density '${options.density}'.`);
const raw = new Map<string, { token: DesignToken; source: string }>();
const merge = (tokens: TokenMap | undefined, source: string) => {
for (const [name, token] of Object.entries(tokens ?? {})) {
const previous = raw.get(name)?.token;
if (previous && previous.type !== token.type)
fail("WRN-DS-TYPE", `${source}.${name}`, "Overrides cannot change token type.");
// An override may omit metadata; retain the inherited legacy binding.
raw.set(name, { token: { ...previous, ...token }, source: `${source}.${name}` });
}
};
for (const layer of layers) merge(layer.tokens, `${layer.id}.tokens`);
if (mode) for (const layer of layers) merge(layer.modes?.[mode], `${layer.id}.modes.${mode}`);
if (options.density)
for (const layer of layers)
merge(layer.densities?.[options.density], `${layer.id}.densities.${options.density}`);
const resolved = new Map<string, ResolvedToken>();
const active = new Set<string>();
const token = (name: string): ResolvedToken => {
const cached = resolved.get(name);
if (cached) return cached;
const entry = raw.get(name);
if (!entry) fail("WRN-DS-REFERENCE-MISSING", name, "Referenced token does not exist.");
if (active.has(name)) fail("WRN-DS-REFERENCE-CYCLE", entry.source, "Token reference cycle.");
if (active.size >= 256) fail("WRN-DS-LIMIT", name, "Token reference depth exceeds 256.");
active.add(name);
const definition = entry.token;
let value: string | number;
let reference: string | undefined;
if (isReference(definition.value)) {
reference = definition.value.ref;
const target = token(reference);
if (target.type !== definition.type)
fail(
"WRN-DS-REFERENCE-TYPE",
entry.source,
`Reference '${reference}' has a different type.`,
);
value = target.value;
} else value = definition.value;
const result: ResolvedToken = {
type: definition.type,
value,
source: entry.source,
...(definition.legacy ? { legacy: definition.legacy } : {}),
...(reference ? { reference } : {}),
};
resolved.set(name, result);
active.delete(name);
return result;
};
const tokens: Record<string, ResolvedToken> = {};
for (const name of [...raw.keys()].sort()) tokens[name] = token(name);
const recipes: Record<string, ComponentRecipe> = {};
for (const layer of layers) mergeRecipes(recipes, layer.recipes);
return {
id: system.id,
name: system.name,
version: system.version,
...(mode ? { mode } : {}),
...(options.density ? { density: options.density } : {}),
modes,
tokens,
recipes,
};
}
+84
View File
@@ -0,0 +1,84 @@
export type TokenType =
| "color"
| "dimension"
| "number"
| "fontFamily"
| "duration"
| "shadow"
| "fontWeight"
| "colorScheme";
export type RecipeValue = string | number | TokenReference;
export type RecipeStyles = Record<string, RecipeValue>;
export type RecipeParts = Record<string, RecipeStyles>;
export interface RecipeRule {
when: Record<string, string>;
parts: RecipeParts;
}
export interface RecipeStateRule {
state: string;
parts: RecipeParts;
}
export interface ComponentRecipe {
contractVersion: 1;
base?: RecipeParts;
variants?: RecipeRule[];
states?: RecipeStateRule[];
}
export interface TokenReference {
ref: string;
}
export interface DesignToken {
type: TokenType;
value: string | number | TokenReference;
/** Existing --wrn-* suffix, such as radius-sm. No implicit name conversion. */
legacy?: string;
description?: string;
}
export type TokenMap = Record<string, DesignToken>;
export interface DesignSystem {
schemaVersion: 1;
id: string;
name: string;
version: string;
description?: string;
extends?: string[];
tokens: TokenMap;
modes?: Record<string, TokenMap>;
densities?: Record<string, TokenMap>;
defaultMode?: string;
recipes?: Record<string, ComponentRecipe>;
}
export interface DesignDiagnostic {
code: string;
path: string;
message: string;
}
export interface ResolveOptions {
registry?: Record<string, DesignSystem>;
mode?: string;
density?: string;
}
export interface DesignSystemInput extends ResolveOptions {
system: DesignSystem;
}
export interface ResolvedToken {
type: TokenType;
value: string | number;
legacy?: string;
source: string;
reference?: string;
}
export interface ResolvedDesignSystem {
id: string;
name: string;
version: string;
mode?: string;
density?: string;
modes: string[];
tokens: Record<string, ResolvedToken>;
recipes: Record<string, ComponentRecipe>;
}
export interface LegacyTheme {
default?: string;
themes?: Record<string, Record<string, string>>;
}
+47
View File
@@ -0,0 +1,47 @@
import type { DesignDiagnostic, ResolveOptions } from "./types.ts";
import {
validateDesignSystem as validateSchema,
DesignSystemError,
assertDesignSystem,
} from "./validation.ts";
import { compileDesignSystem } from "./compile.ts";
/** Validate schema and every available mode/density combination, including output bindings. */
export function validateDesignSystem(
input: unknown,
options: ResolveOptions = {},
): DesignDiagnostic[] {
const diagnostics = validateSchema(input);
if (diagnostics.length) return diagnostics;
assertDesignSystem(input);
try {
const initial = compileDesignSystem(input, options);
// Discover inherited density names using a bounded traversal of validated parents.
const densities = new Set<string>();
const seen = new Set<string>();
const walk = (system: typeof input) => {
if (seen.has(system.id)) return;
seen.add(system.id);
for (const name of Object.keys(system.densities ?? {})) densities.add(name);
for (const id of system.extends ?? []) {
const parent = options.registry?.[id];
if (parent) walk(parent);
}
};
walk(input);
if (densities.size * Math.max(initial.resolved.modes.length, 1) > 1024)
return [
{
code: "WRN-DS-LIMIT",
path: "densities",
message: "Validation exceeds 1024 mode/density combinations.",
},
];
for (const density of [...densities].sort())
compileDesignSystem(input, { ...options, density });
return [];
} catch (error) {
if (error instanceof DesignSystemError) return error.diagnostics;
throw error;
}
}
+221
View File
@@ -0,0 +1,221 @@
import type { DesignDiagnostic, DesignSystem, DesignToken, TokenType } from "./types.ts";
import { validateRecipes } from "./recipes.ts";
export const DESIGN_SYSTEM_SCHEMA_VERSION = 1;
export const TOKEN_TYPES: readonly TokenType[] = [
"color",
"dimension",
"number",
"fontFamily",
"duration",
"shadow",
"fontWeight",
"colorScheme",
];
const forbidden = new Set(["__proto__", "prototype", "constructor"]);
export const validName = (value: string): boolean =>
/^[a-zA-Z][\w.-]*$/.test(value) && !value.split(/[.-]/).some((part) => forbidden.has(part));
export const isRecord = (v: unknown): v is Record<string, unknown> =>
typeof v === "object" &&
v !== null &&
!Array.isArray(v) &&
[Object.prototype, null].includes(Object.getPrototypeOf(v));
const length = /^-?(?:\d*\.)?\d+(?:px|rem|em|%|vw|vh|dvh|svh|ch|ex|vmin|vmax)$/;
/** Deliberately bounded CSS subset. References use { ref }, never arbitrary var()/url(). */
export function validTokenValue(type: TokenType, value: unknown): boolean {
if (isRecord(value))
return Object.keys(value).length === 1 && typeof value.ref === "string" && validName(value.ref);
if (typeof value === "number")
return (
Number.isFinite(value) &&
(type === "number" || (type === "fontWeight" && value >= 1 && value <= 1000))
);
if (
typeof value !== "string" ||
!value.trim() ||
value.length > 2048 ||
/[;{}<>\\]|\/\*|\*\//.test(value) ||
[...value].some((character) => character.charCodeAt(0) < 32)
)
return false;
if (type === "colorScheme") return value === "light" || value === "dark";
if (type === "dimension") return value === "0" || length.test(value);
if (type === "duration") return /^(?:\d*\.)?\d+(ms|s)$/.test(value);
if (type === "fontWeight") return /^(normal|bold|[1-9]\d{0,2}|1000)$/.test(value);
if (type === "number") return /^-?(?:\d*\.)?\d+$/.test(value) && Number.isFinite(Number(value));
if (type === "fontFamily")
return /^(?:"[\w -]+"|'[\w -]+'|[a-zA-Z][\w -]*)(?:\s*,\s*(?:"[\w -]+"|'[\w -]+'|[a-zA-Z][\w -]*))*$/.test(
value,
);
if (type === "color") {
if (
/^(?:#[\da-f]{3}|#[\da-f]{4}|#[\da-f]{6}|#[\da-f]{8})$/i.test(value) ||
/^(?:transparent|currentColor|black|white|red|green|blue|gray|grey|orange|purple|yellow|pink|rebeccapurple)$/i.test(
value,
)
)
return true;
const call = /^(rgb|rgba|hsl|hsla|oklch|oklab|lab|lch)\(([^()]+)\)$/i.exec(value);
if (!call) return false;
const args = call[2]!.trim();
const kind = call[1]!.toLowerCase();
let channels: string[];
if (args.includes(",")) {
if (args.includes("/") || !["rgb", "rgba", "hsl", "hsla"].includes(kind)) return false;
channels = args.split(/\s*,\s*/);
} else {
const sections = args.split(/\s*\/\s*/);
if (sections.length > 2) return false;
channels = sections[0]!.trim().split(/\s+/);
if (channels.length !== 3) return false;
if (sections.length === 2) channels.push(sections[1]!);
}
return (
(channels.length === 3 || channels.length === 4) &&
channels.every((channel, index) => {
const hue =
(["hsl", "hsla"].includes(kind) && index === 0) ||
(["lch", "oklch"].includes(kind) && index === 2);
const pattern = hue ? /^[+-]?(?:\d*\.)?\d+(?:deg)?$/ : /^[+-]?(?:\d*\.)?\d+%?$/;
if (!pattern.test(channel) || !Number.isFinite(parseFloat(channel))) return false;
return (
!(["hsl", "hsla"].includes(kind) && (index === 1 || index === 2)) || channel.endsWith("%")
);
})
);
}
if (type === "shadow") {
if (value === "none") return true;
const shadows = value.split(/,(?![^()]*\))/);
return shadows.every((shadow) => {
let colors = 0;
const dimensions = shadow
.replace(
/(?:rgba?|hsla?|oklch|oklab|lab|lch)\([^()]*\)|#[\da-f]+|\b(?:black|white|transparent|currentColor)\b/gi,
(color) => {
if (!validTokenValue("color", color)) return "INVALID";
colors++;
return "";
},
)
.trim()
.split(/\s+/);
const inset = dimensions.indexOf("inset");
if (inset !== -1) dimensions.splice(inset, 1);
return (
colors <= 1 &&
dimensions.length >= 2 &&
dimensions.length <= 4 &&
dimensions.every((part) => part === "0" || (length.test(part) && !part.endsWith("%"))) &&
(dimensions.length < 3 || parseFloat(dimensions[2]!) >= 0)
);
});
}
return false;
}
export function validateDesignSystem(input: unknown): DesignDiagnostic[] {
const issues: DesignDiagnostic[] = [];
const error = (code: string, path: string, message: string) =>
issues.push({ code, path, message });
if (!isRecord(input))
return [{ code: "WRN-DS-SCHEMA", path: "$", message: "Expected a data-only object." }];
const fields = new Set([
"schemaVersion",
"id",
"name",
"version",
"description",
"extends",
"tokens",
"modes",
"densities",
"defaultMode",
"recipes",
]);
for (const key of Object.keys(input).sort())
if (!fields.has(key)) error("WRN-DS-FIELD", key, "Unknown or unsupported field.");
if (input.schemaVersion !== 1)
error("WRN-DS-VERSION", "schemaVersion", "Only schemaVersion 1 is supported.");
for (const key of ["id", "name", "version"] as const)
if (typeof input[key] !== "string" || !input[key].trim())
error("WRN-DS-SCHEMA", key, "Expected a nonempty string.");
if (typeof input.id === "string" && !validName(input.id))
error("WRN-DS-NAME", "id", "Invalid system identifier.");
if (input.description !== undefined && typeof input.description !== "string")
error("WRN-DS-SCHEMA", "description", "Expected a string.");
if (
input.extends !== undefined &&
(!Array.isArray(input.extends) ||
input.extends.some((v) => typeof v !== "string" || !validName(v)))
)
error("WRN-DS-SCHEMA", "extends", "Expected system identifier array.");
if (
input.defaultMode !== undefined &&
(typeof input.defaultMode !== "string" || !validName(input.defaultMode))
)
error("WRN-DS-NAME", "defaultMode", "Invalid mode name.");
const map = (value: unknown, path: string) => {
if (!isRecord(value)) {
error("WRN-DS-SCHEMA", path, "Expected a token map.");
return;
}
for (const [key, token] of Object.entries(value).sort()) {
const at = `${path}.${key}`;
if (!validName(key)) error("WRN-DS-NAME", at, "Invalid token name.");
if (!isRecord(token)) {
error("WRN-DS-SCHEMA", at, "Expected a token object.");
continue;
}
for (const field of Object.keys(token))
if (!["type", "value", "legacy", "description"].includes(field))
error("WRN-DS-FIELD", `${at}.${field}`, "Unknown token field.");
if (!TOKEN_TYPES.includes(token.type as TokenType))
error("WRN-DS-TYPE", `${at}.type`, "Unsupported token type.");
else if (!validTokenValue(token.type as TokenType, token.value))
error("WRN-DS-VALUE", `${at}.value`, "Invalid or unsupported token value.");
if (
token.legacy !== undefined &&
(typeof token.legacy !== "string" ||
!/^[a-z][a-z0-9-]*$/.test(token.legacy) ||
!validName(token.legacy))
)
error("WRN-DS-NAME", `${at}.legacy`, "Invalid legacy token suffix.");
if (token.legacy === "color-scheme" && token.type !== "colorScheme")
error("WRN-DS-TYPE", `${at}.legacy`, "color-scheme requires colorScheme type.");
if (token.description !== undefined && typeof token.description !== "string")
error("WRN-DS-SCHEMA", `${at}.description`, "Expected a string.");
}
};
map(input.tokens, "tokens");
for (const axis of ["modes", "densities"])
if (input[axis] !== undefined) {
if (!isRecord(input[axis])) error("WRN-DS-SCHEMA", axis, "Expected a map.");
else
for (const [key, tokens] of Object.entries(input[axis]).sort()) {
if (!validName(key)) error("WRN-DS-NAME", `${axis}.${key}`, "Invalid axis name.");
map(tokens, `${axis}.${key}`);
}
}
issues.push(...validateRecipes(input.recipes));
return issues;
}
export class DesignSystemError extends Error {
constructor(public readonly diagnostics: DesignDiagnostic[]) {
super(diagnostics.map((d) => `${d.code} ${d.path}: ${d.message}`).join("\n"));
this.name = "DesignSystemError";
}
}
export function assertDesignSystem(input: unknown): asserts input is DesignSystem {
const issues = validateDesignSystem(input);
if (issues.length) throw new DesignSystemError(issues);
}
export function defineDesignSystem<T extends DesignSystem>(input: T): T {
assertDesignSystem(input);
return input;
}
export function isReference(value: DesignToken["value"]): value is { ref: string } {
return isRecord(value);
}