export const themeNames = ["violet", "jade", "sunset"] as const; export type ThemeName = (typeof themeNames)[number]; export const defaultTheme: ThemeName = "violet"; export const themeDetails = { violet: { label: "Violet Seed", note: "A Material baseline seeded from a soft violet, close to the reference M3 demos.", seed: "#6750A4" }, jade: { label: "Jade Seed", note: "Cool green-blue tonal families for calmer product surfaces and lower visual heat.", seed: "#0B8F83" }, sunset: { label: "Sunset Seed", note: "Warm coral tonal families for more human, expressive Material palettes.", seed: "#B75A46" } } as const satisfies Record; export const motionModeNames = ["interactive", "static"] as const; export type MotionModeName = (typeof motionModeNames)[number]; export const defaultMotionMode: MotionModeName = "interactive"; export const motionModeDetails = { interactive: { label: "Standard", note: "The default Material motion language for state, depth, and spatial feedback." }, static: { label: "Static", note: "Preserve clarity while removing motion-heavy transitions and hover choreography." } } as const satisfies Record; export const motionScale = { instant: "var(--dur-instant)", fast: "var(--dur-fast)", base: "var(--dur-base)", slow: "var(--dur-slow)", deliberate: "var(--dur-deliberate)" } as const; export const colorTokens = [ { name: "background", cssVar: "--color-background", role: "Application canvas" }, { name: "surface", cssVar: "--color-surface", role: "Base surface container" }, { name: "surface-container", cssVar: "--color-surface-container", role: "Default tonal container for cards and supporting panels" }, { name: "surface-container-high", cssVar: "--color-surface-container-high", role: "Raised tonal container for overlays and prominent groups" }, { name: "surface-container-highest", cssVar: "--color-surface-container-highest", role: "Highest emphasis surface used for fields and selected chips" }, { name: "foreground", cssVar: "--color-foreground", role: "Primary text and icons" }, { name: "on-surface-variant", cssVar: "--color-on-surface-variant", role: "Supporting text, dividers, and lower-emphasis iconography" }, { name: "primary", cssVar: "--color-primary", role: "Filled actions and active emphasis" }, { name: "primary-container", cssVar: "--color-primary-container", role: "Tonal action backgrounds and highlighted containers" }, { name: "secondary-container", cssVar: "--color-secondary-container", role: "Filled tonal surfaces for secondary emphasis" }, { name: "tertiary-container", cssVar: "--color-tertiary-container", role: "Expressive accent container for supportive highlights" }, { name: "outline", cssVar: "--color-outline", role: "Primary stroke and input outline color" }, { name: "outline-variant", cssVar: "--color-outline-variant", role: "Lower-emphasis border and separator color" }, { name: "surface-tint", cssVar: "--color-surface-tint", role: "Tint color for tonal elevation" }, { name: "error", cssVar: "--color-error", role: "Error and destructive feedback" }, { name: "success", cssVar: "--color-success", role: "Positive validation and confirmation" }, { name: "warning", cssVar: "--color-warning", role: "Cautionary feedback" } ] as const; export const typographyTokens = [ { name: "label", fontVar: "--text-sm", lineHeightVar: "--leading-snug", familyVar: "--font-sans", sample: "Labels stay crisp, compact, and readable across controls." }, { name: "body", fontVar: "--text-base", lineHeightVar: "--leading-normal", familyVar: "--font-sans", sample: "Body copy stays clear and quiet so color and hierarchy do the expressive work." }, { name: "title", fontVar: "--text-xl", lineHeightVar: "--leading-snug", familyVar: "--font-display", sample: "Titles feel warm and rounded, without drifting into editorial flourish." }, { name: "display", fontVar: "--text-4xl", lineHeightVar: "--leading-tight", familyVar: "--font-display", sample: "Display copy should feel optimistic, human, and distinctly Material." } ] as const; export const radiusTokens = [ { name: "xs", cssVar: "--radius-xs" }, { name: "sm", cssVar: "--radius-sm" }, { name: "md", cssVar: "--radius-md" }, { name: "lg", cssVar: "--radius-lg" }, { name: "xl", cssVar: "--radius-xl" }, { name: "full", cssVar: "--radius-full" } ] as const; export const shadowTokens = [ { name: "xs", cssVar: "--shadow-xs" }, { name: "sm", cssVar: "--shadow-sm" }, { name: "md", cssVar: "--shadow-md" }, { name: "lg", cssVar: "--shadow-lg" } ] as const; export const motionTokens = { durations: [ { name: "instant", cssVar: "--dur-instant" }, { name: "fast", cssVar: "--dur-fast" }, { name: "base", cssVar: "--dur-base" }, { name: "slow", cssVar: "--dur-slow" }, { name: "deliberate", cssVar: "--dur-deliberate" } ], easings: [ { name: "standard", cssVar: "--ease-standard" }, { name: "emphasized", cssVar: "--ease-emphasized" }, { name: "exit", cssVar: "--ease-exit" } ], distances: [ { name: "xs", cssVar: "--distance-xs" }, { name: "sm", cssVar: "--distance-sm" }, { name: "md", cssVar: "--distance-md" }, { name: "lg", cssVar: "--distance-lg" } ], scales: [ { name: "press", cssVar: "--scale-press" }, { name: "hover", cssVar: "--scale-hover" }, { name: "pop", cssVar: "--scale-pop" } ] } as const; type DynamicColorVariableName = | "--color-background" | "--color-foreground" | "--color-surface" | "--color-surface-strong" | "--color-surface-contrast" | "--color-surface-dim" | "--color-surface-bright" | "--color-surface-container-low" | "--color-surface-container" | "--color-surface-container-high" | "--color-surface-container-highest" | "--color-border" | "--color-border-strong" | "--color-input" | "--color-ring" | "--color-primary" | "--color-primary-foreground" | "--color-primary-container" | "--color-on-primary-container" | "--color-secondary" | "--color-secondary-foreground" | "--color-secondary-container" | "--color-on-secondary-container" | "--color-tertiary" | "--color-tertiary-foreground" | "--color-tertiary-container" | "--color-on-tertiary-container" | "--color-muted" | "--color-muted-foreground" | "--color-accent" | "--color-accent-foreground" | "--color-success" | "--color-success-foreground" | "--color-warning" | "--color-warning-foreground" | "--color-destructive" | "--color-destructive-foreground" | "--color-card" | "--color-card-foreground" | "--color-overlay" | "--color-outline" | "--color-outline-variant" | "--color-on-surface" | "--color-on-surface-variant" | "--color-surface-tint" | "--color-error" | "--color-on-error" | "--color-error-container" | "--color-on-error-container" | "--color-inverse-surface" | "--color-inverse-on-surface"; export type DynamicColorVariables = Record; function getTargetElement(root?: HTMLElement) { if (root) { return root; } if (typeof document === "undefined") { return undefined; } return document.documentElement; } function clamp(value: number, min: number, max: number) { return Math.min(Math.max(value, min), max); } function normalizeHexColor(seed: string) { const normalized = seed.trim().replace(/^#/, ""); if (/^[0-9a-f]{3}$/i.test(normalized)) { return `#${normalized .split("") .map((char) => `${char}${char}`) .join("") .toLowerCase()}`; } if (/^[0-9a-f]{6}$/i.test(normalized)) { return `#${normalized.toLowerCase()}`; } throw new Error(`Expected a hex seed color such as #6750A4. Received "${seed}".`); } function hexToRgb(seed: string) { const normalized = normalizeHexColor(seed).slice(1); return { b: Number.parseInt(normalized.slice(4, 6), 16), g: Number.parseInt(normalized.slice(2, 4), 16), r: Number.parseInt(normalized.slice(0, 2), 16) }; } function rgbToHsl({ b, g, r }: { b: number; g: number; r: number }) { const red = r / 255; const green = g / 255; const blue = b / 255; const max = Math.max(red, green, blue); const min = Math.min(red, green, blue); const lightness = (max + min) / 2; const delta = max - min; if (delta === 0) { return { h: 0, l: lightness * 100, s: 0 }; } const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min); let hue = 0; switch (max) { case red: hue = (green - blue) / delta + (green < blue ? 6 : 0); break; case green: hue = (blue - red) / delta + 2; break; default: hue = (red - green) / delta + 4; break; } return { h: hue * 60, l: lightness * 100, s: saturation * 100 }; } function shiftHue(hue: number, delta: number) { return (hue + delta + 360) % 360; } function toColor(hue: number, saturation: number, lightness: number) { return `hsl(${Math.round(hue)} ${Math.round(clamp(saturation, 0, 100))}% ${Math.round( clamp(lightness, 0, 100) )}%)`; } export function createDynamicColorVariables(seed: string): DynamicColorVariables { const { h, s } = rgbToHsl(hexToRgb(seed)); const accentSaturation = clamp(Math.max(42, s * 0.92), 42, 82); const neutralHue = shiftHue(h, 8); const secondaryHue = shiftHue(h, 12); const tertiaryHue = shiftHue(h, -118); const neutralSaturation = clamp(s * 0.1, 5, 13); const neutralVariantSaturation = clamp(s * 0.22, 9, 20); const containerSaturation = clamp(s * 0.24, 12, 22); return { "--color-background": toColor(neutralHue, neutralSaturation, 97), "--color-foreground": toColor(neutralHue, neutralSaturation + 2, 15), "--color-surface": toColor(neutralHue, neutralSaturation, 95), "--color-surface-strong": toColor(secondaryHue, clamp(neutralVariantSaturation * 1.15, 10, 24), 89), "--color-surface-contrast": toColor(neutralHue, neutralVariantSaturation + 2, 28), "--color-surface-dim": toColor(neutralHue, neutralSaturation, 87), "--color-surface-bright": toColor(neutralHue, neutralSaturation, 99), "--color-surface-container-low": toColor(neutralHue, neutralSaturation, 96), "--color-surface-container": toColor(secondaryHue, clamp(neutralVariantSaturation * 0.8, 8, 18), 92), "--color-surface-container-high": toColor(h, clamp(neutralVariantSaturation * 0.95, 9, 20), 88), "--color-surface-container-highest": toColor(tertiaryHue, clamp(s * 0.14, 9, 18), 84), "--color-border": toColor(secondaryHue, clamp(neutralVariantSaturation * 0.9, 8, 18), 80), "--color-border-strong": toColor(secondaryHue, clamp(neutralVariantSaturation * 1.2, 10, 24), 54), "--color-input": toColor(secondaryHue, clamp(neutralVariantSaturation * 1.2, 10, 24), 54), "--color-ring": toColor(h, accentSaturation, 40), "--color-primary": toColor(h, accentSaturation, 44), "--color-primary-foreground": toColor(h, 26, 98), "--color-primary-container": toColor(h, clamp(s * 0.56, 24, 58), 86), "--color-on-primary-container": toColor(h, accentSaturation, 18), "--color-secondary": toColor(secondaryHue, containerSaturation, 90), "--color-secondary-foreground": toColor(secondaryHue, clamp(s * 0.28, 14, 22), 22), "--color-secondary-container": toColor(secondaryHue, containerSaturation, 89), "--color-on-secondary-container": toColor(secondaryHue, clamp(s * 0.28, 14, 22), 22), "--color-tertiary": toColor(tertiaryHue, clamp(s * 0.26, 14, 24), 40), "--color-tertiary-foreground": toColor(tertiaryHue, 20, 98), "--color-tertiary-container": toColor(tertiaryHue, clamp(s * 0.18, 12, 18), 86), "--color-on-tertiary-container": toColor(tertiaryHue, clamp(s * 0.22, 14, 22), 18), "--color-muted": toColor(neutralHue, neutralSaturation, 93), "--color-muted-foreground": toColor(neutralHue, neutralVariantSaturation, 34), "--color-accent": toColor(tertiaryHue, clamp(s * 0.18, 12, 18), 86), "--color-accent-foreground": toColor(tertiaryHue, clamp(s * 0.22, 14, 22), 18), "--color-success": toColor(152, 35, 42), "--color-success-foreground": toColor(152, 18, 98), "--color-warning": toColor(76, 62, 48), "--color-warning-foreground": toColor(76, 20, 14), "--color-destructive": toColor(12, 72, 44), "--color-destructive-foreground": toColor(12, 20, 98), "--color-card": toColor(secondaryHue, clamp(neutralSaturation * 0.9, 5, 14), 96), "--color-card-foreground": toColor(neutralHue, neutralSaturation + 2, 15), "--color-overlay": "color-mix(in oklch, black 24%, transparent)", "--color-outline": toColor(secondaryHue, clamp(neutralVariantSaturation * 1.1, 10, 22), 54), "--color-outline-variant": toColor(secondaryHue, clamp(neutralVariantSaturation * 0.95, 9, 20), 82), "--color-on-surface": toColor(neutralHue, neutralSaturation + 2, 15), "--color-on-surface-variant": toColor(neutralHue, neutralVariantSaturation, 34), "--color-surface-tint": toColor(h, accentSaturation, 40), "--color-error": toColor(12, 72, 44), "--color-on-error": toColor(12, 20, 98), "--color-error-container": toColor(12, 58, 88), "--color-on-error-container": toColor(12, 72, 20), "--color-inverse-surface": toColor(neutralHue, neutralSaturation + 2, 20), "--color-inverse-on-surface": toColor(neutralHue, neutralSaturation, 96) }; } function applyDynamicColorVariables( variables: DynamicColorVariables, target: HTMLElement, metadata: { seed: string; theme: string } ) { for (const [name, value] of Object.entries(variables)) { target.style.setProperty(name, value); } target.dataset.theme = metadata.theme; target.dataset.seedColor = normalizeHexColor(metadata.seed); } export function setTheme(theme: ThemeName, root?: HTMLElement) { const target = getTargetElement(root); if (!target) { return; } applyDynamicColorVariables(createDynamicColorVariables(themeDetails[theme].seed), target, { seed: themeDetails[theme].seed, theme }); } export function setDynamicColor(seed: string, root?: HTMLElement) { const target = getTargetElement(root); if (!target) { return; } applyDynamicColorVariables(createDynamicColorVariables(seed), target, { seed, theme: "dynamic" }); } export function setMotionMode(mode: MotionModeName, root?: HTMLElement) { const target = getTargetElement(root); if (!target) { return; } target.dataset.motion = mode; }