import { validateDesignSystem, type DesignSystem, type DesignToken } from "@wrnexus/design-system"; export const MANUAL_COLORS = { primary: "Brand / primary", secondary: "Secondary", bg: "Page background", surface: "Surface", "surface-2": "Subtle surface", text: "Text", "text-muted": "Muted text", border: "Borders", success: "Success", warning: "Warning", danger: "Error", info: "Information", } as const; export const MANUAL_FONTS = { sans: '"Helvetica Neue", Arial, sans-serif', humanist: '"Trebuchet MS", Arial, sans-serif', serif: 'Georgia, "Times New Roman", serif', mono: '"Courier New", monospace', }; export type ManualEdit = | { kind: "color"; mode: string; name: string; value: string } | { kind: "token"; name: string; value: string } | { kind: "mode"; value: string } | { kind: "buttonStyle"; value: "solid" | "outline" } | { kind: "tilt"; value: "none" | "subtle" | "expressive" }; function ink(hex: string) { const channels = hex .slice(1) .match(/../g)! .map((s) => parseInt(s, 16) / 255) .map((c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)); const luminance = channels.reduce((sum, c, i) => sum + c * [0.2126, 0.7152, 0.0722][i]!, 0); return (luminance + 0.05) / 0.05 >= 1.05 / (luminance + 0.05) ? "#000000" : "#FFFFFF"; } export function manualDesign(source: DesignSystem, edit: ManualEdit): DesignSystem { const system = structuredClone(source); const set = ( tokens: Record, name: string, value: string, type: DesignToken["type"] = "color", legacy?: string, ) => { tokens[name] = { ...tokens[name], type, value, ...(legacy ? { legacy: tokens[name]?.legacy ?? legacy } : {}), }; }; if (edit.kind === "buttonStyle") { const recipe = ((system.recipes ??= {}).Button ??= { contractVersion: 1, base: {} }); const variant = { when: { variant: "default" }, parts: { control: { backgroundImage: "none", backgroundColor: edit.value === "outline" ? "transparent" : { ref: "color.primary" }, color: { ref: edit.value === "outline" ? "color.primary" : "color.primary-contrast" }, borderColor: { ref: "color.primary" }, }, }, }; recipe.variants = [ ...(recipe.variants ?? []).filter( (rule) => !(Object.keys(rule.when).length === 1 && rule.when.variant === "default"), ), variant, ]; } else if (edit.kind === "tilt") { for (const [name, part, angle] of [ [ "Button", "control", edit.value === "none" ? "0deg" : edit.value === "subtle" ? "-0.5deg" : "-2deg", ], [ "Card", "panel", edit.value === "none" ? "0deg" : edit.value === "subtle" ? "0.25deg" : "1deg", ], ]) { const recipe = ((system.recipes ??= {})[name!] ??= { contractVersion: 1, base: {} }); ((recipe.base ??= {})[part!] ??= {}).rotate = angle!; } } else if (edit.kind === "mode") { if (!Object.hasOwn(system.modes ?? {}, edit.value)) throw Error("Choose an existing light or dark mode."); system.defaultMode = edit.value; } else if (edit.kind === "color") { if ( (!Object.hasOwn(MANUAL_COLORS, edit.name) && system.tokens["color." + edit.name]?.type !== "color" && system.modes?.[edit.mode]?.["color." + edit.name]?.type !== "color") || !/^#[a-f0-9]{6}$/i.test(edit.value) ) throw Error("Choose a color or enter a six-digit hex value, such as #00677B."); const layer = edit.mode === "base" ? system.tokens : system.modes?.[edit.mode]; if (!layer) throw Error("Choose an existing light or dark mode."); const color = (name: string, value = edit.value) => set( layer, "color." + name, value, "color", layer["color." + name]?.legacy ?? system.tokens["color." + name]?.legacy ?? "color-" + name, ); color(edit.name); for (const alias of ( { bg: ["background"], surface: ["surface-raised", "input-background"], "surface-2": [ "surface-soft", "surface-muted", "primary-soft", "primary-muted", "code-background", ], text: ["foreground", "code-text"], "text-muted": ["text-subtle", "muted"], border: ["border-strong", "input-border", "code-border"], } as Record )[edit.name] ?? []) color(alias); if (["primary", "secondary", "success", "warning", "danger", "info"].includes(edit.name)) { color(edit.name + "-contrast", ink(edit.value)); color(edit.name + "-soft", edit.value + "1A"); if (edit.name === "primary") color("primary-muted", edit.value + "1A"); color(edit.name + "-text"); for (const [suffix, factor] of [ ["-hover", 0.88], ["-active", 0.75], ] as const) color( edit.name + suffix, "#" + edit.value .slice(1) .match(/../g)! .map((c) => Math.round(parseInt(c, 16) * factor) .toString(16) .padStart(2, "0"), ) .join(""), ); if (edit.name === "primary") color("focus"); } } else { const allowed: Record = { "shape.control": ["dimension", "radius-sm"], "shape.panel": ["dimension", "radius-md"], "shape.dialog": ["dimension", "radius-lg"], "type.body": ["fontFamily", "font-sans"], "type.heading": ["fontFamily", undefined], "elevation.panel": ["shadow", "shadow-1"], "space.control": ["dimension", undefined], "space.panel": ["dimension", "space-panel"], }; if (!allowed[edit.name]) throw Error("Unsupported manual design setting."); const [type, legacy] = allowed[edit.name]!; if ( type === "dimension" && (!/^\d+(?:\.\d+)?px$/.test(edit.value) || parseFloat(edit.value) > (edit.name.startsWith("shape.") ? 999 : 64)) ) throw Error("Enter corners between 0 and 999 pixels, or padding between 0 and 64 pixels."); set(system.tokens, edit.name, edit.value, type, legacy); // Existing mode overrides must not silently win over a shared foundation edit. for (const layer of Object.values(system.modes ?? {})) if (layer[edit.name]) set(layer, edit.name, edit.value, type, legacy); if (edit.name === "shape.panel") set(system.tokens, "shape.base", edit.value, "dimension", "radius"); } const errors = validateDesignSystem(system); if (errors.length) throw Error(errors.map((e) => e.message).join("\n")); return system; }