- Card 新增 tone(panel/card/soft)/flat/interactive;cardClass 兼容旧字符串签名 - Button 新增 link(coral 文字链) 变体与 lg 尺寸 - 新增原子件 Eyebrow(统一眉题) / StatusDot(状态点·读屏可见) / Skeleton(motion-safe 占位) / Meter(进度条·progressbar) - PageHeader/SectionHeader 用 display/title 字阶 + Eyebrow 眉题
179 lines
5.8 KiB
TypeScript
179 lines
5.8 KiB
TypeScript
export type ButtonVariant =
|
||
| "primary"
|
||
| "secondary"
|
||
| "outline"
|
||
| "ghost"
|
||
| "danger"
|
||
| "link";
|
||
|
||
export type ButtonSize = "sm" | "md" | "lg" | "icon";
|
||
|
||
export type CardTone = "panel" | "card" | "soft";
|
||
|
||
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}`,
|
||
link: `border-transparent bg-transparent text-cinnabar underline-offset-4 hover:text-cinnabar-active hover:underline ${disabledFade}`,
|
||
};
|
||
|
||
const buttonSizes: Record<ButtonSize, string> = {
|
||
sm: "px-3 py-1.5 text-xs",
|
||
md: "px-4 py-2",
|
||
lg: "px-5 py-2.5 text-base",
|
||
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);
|
||
}
|
||
|
||
const cardTones: Record<CardTone, string> = {
|
||
panel: "bg-panel",
|
||
card: "bg-surface-card",
|
||
soft: "bg-surface-soft",
|
||
};
|
||
|
||
interface CardClassOptions {
|
||
tone?: CardTone;
|
||
flat?: boolean;
|
||
interactive?: boolean;
|
||
className?: string;
|
||
}
|
||
|
||
// 兼容旧签名 cardClass("extra"),同时支持面色分层/去阴影/可交互卡片。
|
||
export function cardClass(opts?: string | CardClassOptions): string {
|
||
const o: CardClassOptions = typeof opts === "string" ? { className: opts } : opts ?? {};
|
||
const tone = o.tone ?? "panel";
|
||
return cn(
|
||
"rounded-lg border border-line",
|
||
cardTones[tone],
|
||
o.flat ? null : "shadow-paper",
|
||
o.interactive ? `${transitionUi} hover:border-cinnabar/40 hover:shadow-paper` : null,
|
||
o.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);
|
||
}
|