---
title: 设计系统 · DESIGN.md
description: PAC 前端的设计系统(目标态),按 google-labs design.md 格式编写 —— 三层 token,给工程师与编码 agent 一致的视觉依据。
icon: Palette
---

这是 PAC 前端(Next.js + Tailwind v4 + shadcn/ui + Radix)的**设计系统规范**,按 [google-labs `design.md`](https://github.com/google-labs-code/design.md) 格式写:**token 给精确值,prose 给"为什么 / 怎么用"** —— 双受众,既给工程师,也给以后生成 PAC UI 的编码 agent 当持久依据。

token 分**三层**:原始(L1)→ 语义(L2)→ 组件(L3),层层指向。**这是目标态** —— 当前 `globals.css` 是 shadcn 两层 HSL,本文件的三层 oklch 是 CSS 应收敛到的方向;语义层 CSS 变量名(`--primary` / `--muted` / `--border`…)与现有 shadcn 命名一致,组件无需改。下面 YAML 可整段拷成 `apps/pac-web/DESIGN.md` 交给 `npx @google/design.md lint`(原始层用 oklch,官方 CLI 据此跑 WCAG)。

```yaml
version: alpha
name: PAC Workbench
description: 临床召回客服工作台 —— 嵌进宿主系统的克制仪器 · 设计版本 0.1.0。三层 token(原始/语义/组件);原始层 1:1 采用 Tailwind v4 oklch。
colors:
  # ============================================================
  # L1 · 原始层 PRIMITIVE — 纯色阶,无语义。值 = Tailwind v4 oklch(1:1)
  # ============================================================
  # 品牌 teal
  teal-50:  "oklch(98.4% 0.014 180.72)"
  teal-500: "oklch(70.4% 0.14 182.503)"
  teal-600: "oklch(60% 0.118 184.704)"
  teal-700: "oklch(51.1% 0.096 186.391)"
  # 中性 slate(承载约 90% 界面)
  slate-50:  "oklch(98.4% 0.003 247.858)"
  slate-100: "oklch(96.8% 0.007 247.896)"
  slate-200: "oklch(92.9% 0.013 255.508)"
  slate-300: "oklch(86.9% 0.022 252.894)"
  slate-400: "oklch(70.4% 0.04 256.788)"
  slate-500: "oklch(55.4% 0.046 257.417)"
  slate-600: "oklch(44.6% 0.043 257.281)"
  slate-700: "oklch(37.2% 0.044 257.287)"
  slate-800: "oklch(27.9% 0.041 260.031)"
  slate-900: "oklch(20.8% 0.042 265.755)"
  slate-950: "oklch(12.9% 0.042 264.695)"
  # AI 强调 indigo
  indigo-50:  "oklch(96.2% 0.018 272.314)"
  indigo-500: "oklch(58.5% 0.233 277.117)"
  # 语义原料(仅纳入用到的档)
  emerald-50:  "oklch(97.9% 0.021 166.113)"
  emerald-600: "oklch(59.6% 0.145 163.225)"
  emerald-700: "oklch(50.8% 0.118 165.612)"
  amber-50:  "oklch(98.7% 0.022 95.277)"
  amber-600: "oklch(66.6% 0.179 58.318)"
  amber-700: "oklch(55.5% 0.163 48.998)"
  rose-50:  "oklch(96.9% 0.015 12.422)"
  rose-600: "oklch(58.6% 0.253 17.585)"
  rose-700: "oklch(51.4% 0.222 16.935)"
  sky-50:  "oklch(97.7% 0.013 236.62)"
  sky-600: "oklch(58.8% 0.158 241.966)"
  violet-500: "oklch(60.6% 0.25 292.717)"
  white: "#FFFFFF"

  # ============================================================
  # L2 · 语义层 SEMANTIC — 角色映射,引用原始层。组件唯一入口
  # ============================================================
  primary: "{colors.teal-600}"           # 唯一交互驱动色
  primary-hover: "{colors.teal-700}"
  primary-subtle: "{colors.teal-50}"     # 标签底 / 高亮区
  on-primary: "{colors.white}"
  ring: "{colors.teal-600}"              # focus ring 同 primary
  accent-ai: "{colors.indigo-500}"       # 只标 AI 产物 / 画像
  accent-ai-subtle: "{colors.indigo-50}"
  # 中性骨架(与宿主同频)
  background: "{colors.white}"           # 页面底(border-led,不用灰底)
  surface: "{colors.white}"              # 卡片 / 表格面
  surface-muted: "{colors.slate-100}"    # 表头 / 斑马纹 / 二级数据条
  on-surface: "{colors.slate-900}"       # 正文
  on-surface-muted: "{colors.slate-500}" # 次要文字 / 元数据(对白底 4.76,别再调浅)
  border: "{colors.slate-300}"           # 故意比常规深一档(border-led,~87% L)
  border-strong: "{colors.slate-400}"    # 输入框 / 需更明确分隔处
  # 语义状态(只在状态反馈时出现)
  success: "{colors.emerald-600}"
  warning: "{colors.amber-600}"
  danger: "{colors.rose-600}"
  on-danger: "{colors.white}"
  info: "{colors.sky-600}"
  # 召回生命周期(plan/recall 状态机的视觉投影)— 各配 -surface 浅底做徽章
  status-progress: "{colors.sky-600}"            # active / 已联系
  status-progress-surface: "{colors.sky-50}"
  status-pending: "{colors.amber-600}"           # 待跟进 / 约定回访
  status-pending-surface: "{colors.amber-50}"
  status-won: "{colors.emerald-600}"             # completed / 成单
  status-won-surface: "{colors.emerald-50}"
  status-closed: "{colors.slate-400}"            # abandoned / 流失
  status-closed-surface: "{colors.slate-100}"
  # 渠道色
  channel-phone: "{colors.sky-600}"
  channel-wecom: "{colors.emerald-600}"
  channel-sms: "{colors.violet-500}"
  channel-other: "{colors.slate-500}"
typography:
  # sans = 系统中文栈(不引外部 webfont);mono = 系统等宽栈
  h1:
    fontFamily: "PingFang SC, Noto Sans CJK SC, ui-sans-serif, system-ui, -apple-system, sans-serif"
    fontSize: 1.5rem
    fontWeight: 600
    lineHeight: 1.2
    letterSpacing: -0.01em
  h2:
    fontFamily: "PingFang SC, Noto Sans CJK SC, ui-sans-serif, system-ui, -apple-system, sans-serif"
    fontSize: 1.25rem
    fontWeight: 600
    lineHeight: 1.3
  body-md:
    fontFamily: "PingFang SC, Noto Sans CJK SC, ui-sans-serif, system-ui, -apple-system, sans-serif"
    fontSize: 1rem
    fontWeight: 400
    lineHeight: 1.6
  label:
    fontFamily: "PingFang SC, Noto Sans CJK SC, ui-sans-serif, system-ui, -apple-system, sans-serif"
    fontSize: 0.875rem
    fontWeight: 500
    lineHeight: 1.4
  label-caps:                            # 技术数据 / ID / 时间戳 —— 系统等宽
    fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
    fontSize: 0.75rem
    fontWeight: 500
    lineHeight: 1
    letterSpacing: 0.04em
  data-num:                              # 表格等宽数字,纵向对齐不跳动
    fontFamily: "PingFang SC, ui-sans-serif, system-ui, sans-serif"
    fontSize: 0.875rem
    fontWeight: 400
    lineHeight: 1.4
    fontFeature: "tnum"
rounded:
  sm: 4px
  md: 6px
  lg: 8px                                # --radius base
  full: 9999px                           # chip / 徽章 / 头像
spacing:
  xs: 4px
  sm: 8px
  md: 12px                               # 列表 gap-3
  lg: 16px                               # 卡片内边距 mobile p-4
  xl: 20px                               # 卡片内边距 desktop p-5
  "2xl": 24px
  touch: 44px                            # 最小触控目标
components:
  # ============================================================
  # L3 · 组件层 COMPONENT — 引用语义层。框架无关规格;
  # Web 端实现以 shadcn/ui + Radix 为准(变体消费语义 token,如 bg-primary)
  # ============================================================
  button-primary:
    backgroundColor: "{colors.primary}"
    textColor: "{colors.on-primary}"
    rounded: "{rounded.md}"
    padding: "8px 16px"
  button-primary-hover:
    backgroundColor: "{colors.primary-hover}"
  button-secondary:
    backgroundColor: "{colors.surface}"
    textColor: "{colors.on-surface}"
    rounded: "{rounded.md}"
    padding: "8px 16px"
  button-danger:
    backgroundColor: "{colors.danger}"
    textColor: "{colors.on-danger}"
    rounded: "{rounded.md}"
    padding: "8px 16px"
  input:
    backgroundColor: "{colors.surface}"
    textColor: "{colors.on-surface}"
    rounded: "{rounded.md}"
    padding: "8px 12px"
  card:
    backgroundColor: "{colors.surface}"
    rounded: "{rounded.lg}"
    padding: "{spacing.lg}"
  reason-chip:
    backgroundColor: "{colors.primary-subtle}"
    textColor: "{colors.primary}"
    rounded: "{rounded.full}"
    typography: "{typography.label}"
    padding: "4px 10px"
  badge-won:
    backgroundColor: "{colors.status-won-surface}"
    textColor: "{colors.status-won}"
    rounded: "{rounded.full}"
    padding: "4px 10px"
  badge-pending:
    backgroundColor: "{colors.status-pending-surface}"
    textColor: "{colors.status-pending}"
    rounded: "{rounded.full}"
    padding: "4px 10px"
```

## Overview

> 一句话:**把 Anthropic / Linear 那种控制台的克制,搬进诊室。**

PAC 工作台读起来像一台**嵌进宿主系统的临床仪器**:哑光、近乎无投影,靠**实线边框**而非阴影来分区;一块克制的 **teal** 负责所有"该点这里",其余颜色只在表达状态时出现;数字像电子病历一样**等宽对齐**;它礼貌地嵌在宿主页面里,不抢宿主风头,但一眼能认出是 PAC。

它服务的人是**边打电话边看边记的客服**:信息密度高、单手可达、主操作(CTA)显眼,**移动优先**。它的反面 —— 我们不做的 —— 是花哨的营销落地页、彩虹色仪表盘、卡片堆叠重投影的拟物风。

> **铁律**:单一 **teal** 驱动全部交互,中性 **slate** 承载界面主体,**indigo** 只标 AI 产物,语义色只在状态反馈时出现。**颜色稀缺,所以颜色有意义。**

## Colors

调色板 = 高对比中性 + 单一品牌色 + 语义反馈,经**三层** token 暴露给组件。色值用 oklch(与 Tailwind v4 同源,可上广色域)。

**层与纪律**:原始层(`teal-*` / `slate-*`,纯色阶)→ 语义层(`primary` / `surface` / `status-*`,角色)→ 组件层。**组件只准引语义层** —— 代码里不写裸色值、不直引原始层(不写 `bg-teal-600`,写 `bg-primary`)。状态语义变了只改语义 token,组件不动。

- **`primary`(→ teal-600):** 唯一交互驱动色。每屏只用于**单个最重要的操作** + 选中 / 链接 / 焦点环。`primary-subtle`(teal-50)做标签底。
- **中性(→ slate):** 承载约 90% 界面。`background` 白底页面(border-led,不铺灰底),`surface` 卡片面,`surface-muted` 表头 / 斑马纹,`border` 比常规深一档(`slate-300`)—— 因为我们靠边框而非阴影建立层次。
- **`accent-ai`(→ indigo-500):** 第二根色轴,**只标 AI 产物与患者画像**,跟行动色 teal 分开"这是系统给的判断"。
- **语义 / 状态色:** `success`/`warning`/`danger`/`info` 仅状态反馈,不作大面积填充;`status-*`(progress/pending/won/closed)是召回状态机的投影,各配 `-surface` 浅底做徽章。

### 可达性(WCAG)

实算的关键对比度(目标:正文 4.5:1,大文本 / UI 3:1):

| 前景 / 背景 | 比值 | 等级 | 用法 |
|---|---|---|---|
| 白字 / `primary` teal-600 | **3.7:1** | AA-large | ⚠️ teal 偏浅 —— 正文白字改用 `primary-hover`(teal-700,**5.4:1** 过 AA);teal-600 填色只配 ≥大字 / 半粗 |
| 白字 / `danger` rose-600 | 4.7:1 | AA | 可承载正文白字 |
| 白字 / `success` emerald-600 | 3.8:1 | AA-large | 填色白字须放大加粗,或改深字 / 降 -700 |
| 白字 / `warning` amber-600 | 3.2:1 | AA-large | 同上(amber 尤甚,建议直接用深字) |
| 白字 / `info` sky-600 | 4.1:1 | AA-large | 同上 |
| `on-surface` slate-900 / 白 | ~16:1 | AAA | 正文 |
| `on-surface-muted` slate-500 / 白 | 4.76:1 | AA | 次要文字下限,**别再调浅** |

徽章为 600 字 + 50 底,属 AA-large —— 文字须 ≥13px 半粗,使其按"大文本"达标。

## Typography

- **系统字体,不引外部 webfont**:中文 PingFang SC,系统栈兜底。原因有二 —— iframe 约定要求走宿主 system 字体链;且 PAC 部署网络 Google Fonts 不可达(`next/font/google` 会挂)。所以**不学需要 bundle webfont 的方案**。
- **`label-caps` 用系统等宽栈**(`ui-monospace`…)承载技术数据 / ID / 时间戳 —— 不依赖 JetBrains Mono 等 webfont。
- **`data-num` 启用 `tabular-nums`**:金额、时间窗、计数对齐成列,扫读不跳。
- 字号阶梯(移动)**12 / 14 / 16 / 20 / 24**,标题最大 24;单屏字重不超过两种。

## Layout & Spacing

- 基于 **4px 网格**,间距取自 `spacing`(4/8/12/16/20/24)。后台密度优先:行距紧凑,留白用于分隔信息块而非营造氛围。
- 卡片内边距:移动 `p-4`(16)/ 桌面 `p-5`(20);列表项 `gap-3`(12)。
- **触控目标最小 44×44px**,客服单手在手机上操作。默认 mobile,`sm/md/lg` 渐进增强。

## Elevation & Depth

层次靠**边框 + 留白**,不靠阴影 —— 这是 PAC 与"无边框/重阴影"风格的明确分野。阴影整体压到 `0.03–0.06` 透明度(对照 shadcn / Tailwind 默认 `0.05–0.10`),**只用于真正"浮起"的层**:

| token | 用途 |
|---|---|
| `--shadow-sm` `0 1px 2px /0.04` | 卡片默认(几乎不可见)|
| `--shadow-md` | hover 微抬 |
| `--shadow-lg` / `--shadow-xl` | popover / dialog 浮起 |

口诀:**平面上用边框,浮起来才用影。**

## Shapes

| 形 | 值 |
|---|---|
| 圆角 base(卡片 / 面板)| `rounded.lg` 8px |
| 输入 / 按钮 | `rounded.md` 6px |
| chip / 徽章 / 头像 | `rounded.full` |

全局只用一种圆角语言 —— 不在同一视图混用直角与大圆角。不用大圆角(消费级语气),也不用直角(太硬)。

## Components

原子组件 token 见 front matter 的 L3。Web 端实现以 **shadcn/ui + Radix** 为准:变体消费语义 token(`bg-primary` 等),与本文件语义层对齐。PAC 特有的**业务组件**:

| 组件 | 用途 | 优先级 |
|---|---|---|
| `<PlanCard>` | 召回任务列表项 | P0 |
| `<PersonaCard>` | 患者画像快读 | P0 |
| `<ReasonChip>` | scenario 标签 | P0 |
| `<PriorityBar>` | 三段渐变优先级 | P0 |
| `<ChannelIcon>` | 渠道图标 | P0 |
| `<TreatmentChainTimeline>` | 治疗链横向时间轴 ⭐ 差异化 | P0 |
| `<OutcomeDialog>` | 执行回写(强枚举)| P0 |
| `<ScriptViewer>` | Markdown 话术(开场/跟进/异议折叠)| P1 |
| `<EvidenceDrawer>` | factIds → transaction 原文 | P1 |

复用的轻量视觉模式走 Tailwind v4 `@utility`(`globals.css`):`row-quiet`(二级数据条)、`surface-inset`(卡内分组框)、`nums`(等宽数字)。

## Motion

**基线克制**:通用过渡 ease-out **150ms**,状态切换 / 列表插入 pulse **300ms**;禁 bounce / 大幅 scale / 旋转。

**唯一的例外是 AI 场景** —— 这层动效是有意为之的"活气",只绑定特定功能,别处不许用:

- `shimmer-text` —— AI 流式生成时标题扫光(GPT / Claude Code 风)。
- 实时教练:`audioWave`(语音波形)、`coachPulse`(录音呼吸光圈)、`lyricIn`(字幕当前句滑入)。

## 信息密度纪律

> 视觉焦点**只放决策必需**的信息,其余收进抽屉 / 折叠 / 二级页。

| 层级 | 形态 | 内容 |
|---|---|---|
| 焦点 | always-on | 身份 / 优先级 / 召回理由摘要 / CTA |
| 次级 | accordion / tab | reason 证据 / 治疗链时间轴 / AI 产物 |
| 详情 | 抽屉 / 全屏 modal | 完整 transaction timeline / EMR 原文 / 影像 |
| 调试 | 角色门控隐藏 | rawPayload / token / fact version 历史 |

不强制卡片结构,但**强制信息分层** —— 每个页面都要明确"什么进焦点、什么进抽屉"。

## 嵌入约束(iframe)

PAC UI 以 iframe 嵌入多个宿主,所以:

- 容器用 `bg-background` token,**不假设白底**。
- **不用 fixed / absolute 全屏遮罩**(会越出 iframe 边界)。
- **不引外部字体**,走宿主 system 字体链(另见 Typography)。
- 跳转链接 `target="_top"`(deep-link 跳回宿主原页)。

## Do's & Don'ts

**Do**
- 用**边框 + 留白**分区;阴影只给真正浮起的层(popover / dialog)。
- 一块 **teal** 管所有主操作 / 选中;AI 产物用 indigo;状态才上语义色。
- 组件一律经**语义 token**(`bg-primary`),状态语义变了只改语义层。
- 金额 / 时间 / 计数一律 **`data-num`(tabular-nums)** 对齐;数字列尤其。
- 维持 WCAG AA:正文 4.5:1、大文本 / UI 3:1;徽章文字 ≥13px 半粗。
- 触控目标 ≥ **44px**,移动优先;焦点区只放决策必需信息。

**Don't**
- 不在代码里写裸色值,或直引原始层(不写 `bg-teal-600`)。
- 不在 `success`/`warning`/`info` 填色上放白色正文(仅 AA-large)—— 改深字或降 -700;teal-600 填色配正文白字同理,改用 teal-700。
- 不堆重投影、不用投影替代边框;不彩虹、不让多个品牌色争注意力。
- 不引外部字体、不假设白底(iframe 会翻车)。
- 不 bounce / 大幅 scale / 旋转 —— **AI 流式与实时教练的专属动效除外**(见 Motion)。
- 单屏不超过两种字重;不在焦点区堆调试信息(角色门控隐藏)。
