- 面色阶 surface-soft/card/strong + 文字 5 级 body-strong/body/muted-soft - 边框 line-soft、coral 状态 cinnabar-active/disabled(按压变深 + 奶油化禁用) - 圆角刻度 xs..xl/full:按钮 rounded-md、卡片 rounded-lg、徽章 pill - 排版档 display/title/caption/eyebrow/prose(衬线负字距) - 动效 token duration-fast/base + ease-standard,transitionUi 常量统一过渡 - 焦点环统一 cinnabar/35;双主题成对定义
152 lines
5.0 KiB
TypeScript
152 lines
5.0 KiB
TypeScript
export type ButtonVariant =
|
||
| "primary"
|
||
| "secondary"
|
||
| "outline"
|
||
| "ghost"
|
||
| "danger";
|
||
|
||
export type ButtonSize = "sm" | "md" | "icon";
|
||
|
||
export type BadgeVariant =
|
||
| "neutral"
|
||
| "accent"
|
||
| "success"
|
||
| "warning"
|
||
| "danger"
|
||
| "info";
|
||
|
||
export type InputState = "default" | "error";
|
||
|
||
export type StatusNoteVariant = "info" | "success" | "warning" | "danger";
|
||
|
||
export function cn(...classes: Array<string | false | null | undefined>): string {
|
||
return classes.filter(Boolean).join(" ");
|
||
}
|
||
|
||
// 统一焦点环(供按钮/输入/导航等所有可聚焦元素复用,统一到 cinnabar/35)。
|
||
export const focusRing =
|
||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cinnabar/35";
|
||
|
||
// 统一交互过渡(颜色变化 + 动效 token),供按钮/输入/导航复用,全局可调。
|
||
export const transitionUi = "transition-colors duration-fast ease-standard";
|
||
|
||
// 手稿正文统一排版:衬线 18px(text-prose 档)、行高 1.9。
|
||
export const proseBody = "font-serif text-prose";
|
||
|
||
// Drawer / CommandPalette 共用的全屏遮罩。
|
||
export const overlayScrim = "fixed inset-0 z-40 bg-black/30";
|
||
|
||
const buttonBase = `inline-flex items-center justify-center gap-1.5 rounded-md border text-sm disabled:cursor-not-allowed ${transitionUi} ${focusRing}`;
|
||
|
||
// 非主按钮的禁用淡化(主按钮改用奶油化禁用底,见下)。
|
||
const disabledFade = "disabled:opacity-45";
|
||
|
||
const buttonVariants: Record<ButtonVariant, string> = {
|
||
primary:
|
||
"border-cinnabar bg-cinnabar text-panel shadow-paper hover:bg-cinnabar-active active:bg-cinnabar-active disabled:border-cinnabar-disabled disabled:bg-cinnabar-disabled disabled:text-muted-soft",
|
||
secondary: `border-line bg-panel text-ink hover:border-cinnabar hover:text-cinnabar ${disabledFade}`,
|
||
outline: `border-cinnabar bg-transparent text-cinnabar hover:bg-[var(--color-cinnabar-wash)] active:bg-[var(--color-cinnabar-wash)] ${disabledFade}`,
|
||
ghost: `border-transparent bg-transparent text-ink-soft hover:bg-[var(--color-cinnabar-wash)] hover:text-cinnabar ${disabledFade}`,
|
||
danger: `border-conflict bg-transparent text-conflict hover:bg-conflict/10 ${disabledFade}`,
|
||
};
|
||
|
||
const buttonSizes: Record<ButtonSize, string> = {
|
||
sm: "px-3 py-1.5 text-xs",
|
||
md: "px-4 py-2",
|
||
icon: "h-9 w-9 p-0",
|
||
};
|
||
|
||
export function buttonClass({
|
||
variant = "secondary",
|
||
size = "md",
|
||
className,
|
||
}: {
|
||
variant?: ButtonVariant;
|
||
size?: ButtonSize;
|
||
className?: string;
|
||
} = {}): string {
|
||
return cn(buttonBase, buttonVariants[variant], buttonSizes[size], className);
|
||
}
|
||
|
||
const badgeBase =
|
||
"inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs leading-5";
|
||
|
||
const badgeVariants: Record<BadgeVariant, string> = {
|
||
neutral: "border-line bg-bg text-ink-soft",
|
||
accent:
|
||
"border-cinnabar/20 bg-[var(--color-cinnabar-wash)] text-cinnabar",
|
||
success: "border-pass/25 bg-pass/10 text-pass",
|
||
warning: "border-overdue/35 bg-overdue/10 text-ink",
|
||
danger: "border-conflict/25 bg-conflict/10 text-conflict",
|
||
info: "border-info/25 bg-info/10 text-info",
|
||
};
|
||
|
||
export function badgeClass({
|
||
variant = "neutral",
|
||
className,
|
||
}: {
|
||
variant?: BadgeVariant;
|
||
className?: string;
|
||
} = {}): string {
|
||
return cn(badgeBase, badgeVariants[variant], className);
|
||
}
|
||
|
||
export function cardClass(className?: string): string {
|
||
return cn("rounded-lg border border-line bg-panel shadow-paper", className);
|
||
}
|
||
|
||
const fieldTextBase = "block text-sm font-medium text-ink";
|
||
|
||
export function fieldLabelClass(className?: string): string {
|
||
return cn(fieldTextBase, className);
|
||
}
|
||
|
||
export function fieldHelpClass(className?: string): string {
|
||
return cn("mt-1 text-xs leading-5 text-ink-soft", className);
|
||
}
|
||
|
||
export function fieldErrorClass(className?: string): string {
|
||
return cn("mt-1 text-xs leading-5 text-conflict", className);
|
||
}
|
||
|
||
const inputBase = `w-full rounded-md border bg-bg text-ink placeholder:text-ink-soft/80 disabled:cursor-not-allowed disabled:bg-line/20 disabled:text-ink-soft ${transitionUi} ${focusRing}`;
|
||
|
||
const inputStates: Record<InputState, string> = {
|
||
default: "border-line focus:border-cinnabar",
|
||
error: "border-conflict focus:border-conflict focus-visible:ring-conflict/25",
|
||
};
|
||
|
||
export function inputClass({
|
||
state = "default",
|
||
className,
|
||
}: {
|
||
state?: InputState;
|
||
className?: string;
|
||
} = {}): string {
|
||
return cn(inputBase, inputStates[state], className);
|
||
}
|
||
|
||
const statusNoteBase =
|
||
"rounded border px-3 py-2 text-sm leading-6";
|
||
|
||
const statusNoteVariants: Record<StatusNoteVariant, string> = {
|
||
info: "border-info/25 bg-info/10 text-info",
|
||
success: "border-pass/25 bg-pass/10 text-pass",
|
||
warning: "border-overdue/35 bg-overdue/10 text-ink",
|
||
danger: "border-conflict/25 bg-conflict/10 text-conflict",
|
||
};
|
||
|
||
export function statusNoteClass({
|
||
variant = "info",
|
||
className,
|
||
}: {
|
||
variant?: StatusNoteVariant;
|
||
className?: string;
|
||
} = {}): string {
|
||
return cn(statusNoteBase, statusNoteVariants[variant], className);
|
||
}
|
||
|
||
export function segmentedClass(className?: string): string {
|
||
return cn("inline-flex rounded border border-line bg-bg p-1", className);
|
||
}
|