Files
writer-work-flow/apps/web/lib/ui/variants.ts
Yaojia Wang 355a2d11cd feat(ui): Checkbox/Radio 原子件 + 3 处原生控件采用(P2-5)
- checkboxClass/radioClass(accent-cinnabar + 统一焦点环)+ 同名测试
- Checkbox/Radio 原子件:无 label 只渲染受控原生 input 便于就地替换
- 采用:ChainStarter 链类型单选、GeneratorRunner 入库勾选、CharacterCardItem 角色勾选
2026-07-11 14:04:01 +02:00

191 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
// 手稿正文统一排版:衬线 18pxtext-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);
}
// 勾选/单选控件:原生 input + accent 上色,保留原生键盘/读屏语义,套统一焦点环。
const controlBoxBase =
"h-4 w-4 shrink-0 border-line accent-cinnabar disabled:cursor-not-allowed disabled:opacity-45";
export function checkboxClass(className?: string): string {
return cn(controlBoxBase, "rounded-xs", focusRing, className);
}
export function radioClass(className?: string): string {
return cn(controlBoxBase, "rounded-full", focusRing, className);
}