feat(ui): P3 页面级应用(作品库卡片/空态/加载/设置/次级页)
- 作品库卡片重设计:书脊封面字块 + 衬线书名 + 一句话故事(缺失给占位) + 元信息行(题材徽章/StatusDot 待审/mono 时间),Card tone=card 浮起消塌陷 - EmptyState 中性化 + inline/card/bare 变体 + eyebrow(向后兼容) - loading 骨架化(Skeleton 拼作品库结构,motion-safe) - 设置页拆分 ProvidersSettings 489→198 + 5 个子组件,凭据点用 StatusDot - 次级页(向导/模板/codex/大纲)排版字阶与圆角 token 收敛
This commit is contained in:
@@ -1,38 +1,117 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Eyebrow } from "@/components/ui/Eyebrow";
|
||||
import { cn } from "@/lib/ui/variants";
|
||||
|
||||
// 空态外框形态:
|
||||
// - "inline"(默认):虚线中性框,用于列表/面板内联占位(向后兼容旧默认外观)。
|
||||
// - "card":包裹在 Card(tone=soft) 内,用于独立区块的引导卡片。
|
||||
// - "bare":无外框,仅内容,交由调用方自行放置(如已在卡片里)。
|
||||
type EmptyStateVariant = "inline" | "card" | "bare";
|
||||
type EmptyStateSize = "sm" | "md";
|
||||
|
||||
interface EmptyStateProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
action?: ReactNode;
|
||||
/** 可选眉题,置于标题上方,用于分类/上下文提示。 */
|
||||
eyebrow?: string;
|
||||
/** 外框形态,默认 "inline"。 */
|
||||
variant?: EmptyStateVariant;
|
||||
/** 留白密度,默认 "md"。 */
|
||||
size?: EmptyStateSize;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function EmptyState({
|
||||
const SIZE_PAD: Record<EmptyStateSize, string> = {
|
||||
sm: "px-5 py-8",
|
||||
md: "px-6 py-10",
|
||||
};
|
||||
|
||||
const SIZE_CHIP: Record<EmptyStateSize, string> = {
|
||||
sm: "h-10 w-10",
|
||||
md: "h-11 w-11",
|
||||
};
|
||||
|
||||
// 内容主体:图标片 + 眉题 + 标题 + 描述 + 动作。中性、编辑部化,留白克制。
|
||||
function EmptyStateBody({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
className,
|
||||
}: EmptyStateProps) {
|
||||
eyebrow,
|
||||
size,
|
||||
}: Pick<
|
||||
EmptyStateProps,
|
||||
"icon" | "title" | "description" | "action" | "eyebrow"
|
||||
> & {
|
||||
size: EmptyStateSize;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded border border-dashed border-line bg-panel/70 px-6 py-10 text-center",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded bg-[var(--color-cinnabar-wash)] text-cinnabar">
|
||||
<div className="text-center">
|
||||
<div
|
||||
className={cn(
|
||||
"mx-auto mb-3 flex items-center justify-center rounded-md bg-surface-soft text-muted-soft",
|
||||
SIZE_CHIP[size],
|
||||
)}
|
||||
>
|
||||
<Icon className="h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
<h2 className="font-serif text-lg text-ink">{title}</h2>
|
||||
<p className="mx-auto mt-2 max-w-md text-sm leading-6 text-ink-soft">
|
||||
{eyebrow ? <Eyebrow className="mb-2">{eyebrow}</Eyebrow> : null}
|
||||
<h2 className="font-serif text-title-md text-ink">{title}</h2>
|
||||
<p className="mx-auto mt-2 max-w-sm text-caption leading-6 text-ink-soft">
|
||||
{description}
|
||||
</p>
|
||||
{action ? <div className="mt-4">{action}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EmptyState({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
eyebrow,
|
||||
variant = "inline",
|
||||
size = "md",
|
||||
className,
|
||||
}: EmptyStateProps) {
|
||||
const body = (
|
||||
<EmptyStateBody
|
||||
icon={icon}
|
||||
title={title}
|
||||
description={description}
|
||||
action={action}
|
||||
eyebrow={eyebrow}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
|
||||
if (variant === "bare") {
|
||||
return <div className={cn(SIZE_PAD[size], className)}>{body}</div>;
|
||||
}
|
||||
|
||||
if (variant === "card") {
|
||||
return (
|
||||
<Card tone="soft" flat className={cn(SIZE_PAD[size], className)}>
|
||||
{body}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border border-dashed border-line bg-panel/60",
|
||||
SIZE_PAD[size],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user