Files
cadence-ui/packages/tokens/src/index.ts
T

432 lines
14 KiB
TypeScript

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<ThemeName, { label: string; note: string; seed: string }>;
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<MotionModeName, { label: string; note: string }>;
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<DynamicColorVariableName, string>;
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;
}