Add harness workflow and Material showcase design system

This commit is contained in:
2026-03-23 17:30:30 +08:00
parent c570431dba
commit 5d02bf9df4
46 changed files with 3343 additions and 1068 deletions
+296 -47
View File
@@ -1,22 +1,25 @@
export const themeNames = ["morandi", "earth", "brand"] as const;
export const themeNames = ["violet", "jade", "sunset"] as const;
export type ThemeName = (typeof themeNames)[number];
export const defaultTheme: ThemeName = "morandi";
export const defaultTheme: ThemeName = "violet";
export const themeDetails = {
morandi: {
label: "Morandi",
note: "Muted dusty neutrals with a calm, understated luxury mood"
violet: {
label: "Violet Seed",
note: "A Material baseline seeded from a soft violet, close to the reference M3 demos.",
seed: "#6750A4"
},
earth: {
label: "Earth",
note: "Organic browns, terracotta warmth, olive depth, and sandstone calm"
jade: {
label: "Jade Seed",
note: "Cool green-blue tonal families for calmer product surfaces and lower visual heat.",
seed: "#0B8F83"
},
brand: {
label: "Brand",
note: "Verdant accent scaffold"
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 }>;
} as const satisfies Record<ThemeName, { label: string; note: string; seed: string }>;
export const motionModeNames = ["interactive", "static"] as const;
export type MotionModeName = (typeof motionModeNames)[number];
@@ -25,12 +28,12 @@ export const defaultMotionMode: MotionModeName = "interactive";
export const motionModeDetails = {
interactive: {
label: "Interactive",
note: "Micro-interactions with hover lift, press feedback, focus transitions, and animated state changes"
label: "Standard",
note: "The default Material motion language for state, depth, and spatial feedback."
},
static: {
label: "Static",
note: "Keep visual states readable while removing motion-heavy feedback and animation"
note: "Preserve clarity while removing motion-heavy transitions and hover choreography."
}
} as const satisfies Record<MotionModeName, { label: string; note: string }>;
@@ -44,66 +47,84 @@ export const motionScale = {
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: "surface", cssVar: "--color-surface", role: "Secondary surface backgrounds" },
{
name: "surface-strong",
cssVar: "--color-surface-strong",
role: "Elevated surface emphasis"
name: "on-surface-variant",
cssVar: "--color-on-surface-variant",
role: "Supporting text, dividers, and lower-emphasis iconography"
},
{ name: "card", cssVar: "--color-card", role: "Cards and floating panels" },
{ name: "border", cssVar: "--color-border", role: "Default dividers and input borders" },
{ name: "primary", cssVar: "--color-primary", role: "Filled actions and active emphasis" },
{
name: "border-strong",
cssVar: "--color-border-strong",
role: "Higher emphasis dividers"
name: "primary-container",
cssVar: "--color-primary-container",
role: "Tonal action backgrounds and highlighted containers"
},
{ name: "primary", cssVar: "--color-primary", role: "Primary actions and highlights" },
{
name: "secondary",
cssVar: "--color-secondary",
role: "Secondary fills and supporting actions"
name: "secondary-container",
cssVar: "--color-secondary-container",
role: "Filled tonal surfaces for secondary emphasis"
},
{ name: "muted", cssVar: "--color-muted", role: "Subtle supporting surfaces" },
{
name: "muted-foreground",
cssVar: "--color-muted-foreground",
role: "Secondary text and captions"
name: "tertiary-container",
cssVar: "--color-tertiary-container",
role: "Expressive accent container for supportive highlights"
},
{ name: "accent", cssVar: "--color-accent", role: "Moments of emphasis or delight" },
{ name: "success", cssVar: "--color-success", role: "Success feedback" },
{ name: "warning", cssVar: "--color-warning", role: "Warning feedback" },
{ name: "destructive", cssVar: "--color-destructive", role: "Destructive actions" }
{ 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: "caption",
fontVar: "--text-xs",
lineHeightVar: "--leading-normal",
name: "label",
fontVar: "--text-sm",
lineHeightVar: "--leading-snug",
familyVar: "--font-sans",
sample: "Small labels, metadata, and supporting notes."
sample: "Labels stay crisp, compact, and readable across controls."
},
{
name: "body",
fontVar: "--text-base",
lineHeightVar: "--leading-normal",
familyVar: "--font-sans",
sample: "Body copy stays warm, readable, and stable across themes."
sample: "Body copy stays clear and quiet so color and hierarchy do the expressive work."
},
{
name: "lead",
name: "title",
fontVar: "--text-xl",
lineHeightVar: "--leading-loose",
familyVar: "--font-sans",
sample: "Lead text introduces a surface without becoming display copy."
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 text carries the editorial voice of the system."
sample: "Display copy should feel optimistic, human, and distinctly Material."
}
] as const;
@@ -149,6 +170,61 @@ export const motionTokens = {
]
} 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;
@@ -161,6 +237,163 @@ function getTargetElement(root?: HTMLElement) {
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);
@@ -168,7 +401,23 @@ export function setTheme(theme: ThemeName, root?: HTMLElement) {
return;
}
target.dataset.theme = theme;
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) {
+108 -6
View File
@@ -2,14 +2,14 @@
:root[data-motion="interactive"],
[data-motion="interactive"] {
--dur-instant: 1ms;
--dur-fast: 140ms;
--dur-base: 200ms;
--dur-fast: 120ms;
--dur-base: 180ms;
--dur-slow: 280ms;
--dur-deliberate: 300ms;
--dur-deliberate: 360ms;
--ease-standard: cubic-bezier(0.25, 1, 0.5, 1);
--ease-emphasized: cubic-bezier(0.22, 1, 0.36, 1);
--ease-exit: cubic-bezier(0.3, 1, 0.5, 1);
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
--ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
--ease-exit: cubic-bezier(0.4, 0, 1, 1);
--distance-xs: 4px;
--distance-sm: 8px;
@@ -105,6 +105,81 @@
}
}
@keyframes aiui-float-soft {
0%,
100% {
transform: translate3d(0, 0, 0) rotate(0deg);
}
50% {
transform: translate3d(calc(var(--distance-xs) * 0.5), calc(var(--distance-sm) * -1), 0)
rotate(-0.8deg);
}
}
@keyframes aiui-breathe {
0%,
100% {
opacity: 0.8;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(var(--scale-pop));
}
}
@keyframes aiui-drift {
0%,
100% {
transform: translate3d(0, 0, 0) scale(1);
}
33% {
transform: translate3d(2.5%, -3%, 0) scale(1.025);
}
66% {
transform: translate3d(-2%, 1.8%, 0) scale(0.992);
}
}
@keyframes aiui-float-hero {
0%,
100% {
transform: translate3d(0, 0, 0) rotate(0deg) scale(1);
}
25% {
transform: translate3d(1.2%, calc(var(--distance-sm) * -0.7), 0) rotate(-1deg)
scale(1.01);
}
50% {
transform: translate3d(-0.8%, calc(var(--distance-md) * -0.85), 0) rotate(0.8deg)
scale(1.02);
}
75% {
transform: translate3d(-1.2%, calc(var(--distance-sm) * -0.35), 0) rotate(-0.4deg)
scale(1.005);
}
}
@keyframes aiui-glimmer {
0% {
opacity: 0;
transform: translateX(-120%);
}
20%,
100% {
opacity: 1;
transform: translateX(120%);
}
}
.motion-transition {
transition-duration: var(--dur-base);
transition-property: color, background-color, border-color, box-shadow, opacity,
@@ -166,6 +241,33 @@
both;
}
.motion-float {
animation: aiui-float-soft calc(var(--dur-deliberate) * 8) var(--ease-emphasized) infinite;
}
.motion-float-delayed {
animation: aiui-float-soft calc(var(--dur-deliberate) * 9) var(--ease-emphasized) infinite;
animation-delay: 180ms;
}
.motion-float-hero {
animation: aiui-float-hero calc(var(--dur-deliberate) * 11) var(--ease-emphasized)
infinite;
}
.motion-breathe {
animation: aiui-breathe calc(var(--dur-deliberate) * 6) var(--ease-standard) infinite;
}
.motion-drift {
animation: aiui-drift calc(var(--dur-deliberate) * 14) var(--ease-standard) infinite;
transform-origin: center;
}
.motion-glimmer {
animation: aiui-glimmer calc(var(--dur-deliberate) * 3.5) var(--ease-standard) infinite;
}
.motion-ring {
transition-duration: var(--dur-fast);
transition-property: box-shadow, outline-color, border-color;
+76 -106
View File
@@ -1,130 +1,100 @@
:root {
--font-sans: "Avenir Next", "Segoe UI", sans-serif;
--font-display: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia,
serif;
color-scheme: light;
--font-sans:
"Google Sans Text", "Google Sans", "Roboto Flex", "Roboto", "Segoe UI", sans-serif;
--font-display:
"Google Sans Display", "Google Sans", "Roboto Flex", "Roboto", "Segoe UI", sans-serif;
--font-mono: "SF Mono", "SFMono-Regular", "Consolas", monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 2rem;
--text-4xl: clamp(2.5rem, 4vw, 4rem);
--text-xl: 1.375rem;
--text-2xl: 1.75rem;
--text-3xl: 2.25rem;
--text-4xl: clamp(2.75rem, 4vw, 4.75rem);
--leading-tight: 1.1;
--leading-snug: 1.25;
--leading-snug: 1.3;
--leading-normal: 1.5;
--leading-loose: 1.7;
--leading-loose: 1.65;
--tracking-tight: -0.03em;
--tracking-tight: -0.02em;
--tracking-normal: 0;
--tracking-caps: 0.18em;
--tracking-caps: 0.12em;
--border-width-thin: 1px;
--border-width-strong: 1.5px;
--border-width-strong: 1px;
--radius-xs: 8px;
--radius-sm: 12px;
--radius-md: 18px;
--radius-sm: 16px;
--radius-md: 20px;
--radius-lg: 28px;
--radius-xl: 40px;
--radius-xl: 36px;
--radius-full: 999px;
--shadow-xs: 0 1px 2px oklch(0.28 0.02 55 / 0.06);
--shadow-sm: 0 8px 24px oklch(0.28 0.02 55 / 0.08);
--shadow-md: 0 18px 48px oklch(0.28 0.03 55 / 0.12);
--shadow-lg: 0 32px 72px oklch(0.2 0.02 55 / 0.16);
}
--shadow-xs: 0 1px 2px rgb(25 18 42 / 0.08), 0 4px 10px rgb(94 74 145 / 0.05);
--shadow-sm: 0 8px 22px rgb(83 63 128 / 0.12), 0 2px 8px rgb(25 18 42 / 0.06);
--shadow-md: 0 18px 42px rgb(83 63 128 / 0.16), 0 6px 18px rgb(25 18 42 / 0.1);
--shadow-lg: 0 30px 70px rgb(83 63 128 / 0.18), 0 14px 32px rgb(25 18 42 / 0.12);
:root,
[data-theme="morandi"] {
color-scheme: light;
--color-background: color-mix(in oklch, #d4b5a0 10%, white 90%);
--color-foreground: #544c46;
--color-surface: color-mix(in oklch, #a6b3a7 12%, white 88%);
--color-surface-strong: color-mix(in oklch, #d4b5a0 20%, white 80%);
--color-surface-contrast: #6a615a;
--color-border: color-mix(in oklch, #9b8e82 34%, white 66%);
--color-border-strong: #9b8e82;
--color-input: var(--color-border);
--color-ring: #8e9aaf;
--color-primary: #6f7785;
--color-primary-foreground: #f7f3ef;
--color-secondary: #a6b3a7;
--color-secondary-foreground: #495247;
--color-muted: color-mix(in oklch, #d4b5a0 18%, white 82%);
--color-muted-foreground: #776c64;
--color-accent: #c4a882;
--color-accent-foreground: #4f4334;
--color-success: #879686;
--color-success-foreground: #f6f1ec;
--color-warning: #ba9c73;
--color-warning-foreground: #4d4031;
--color-destructive: #a7837c;
--color-destructive-foreground: #f9f4ef;
--color-card: color-mix(in oklch, var(--color-surface) 82%, white 18%);
--color-card-foreground: var(--color-foreground);
--color-overlay: rgb(84 76 70 / 0.42);
}
--color-background: hsl(22 18% 96%);
--color-foreground: hsl(259 6% 15%);
--color-surface: hsl(22 12% 94%);
--color-surface-strong: hsl(278 24% 87%);
--color-surface-contrast: hsl(259 10% 28%);
--color-surface-dim: hsl(22 10% 87%);
--color-surface-bright: hsl(22 18% 98%);
--color-surface-container-low: hsl(18 20% 97%);
--color-surface-container: hsl(278 20% 91%);
--color-surface-container-high: hsl(274 24% 88%);
--color-surface-container-highest: hsl(112 22% 84%);
--color-outline: hsl(259 10% 56%);
--color-outline-variant: hsl(259 12% 82%);
--color-border: var(--color-outline-variant);
--color-border-strong: var(--color-outline);
--color-input: var(--color-outline);
--color-ring: hsl(259 40% 42%);
[data-theme="earth"] {
color-scheme: light;
--color-background: color-mix(in oklch, #d4c5a9 34%, white 66%);
--color-foreground: #4f3c27;
--color-surface: color-mix(in oklch, #d4c5a9 52%, white 48%);
--color-surface-strong: color-mix(in oklch, #c4956a 24%, white 76%);
--color-surface-contrast: #6f5437;
--color-border: color-mix(in oklch, #8b6f47 40%, white 60%);
--color-border-strong: #8b6f47;
--color-input: var(--color-border);
--color-ring: #a0522d;
--color-primary: #8b6f47;
--color-primary-foreground: #f7f1e7;
--color-secondary: #6b7c3f;
--color-secondary-foreground: #f4efe6;
--color-muted: color-mix(in oklch, #c4956a 18%, white 82%);
--color-muted-foreground: #78644a;
--color-accent: #a0522d;
--color-accent-foreground: #f8efe6;
--color-success: #72864c;
--color-success-foreground: #f5f1e8;
--color-warning: #c4956a;
--color-warning-foreground: #4a3826;
--color-destructive: #93492b;
--color-destructive-foreground: #faefe7;
--color-card: color-mix(in oklch, var(--color-surface) 84%, white 16%);
--color-card-foreground: var(--color-foreground);
--color-overlay: rgb(79 60 39 / 0.4);
}
--color-primary: hsl(264 38% 45%);
--color-primary-foreground: hsl(259 24% 98%);
--color-primary-container: hsl(272 38% 86%);
--color-on-primary-container: hsl(264 30% 20%);
[data-theme="brand"] {
color-scheme: light;
--color-background: oklch(0.972 0.016 172);
--color-foreground: oklch(0.24 0.03 182);
--color-surface: oklch(0.946 0.018 172);
--color-surface-strong: oklch(0.91 0.024 172);
--color-surface-contrast: oklch(0.29 0.034 182);
--color-border: oklch(0.83 0.026 172);
--color-border-strong: oklch(0.67 0.045 176);
--color-input: var(--color-border);
--color-ring: oklch(0.53 0.12 190);
--color-primary: oklch(0.48 0.12 188);
--color-primary-foreground: oklch(0.97 0.008 172);
--color-secondary: oklch(0.82 0.066 156);
--color-secondary-foreground: oklch(0.2 0.02 178);
--color-muted: oklch(0.91 0.018 172);
--color-muted-foreground: oklch(0.42 0.03 180);
--color-accent: oklch(0.75 0.105 130);
--color-accent-foreground: oklch(0.2 0.02 160);
--color-success: oklch(0.6 0.12 155);
--color-success-foreground: oklch(0.98 0.006 170);
--color-warning: oklch(0.76 0.13 86);
--color-warning-foreground: oklch(0.22 0.02 74);
--color-destructive: oklch(0.53 0.16 30);
--color-destructive-foreground: oklch(0.98 0.01 80);
--color-card: color-mix(in oklch, var(--color-surface) 88%, white 12%);
--color-secondary: hsl(286 24% 90%);
--color-secondary-foreground: hsl(284 24% 22%);
--color-secondary-container: hsl(286 24% 90%);
--color-on-secondary-container: hsl(284 24% 22%);
--color-tertiary: hsl(128 18% 40%);
--color-tertiary-foreground: hsl(128 16% 98%);
--color-tertiary-container: hsl(112 24% 84%);
--color-on-tertiary-container: hsl(128 18% 22%);
--color-muted: var(--color-surface-container);
--color-muted-foreground: hsl(259 10% 34%);
--color-accent: var(--color-tertiary-container);
--color-accent-foreground: var(--color-on-tertiary-container);
--color-success: hsl(152 35% 42%);
--color-success-foreground: hsl(152 18% 98%);
--color-warning: hsl(76 62% 48%);
--color-warning-foreground: hsl(76 20% 14%);
--color-destructive: hsl(12 72% 44%);
--color-destructive-foreground: hsl(12 20% 98%);
--color-error: var(--color-destructive);
--color-on-error: var(--color-destructive-foreground);
--color-error-container: hsl(12 58% 88%);
--color-on-error-container: hsl(12 72% 20%);
--color-card: var(--color-surface-container-low);
--color-card-foreground: var(--color-foreground);
--color-overlay: oklch(0.13 0.015 185 / 0.5);
--color-overlay: color-mix(in oklch, black 24%, transparent);
--color-on-surface: var(--color-foreground);
--color-on-surface-variant: var(--color-muted-foreground);
--color-surface-tint: var(--color-primary);
--color-inverse-surface: hsl(259 6% 20%);
--color-inverse-on-surface: hsl(259 4% 96%);
}
@@ -1,3 +1,5 @@
import type { ComponentProps } from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
@@ -9,7 +11,9 @@ import {
AccordionTrigger
} from "./accordion";
function ExampleAccordion(props: any = {}) {
type ExampleAccordionProps = ComponentProps<typeof Accordion>;
function ExampleAccordion(props: ExampleAccordionProps = {}) {
return (
<Accordion {...props}>
<AccordionItem value="editorial">
@@ -85,7 +89,9 @@ describe("Accordion", () => {
await user.click(trigger);
const content = screen.getByText("Copy is locked for launch review.").closest('[data-slot="content"]');
const content = screen
.getByText("Copy is locked for launch review.")
.closest('[data-slot="content"]');
expect(trigger).toHaveAttribute("aria-expanded", "true");
expect(content).toHaveAttribute("data-state", "open");
+2 -1
View File
@@ -19,7 +19,8 @@ export const cardVariants = cva(
},
interactive: {
false: "",
true: "hover:translate-y-[var(--ui-card-hover-translate)] hover:shadow-[var(--ui-card-hover-shadow)]"
true:
"hover:translate-y-[var(--ui-card-hover-translate)] hover:scale-[var(--ui-card-hover-scale,1)] hover:shadow-[var(--ui-card-hover-shadow)]"
}
},
defaultVariants: {
+1 -1
View File
@@ -42,7 +42,7 @@ describe("Combobox", () => {
it("renders a selected value, filters options, and updates uncontrolled state", async () => {
const user = userEvent.setup();
const loadingView = render(
render(
<Combobox
aria-label="Review lane"
defaultValue="design"
+1 -1
View File
@@ -157,7 +157,7 @@ export const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(function Co
return haystack.includes(query);
});
}, [items, resolvedSearchValue]);
}, [filter, items, resolvedSearchValue]);
const listboxId = filteredItems.length > 0 ? `${controlId}-listbox` : undefined;
const groupedItems = useMemo(() => {
+90 -97
View File
@@ -32,11 +32,9 @@ import {
datePickerFooterVariants,
datePickerGridVariants,
datePickerHeaderVariants,
datePickerMonthLabelVariants,
datePickerNavigationVariants,
datePickerRootVariants,
datePickerSelectorsVariants,
datePickerTriggerVariants,
datePickerWeekdayVariants
} from "./date-picker.variants";
import { cn } from "../lib/cn";
@@ -56,10 +54,6 @@ function normalizeDate(value?: Date) {
: undefined;
}
function getDateKey(value?: Date) {
return value ? `${value.getFullYear()}-${value.getMonth()}-${value.getDate()}` : "";
}
function sameDay(left?: Date, right?: Date) {
if (!left || !right) {
return false;
@@ -232,111 +226,109 @@ export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(function
},
ref
) {
const reactId = useId();
const today = useMemo(() => normalizeDate(new Date()), []);
const normalizedControlledValue = useMemo(
() => normalizeDate(value),
[value ? getDateKey(value) : ""]
);
const normalizedDefaultValue = useMemo(
() => normalizeDate(defaultValue),
[defaultValue ? getDateKey(defaultValue) : ""]
);
const normalizedDefaultMonth = useMemo(
() => normalizeDate(defaultMonth),
[defaultMonth ? getDateKey(defaultMonth) : ""]
);
const [selectedDate, setSelectedDate] = useControllableState<DatePickerValue>({
controlledValue: normalizedControlledValue,
defaultValue: normalizedDefaultValue,
onChange: onValueChange
const reactId = useId();
const today = useMemo(() => normalizeDate(new Date()), []);
const normalizedControlledValue = normalizeDate(value);
const normalizedDefaultValue = normalizeDate(defaultValue);
const normalizedDefaultMonth = normalizeDate(defaultMonth);
const [selectedDate, setSelectedDate] = useControllableState<DatePickerValue>({
controlledValue: normalizedControlledValue,
defaultValue: normalizedDefaultValue,
onChange: onValueChange
});
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
const resolvedOpen = open ?? uncontrolledOpen;
const [visibleMonth, setVisibleMonth] = useState(
startOfMonth(
normalizedDefaultMonth ?? normalizedControlledValue ?? normalizedDefaultValue ?? today ?? new Date()
)
);
const dayRefs = useRef<Array<HTMLButtonElement | null>>([]);
const controlId = id ?? `date-picker-${reactId.replace(/:/g, "")}`;
const popupId = `${controlId}-dialog`;
useEffect(() => {
if (!selectedDate) {
return;
}
const frame = requestAnimationFrame(() => {
setVisibleMonth(startOfMonth(selectedDate));
});
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
const resolvedOpen = open ?? uncontrolledOpen;
const [visibleMonth, setVisibleMonth] = useState(
startOfMonth(normalizedDefaultMonth ?? normalizedDefaultValue ?? today ?? new Date())
);
const dayRefs = useRef<Array<HTMLButtonElement | null>>([]);
const controlId = id ?? `date-picker-${reactId.replace(/:/g, "")}`;
useEffect(() => {
if (selectedDate) {
setVisibleMonth(startOfMonth(selectedDate));
}
}, [selectedDate]);
return () => cancelAnimationFrame(frame);
}, [selectedDate]);
const monthLabel = formatMonthLabel(visibleMonth, locale);
const weekdays = useMemo(() => {
const formatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
const base = new Date(2025, 0, 5 + getWeekStartIndex(weekStartsOn));
const monthLabel = formatMonthLabel(visibleMonth, locale);
const weekdays = useMemo(() => {
const formatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
const base = new Date(2025, 0, 5 + getWeekStartIndex(weekStartsOn));
return Array.from({ length: 7 }, (_, index) => {
const day = new Date(base);
day.setDate(base.getDate() + index);
return formatter.format(day);
});
}, [locale, weekStartsOn]);
const days = useMemo(
() => buildMonthGrid(visibleMonth, weekStartsOn),
[visibleMonth, weekStartsOn]
);
const yearOptions = useMemo(
() => getYearOptions(visibleMonth, selectedDate),
[selectedDate, visibleMonth]
);
const selectedIndex = days.findIndex((day) => sameDay(day, selectedDate));
return Array.from({ length: 7 }, (_, index) => {
const day = new Date(base);
day.setDate(base.getDate() + index);
return formatter.format(day);
});
}, [locale, weekStartsOn]);
const days = useMemo(
() => buildMonthGrid(visibleMonth, weekStartsOn),
[visibleMonth, weekStartsOn]
);
const yearOptions = useMemo(
() => getYearOptions(visibleMonth, selectedDate),
[selectedDate, visibleMonth]
);
const selectedIndex = days.findIndex((day) => sameDay(day, selectedDate));
useEffect(() => {
dayRefs.current = [];
}, [visibleMonth]);
useEffect(() => {
dayRefs.current = [];
}, [visibleMonth]);
useEffect(() => {
if (!resolvedOpen) {
return;
}
useEffect(() => {
if (!resolvedOpen) {
return;
}
const focusIndex =
selectedIndex >= 0
? selectedIndex
: days.findIndex(
(day) =>
day.getMonth() === visibleMonth.getMonth() &&
sameDay(day, today)
);
const focusIndex =
selectedIndex >= 0
? selectedIndex
: days.findIndex(
(day) => day.getMonth() === visibleMonth.getMonth() && sameDay(day, today)
);
const frame = requestAnimationFrame(() => {
dayRefs.current[focusIndex >= 0 ? focusIndex : 0]?.focus();
});
const frame = requestAnimationFrame(() => {
dayRefs.current[focusIndex >= 0 ? focusIndex : 0]?.focus();
});
return () => cancelAnimationFrame(frame);
}, [days, resolvedOpen, selectedIndex, today, visibleMonth]);
return () => cancelAnimationFrame(frame);
}, [days, resolvedOpen, selectedIndex, today, visibleMonth]);
const setOpenState = (nextOpen: boolean) => {
if (open === undefined) {
setUncontrolledOpen(nextOpen);
}
const setOpenState = (nextOpen: boolean) => {
if (open === undefined) {
setUncontrolledOpen(nextOpen);
}
onOpenChange?.(nextOpen);
};
onOpenChange?.(nextOpen);
};
const goToMonth = (offset: number) => {
const next = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() + offset, 1);
setVisibleMonth(next);
onMonthChange?.(next);
};
const goToMonth = (offset: number) => {
const next = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() + offset, 1);
setVisibleMonth(next);
onMonthChange?.(next);
};
const handleTriggerKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
event.preventDefault();
setOpenState(true);
}
const handleTriggerKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
event.preventDefault();
setOpenState(true);
}
if (event.key === "Escape") {
setOpenState(false);
}
};
if (event.key === "Escape") {
setOpenState(false);
}
};
const handleDayKeyDown = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {
const handleDayKeyDown = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {
const movementMap: Record<string, number> = {
ArrowDown: 7,
ArrowLeft: -1,
@@ -408,6 +400,7 @@ export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(function
<div {...createSlot("field")} className={datePickerFieldVariants()}>
<Input
{...props}
aria-controls={popupId}
aria-expanded={resolvedOpen}
aria-haspopup="dialog"
className={cn("cursor-pointer pr-20", className)}
@@ -430,7 +423,7 @@ export const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(function
</div>
</PopoverAnchor>
<PopoverContent className={datePickerContentVariants()} padding="sm" size="xl">
<PopoverContent className={datePickerContentVariants()} id={popupId} padding="sm" size="xl">
<div className="grid gap-3">
<div {...createSlot("header")} className={datePickerHeaderVariants()}>
<div className={datePickerNavigationVariants()}>
@@ -16,5 +16,5 @@ export const switchVariants = cva(
export const switchThumbVariants = cva([
"pointer-events-none block size-5 rounded-[var(--ui-switch-thumb-radius)] bg-[var(--ui-switch-thumb-bg)] shadow-[var(--ui-switch-thumb-shadow)]",
"translate-x-0.5 will-change-transform transition-[transform,box-shadow,background-color] duration-[var(--ui-switch-transition-duration,var(--dur-base))] ease-[var(--ease-emphasized)]",
"data-[state=checked]:translate-x-[1.55rem] data-[state=checked]:shadow-[var(--ui-switch-thumb-checked-shadow,var(--ui-switch-thumb-shadow))]"
"data-[state=checked]:translate-x-[1.55rem] data-[state=checked]:bg-[var(--ui-switch-thumb-checked-bg,var(--ui-switch-thumb-bg))] data-[state=checked]:shadow-[var(--ui-switch-thumb-checked-shadow,var(--ui-switch-thumb-shadow))]"
]);
+6
View File
@@ -34,6 +34,12 @@ export const motionRecipes = {
overlayExit: "motion-overlay-exit",
exitFade: "motion-exit-fade",
exitDrop: "motion-exit-drop",
float: "motion-float",
floatDelayed: "motion-float-delayed",
floatHero: "motion-float-hero",
breathe: "motion-breathe",
drift: "motion-drift",
glimmer: "motion-glimmer",
ring: "motion-ring"
} as const;
+4 -4
View File
@@ -9,16 +9,16 @@ describe("skin contract", () => {
});
it("sets the document root skin when no target element is provided", () => {
setSkin("glass");
setSkin("material");
expect(document.documentElement.dataset.skin).toBe("glass");
expect(document.documentElement.dataset.skin).toBe("material");
});
it("sets the provided target element instead of the document root", () => {
const target = document.createElement("div");
setSkin("pixel", target);
setSkin("material", target);
expect(target.dataset.skin).toBe("pixel");
expect(target.dataset.skin).toBe("material");
});
});
+5 -13
View File
@@ -1,20 +1,12 @@
export const skinNames = ["minimal", "glass", "pixel"] as const;
export const skinNames = ["material"] as const;
export type SkinName = (typeof skinNames)[number];
export const defaultSkin: SkinName = "minimal";
export const defaultSkin: SkinName = "material";
export const skinDetails = {
minimal: {
label: "Minimal",
note: "Restrained surfaces and low-ornament defaults"
},
glass: {
label: "Glass",
note: "Translucent layers, brighter edges, and blurred panels"
},
pixel: {
label: "Pixel",
note: "Hard edges, crisp borders, and stepped shadows"
material: {
label: "Material",
note: "One tonal, rounded, dynamic-color-first component language"
}
} as const satisfies Record<SkinName, { label: string; note: string }>;
+205 -376
View File
@@ -1,422 +1,251 @@
:root,
[data-skin="minimal"] {
--ui-canvas-image: radial-gradient(
circle at top,
color-mix(in oklch, var(--color-primary) 8%, transparent),
transparent 58%
);
[data-skin="material"] {
--ui-canvas-image:
radial-gradient(
circle at 18% 12%,
color-mix(in oklch, var(--color-primary-container) 62%, transparent),
transparent 28%
),
radial-gradient(
circle at 82% 22%,
color-mix(in oklch, var(--color-tertiary-container) 52%, transparent),
transparent 26%
),
radial-gradient(
circle at 50% 0%,
color-mix(in oklch, white 78%, transparent),
transparent 58%
),
linear-gradient(
180deg,
color-mix(in oklch, var(--color-background) 90%, white 10%),
color-mix(in oklch, var(--color-surface) 88%, white 12%)
);
--ui-canvas-size: auto;
--ui-surface-bg: color-mix(in oklch, var(--color-card) 88%, white 12%);
--ui-surface-border: color-mix(in oklch, var(--color-border) 92%, white 8%);
--ui-surface-shadow: var(--shadow-sm);
--ui-surface-radius: var(--radius-lg);
--ui-surface-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-surface) 76%, var(--color-surface-bright) 24%),
color-mix(in oklch, var(--color-surface-container-low) 86%, white 14%)
);
--ui-surface-border: transparent;
--ui-surface-shadow:
inset 0 1px 0 color-mix(in oklch, white 54%, transparent),
0 14px 34px color-mix(in oklch, var(--color-primary) 10%, transparent);
--ui-surface-radius: var(--radius-xl);
--ui-surface-backdrop-blur: 0px;
--ui-control-bg: color-mix(in oklch, var(--color-background) 92%, white 8%);
--ui-control-border: var(--color-border);
--ui-control-shadow: var(--shadow-xs);
--ui-control-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-surface-container) 82%, var(--color-surface-bright) 18%),
color-mix(in oklch, var(--color-surface-container-high) 78%, var(--color-surface-bright) 22%)
);
--ui-control-border: transparent;
--ui-control-shadow: inset 0 1px 0 color-mix(in oklch, white 40%, transparent);
--ui-control-radius: var(--radius-md);
--ui-ornament-opacity: 0.1;
--ui-ornament-opacity: 0;
--ui-ornament-mix: normal;
--ui-button-radius: var(--radius-sm);
--ui-button-radius: var(--radius-full);
--ui-button-border-width: 1px;
--ui-button-transition-duration: var(--dur-fast);
--ui-button-sheen-opacity: 0.14;
--ui-button-sheen-mix: screen;
--ui-button-sheen-gradient: linear-gradient(
120deg,
transparent 0%,
rgba(255, 255, 255, 0.24) 45%,
transparent 100%
--ui-button-sheen-opacity: 0;
--ui-button-sheen-mix: normal;
--ui-button-sheen-gradient: linear-gradient(180deg, transparent, transparent);
--ui-button-primary-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-primary-container) 82%, white 18%),
color-mix(in oklch, var(--color-primary-container) 74%, var(--color-secondary-container) 26%)
);
--ui-button-primary-bg: var(--color-primary);
--ui-button-primary-hover-bg: color-mix(in oklch, var(--color-primary) 90%, black 10%);
--ui-button-primary-fg: var(--color-primary-foreground);
--ui-button-primary-hover-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-primary-container) 72%, white 28%),
color-mix(in oklch, var(--color-primary-container) 74%, var(--color-on-primary-container) 26%)
);
--ui-button-primary-fg: var(--color-on-primary-container);
--ui-button-primary-border: transparent;
--ui-button-primary-shadow: var(--shadow-xs);
--ui-button-secondary-bg: var(--color-secondary);
--ui-button-secondary-hover-bg: color-mix(in oklch, var(--color-secondary) 88%, black 12%);
--ui-button-secondary-fg: var(--color-secondary-foreground);
--ui-button-secondary-border: var(--color-border-strong);
--ui-button-secondary-shadow: none;
--ui-button-primary-shadow:
inset 0 1px 0 color-mix(in oklch, white 60%, transparent),
0 14px 26px color-mix(in oklch, var(--color-primary) 16%, transparent);
--ui-button-secondary-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-tertiary-container) 84%, white 16%),
color-mix(in oklch, var(--color-tertiary-container) 72%, var(--color-surface-container-highest) 28%)
);
--ui-button-secondary-hover-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-tertiary-container) 76%, white 24%),
color-mix(in oklch, var(--color-tertiary-container) 74%, var(--color-on-tertiary-container) 26%)
);
--ui-button-secondary-fg: var(--color-on-tertiary-container);
--ui-button-secondary-border: transparent;
--ui-button-secondary-shadow:
inset 0 1px 0 color-mix(in oklch, white 54%, transparent),
0 12px 24px color-mix(in oklch, var(--color-tertiary) 12%, transparent);
--ui-button-ghost-bg: transparent;
--ui-button-ghost-hover-bg: var(--color-surface);
--ui-button-ghost-fg: var(--color-foreground);
--ui-button-ghost-hover-bg: color-mix(
in oklch,
var(--color-surface-container-high) 72%,
transparent
);
--ui-button-ghost-fg: var(--color-primary);
--ui-button-ghost-border: transparent;
--ui-button-ghost-shadow: none;
--ui-button-subtle-bg: var(--color-card);
--ui-button-subtle-hover-bg: color-mix(in oklch, var(--color-card) 88%, black 12%);
--ui-button-subtle-fg: var(--color-foreground);
--ui-button-subtle-border: var(--color-border);
--ui-button-subtle-shadow: var(--shadow-xs);
--ui-button-destructive-bg: var(--color-destructive);
--ui-button-destructive-hover-bg: color-mix(
in oklch,
var(--color-destructive) 88%,
black 12%
--ui-button-subtle-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-surface-container-high) 64%, var(--color-surface-bright) 36%),
color-mix(in oklch, var(--color-surface-container) 74%, var(--color-surface-bright) 26%)
);
--ui-button-destructive-fg: var(--color-destructive-foreground);
--ui-button-subtle-hover-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-surface-container-high) 70%, var(--color-surface-bright) 30%),
color-mix(in oklch, var(--color-surface-container-high) 82%, white 18%)
);
--ui-button-subtle-fg: var(--color-foreground);
--ui-button-subtle-border: transparent;
--ui-button-subtle-shadow:
inset 0 1px 0 color-mix(in oklch, white 40%, transparent),
0 10px 20px color-mix(in oklch, var(--color-primary) 8%, transparent);
--ui-button-destructive-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-error-container) 86%, white 14%),
color-mix(in oklch, var(--color-error-container) 76%, var(--color-surface-bright) 24%)
);
--ui-button-destructive-hover-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-error-container) 76%, white 24%),
color-mix(in oklch, var(--color-error) 18%, var(--color-error-container) 82%)
);
--ui-button-destructive-fg: var(--color-on-error-container);
--ui-button-destructive-border: transparent;
--ui-button-destructive-shadow: var(--shadow-xs);
--ui-button-hover-scale: 1.02;
--ui-button-press-scale: 0.98;
--ui-button-hover-translate: -1px;
--ui-button-hover-shadow: var(--shadow-sm);
--ui-button-active-shadow: var(--shadow-xs);
--ui-button-destructive-shadow:
inset 0 1px 0 color-mix(in oklch, white 42%, transparent),
0 12px 24px color-mix(in oklch, var(--color-error) 12%, transparent);
--ui-button-hover-scale: 1.024;
--ui-button-press-scale: 0.985;
--ui-button-hover-translate: -2px;
--ui-button-hover-shadow: 0 16px 30px color-mix(in oklch, var(--color-primary) 14%, transparent);
--ui-button-active-shadow: 0 8px 16px color-mix(in oklch, var(--color-primary) 12%, transparent);
--ui-spinner-radius: var(--radius-full);
--ui-spinner-border-width: 2px;
--ui-card-radius: var(--radius-lg);
--ui-card-border-width: 1px;
--ui-card-bg: var(--color-card);
--ui-card-shadow: var(--shadow-sm);
--ui-card-default-bg: var(--color-card);
--ui-card-default-border: var(--color-border);
--ui-card-default-shadow: var(--shadow-sm);
--ui-card-subtle-bg: var(--color-surface);
--ui-card-subtle-border: color-mix(in oklch, var(--color-border) 86%, transparent);
--ui-card-subtle-shadow: var(--shadow-xs);
--ui-card-accent-bg: color-mix(in oklch, var(--color-primary) 8%, var(--color-card));
--ui-card-accent-border: color-mix(in oklch, var(--color-primary) 26%, var(--color-border));
--ui-card-accent-shadow: var(--shadow-sm);
--ui-card-hover-translate: -2px;
--ui-card-hover-shadow: var(--shadow-md);
--ui-card-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-surface-container-low) 78%, var(--color-surface-bright) 22%),
color-mix(in oklch, var(--color-surface-container) 18%, var(--color-surface-container-low) 82%)
);
--ui-card-shadow:
inset 0 1px 0 color-mix(in oklch, white 48%, transparent),
0 18px 36px color-mix(in oklch, var(--color-primary) 12%, transparent);
--ui-card-default-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-surface-container-low) 78%, var(--color-surface-bright) 22%),
color-mix(in oklch, var(--color-surface-container) 18%, var(--color-surface-container-low) 82%)
);
--ui-card-default-border: transparent;
--ui-card-default-shadow:
inset 0 1px 0 color-mix(in oklch, white 48%, transparent),
0 18px 36px color-mix(in oklch, var(--color-primary) 12%, transparent);
--ui-card-subtle-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-surface-container) 82%, var(--color-surface-bright) 18%),
color-mix(in oklch, var(--color-surface-container-high) 72%, var(--color-surface-bright) 28%)
);
--ui-card-subtle-border: transparent;
--ui-card-subtle-shadow:
inset 0 1px 0 color-mix(in oklch, white 40%, transparent),
0 12px 28px color-mix(in oklch, var(--color-primary) 10%, transparent);
--ui-card-accent-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-primary-container) 70%, var(--color-surface-bright) 30%),
color-mix(in oklch, var(--color-secondary-container) 18%, var(--color-primary-container) 82%)
);
--ui-card-accent-border: transparent;
--ui-card-accent-shadow:
inset 0 1px 0 color-mix(in oklch, white 44%, transparent),
0 18px 36px color-mix(in oklch, var(--color-primary) 14%, transparent);
--ui-card-hover-translate: -6px;
--ui-card-hover-scale: 1.016;
--ui-card-hover-shadow: 0 24px 44px color-mix(in oklch, var(--color-primary) 16%, transparent);
--ui-input-radius: var(--radius-md);
--ui-input-radius: var(--radius-sm);
--ui-input-border-width: 1px;
--ui-input-bg: var(--color-card);
--ui-input-border: var(--color-input);
--ui-input-fg: var(--color-foreground);
--ui-input-shadow: var(--shadow-xs);
--ui-input-focus-border: color-mix(in oklch, var(--color-primary) 32%, var(--color-input));
--ui-input-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-surface-container-highest) 56%, var(--color-surface-bright) 44%),
color-mix(in oklch, var(--color-surface-container) 14%, var(--color-surface-bright) 86%)
);
--ui-input-border: transparent;
--ui-input-fg: var(--color-on-surface);
--ui-input-shadow:
inset 0 1px 0 color-mix(in oklch, white 42%, transparent),
0 4px 12px color-mix(in oklch, var(--color-primary) 6%, transparent);
--ui-input-focus-border: var(--color-primary);
--ui-input-focus-shadow:
0 0 0 1px color-mix(in oklch, var(--color-primary) 18%, transparent),
var(--shadow-sm);
--ui-input-focus-lift: -1px;
--ui-input-disabled-bg: var(--color-surface);
--ui-input-readonly-bg: var(--color-surface);
0 0 0 3px color-mix(in oklch, var(--color-primary) 18%, transparent),
0 14px 28px color-mix(in oklch, var(--color-primary) 12%, transparent);
--ui-input-focus-lift: 0px;
--ui-input-disabled-bg: var(--color-surface-container);
--ui-input-readonly-bg: var(--color-surface-container-low);
--ui-input-backdrop-blur: 0px;
--ui-panel-radius: var(--radius-lg);
--ui-panel-border-width: 1px;
--ui-panel-bg: var(--color-card);
--ui-panel-border: var(--color-border);
--ui-panel-shadow: var(--shadow-md);
--ui-panel-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-surface-container-high) 76%, var(--color-surface-bright) 24%),
color-mix(in oklch, var(--color-surface-container) 18%, var(--color-surface-container-high) 82%)
);
--ui-panel-border: transparent;
--ui-panel-shadow:
inset 0 1px 0 color-mix(in oklch, white 46%, transparent),
0 28px 64px color-mix(in oklch, var(--color-primary) 16%, transparent);
--ui-panel-backdrop-blur: 0px;
--ui-panel-overlay-bg: var(--color-overlay);
--ui-panel-overlay-blur: 2px;
--ui-panel-overlay-bg: color-mix(in oklch, var(--color-overlay) 88%, transparent);
--ui-panel-overlay-blur: 10px;
--ui-switch-track-radius: var(--radius-full);
--ui-switch-track-border-width: 1px;
--ui-switch-track-bg: var(--color-border);
--ui-switch-track-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-surface-container-highest) 72%, var(--color-surface-bright) 28%),
color-mix(in oklch, var(--color-surface-container) 18%, var(--color-surface-container-highest) 82%)
);
--ui-switch-track-border: transparent;
--ui-switch-track-shadow: var(--shadow-xs);
--ui-switch-track-checked-bg: var(--color-primary);
--ui-switch-track-shadow:
inset 0 1px 0 color-mix(in oklch, white 38%, transparent),
0 6px 14px color-mix(in oklch, var(--color-primary) 8%, transparent);
--ui-switch-track-checked-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-primary-container) 82%, white 18%),
color-mix(in oklch, var(--color-primary-container) 72%, var(--color-secondary-container) 28%)
);
--ui-switch-track-checked-border: transparent;
--ui-switch-thumb-radius: var(--radius-full);
--ui-switch-thumb-bg: white;
--ui-switch-thumb-shadow: var(--shadow-xs);
--ui-switch-thumb-checked-shadow: var(--shadow-sm);
--ui-switch-thumb-bg: var(--color-surface-bright);
--ui-switch-thumb-checked-bg: var(--color-primary);
--ui-switch-thumb-shadow:
inset 0 1px 0 color-mix(in oklch, white 54%, transparent),
var(--shadow-xs);
--ui-switch-thumb-checked-shadow:
inset 0 1px 0 color-mix(in oklch, white 36%, transparent),
var(--shadow-xs);
--ui-switch-transition-duration: var(--dur-base);
--ui-skeleton-radius: var(--radius-sm);
--ui-skeleton-block-radius: var(--radius-md);
--ui-skeleton-pill-radius: var(--radius-full);
--ui-skeleton-avatar-radius: var(--radius-full);
--ui-skeleton-bg: color-mix(in oklch, var(--color-surface) 74%, var(--color-border));
--ui-skeleton-muted-bg: var(--color-muted);
--ui-skeleton-bg: linear-gradient(
180deg,
color-mix(in oklch, var(--color-surface-container-highest) 82%, white 18%),
color-mix(in oklch, var(--color-outline-variant) 18%, var(--color-surface-container-highest) 82%)
);
--ui-skeleton-muted-bg: var(--color-surface-container);
--ui-skeleton-gradient: linear-gradient(
110deg,
transparent 0%,
rgba(255, 255, 255, 0.48) 42%,
color-mix(in oklch, var(--color-primary-container) 40%, white 60%) 42%,
transparent 72%
);
}
[data-skin="glass"] {
--ui-canvas-image:
radial-gradient(
circle at top left,
color-mix(in oklch, var(--color-primary) 22%, transparent),
transparent 42%
),
radial-gradient(
circle at top right,
color-mix(in oklch, var(--color-accent) 18%, transparent),
transparent 48%
),
linear-gradient(
180deg,
color-mix(in oklch, var(--color-background) 64%, white 36%),
var(--color-background)
);
--ui-canvas-size: auto;
--ui-surface-bg: color-mix(in oklch, var(--color-card) 58%, transparent);
--ui-surface-border: color-mix(in oklch, white 46%, var(--color-border));
--ui-surface-shadow: 0 24px 64px oklch(0.18 0.03 255 / 0.18);
--ui-surface-radius: var(--radius-xl);
--ui-surface-backdrop-blur: 20px;
--ui-control-bg: color-mix(in oklch, var(--color-card) 52%, transparent);
--ui-control-border: color-mix(in oklch, white 36%, var(--color-border-strong));
--ui-control-shadow: 0 14px 38px oklch(0.2 0.03 255 / 0.14);
--ui-control-radius: var(--radius-lg);
--ui-ornament-opacity: 0.36;
--ui-ornament-mix: screen;
--ui-button-radius: var(--radius-lg);
--ui-button-border-width: 1px;
--ui-button-transition-duration: var(--dur-base);
--ui-button-sheen-opacity: 0.42;
--ui-button-sheen-mix: screen;
--ui-button-sheen-gradient: linear-gradient(
120deg,
transparent 0%,
rgba(255, 255, 255, 0.34) 45%,
transparent 100%
);
--ui-button-primary-bg: color-mix(in oklch, var(--color-primary) 72%, white 28%);
--ui-button-primary-hover-bg: color-mix(in oklch, var(--color-primary) 78%, white 22%);
--ui-button-primary-fg: var(--color-foreground);
--ui-button-primary-border: color-mix(in oklch, white 28%, var(--color-primary));
--ui-button-primary-shadow: 0 16px 34px oklch(0.24 0.06 250 / 0.18);
--ui-button-secondary-bg: color-mix(in oklch, var(--color-secondary) 52%, transparent);
--ui-button-secondary-hover-bg: color-mix(in oklch, var(--color-secondary) 64%, transparent);
--ui-button-secondary-fg: var(--color-secondary-foreground);
--ui-button-secondary-border: color-mix(in oklch, white 34%, var(--color-border-strong));
--ui-button-secondary-shadow: 0 12px 28px oklch(0.24 0.04 250 / 0.12);
--ui-button-ghost-bg: transparent;
--ui-button-ghost-hover-bg: color-mix(in oklch, white 18%, transparent);
--ui-button-ghost-fg: var(--color-foreground);
--ui-button-ghost-border: color-mix(in oklch, white 20%, transparent);
--ui-button-ghost-shadow: none;
--ui-button-subtle-bg: color-mix(in oklch, var(--color-card) 56%, transparent);
--ui-button-subtle-hover-bg: color-mix(in oklch, var(--color-card) 66%, transparent);
--ui-button-subtle-fg: var(--color-foreground);
--ui-button-subtle-border: color-mix(in oklch, white 30%, var(--color-border));
--ui-button-subtle-shadow: 0 12px 30px oklch(0.24 0.04 250 / 0.12);
--ui-button-destructive-bg: color-mix(in oklch, var(--color-destructive) 74%, white 26%);
--ui-button-destructive-hover-bg: color-mix(
in oklch,
var(--color-destructive) 80%,
white 20%
);
--ui-button-destructive-fg: var(--color-destructive-foreground);
--ui-button-destructive-border: color-mix(in oklch, white 28%, var(--color-destructive));
--ui-button-destructive-shadow: 0 16px 34px oklch(0.32 0.07 18 / 0.18);
--ui-button-hover-scale: 1.02;
--ui-button-press-scale: 0.985;
--ui-button-hover-translate: -2px;
--ui-button-hover-shadow: 0 20px 42px oklch(0.2 0.04 250 / 0.18);
--ui-button-active-shadow: 0 10px 24px oklch(0.2 0.04 250 / 0.14);
--ui-spinner-radius: var(--radius-full);
--ui-spinner-border-width: 2px;
--ui-card-radius: var(--radius-xl);
--ui-card-border-width: 1px;
--ui-card-bg: color-mix(in oklch, var(--color-card) 58%, transparent);
--ui-card-shadow: 0 24px 64px oklch(0.18 0.03 255 / 0.18);
--ui-card-default-bg: color-mix(in oklch, var(--color-card) 58%, transparent);
--ui-card-default-border: color-mix(in oklch, white 42%, var(--color-border));
--ui-card-default-shadow: 0 24px 64px oklch(0.18 0.03 255 / 0.18);
--ui-card-subtle-bg: color-mix(in oklch, var(--color-surface) 52%, transparent);
--ui-card-subtle-border: color-mix(in oklch, white 32%, var(--color-border));
--ui-card-subtle-shadow: 0 18px 46px oklch(0.18 0.03 255 / 0.12);
--ui-card-accent-bg: color-mix(in oklch, var(--color-primary) 16%, transparent);
--ui-card-accent-border: color-mix(in oklch, white 26%, var(--color-primary));
--ui-card-accent-shadow: 0 24px 54px oklch(0.22 0.08 245 / 0.18);
--ui-card-hover-translate: -4px;
--ui-card-hover-shadow: 0 30px 72px oklch(0.18 0.03 255 / 0.22);
--ui-input-radius: var(--radius-lg);
--ui-input-border-width: 1px;
--ui-input-bg: color-mix(in oklch, var(--color-card) 50%, transparent);
--ui-input-border: color-mix(in oklch, white 34%, var(--color-border));
--ui-input-fg: var(--color-foreground);
--ui-input-shadow: 0 14px 34px oklch(0.2 0.03 255 / 0.12);
--ui-input-focus-border: color-mix(in oklch, white 44%, var(--color-primary));
--ui-input-focus-shadow:
0 0 0 1px color-mix(in oklch, white 22%, var(--color-primary)),
0 18px 40px oklch(0.2 0.03 255 / 0.18);
--ui-input-focus-lift: -1px;
--ui-input-disabled-bg: color-mix(in oklch, var(--color-surface) 72%, transparent);
--ui-input-readonly-bg: color-mix(in oklch, var(--color-surface) 68%, transparent);
--ui-input-backdrop-blur: 12px;
--ui-panel-radius: var(--radius-xl);
--ui-panel-border-width: 1px;
--ui-panel-bg: color-mix(in oklch, var(--color-card) 54%, transparent);
--ui-panel-border: color-mix(in oklch, white 40%, var(--color-border));
--ui-panel-shadow: 0 28px 72px oklch(0.16 0.03 255 / 0.24);
--ui-panel-backdrop-blur: 20px;
--ui-panel-overlay-bg: color-mix(in oklch, var(--color-overlay) 74%, transparent);
--ui-panel-overlay-blur: 8px;
--ui-switch-track-radius: var(--radius-full);
--ui-switch-track-border-width: 1px;
--ui-switch-track-bg: color-mix(in oklch, var(--color-card) 44%, transparent);
--ui-switch-track-border: color-mix(in oklch, white 32%, var(--color-border));
--ui-switch-track-shadow: 0 10px 24px oklch(0.18 0.03 255 / 0.14);
--ui-switch-track-checked-bg: color-mix(in oklch, var(--color-primary) 72%, white 28%);
--ui-switch-track-checked-border: color-mix(in oklch, white 30%, var(--color-primary));
--ui-switch-thumb-radius: var(--radius-full);
--ui-switch-thumb-bg: color-mix(in oklch, white 84%, var(--color-card));
--ui-switch-thumb-shadow: 0 8px 18px oklch(0.16 0.02 255 / 0.22);
--ui-switch-thumb-checked-shadow: 0 12px 24px oklch(0.18 0.03 255 / 0.28);
--ui-switch-transition-duration: var(--dur-base);
--ui-skeleton-radius: var(--radius-md);
--ui-skeleton-block-radius: var(--radius-lg);
--ui-skeleton-pill-radius: var(--radius-full);
--ui-skeleton-avatar-radius: var(--radius-full);
--ui-skeleton-bg: color-mix(in oklch, var(--color-surface) 42%, transparent);
--ui-skeleton-muted-bg: color-mix(in oklch, var(--color-muted) 54%, transparent);
--ui-skeleton-gradient: linear-gradient(
110deg,
transparent 0%,
rgba(255, 255, 255, 0.58) 44%,
transparent 74%
);
}
[data-skin="pixel"] {
--ui-canvas-image:
linear-gradient(
90deg,
color-mix(in oklch, var(--color-foreground) 7%, transparent) 1px,
transparent 1px
),
linear-gradient(
180deg,
color-mix(in oklch, var(--color-foreground) 7%, transparent) 1px,
transparent 1px
),
linear-gradient(
180deg,
color-mix(in oklch, var(--color-background) 92%, black 8%),
var(--color-background)
);
--ui-canvas-size: 12px 12px, 12px 12px, auto;
--ui-surface-bg: color-mix(in oklch, var(--color-card) 96%, white 4%);
--ui-surface-border: var(--color-foreground);
--ui-surface-shadow: 6px 6px 0 color-mix(in oklch, var(--color-foreground) 38%, transparent);
--ui-surface-radius: 0px;
--ui-surface-backdrop-blur: 0px;
--ui-control-bg: var(--color-background);
--ui-control-border: var(--color-foreground);
--ui-control-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 30%, transparent);
--ui-control-radius: 0px;
--ui-ornament-opacity: 0.2;
--ui-ornament-mix: multiply;
--ui-button-radius: 0px;
--ui-button-border-width: 2px;
--ui-button-transition-duration: var(--dur-instant);
--ui-button-sheen-opacity: 0;
--ui-button-sheen-mix: normal;
--ui-button-sheen-gradient: linear-gradient(90deg, transparent, transparent);
--ui-button-primary-bg: var(--color-primary);
--ui-button-primary-hover-bg: color-mix(in oklch, var(--color-primary) 88%, white 12%);
--ui-button-primary-fg: var(--color-primary-foreground);
--ui-button-primary-border: var(--color-foreground);
--ui-button-primary-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 34%, transparent);
--ui-button-secondary-bg: var(--color-secondary);
--ui-button-secondary-hover-bg: color-mix(in oklch, var(--color-secondary) 86%, black 14%);
--ui-button-secondary-fg: var(--color-secondary-foreground);
--ui-button-secondary-border: var(--color-foreground);
--ui-button-secondary-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 30%, transparent);
--ui-button-ghost-bg: transparent;
--ui-button-ghost-hover-bg: color-mix(in oklch, var(--color-surface) 86%, black 14%);
--ui-button-ghost-fg: var(--color-foreground);
--ui-button-ghost-border: var(--color-foreground);
--ui-button-ghost-shadow: none;
--ui-button-subtle-bg: var(--color-card);
--ui-button-subtle-hover-bg: color-mix(in oklch, var(--color-card) 88%, black 12%);
--ui-button-subtle-fg: var(--color-foreground);
--ui-button-subtle-border: var(--color-foreground);
--ui-button-subtle-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 30%, transparent);
--ui-button-destructive-bg: var(--color-destructive);
--ui-button-destructive-hover-bg: color-mix(
in oklch,
var(--color-destructive) 88%,
white 12%
);
--ui-button-destructive-fg: var(--color-destructive-foreground);
--ui-button-destructive-border: var(--color-foreground);
--ui-button-destructive-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 34%, transparent);
--ui-button-hover-scale: 1;
--ui-button-press-scale: 0.98;
--ui-button-hover-translate: -1px;
--ui-button-hover-shadow: 5px 5px 0 color-mix(in oklch, var(--color-foreground) 36%, transparent);
--ui-button-active-shadow: 1px 1px 0 color-mix(in oklch, var(--color-foreground) 28%, transparent);
--ui-spinner-radius: 0px;
--ui-spinner-border-width: 2px;
--ui-card-radius: 0px;
--ui-card-border-width: 2px;
--ui-card-bg: var(--color-card);
--ui-card-shadow: 6px 6px 0 color-mix(in oklch, var(--color-foreground) 38%, transparent);
--ui-card-default-bg: var(--color-card);
--ui-card-default-border: var(--color-foreground);
--ui-card-default-shadow: 6px 6px 0 color-mix(in oklch, var(--color-foreground) 38%, transparent);
--ui-card-subtle-bg: var(--color-surface);
--ui-card-subtle-border: var(--color-foreground);
--ui-card-subtle-shadow: 4px 4px 0 color-mix(in oklch, var(--color-foreground) 34%, transparent);
--ui-card-accent-bg: color-mix(in oklch, var(--color-primary) 12%, var(--color-card));
--ui-card-accent-border: var(--color-foreground);
--ui-card-accent-shadow: 6px 6px 0 color-mix(in oklch, var(--color-primary) 34%, transparent);
--ui-card-hover-translate: -2px;
--ui-card-hover-shadow: 8px 8px 0 color-mix(in oklch, var(--color-foreground) 42%, transparent);
--ui-input-radius: 0px;
--ui-input-border-width: 2px;
--ui-input-bg: var(--color-background);
--ui-input-border: var(--color-foreground);
--ui-input-fg: var(--color-foreground);
--ui-input-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 28%, transparent);
--ui-input-focus-border: var(--color-primary);
--ui-input-focus-shadow:
0 0 0 2px color-mix(in oklch, var(--color-primary) 42%, transparent),
4px 4px 0 color-mix(in oklch, var(--color-foreground) 32%, transparent);
--ui-input-focus-lift: 0px;
--ui-input-disabled-bg: var(--color-surface);
--ui-input-readonly-bg: var(--color-surface);
--ui-input-backdrop-blur: 0px;
--ui-panel-radius: 0px;
--ui-panel-border-width: 2px;
--ui-panel-bg: var(--color-card);
--ui-panel-border: var(--color-foreground);
--ui-panel-shadow: 8px 8px 0 color-mix(in oklch, var(--color-foreground) 40%, transparent);
--ui-panel-backdrop-blur: 0px;
--ui-panel-overlay-bg: color-mix(in oklch, var(--color-overlay) 92%, black 8%);
--ui-panel-overlay-blur: 0px;
--ui-switch-track-radius: 0px;
--ui-switch-track-border-width: 2px;
--ui-switch-track-bg: var(--color-border);
--ui-switch-track-border: var(--color-foreground);
--ui-switch-track-shadow: 2px 2px 0 color-mix(in oklch, var(--color-foreground) 24%, transparent);
--ui-switch-track-checked-bg: var(--color-primary);
--ui-switch-track-checked-border: var(--color-foreground);
--ui-switch-thumb-radius: 0px;
--ui-switch-thumb-bg: var(--color-background);
--ui-switch-thumb-shadow: 2px 2px 0 color-mix(in oklch, var(--color-foreground) 24%, transparent);
--ui-switch-thumb-checked-shadow: 3px 3px 0 color-mix(in oklch, var(--color-foreground) 28%, transparent);
--ui-switch-transition-duration: var(--dur-fast);
--ui-skeleton-radius: 0px;
--ui-skeleton-block-radius: 0px;
--ui-skeleton-pill-radius: 0px;
--ui-skeleton-avatar-radius: 0px;
--ui-skeleton-bg: color-mix(in oklch, var(--color-foreground) 18%, var(--color-background));
--ui-skeleton-muted-bg: color-mix(in oklch, var(--color-muted) 72%, black 28%);
--ui-skeleton-gradient: linear-gradient(
90deg,
transparent 0%,
transparent 28%,
rgba(255, 255, 255, 0.2) 28%,
rgba(255, 255, 255, 0.2) 42%,
transparent 42%,
transparent 100%
);
}