mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-25 17:22:27 +08:00
feat(ui): add v7 design tokens and shared primitives
Foundation for the v7 redesign; no layout changes yet. - Add the v7 tokens (surfaces, text, action, inverse, direct / route / stack, status, chart, shadows) to index.css and expose them in Tailwind together with v7 radius, shadow and type-role scales; teach tailwind-merge the new names so text-body no longer cancels text-fg-1. - Align the shadcn HSL tokens (background, card, muted, border, ring) with the v7 palette and use the neutral ring for focus outlines. - Rename light / dark to Light / Dark Side of the Moon with System first, and pause transitions while switching so colors don't fade in piece by piece. - Add v7 button variants (solid, neutral, quiet, direct, route, stack) and sizes, plus HelpTip / DisabledReason (the "?" card), SegmentedControl, Notice / NoticeSlot and Sheet. - Respect the OS reduced-motion setting for framer-motion.
This commit is contained in:
1 parent
98798552c0
commit
947a8a8b06
16 files changed
+938
-65
No files matched your search
@@ -17,6 +17,13 @@ export function ThemeSettings() {
|
||||
</p>
|
||||
</header>
|
||||
<div className="inline-flex gap-1 rounded-md border border-border-default bg-background p-1">
|
||||
<ThemeButton
|
||||
active={theme === "system"}
|
||||
onClick={() => setTheme("system")}
|
||||
icon={Monitor}
|
||||
>
|
||||
{t("settings.themeSystem")}
|
||||
</ThemeButton>
|
||||
<ThemeButton
|
||||
active={theme === "light"}
|
||||
onClick={() => setTheme("light")}
|
||||
@@ -31,13 +38,6 @@ export function ThemeSettings() {
|
||||
>
|
||||
{t("settings.themeDark")}
|
||||
</ThemeButton>
|
||||
<ThemeButton
|
||||
active={theme === "system"}
|
||||
onClick={() => setTheme("system")}
|
||||
icon={Monitor}
|
||||
>
|
||||
{t("settings.themeSystem")}
|
||||
</ThemeButton>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,30 @@ const ThemeProviderContext = createContext<ThemeContextValue | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
/**
|
||||
* 切换 html 上的 light / dark。切换期间暂停所有过渡(`.theme-switching`),
|
||||
* 否则各处的 transition-colors 会让颜色一块块慢慢变过去。
|
||||
*/
|
||||
function applyColorScheme(isDark: boolean) {
|
||||
const root = window.document.documentElement;
|
||||
if (
|
||||
root.classList.contains("dark") === isDark &&
|
||||
root.classList.contains("light") === !isDark
|
||||
) {
|
||||
return;
|
||||
}
|
||||
root.classList.add("theme-switching");
|
||||
root.classList.toggle("dark", isDark);
|
||||
root.classList.toggle("light", !isDark);
|
||||
// 强制重排一次,让不带过渡的新颜色先落定,再在第二帧恢复过渡。
|
||||
void window.document.body?.offsetHeight;
|
||||
window.requestAnimationFrame(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
root.classList.remove("theme-switching");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
@@ -57,18 +81,12 @@ export function ThemeProvider({
|
||||
return;
|
||||
}
|
||||
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if (theme === "system") {
|
||||
const isDark =
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
root.classList.add(isDark ? "dark" : "light");
|
||||
return;
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
const isDark =
|
||||
theme === "system"
|
||||
? window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
: theme === "dark";
|
||||
applyColorScheme(isDark);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -82,9 +100,7 @@ export function ThemeProvider({
|
||||
return;
|
||||
}
|
||||
|
||||
const root = window.document.documentElement;
|
||||
root.classList.toggle("dark", mediaQuery.matches);
|
||||
root.classList.toggle("light", !mediaQuery.matches);
|
||||
applyColorScheme(mediaQuery.matches);
|
||||
};
|
||||
|
||||
if (theme === "system") {
|
||||
|
||||
@@ -27,12 +27,35 @@ const buttonVariants = cva(
|
||||
mcp: "bg-emerald-500 text-white hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700",
|
||||
// 链接按钮
|
||||
link: "text-blue-500 underline-offset-4 hover:underline dark:text-blue-400",
|
||||
// ── v7(tokens.css 的 btn-*):一个视图只有一个实心按钮,其余用 neutral ──
|
||||
// 中性实心:页头主操作(操作色,以后的主题色只改这一组变量)
|
||||
solid:
|
||||
"border border-transparent bg-action text-action-fg hover:bg-action-hover",
|
||||
// 中性描边:卡片上的「切换」「添加」「设为默认」等
|
||||
neutral:
|
||||
"border border-border-strong bg-surface text-fg-1 hover:bg-subtle disabled:opacity-100 disabled:border-border disabled:bg-transparent disabled:text-fg-3",
|
||||
// 透明:页头里带 ⌄ / › 的次要操作
|
||||
quiet: "border border-transparent text-fg-1 hover:bg-subtle",
|
||||
// 模式色实心:只用于模式切换的主操作
|
||||
direct:
|
||||
"border border-transparent bg-direct-solid text-direct-on font-semibold hover:brightness-110",
|
||||
route:
|
||||
"border border-transparent bg-route-solid text-route-on font-semibold hover:brightness-110",
|
||||
stack:
|
||||
"border border-transparent bg-stack-solid text-stack-on font-semibold hover:brightness-110",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9 p-1.5",
|
||||
// v7:高 28(卡片、对话框里)/ 高 32(页头、模式切换)/ 28 见方的图标按钮
|
||||
compact:
|
||||
"h-7 rounded-control px-3 text-body font-medium transition-[background-color,filter,scale] active:scale-[0.96]",
|
||||
regular:
|
||||
"h-8 rounded-control px-3.5 text-body font-medium transition-[background-color,filter,scale] active:scale-[0.96]",
|
||||
"icon-compact":
|
||||
"h-7 w-7 rounded-control p-0 text-fg-2 hover:text-fg-1 transition-[background-color,color,scale] active:scale-[0.96]",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import { CircleHelp } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* 说明卡(v7 第八轮「?」,规格见 docs/research/2026-09-30-ui-design-v7/HELP_BRIEF.md)。
|
||||
*
|
||||
* - 悬停 300ms 打开,移开 200ms 关闭(移进卡片不关);键盘聚焦立即打开。
|
||||
* - 点一下固定,Escape 或点别处关闭。
|
||||
* - 不抢焦点(modal=false),卡片里不放按钮、表单、链接。
|
||||
* - 只放规则和术语解释;会丢东西、要重启这类后果写在按钮、确认框或 toast 里,不进这里。
|
||||
*
|
||||
* 深色小气泡(tooltip.tsx)只给纯图标按钮报名字,不放说明句子。
|
||||
*/
|
||||
|
||||
const OPEN_DELAY = 300;
|
||||
const CLOSE_DELAY = 200;
|
||||
|
||||
function useHoverCard() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [pinned, setPinned] = React.useState(false);
|
||||
const timer = React.useRef<number | undefined>(undefined);
|
||||
|
||||
const clear = () => {
|
||||
if (timer.current !== undefined) {
|
||||
window.clearTimeout(timer.current);
|
||||
timer.current = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => clear, []);
|
||||
|
||||
const schedule = (next: boolean, delay: number) => {
|
||||
clear();
|
||||
timer.current = window.setTimeout(() => setOpen(next), delay);
|
||||
};
|
||||
|
||||
return {
|
||||
open: open || pinned,
|
||||
pinned,
|
||||
onPointerEnter: () => {
|
||||
if (!pinned) schedule(true, OPEN_DELAY);
|
||||
},
|
||||
onPointerLeave: () => {
|
||||
if (!pinned) schedule(false, CLOSE_DELAY);
|
||||
},
|
||||
onFocus: () => {
|
||||
clear();
|
||||
setOpen(true);
|
||||
},
|
||||
onBlur: () => {
|
||||
if (!pinned) {
|
||||
clear();
|
||||
setOpen(false);
|
||||
}
|
||||
},
|
||||
onClick: () => {
|
||||
clear();
|
||||
setPinned((value) => !value);
|
||||
setOpen(true);
|
||||
},
|
||||
onOpenChange: (next: boolean) => {
|
||||
// Escape / 点别处
|
||||
if (!next) {
|
||||
clear();
|
||||
setPinned(false);
|
||||
setOpen(false);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface HelpCardProps {
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
side?: "top" | "bottom";
|
||||
align?: "start" | "end";
|
||||
onPointerEnter: () => void;
|
||||
onPointerLeave: () => void;
|
||||
}
|
||||
|
||||
function HelpCard({
|
||||
id,
|
||||
title,
|
||||
children,
|
||||
side = "bottom",
|
||||
align = "start",
|
||||
onPointerEnter,
|
||||
onPointerLeave,
|
||||
}: HelpCardProps) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
id={id}
|
||||
role="tooltip"
|
||||
side={side}
|
||||
align={align}
|
||||
sideOffset={6}
|
||||
alignOffset={-8}
|
||||
collisionPadding={8}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
onPointerEnter={onPointerEnter}
|
||||
onPointerLeave={onPointerLeave}
|
||||
className="z-[120] w-max max-w-[300px] rounded-[8px] border border-border bg-surface px-3 pb-2.5 pt-[9px] text-caption text-fg-1 shadow-v7-md outline-none data-[state=open]:animate-in data-[state=open]:fade-in-0"
|
||||
>
|
||||
{title ? (
|
||||
<span className="mb-0.5 block font-semibold">{title}</span>
|
||||
) : null}
|
||||
{children}
|
||||
</PopoverPrimitive.Content>
|
||||
</PopoverPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export interface HelpTipProps {
|
||||
/** 卡片标题,同时是「?」按钮的无障碍名字。写成问题或名词。 */
|
||||
title: string;
|
||||
/** 正文,一句话,≤ 80 字。 */
|
||||
children: React.ReactNode;
|
||||
side?: "top" | "bottom";
|
||||
align?: "start" | "end";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** 放在被解释的词后面的「?」按钮。 */
|
||||
export function HelpTip({
|
||||
title,
|
||||
children,
|
||||
side,
|
||||
align,
|
||||
className,
|
||||
}: HelpTipProps) {
|
||||
const id = React.useId();
|
||||
const descId = `${id}-desc`;
|
||||
const hover = useHoverCard();
|
||||
|
||||
return (
|
||||
<PopoverPrimitive.Root open={hover.open} onOpenChange={hover.onOpenChange}>
|
||||
{/* 卡片只在打开时挂载;屏幕阅读器读这份一直在的副本 */}
|
||||
<span id={descId} className="sr-only">
|
||||
{children}
|
||||
</span>
|
||||
<PopoverPrimitive.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={title}
|
||||
aria-describedby={descId}
|
||||
onPointerEnter={hover.onPointerEnter}
|
||||
onPointerLeave={hover.onPointerLeave}
|
||||
onFocus={hover.onFocus}
|
||||
onBlur={hover.onBlur}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
hover.onClick();
|
||||
}}
|
||||
className={cn(
|
||||
"-my-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full align-middle text-fg-3 transition-colors hover:bg-subtle hover:text-fg-1 data-[state=open]:bg-subtle data-[state=open]:text-fg-1",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<CircleHelp className="h-3.5 w-3.5" strokeWidth={1.5} />
|
||||
</button>
|
||||
</PopoverPrimitive.Trigger>
|
||||
<HelpCard
|
||||
id={id}
|
||||
title={title}
|
||||
side={side}
|
||||
align={align}
|
||||
onPointerEnter={hover.onPointerEnter}
|
||||
onPointerLeave={hover.onPointerLeave}
|
||||
>
|
||||
{children}
|
||||
</HelpCard>
|
||||
</PopoverPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export interface DisabledReasonProps {
|
||||
/** 为什么不能点。为空时原样渲染子元素。 */
|
||||
reason?: React.ReactNode;
|
||||
/** 一个按钮。有原因时会被设成 aria-disabled(不用原生 disabled,否则收不到悬停也进不了 Tab 顺序),点击无效。 */
|
||||
children: React.ReactElement<{
|
||||
onClick?: React.MouseEventHandler;
|
||||
"aria-disabled"?: boolean | "true" | "false";
|
||||
"aria-describedby"?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}>;
|
||||
side?: "top" | "bottom";
|
||||
align?: "start" | "end";
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用原因挂在按钮本身上:悬停 / 聚焦按钮时出现说明卡。
|
||||
* 菜单、对话框、抽屉这类浮层里的禁用原因照旧写成看得见的第二行,不用这个组件。
|
||||
*/
|
||||
export function DisabledReason({
|
||||
reason,
|
||||
children,
|
||||
side,
|
||||
align,
|
||||
}: DisabledReasonProps) {
|
||||
const id = React.useId();
|
||||
const descId = `${id}-desc`;
|
||||
const hover = useHoverCard();
|
||||
|
||||
if (!reason) return children;
|
||||
|
||||
const button = React.cloneElement(children, {
|
||||
disabled: false,
|
||||
"aria-disabled": "true",
|
||||
"aria-describedby": descId,
|
||||
onClick: (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
hover.onClick();
|
||||
},
|
||||
className: cn(children.props.className, "cursor-not-allowed opacity-45"),
|
||||
});
|
||||
|
||||
return (
|
||||
<PopoverPrimitive.Root open={hover.open} onOpenChange={hover.onOpenChange}>
|
||||
<PopoverPrimitive.Anchor asChild>
|
||||
<span
|
||||
className="inline-flex"
|
||||
onPointerEnter={hover.onPointerEnter}
|
||||
onPointerLeave={hover.onPointerLeave}
|
||||
onFocus={hover.onFocus}
|
||||
onBlur={hover.onBlur}
|
||||
>
|
||||
{button}
|
||||
<span id={descId} className="sr-only">
|
||||
{reason}
|
||||
</span>
|
||||
</span>
|
||||
</PopoverPrimitive.Anchor>
|
||||
<HelpCard
|
||||
id={id}
|
||||
side={side}
|
||||
align={align}
|
||||
onPointerEnter={hover.onPointerEnter}
|
||||
onPointerLeave={hover.onPointerLeave}
|
||||
>
|
||||
{reason}
|
||||
</HelpCard>
|
||||
</PopoverPrimitive.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CircleAlert,
|
||||
Info,
|
||||
Layers,
|
||||
Route,
|
||||
X,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* 通知条(v7 AUTHORING「通知条」):圆角 10,左侧 16px 图标,不用左边框强调色。
|
||||
* 应用页的通知槽(NoticeSlot)里排成一列,按严重程度从上到下。
|
||||
*/
|
||||
export type NoticeTone =
|
||||
| "neutral"
|
||||
| "warning"
|
||||
| "danger"
|
||||
| "route"
|
||||
| "stack"
|
||||
| "direct";
|
||||
|
||||
const TONE: Record<
|
||||
NoticeTone,
|
||||
{ box: string; icon: string; Icon: LucideIcon }
|
||||
> = {
|
||||
neutral: { box: "bg-subtle", icon: "text-fg-2", Icon: Info },
|
||||
warning: {
|
||||
box: "bg-warning-soft",
|
||||
icon: "text-warning-text",
|
||||
Icon: AlertTriangle,
|
||||
},
|
||||
danger: {
|
||||
box: "bg-danger-soft",
|
||||
icon: "text-danger-text",
|
||||
Icon: CircleAlert,
|
||||
},
|
||||
route: { box: "bg-route-soft", icon: "text-route-text", Icon: Route },
|
||||
stack: { box: "bg-stack-soft", icon: "text-stack-text", Icon: Layers },
|
||||
direct: { box: "bg-direct-soft", icon: "text-direct-text", Icon: Info },
|
||||
};
|
||||
|
||||
export interface NoticeProps {
|
||||
tone?: NoticeTone;
|
||||
icon?: LucideIcon;
|
||||
/** 第一行,13px */
|
||||
title: React.ReactNode;
|
||||
/** 第二行,12px --text-2 */
|
||||
children?: React.ReactNode;
|
||||
/** 右侧按钮(用 Button size="compact") */
|
||||
actions?: React.ReactNode;
|
||||
/** 可关闭时传;关闭按钮的无障碍名字 */
|
||||
onDismiss?: () => void;
|
||||
dismissLabel?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Notice({
|
||||
tone = "neutral",
|
||||
icon,
|
||||
title,
|
||||
children,
|
||||
actions,
|
||||
onDismiss,
|
||||
dismissLabel,
|
||||
className,
|
||||
}: NoticeProps) {
|
||||
const style = TONE[tone];
|
||||
const Icon = icon ?? style.Icon;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-start gap-2.5 rounded-panel py-2.5 pe-3.5 ps-[19px] text-body text-fg-1",
|
||||
style.box,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
aria-hidden="true"
|
||||
strokeWidth={1.5}
|
||||
className={cn("mt-0.5 h-4 w-4 shrink-0", style.icon)}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>{title}</div>
|
||||
{children ? (
|
||||
<div className="mt-0.5 text-caption text-fg-2">{children}</div>
|
||||
) : null}
|
||||
</div>
|
||||
{actions ? (
|
||||
<div className="flex shrink-0 items-center gap-2 self-center">
|
||||
{actions}
|
||||
</div>
|
||||
) : null}
|
||||
{onDismiss ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={dismissLabel}
|
||||
onClick={onDismiss}
|
||||
className="-me-1 inline-flex h-6 w-6 shrink-0 items-center justify-center self-center rounded-control text-fg-2 transition-colors hover:bg-black/5 hover:text-fg-1 dark:hover:bg-white/10"
|
||||
>
|
||||
<X className="h-4 w-4" strokeWidth={1.5} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知槽:一直渲染的 role="status" 容器(R4),操作后出现的提示放进来才会被读出。
|
||||
* 页面一打开就在的提示不要用 role="alert"。
|
||||
*/
|
||||
export function NoticeSlot({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
className={cn("flex flex-col gap-2 empty:hidden", className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* 分段控件(v7 AUTHORING「分段控件」+ R1):按钮组 + aria-pressed,不是 tablist。
|
||||
* 轨道 --bg-subtle,内衬 3px;选中项 --bg-card + 小阴影。内圆角 = 外圆角 − 内衬。
|
||||
*/
|
||||
export interface SegmentedItem<T extends string> {
|
||||
value: T;
|
||||
label: React.ReactNode;
|
||||
icon?: React.ComponentType<{ className?: string; strokeWidth?: number }>;
|
||||
/** 跟在文字后面的附加内容(例如模式 tab 的「生效中」圆点) */
|
||||
trailing?: React.ReactNode;
|
||||
/** 选中项以外的附加样式(例如生效但没在看的模式格用模式 soft 底) */
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface SegmentedControlProps<T extends string> {
|
||||
items: SegmentedItem<T>[];
|
||||
value: T;
|
||||
onValueChange: (value: T) => void;
|
||||
"aria-label": string;
|
||||
/** 36:模式 tab、页头下的子页面切换;28:面板里的小切换 */
|
||||
size?: "md" | "sm";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SegmentedControl<T extends string>({
|
||||
items,
|
||||
value,
|
||||
onValueChange,
|
||||
size = "md",
|
||||
className,
|
||||
...rest
|
||||
}: SegmentedControlProps<T>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-label={rest["aria-label"]}
|
||||
className={cn(
|
||||
"inline-flex items-stretch bg-subtle p-[3px]",
|
||||
size === "md" ? "h-9 rounded-[10px]" : "h-7 rounded-[8px]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{items.map((item) => {
|
||||
const pressed = item.value === value;
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
aria-pressed={pressed}
|
||||
disabled={item.disabled}
|
||||
onClick={() => onValueChange(item.value)}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center gap-1.5 whitespace-nowrap font-medium text-fg-2 transition-[background-color,color,box-shadow] duration-150 hover:text-fg-1 disabled:cursor-not-allowed disabled:opacity-45",
|
||||
size === "md"
|
||||
? "rounded-[7px] px-3.5 text-body"
|
||||
: "rounded-[5px] px-2.5 text-caption",
|
||||
item.className,
|
||||
pressed && "bg-surface text-fg-1 shadow-v7-sm",
|
||||
)}
|
||||
>
|
||||
{Icon ? (
|
||||
<Icon
|
||||
className={size === "md" ? "h-4 w-4" : "h-3.5 w-3.5"}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
) : null}
|
||||
{item.label}
|
||||
{item.trailing}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* 右侧抽屉(v7:圆角 14、大阴影,宽度按内容 420 / 560)。基于 Radix Dialog:
|
||||
* Escape 关闭、打开时背景不可操作、关闭后焦点回到触发按钮。
|
||||
*/
|
||||
const Sheet = DialogPrimitive.Root;
|
||||
const SheetTrigger = DialogPrimitive.Trigger;
|
||||
const SheetClose = DialogPrimitive.Close;
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
||||
width?: number;
|
||||
/** 右上角关闭按钮的无障碍名字 */
|
||||
closeLabel: string;
|
||||
}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ className, children, width = 420, closeLabel, style, ...props }, ref) => (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
style={{ width, ...style }}
|
||||
className={cn(
|
||||
"fixed inset-y-2 end-2 z-50 flex max-w-[calc(100vw-16px)] flex-col overflow-hidden rounded-dialog border border-border bg-surface text-fg-1 shadow-v7-lg outline-none duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-right-4 data-[state=open]:slide-in-from-right-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close
|
||||
aria-label={closeLabel}
|
||||
className="absolute end-3 top-3 inline-flex h-7 w-7 items-center justify-center rounded-control text-fg-2 transition-colors hover:bg-subtle hover:text-fg-1"
|
||||
>
|
||||
<X className="h-4 w-4" strokeWidth={1.5} />
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
));
|
||||
SheetContent.displayName = "SheetContent";
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("shrink-0 space-y-1 pe-12 ps-5 pt-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const SheetBody = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"min-h-0 flex-1 overflow-y-auto overscroll-contain px-5 py-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex shrink-0 items-center justify-end gap-2 border-t border-border px-5 py-3",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-section", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = "SheetTitle";
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-caption text-fg-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = "SheetDescription";
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetBody,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
@@ -447,10 +447,10 @@
|
||||
},
|
||||
"language": "Language",
|
||||
"languageHint": "Preview interface language immediately after switching, takes effect permanently after saving.",
|
||||
"theme": "Theme",
|
||||
"theme": "Light & dark",
|
||||
"themeHint": "Choose the appearance theme for the app, takes effect immediately.",
|
||||
"themeLight": "Light",
|
||||
"themeDark": "Dark",
|
||||
"themeLight": "Light Side of the Moon",
|
||||
"themeDark": "Dark Side of the Moon",
|
||||
"themeSystem": "System",
|
||||
"importExport": "SQL Import/Export",
|
||||
"importExportHint": "Import or export database SQL backups for migration or restore (import supports only backups exported by CC Switch).",
|
||||
|
||||
@@ -447,11 +447,11 @@
|
||||
},
|
||||
"language": "言語",
|
||||
"languageHint": "切り替えるとすぐにプレビューされ、保存後に永続化されます。",
|
||||
"theme": "テーマ",
|
||||
"theme": "明暗",
|
||||
"themeHint": "アプリのテーマを選択します。すぐに反映されます。",
|
||||
"themeLight": "ライト",
|
||||
"themeDark": "ダーク",
|
||||
"themeSystem": "システム",
|
||||
"themeLight": "月の表側",
|
||||
"themeDark": "月の裏側",
|
||||
"themeSystem": "システムに合わせる",
|
||||
"importExport": "SQL インポート/エクスポート",
|
||||
"importExportHint": "移行や復元用にデータベースの SQL バックアップをインポート/エクスポートします(インポートは CC Switch がエクスポートしたバックアップのみ対応)。",
|
||||
"exportConfig": "SQL バックアップをエクスポート",
|
||||
|
||||
@@ -447,10 +447,10 @@
|
||||
},
|
||||
"language": "介面語言",
|
||||
"languageHint": "切換後立即預覽介面語言,儲存後永久生效。",
|
||||
"theme": "外觀主題",
|
||||
"theme": "明暗",
|
||||
"themeHint": "選擇應用程式的外觀主題,立即生效。",
|
||||
"themeLight": "淺色",
|
||||
"themeDark": "深色",
|
||||
"themeLight": "月之亮面",
|
||||
"themeDark": "月之暗面",
|
||||
"themeSystem": "跟隨系統",
|
||||
"importExport": "SQL 匯入匯出",
|
||||
"importExportHint": "匯入/匯出資料庫 SQL 備份(僅支援匯入由 CC Switch 匯出的備份),便於備份或遷移。",
|
||||
|
||||
@@ -447,10 +447,10 @@
|
||||
},
|
||||
"language": "界面语言",
|
||||
"languageHint": "切换后立即预览界面语言,保存后永久生效。",
|
||||
"theme": "外观主题",
|
||||
"theme": "明暗",
|
||||
"themeHint": "选择应用的外观主题,立即生效。",
|
||||
"themeLight": "浅色",
|
||||
"themeDark": "深色",
|
||||
"themeLight": "月之亮面",
|
||||
"themeDark": "月之暗面",
|
||||
"themeSystem": "跟随系统",
|
||||
"importExport": "SQL 导入导出",
|
||||
"importExportHint": "导入/导出数据库 SQL 备份(仅支持导入由 CC Switch 导出的备份),便于备份或迁移。",
|
||||
|
||||
+153
-28
@@ -3,61 +3,185 @@
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
/* shadcn 组件用的 HSL token(可以带 /50 这类透明度),取值对齐 v7 设计稿的 tokens.css */
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--foreground: 240 5.9% 10%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--card-foreground: 240 5.9% 10%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--popover-foreground: 240 5.9% 10%;
|
||||
|
||||
--primary: 210 100% 56%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary: 240 10.3% 94.3%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--muted: 240 10.3% 94.3%;
|
||||
--muted-foreground: 240 4.3% 41.4%;
|
||||
--accent: 240 10.3% 94.3%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 210 100% 56%;
|
||||
--border: 240 10.2% 90.4%;
|
||||
--input: 240 10.2% 90.4%;
|
||||
--ring: 240 3.8% 46.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 5% 12%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 5% 16%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 5% 16%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--background: 240 3.4% 11.4%;
|
||||
--foreground: 240 4.8% 95.9%;
|
||||
--card: 240 5.2% 19%;
|
||||
--card-foreground: 240 4.8% 95.9%;
|
||||
--popover: 240 5.2% 19%;
|
||||
--popover-foreground: 240 4.8% 95.9%;
|
||||
|
||||
--primary: 210 100% 54%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
|
||||
--secondary: 240 5% 18%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--secondary: 240 5% 15.7%;
|
||||
--secondary-foreground: 240 4.8% 95.9%;
|
||||
|
||||
--muted: 240 5% 18%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 5% 18%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--muted: 240 5% 15.7%;
|
||||
--muted-foreground: 240 5.7% 72.5%;
|
||||
--accent: 240 5% 15.7%;
|
||||
--accent-foreground: 240 4.8% 95.9%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 5% 24%;
|
||||
--input: 240 5% 24%;
|
||||
--ring: 210 100% 54%;
|
||||
--border: 240 5.3% 25.7%;
|
||||
--input: 240 5.3% 25.7%;
|
||||
--ring: 240 5% 64.9%;
|
||||
}
|
||||
|
||||
/* v7 设计 token(docs/research/2026-09-30-ui-design-v7/project/tokens.css)。
|
||||
--border / --ring 沿用上面的 HSL 版本,其余照设计稿的名字。
|
||||
--action-* 是实心按钮、开关的开、勾选框的操作色;以后做主题色只覆盖这一组和 --ring。 */
|
||||
:root {
|
||||
--bg-app: #ffffff;
|
||||
--bg-sidebar: #f5f5f7;
|
||||
--bg-subtle: #efeff2;
|
||||
--bg-selected: #e4e4ea;
|
||||
--bg-card: #ffffff;
|
||||
--border-strong: #cfcfd6;
|
||||
--text-1: #18181b;
|
||||
--text-2: #52525b;
|
||||
--text-3: #65656e;
|
||||
--action-bg: #18181b;
|
||||
--action-fg: #ffffff;
|
||||
--action-hover: #2e2e33;
|
||||
--inverse-bg: #18181b;
|
||||
--inverse-fg: #ffffff;
|
||||
--inverse-hover: #2e2e33;
|
||||
--overlay: rgba(9, 9, 11, 0.36);
|
||||
--control-off: #888890;
|
||||
|
||||
--direct-fill: #0a84ff;
|
||||
--direct-text: #0063c7;
|
||||
--direct-soft: #eaf3ff;
|
||||
--direct-solid: #0066cc;
|
||||
--direct-on: #ffffff;
|
||||
|
||||
--route-fill: #0d9488;
|
||||
--route-text: #06726b;
|
||||
--route-soft: #e7f6f4;
|
||||
--route-solid: #0f766e;
|
||||
--route-on: #ffffff;
|
||||
|
||||
--stack-fill: #7c3aed;
|
||||
--stack-text: #6d28d9;
|
||||
--stack-soft: #f2ecfe;
|
||||
--stack-solid: #6d28d9;
|
||||
--stack-on: #ffffff;
|
||||
|
||||
--success: #16a34a;
|
||||
--success-text: #007634;
|
||||
--success-soft: #e8f7ee;
|
||||
--warning: #d97706;
|
||||
--warning-text: #a64a00;
|
||||
--warning-soft: #fef3c7;
|
||||
--danger: #dc2626;
|
||||
--danger-text: #b91c1c;
|
||||
--danger-soft: #fee2e2;
|
||||
--chart-1: #0891b2;
|
||||
--chart-2: #db2777;
|
||||
--chart-grid: #ececf0;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
|
||||
--shadow-md:
|
||||
0 4px 12px rgba(16, 24, 40, 0.08), 0 1px 3px rgba(16, 24, 40, 0.06);
|
||||
--shadow-lg:
|
||||
0 20px 40px rgba(16, 24, 40, 0.16), 0 4px 12px rgba(16, 24, 40, 0.08);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--bg-app: #1c1c1e;
|
||||
--bg-sidebar: #161618;
|
||||
--bg-subtle: #26262a;
|
||||
--bg-selected: #313136;
|
||||
--bg-card: #2e2e33;
|
||||
--border-strong: #4a4a52;
|
||||
--text-1: #f4f4f5;
|
||||
--text-2: #d0d0d8;
|
||||
--text-3: #b5b5bd;
|
||||
--action-bg: #f4f4f5;
|
||||
--action-fg: #18181b;
|
||||
--action-hover: #e4e4e7;
|
||||
--inverse-bg: #f4f4f5;
|
||||
--inverse-fg: #18181b;
|
||||
--inverse-hover: #e4e4e7;
|
||||
--overlay: rgba(0, 0, 0, 0.55);
|
||||
--control-off: #6f6f77;
|
||||
|
||||
--direct-fill: #409cff;
|
||||
--direct-text: #7cbbff;
|
||||
--direct-soft: #102a45;
|
||||
--direct-solid: #0f77d7;
|
||||
--direct-on: #ffffff;
|
||||
|
||||
--route-fill: #2dd4bf;
|
||||
--route-text: #5eead4;
|
||||
--route-soft: #0c302c;
|
||||
--route-solid: #2dd4bf;
|
||||
--route-on: #062522;
|
||||
|
||||
--stack-fill: #a78bfa;
|
||||
--stack-text: #c4b5fd;
|
||||
--stack-soft: #271e45;
|
||||
--stack-solid: #845be5;
|
||||
--stack-on: #ffffff;
|
||||
|
||||
--success: #4ade80;
|
||||
--success-text: #4ade80;
|
||||
--success-soft: #0f2b1a;
|
||||
--warning: #fbbf24;
|
||||
--warning-text: #fcd34d;
|
||||
--warning-soft: #33260a;
|
||||
--danger: #f87171;
|
||||
--danger-text: #fca5a5;
|
||||
--danger-soft: #3a1414;
|
||||
--chart-1: #38bdf8;
|
||||
--chart-2: #f472b6;
|
||||
--chart-grid: #2c2c31;
|
||||
|
||||
--shadow-sm:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.08), 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45), 0 1px 3px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 20px 40px rgba(0, 0, 0, 0.5), 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* 切换明暗期间暂停所有过渡(theme-provider 加上 theme-switching,两帧后移除) */
|
||||
.theme-switching *,
|
||||
.theme-switching *::before,
|
||||
.theme-switching *::after {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.glass {
|
||||
@@ -231,15 +355,16 @@ html[data-window-active="false"] .status-heartbeat {
|
||||
border: 2px solid transparent;
|
||||
border-radius: 9999px;
|
||||
background-clip: padding-box;
|
||||
background-color: hsl(var(--muted-foreground) / 0.35);
|
||||
background-color: var(--border-strong);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: hsl(var(--muted-foreground) / 0.6);
|
||||
background-color: var(--text-3);
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
@apply outline-2 outline-blue-500 outline-offset-2;
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
||||
+22
-1
@@ -1,5 +1,26 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { extendTailwindMerge } from "tailwind-merge";
|
||||
|
||||
// 告诉 tailwind-merge 哪些是 tailwind.config.cjs 里新加的字号 / 圆角 / 阴影:
|
||||
// 否则 text-body 会被当成颜色,和 text-fg-1 互相覆盖。
|
||||
const twMerge = extendTailwindMerge({
|
||||
extend: {
|
||||
theme: {
|
||||
text: [
|
||||
"badge",
|
||||
"caption",
|
||||
"body",
|
||||
"strong",
|
||||
"section",
|
||||
"title",
|
||||
"page",
|
||||
"metric",
|
||||
],
|
||||
radius: ["control", "panel", "dialog"],
|
||||
shadow: ["v7-sm", "v7-md", "v7-lg"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
|
||||
+5
-1
@@ -8,6 +8,7 @@ import "./index.css";
|
||||
import i18n from "./i18n";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { MotionConfig } from "framer-motion";
|
||||
import { queryClient } from "@/lib/query";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
@@ -123,7 +124,10 @@ async function bootstrap() {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider defaultTheme="system" storageKey="cc-switch-theme">
|
||||
<UpdateProvider>
|
||||
<App />
|
||||
{/* 系统开了「减少动态效果」时,framer-motion 的位移动画一律跳过 */}
|
||||
<MotionConfig reducedMotion="user">
|
||||
<App />
|
||||
</MotionConfig>
|
||||
<Toaster />
|
||||
</UpdateProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -38,6 +38,71 @@ module.exports = {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
// v7 设计 token(见 src/index.css)。纯 var() 颜色不支持 /50 这类透明度修饰。
|
||||
app: "var(--bg-app)",
|
||||
sidebar: "var(--bg-sidebar)",
|
||||
subtle: "var(--bg-subtle)",
|
||||
selected: "var(--bg-selected)",
|
||||
surface: "var(--bg-card)",
|
||||
"border-strong": "var(--border-strong)",
|
||||
fg: {
|
||||
1: "var(--text-1)",
|
||||
2: "var(--text-2)",
|
||||
3: "var(--text-3)",
|
||||
},
|
||||
action: {
|
||||
DEFAULT: "var(--action-bg)",
|
||||
fg: "var(--action-fg)",
|
||||
hover: "var(--action-hover)",
|
||||
},
|
||||
inverse: {
|
||||
DEFAULT: "var(--inverse-bg)",
|
||||
fg: "var(--inverse-fg)",
|
||||
hover: "var(--inverse-hover)",
|
||||
},
|
||||
overlay: "var(--overlay)",
|
||||
"control-off": "var(--control-off)",
|
||||
direct: {
|
||||
DEFAULT: "var(--direct-fill)",
|
||||
text: "var(--direct-text)",
|
||||
soft: "var(--direct-soft)",
|
||||
solid: "var(--direct-solid)",
|
||||
on: "var(--direct-on)",
|
||||
},
|
||||
route: {
|
||||
DEFAULT: "var(--route-fill)",
|
||||
text: "var(--route-text)",
|
||||
soft: "var(--route-soft)",
|
||||
solid: "var(--route-solid)",
|
||||
on: "var(--route-on)",
|
||||
},
|
||||
stack: {
|
||||
DEFAULT: "var(--stack-fill)",
|
||||
text: "var(--stack-text)",
|
||||
soft: "var(--stack-soft)",
|
||||
solid: "var(--stack-solid)",
|
||||
on: "var(--stack-on)",
|
||||
},
|
||||
success: {
|
||||
DEFAULT: "var(--success)",
|
||||
text: "var(--success-text)",
|
||||
soft: "var(--success-soft)",
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: "var(--warning)",
|
||||
text: "var(--warning-text)",
|
||||
soft: "var(--warning-soft)",
|
||||
},
|
||||
danger: {
|
||||
DEFAULT: "var(--danger)",
|
||||
text: "var(--danger-text)",
|
||||
soft: "var(--danger-soft)",
|
||||
},
|
||||
chart: {
|
||||
1: "var(--chart-1)",
|
||||
2: "var(--chart-2)",
|
||||
grid: "var(--chart-grid)",
|
||||
},
|
||||
blue: {
|
||||
400: "#409CFF",
|
||||
500: "#0A84FF",
|
||||
@@ -73,22 +138,48 @@ module.exports = {
|
||||
sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
||||
md: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
|
||||
lg: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
|
||||
// v7:分段控件凸起、卡片悬停 / 菜单、弹层 / 对话框、抽屉
|
||||
"v7-sm": "var(--shadow-sm)",
|
||||
"v7-md": "var(--shadow-md)",
|
||||
"v7-lg": "var(--shadow-lg)",
|
||||
},
|
||||
borderRadius: {
|
||||
sm: "0.375rem",
|
||||
md: "0.5rem",
|
||||
lg: "0.75rem",
|
||||
xl: "0.875rem",
|
||||
// v7:按钮、小控件、侧栏行 / 卡片、分段控件、输入框、弹层、通知条 / 对话框、抽屉
|
||||
control: "6px",
|
||||
panel: "10px",
|
||||
dialog: "14px",
|
||||
},
|
||||
fontSize: {
|
||||
// v7 字体角色(AUTHORING.md「颜色与字体」)
|
||||
badge: ["11px", { lineHeight: "16px", fontWeight: "500" }],
|
||||
caption: ["12px", { lineHeight: "18px" }],
|
||||
body: ["13px", { lineHeight: "20px" }],
|
||||
strong: ["14px", { lineHeight: "20px", fontWeight: "500" }],
|
||||
section: ["15px", { lineHeight: "22px", fontWeight: "600" }],
|
||||
title: ["16px", { lineHeight: "24px", fontWeight: "600" }],
|
||||
page: ["18px", { lineHeight: "26px", fontWeight: "600" }],
|
||||
metric: ["24px", { lineHeight: "32px", fontWeight: "600" }],
|
||||
},
|
||||
fontFamily: {
|
||||
// 使用与之前版本保持一致的系统字体栈
|
||||
sans: [
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
'"SF Pro Text"',
|
||||
'"PingFang SC"',
|
||||
'"Hiragino Sans GB"',
|
||||
'"Segoe UI Variable Text"',
|
||||
'"Segoe UI"',
|
||||
'"Microsoft YaHei UI"',
|
||||
'"Microsoft YaHei"',
|
||||
"Roboto",
|
||||
'"Helvetica Neue"',
|
||||
"Arial",
|
||||
"system-ui",
|
||||
"sans-serif",
|
||||
],
|
||||
mono: [
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
describe("cn", () => {
|
||||
it("keeps a v7 font size next to a v7 text color", () => {
|
||||
expect(cn("text-body", "text-fg-1")).toBe("text-body text-fg-1");
|
||||
});
|
||||
|
||||
it("lets a v7 font size replace a default one", () => {
|
||||
expect(cn("text-sm", "text-body")).toBe("text-body");
|
||||
});
|
||||
|
||||
it("treats v7 radius and shadow names as their own groups", () => {
|
||||
expect(cn("rounded-lg", "rounded-control")).toBe("rounded-control");
|
||||
expect(cn("shadow-sm", "shadow-v7-md")).toBe("shadow-v7-md");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user