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 ( +