From 83ba47ab8f09c4f96a23c151bf784b4b7bdd2561 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Sat, 11 Jul 2026 07:24:08 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E5=85=B1=E4=BA=AB=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E8=A1=A5=E9=BD=90=E2=80=94=E2=80=94Card=20=E9=9D=A2?= =?UTF-8?q?=E8=89=B2=E5=88=86=E5=B1=82=20+=20Button=20link/lg=20+=204=20?= =?UTF-8?q?=E5=8E=9F=E5=AD=90=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Card 新增 tone(panel/card/soft)/flat/interactive;cardClass 兼容旧字符串签名 - Button 新增 link(coral 文字链) 变体与 lg 尺寸 - 新增原子件 Eyebrow(统一眉题) / StatusDot(状态点·读屏可见) / Skeleton(motion-safe 占位) / Meter(进度条·progressbar) - PageHeader/SectionHeader 用 display/title 字阶 + Eyebrow 眉题 --- apps/web/components/ui/Card.tsx | 13 +++++-- apps/web/components/ui/Eyebrow.tsx | 23 ++++++++++++ apps/web/components/ui/Meter.tsx | 45 ++++++++++++++++++++++++ apps/web/components/ui/PageHeader.tsx | 10 +++--- apps/web/components/ui/SectionHeader.tsx | 7 +++- apps/web/components/ui/Skeleton.tsx | 18 ++++++++++ apps/web/components/ui/StatusDot.tsx | 41 +++++++++++++++++++++ apps/web/lib/ui/variants.test.ts | 27 ++++++++++++++ apps/web/lib/ui/variants.ts | 35 +++++++++++++++--- 9 files changed, 206 insertions(+), 13 deletions(-) create mode 100644 apps/web/components/ui/Eyebrow.tsx create mode 100644 apps/web/components/ui/Meter.tsx create mode 100644 apps/web/components/ui/Skeleton.tsx create mode 100644 apps/web/components/ui/StatusDot.tsx diff --git a/apps/web/components/ui/Card.tsx b/apps/web/components/ui/Card.tsx index fac42b5..8a42324 100644 --- a/apps/web/components/ui/Card.tsx +++ b/apps/web/components/ui/Card.tsx @@ -1,20 +1,29 @@ import type { HTMLAttributes, ReactNode } from "react"; -import { cardClass } from "@/lib/ui/variants"; +import { cardClass, type CardTone } from "@/lib/ui/variants"; interface CardProps extends HTMLAttributes { children: ReactNode; as?: "article" | "div" | "section"; + tone?: CardTone; + flat?: boolean; + interactive?: boolean; } export function Card({ as: Component = "div", children, className, + tone, + flat, + interactive, ...props }: CardProps) { return ( - + {children} ); diff --git a/apps/web/components/ui/Eyebrow.tsx b/apps/web/components/ui/Eyebrow.tsx new file mode 100644 index 0000000..d61935b --- /dev/null +++ b/apps/web/components/ui/Eyebrow.tsx @@ -0,0 +1,23 @@ +import type { ReactNode } from "react"; + +import { cn } from "@/lib/ui/variants"; + +interface EyebrowProps { + children: ReactNode; + className?: string; +} + +// 眉题/小标签:无衬线、eyebrow 字号(含正字距 0.08em)、说明色、大写。 +// 统一各页此前 mono/uppercase 互不一致的小标签写法。 +export function Eyebrow({ children, className }: EyebrowProps) { + return ( +

+ {children} +

+ ); +} diff --git a/apps/web/components/ui/Meter.tsx b/apps/web/components/ui/Meter.tsx new file mode 100644 index 0000000..af09301 --- /dev/null +++ b/apps/web/components/ui/Meter.tsx @@ -0,0 +1,45 @@ +import { cn } from "@/lib/ui/variants"; + +export type MeterTone = "accent" | "success" | "warning" | "danger" | "info"; + +const meterTones: Record = { + accent: "bg-cinnabar", + success: "bg-pass", + warning: "bg-overdue", + danger: "bg-conflict", + info: "bg-info", +}; + +interface MeterProps { + /** 0..1,超出范围自动夹取。 */ + value: number; + tone?: MeterTone; + label?: string; + className?: string; +} + +// 进度条:语义色填充,宽度过渡走动效 token;对读屏暴露 progressbar。 +export function Meter({ value, tone = "accent", label, className }: MeterProps) { + const pct = Math.max(0, Math.min(1, value)) * 100; + return ( +
+
+
+ ); +} diff --git a/apps/web/components/ui/PageHeader.tsx b/apps/web/components/ui/PageHeader.tsx index 641fae0..6747ca8 100644 --- a/apps/web/components/ui/PageHeader.tsx +++ b/apps/web/components/ui/PageHeader.tsx @@ -1,5 +1,7 @@ import type { ReactNode } from "react"; +import { Eyebrow } from "./Eyebrow"; + interface PageHeaderProps { title: string; eyebrow?: string; @@ -16,12 +18,8 @@ export function PageHeader({ return (
- {eyebrow ? ( -

- {eyebrow} -

- ) : null} -

{title}

+ {eyebrow ? {eyebrow} : null} +

{title}

{description ? (

{description} diff --git a/apps/web/components/ui/SectionHeader.tsx b/apps/web/components/ui/SectionHeader.tsx index 4bafb40..54c855c 100644 --- a/apps/web/components/ui/SectionHeader.tsx +++ b/apps/web/components/ui/SectionHeader.tsx @@ -2,8 +2,11 @@ import type { ReactNode } from "react"; import { cn } from "@/lib/ui/variants"; +import { Eyebrow } from "./Eyebrow"; + interface SectionHeaderProps { title: string; + eyebrow?: string; description?: ReactNode; action?: ReactNode; className?: string; @@ -11,6 +14,7 @@ interface SectionHeaderProps { export function SectionHeader({ title, + eyebrow, description, action, className, @@ -18,7 +22,8 @@ export function SectionHeader({ return (

-

{title}

+ {eyebrow ? {eyebrow} : null} +

{title}

{description ? (

{description}

) : null} diff --git a/apps/web/components/ui/Skeleton.tsx b/apps/web/components/ui/Skeleton.tsx new file mode 100644 index 0000000..2919bd3 --- /dev/null +++ b/apps/web/components/ui/Skeleton.tsx @@ -0,0 +1,18 @@ +import { cn } from "@/lib/ui/variants"; + +interface SkeletonProps { + className?: string; +} + +// 加载占位:柔和线条底 + motion-safe 脉冲(尊重 prefers-reduced-motion)。 +export function Skeleton({ className }: SkeletonProps) { + return ( +