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
@@ -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%
);
}