import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; import path from "node:path"; type Manifest = { id?: string; name?: string; category?: string; description?: string; files?: { design?: string; tokens?: string; designTokens?: string; }; }; type TokenMap = Map; type AssetSpec = { file: string; label: string; title: string; eyebrow: string; body: string; }; const repoRoot = path.resolve(import.meta.dirname, ".."); const designSystemsRoot = path.join(repoRoot, "design-systems"); const assetSpecs: AssetSpec[] = [ { file: "landing.html", label: "Landing page", title: "Launch narrative", eyebrow: "Landing", body: "Hero, proof points, and conversion actions composed from the design system tokens.", }, { file: "deck.html", label: "Pitch deck", title: "Executive deck", eyebrow: "Deck", body: "Slide-scale title, metric, and section patterns for presentation work.", }, { file: "poster.html", label: "Poster", title: "Campaign poster", eyebrow: "Poster", body: "High-impact display composition using the system color, type, and spacing rhythm.", }, { file: "email.html", label: "Email", title: "Email module", eyebrow: "Email", body: "Transactional header, body copy, and primary call-to-action treatment.", }, { file: "newsletter.html", label: "Newsletter", title: "Editorial issue", eyebrow: "Newsletter", body: "Readable long-form layout with heading hierarchy and compact story modules.", }, { file: "form.html", label: "Form page", title: "Capture form", eyebrow: "Form", body: "Input, selection, helper text, and submission states in one focused page.", }, ]; function escapeHtml(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function token(tokens: TokenMap, names: string[], fallback: string): string { for (const name of names) { const value = tokens.get(name); if (value) return value; } return fallback; } function tokenNumber(tokens: TokenMap, names: string[], fallback: number): number { const raw = token(tokens, names, ""); const match = raw.match(/-?\d+(?:\.\d+)?/); if (!match) return fallback; return Number(match[0]); } function firstHex(tokens: TokenMap, fallback: string): string { for (const value of tokens.values()) { const match = value.match(/#[0-9a-fA-F]{6}\b/); if (match) return match[0]; } return fallback; } function parseCssTokens(css: string): TokenMap { const out = new Map(); const re = /(--[a-zA-Z0-9_-]+)\s*:\s*([^;]+);/g; for (const match of css.matchAll(re)) { out.set(match[1]!, match[2]!.trim()); } return out; } function parseDesignTokensJson(raw: string): TokenMap { const out = new Map(); try { const parsed = JSON.parse(raw) as { tokens?: Array<{ name?: unknown; value?: unknown }> }; for (const item of parsed.tokens ?? []) { if (typeof item.name === "string" && typeof item.value === "string") { out.set(item.name, item.value); } } } catch { return out; } return out; } function mergeTokens(primary: TokenMap, fallback: TokenMap): TokenMap { const out = new Map(fallback); for (const [key, value] of primary) out.set(key, value); return out; } function tokenSubset(tokens: TokenMap): Record { const primary = token(tokens, ["--accent", "--primary", "--color-primary"], firstHex(tokens, "#2563eb")); return { colorPrimary: primary, colorPrimaryBg: token(tokens, ["--accent-bg", "--surface-warm", "--surface"], "#f8fafc"), colorPrimaryHover: token(tokens, ["--accent-hover", "--accent"], primary), colorPrimaryActive: token(tokens, ["--accent-active", "--accent"], primary), fontSize: tokenNumber(tokens, ["--text-base", "--font-size-base"], 14), borderRadius: tokenNumber(tokens, ["--radius-md", "--radius", "--border-radius"], 8), }; } function cssVars(tokens: TokenMap): string { const lines = Array.from(tokens.entries()) .filter(([name]) => /^--[a-zA-Z0-9_-]+$/.test(name)) .sort(([a], [b]) => a.localeCompare(b)) .map(([name, value]) => ` ${name}: ${value};`); return [":root {", ...lines, "}"].join("\n"); } function baseStyle(tokens: TokenMap, mode: "light" | "dark"): string { const bg = mode === "dark" ? "#0f1115" : token(tokens, ["--bg", "--surface"], "#ffffff"); const surface = mode === "dark" ? "#171a21" : token(tokens, ["--surface", "--bg"], "#ffffff"); const fg = mode === "dark" ? "#f8fafc" : token(tokens, ["--fg", "--text"], "#111827"); const muted = mode === "dark" ? "#a7adba" : token(tokens, ["--muted", "--fg-2"], "#6b7280"); const border = mode === "dark" ? "#2a2f3a" : token(tokens, ["--border"], "#e5e7eb"); const accent = token(tokens, ["--accent", "--primary"], firstHex(tokens, "#2563eb")); const fontBody = token(tokens, ["--font-body", "--font-display"], "Inter, system-ui, sans-serif"); const fontDisplay = token(tokens, ["--font-display", "--font-body"], fontBody); const radius = token(tokens, ["--radius-md", "--radius"], "12px"); return ` ${cssVars(tokens)} :root { --od-page-bg: ${bg}; --od-surface: ${surface}; --od-text: ${fg}; --od-muted: ${muted}; --od-border: ${border}; --od-accent: ${accent}; --od-radius: ${radius}; --od-font-body: ${fontBody}; --od-font-display: ${fontDisplay}; } * { box-sizing: border-box; } body { margin: 0; min-height: 100vh; background: var(--od-page-bg); color: var(--od-text); font-family: var(--od-font-body); -webkit-font-smoothing: antialiased; } a { color: inherit; } .page { width: min(1100px, calc(100vw - 48px)); margin: 0 auto; padding: 72px 0; } .eyebrow { color: var(--od-accent); font-size: 12px; font-weight: 750; letter-spacing: 0; text-transform: uppercase; } .hero { display: grid; grid-template-columns: 1.05fr .95fr; gap: 32px; align-items: center; } h1, h2, h3, p { margin: 0; } h1, h2, h3 { font-family: var(--od-font-display); letter-spacing: 0; line-height: 1.05; } h1 { margin-top: 12px; font-size: 64px; } h2 { font-size: 40px; } h3 { font-size: 22px; } .lead { margin-top: 18px; color: var(--od-muted); font-size: 20px; line-height: 1.55; } .actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px; } .button { display: inline-flex; min-height: 44px; align-items: center; justify-content: center; border-radius: var(--od-radius); padding: 0 18px; font-weight: 750; text-decoration: none; border: 1px solid var(--od-border); } .button.primary { background: var(--od-accent); border-color: var(--od-accent); color: ${mode === "dark" ? "#0b0b0b" : "#ffffff"}; } .button.secondary { background: var(--od-surface); } .panel, .card, .field, .slide, .email, .poster { border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-surface); } .panel { padding: 24px; box-shadow: 0 24px 80px rgba(15, 17, 23, ${mode === "dark" ? "0.32" : "0.08"}); } .metric-grid, .card-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin-top: 22px; } .metric, .card { padding: 18px; } .metric strong { display: block; font-size: 30px; line-height: 1; } .metric span, .card p, .field span { color: var(--od-muted); font-size: 14px; line-height: 1.5; } .chip-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; } .chip { border: 1px solid var(--od-border); border-radius: 999px; padding: 8px 12px; color: var(--od-muted); } input, textarea, select { width: 100%; min-height: 44px; border: 1px solid var(--od-border); border-radius: var(--od-radius); background: var(--od-page-bg); color: var(--od-text); font: inherit; padding: 10px 12px; } label { display: grid; gap: 8px; color: var(--od-muted); font-size: 13px; font-weight: 700; } @media (max-width: 820px) { .hero, .metric-grid, .card-grid { grid-template-columns: 1fr; } .page { width: min(100vw - 28px, 1100px); padding: 40px 0; } h1 { font-size: 42px; } h2 { font-size: 30px; } } `; } function renderKitHtml(manifest: Required> & Manifest, tokens: TokenMap, mode: "light" | "dark"): string { const name = escapeHtml(manifest.name); const description = escapeHtml(manifest.description ?? `${manifest.name} reference component kit.`); return ` ${name} - ${mode === "dark" ? "dark " : ""}component kit

${escapeHtml(manifest.category)} system

${name} component kit

${description}

Reference module

Token-driven surface

12Components
4States
1Token contract
Hover Focus Active Disabled

Buttons

Primary, secondary, text, and destructive treatments resolve through shared color tokens.

Inputs

Labels, helper text, and focus state use the same spacing and radius scale.

Cards

Information modules keep elevation, border, and padding consistent across breakpoints.

`; } function renderIndexHtml(manifest: Required> & Manifest, tokens: TokenMap): string { const name = escapeHtml(manifest.name); const links = [ ["kit.html", "Component kit"], ["kit.dark.html", "Dark component kit"], ["artifacts/landing.html", "Landing page"], ["artifacts/deck.html", "Pitch deck"], ["artifacts/poster.html", "Poster"], ["artifacts/email.html", "Email"], ["artifacts/newsletter.html", "Newsletter"], ["artifacts/form.html", "Form page"], ].map(([href, label]) => `

${label}

${href}

`).join("\n"); return ` ${name} - system assets

${escapeHtml(manifest.category)}

${name} system assets

Generated preview modules for bundled official presets.

${links}
`; } function renderAssetHtml(manifest: Required> & Manifest, tokens: TokenMap, spec: AssetSpec): string { const name = escapeHtml(manifest.name); return ` ${name} - ${escapeHtml(spec.label)}

${escapeHtml(spec.eyebrow)} module

${name} ${escapeHtml(spec.title)}

${escapeHtml(spec.body)}

Preview

${escapeHtml(spec.label)}

${escapeHtml(manifest.description ?? `${manifest.name} official preset module.`)}

`; } async function readTokens(systemDir: string, manifest: Manifest): Promise { const tokensCssPath = manifest.files?.tokens ?? "tokens.css"; const designTokensPath = manifest.files?.designTokens ?? "design-tokens.json"; const [cssRaw, jsonRaw] = await Promise.all([ readFile(path.join(systemDir, tokensCssPath), "utf8").catch(() => ""), readFile(path.join(systemDir, designTokensPath), "utf8").catch(() => ""), ]); return mergeTokens(parseDesignTokensJson(jsonRaw), parseCssTokens(cssRaw)); } async function generateForDir(systemDir: string): Promise { const manifestPath = path.join(systemDir, "manifest.json"); const raw = await readFile(manifestPath, "utf8").catch(() => null); if (!raw) return false; const parsed = JSON.parse(raw) as Manifest; if (!parsed.id || !parsed.name || !parsed.category) return false; const manifest = parsed as Required> & Manifest; const tokens = await readTokens(systemDir, manifest); const outDir = path.join(systemDir, "system"); const artifactsDir = path.join(outDir, "artifacts"); await mkdir(artifactsDir, { recursive: true }); await Promise.all([ writeFile(path.join(outDir, "kit.html"), renderKitHtml(manifest, tokens, "light"), "utf8"), writeFile(path.join(outDir, "kit.dark.html"), renderKitHtml(manifest, tokens, "dark"), "utf8"), writeFile(path.join(outDir, "index.html"), renderIndexHtml(manifest, tokens), "utf8"), writeFile(path.join(outDir, "tokens.default.json"), `${JSON.stringify(tokenSubset(tokens), null, 2)}\n`, "utf8"), ...assetSpecs.map((spec) => writeFile(path.join(artifactsDir, spec.file), renderAssetHtml(manifest, tokens, spec), "utf8")), ]); return true; } async function main(): Promise { const entries = await readdir(designSystemsRoot, { withFileTypes: true }); let count = 0; for (const entry of entries) { if (!entry.isDirectory() || entry.name === "_schema") continue; if (await generateForDir(path.join(designSystemsRoot, entry.name))) count++; } console.log(`Generated system assets for ${count} design systems.`); } await main();