# Edu 微前端 UI 设计系统

> 适用于 Edu 项目 4 个微前端（teacher-portal / student-portal / parent-portal / admin-portal）的统一视觉与交互规范。
>
> 本文档从原 CICD 项目（Next.js 16 + Tailwind v4 + shadcn/ui）的设计令牌体系演进而来，针对"当前界面设计不合理"的问题重新确立美学方向：**纸感编辑器 + 学术克制 + 单一视觉锚点**。

---

## 目录

1. [设计哲学](#1-设计哲学)
2. [美学方向](#2-美学方向)
3. [设计令牌体系（微前端共享）](#3-设计令牌体系微前端共享)
4. [布局系统](#4-布局系统)
5. [组件规范](#5-组件规范)
6. [动效规范](#6-动效规范)
7. [微前端架构下的 UI 一致性](#7-微前端架构下的-ui-一致性)
8. [可访问性](#8-可访问性)
9. [反模式清单](#9-反模式清单)
10. [实施清单](#10-实施清单)

---

## 1. 设计哲学

### 1.1 视觉论点（Visual Thesis）

> **Edu 不是一个 SaaS Dashboard，而是一张可以编辑的学术纸。**

教育产品的核心是"阅读—书写—批注"，而非"看图表—点按钮—收通知"。因此 Edu 的视觉论点由三句话定义：

1. **纸感编辑器**：界面是一张可被书写的纸，不是一组卡片堆砌的面板。
2. **学术克制**：参考学术书籍排版的克制感（章节标题、引文、注脚、分隔线），不滥用色彩与图形装饰。
3. **单一视觉锚点**：每屏只有一个焦点（一段文字、一个节点、一张表单），其余皆为衬托。

### 1.2 核心原则

| 原则 | 含义 | 落地手段 |
|------|------|----------|
| 内容优先 | 内容是主角，UI 是配角 | 默认无卡片、无装饰阴影，用分隔线/留白/排版层级划分 |
| 排版为王 | 用字体、字号、字重、行高建立层级，而非用色块和图标 | serif 主文 / sans UI / mono 数据三字体差异化 |
| 克制装饰 | 删除一切不传递信息的视觉元素 | 单一强调色、最少图标、无渐变、无装饰阴影 |
| 单一焦点 | 每屏只有一个视觉锚点 | 大留白、视觉层级递减、动效引导 |

### 1.3 反模式（禁止）

- 通用 SaaS 卡片网格作为首屏
- 紫色渐变白底
- Inter 单字体全场使用
- 过度图标化（每个标题配图标、每行配 emoji）
- 居中布局堆砌（hero 居中标题 + 居中副标 + 居中按钮）
- 均匀圆角（所有元素 `rounded-2xl`）
- 装饰性阴影（卡片下方 `shadow-2xl`）
- 多个竞争强调色（同时出现蓝、紫、绿、橙）

---

## 2. 美学方向

### 2.1 风格定位

**编辑式克制（Editorial Restraint）**

借鉴学术期刊与教辅书籍的排版美学：清晰的章节层级、慷慨的留白、serif 主文带来的"可被认真阅读"的质感。不追求"现代感"与"科技感"，追求"可读、可写、可信赖"。

### 2.2 色调

| 角色 | 颜色 | HSL | 用途 |
|------|------|-----|------|
| 纸面背景 | 纸感米白 | `hsl(40, 20%, 98%)` | 主背景，模拟纸张底色 |
| 主文 | 深炭灰 | `hsl(0, 0%, 15%)` | 正文、标题，不用纯黑避免刺眼 |
| 次文 | 暖灰 | `hsl(30, 5%, 45%)` | 元信息、辅助说明、注脚 |
| 强调（唯一） | 学术深蓝 | `hsl(220, 60%, 35%)` | 链接、激活态、关键操作 |
| 边框 | 细线灰 | `hsl(30, 10%, 90%)` | 分隔线、表格线、节点边框 |
| 危险 | 砖红 | `hsl(0, 70%, 45%)` | 删除、错误，仅此一种红色 |
| 成功 | 墨绿 | `hsl(142, 50%, 35%)` | 仅用于状态指示，不作装饰 |

**铁律：全屏同时只能出现一个强调色（学术深蓝）。** 危险色与成功色仅用于状态语义，不参与视觉竞争。

### 2.3 字体策略

三字体差异化是 Edu 区别于通用 SaaS 的核心手段。

| 字体 | 用途 | 加载方式 |
|------|------|----------|
| Fraunces（serif） | 主文、标题、引文、节点内容 | `next/font/google` + CSS 变量 `--font-serif` |
| Inter（sans-serif） | UI 元素、按钮、表单、导航、元信息 | `next/font/google` + CSS 变量 `--font-sans` |
| JetBrains Mono（mono） | 代码、数据、ID、时间戳、角色标签 | `next/font/google` + CSS 变量 `--font-mono` |

**差异化规则：**

- ✅ **做**：备课画布的主文用 Fraunces，工具条按钮用 Inter
- ✅ **做**：成绩表格的数据用 JetBrains Mono 对齐数字
- ✅ **做**：标题用 Fraunces semibold，副标用 Inter regular
- ❌ **不做**：全场 Inter 单字体（失去学术质感）
- ❌ **不做**：标题用 Inter bold（SaaS 感）

### 2.4 排版阶梯

基于学术书籍排版（章节标题、段落、引文、注脚）：

```css
/* 标题层级 */
--font-size-9: 32px;  /* 页面标题（H1）—— Fraunces semibold */
--font-size-8: 24px;  /* 区块标题（H2）—— Fraunces semibold */
--font-size-7: 20px;  /* 章节标题（H3）—— Fraunces medium */
--font-size-6: 18px;  /* 小节标题（H4）—— Fraunces medium */

/* 正文层级 */
--font-size-5: 16px;  /* 正文 —— Fraunces regular，行高 1.65 */
--font-size-4: 14px;  /* UI 元素 —— Inter regular */
--font-size-3: 13.5px;/* 节点主文 —— Inter regular，纸感优化 */
--font-size-2: 13px;  /* 元信息 —— Inter regular */
--font-size-1: 12px;  /* 角色标签、注脚 —— Inter medium */
--font-size-0: 9px;   /* 微标签 —— Mono semibold uppercase */
```

**行高策略：**

- 标题用 `--leading-tight`（1.2）保持紧凑
- 正文用 `--leading-relaxed`（1.65）保证可读
- UI 文字用 `--leading-normal`（1.5）

### 2.5 间距

**慷慨留白，呼吸感优先。**

- 基准：8px（`--space-2`）
- 段落间距：`--space-5`（20px）
- 区块间距：`--space-8`（32px）~ `--space-12`（48px）
- 页面边距：`--space-18`（72px）桌面端
- ❌ **不做**：信息密集堆砌，元素间距小于 8px

---

## 3. 设计令牌体系（微前端共享）

### 3.1 三层架构

沿用原 CICD 项目的双层令牌体系，新增"微前端共享包"作为第 0 层分发机制。

```mermaid
flowchart TB
    subgraph L0["Layer 0 · 共享分发"]
        PKG["@edu/shared-tokens<br/>npm 包"]
    end

    subgraph L1["Layer 1 · Primitive（原始令牌）"]
        PRIM["primitive.css<br/>色板/字号/间距/阴影/字体<br/>业务代码不直接引用"]
    end

    subgraph L2["Layer 2 · Semantic（语义令牌）"]
        LIGHT["semantic-light.css<br/>:root 明色"]
        DARK["semantic-dark.css<br/>.dark 暗色"]
        NS["module namespace<br/>--lp-* / --grade-* 等"]
    end

    subgraph L3["Layer 3 · Tailwind 暴露"]
        THEME["tailwind-theme.css<br/>@theme inline<br/>bg-* / text-* / font-*"]
    end

    subgraph APPS["微前端消费"]
        T["teacher-portal"]
        S["student-portal"]
        P["parent-portal"]
        A["admin-portal"]
    end

    PKG --> PRIM
    PRIM --> LIGHT
    PRIM --> DARK
    LIGHT --> NS
    DARK --> NS
    NS --> THEME
    THEME --> T
    THEME --> S
    THEME --> P
    THEME --> A
```

**分层规则：**

| 层级 | 职责 | 谁能引用 |
|------|------|----------|
| Layer 0 | npm 包分发，导出 CSS 文件 + TS 常量 | 各微前端 `package.json` 依赖 |
| Layer 1 | 原始色板/字号/间距/阴影 | 仅 Layer 2 引用，**业务代码禁止直接引用** |
| Layer 2 | 语义令牌（明暗双份） | 业务代码唯一引用入口 |
| Layer 3 | `@theme inline` 暴露为 Tailwind 类 | 业务代码通过 `bg-*` / `text-*` / `font-*` 使用 |

### 3.2 令牌文件结构（shared-tokens 包）

```
packages/shared-tokens/
├─ package.json                    # @edu/shared-tokens
├─ src/
│  ├─ tokens/
│  │  ├─ primitive.css             # Layer 1 原始令牌
│  │  ├─ semantic-light.css        # Layer 2 明色语义
│  │  ├─ semantic-dark.css         # Layer 2 暗色语义
│  │  ├─ module-lesson.css         # 备课模块命名空间 --lp-*
│  │  ├─ module-grade.css          # 成绩模块命名空间 --grade-*
│  │  ├─ tailwind-theme.css        # Layer 3 Tailwind 暴露
│  │  └─ index.css                 # 汇总入口
│  ├─ constants.ts                 # TS 常量（间距/字号/z-index）
│  ├─ types.ts                     # 令牌类型定义
│  └─ index.ts                     # 包入口
└─ README.md
```

### 3.3 具体令牌定义

#### Layer 1: Primitive（原始令牌）

```css
/* packages/shared-tokens/src/tokens/primitive.css */
:root {
  /* ============ 色板（学术克制） ============ */
  /* 中性色板（基于 Stone 暖灰，纸感） */
  --color-paper-50:  40 20% 99%;
  --color-paper-100: 40 20% 98%;   /* 纸面背景 */
  --color-paper-200: 40 15% 96%;
  --color-paper-300: 38 12% 92%;
  --color-paper-400: 35 10% 88%;
  --color-paper-500: 32 8% 80%;
  --color-paper-600: 30 6% 65%;
  --color-paper-700: 30 5% 45%;    /* 次文 */
  --color-paper-800: 28 4% 30%;
  --color-paper-900: 25 3% 15%;    /* 主文（深炭灰） */
  --color-paper-950: 20 2% 8%;

  /* 强调色（学术深蓝，唯一） */
  --color-accent-300: 220 60% 55%;
  --color-accent-500: 220 60% 35%; /* 默认强调 */
  --color-accent-700: 220 65% 25%;

  /* 语义状态色（仅状态语义，不参与装饰竞争） */
  --color-danger-500: 0 70% 45%;   /* 砖红 */
  --color-success-500: 142 50% 35%;/* 墨绿 */
  --color-warning-500: 38 80% 45%; /* 琥珀，仅用于警告 */

  /* ============ 字号阶梯 ============ */
  --font-size-0: 9px;       /* 微标签 */
  --font-size-1: 12px;      /* 元信息/注脚 */
  --font-size-2: 13px;      /* 节点元信息 */
  --font-size-3: 13.5px;    /* 节点主文（纸感优化） */
  --font-size-4: 14px;      /* UI 元素/按钮 */
  --font-size-5: 16px;      /* 正文（Fraunces） */
  --font-size-6: 18px;      /* H4 */
  --font-size-7: 20px;      /* H3 */
  --font-size-8: 24px;      /* H2 */
  --font-size-9: 32px;      /* H1 */

  /* ============ 间距阶梯（8px 基准） ============ */
  --space-0: 0;
  --space-0_5: 0.125rem;    /* 2px */
  --space-1: 0.25rem;       /* 4px */
  --space-1_5: 0.375rem;    /* 6px */
  --space-2: 0.5rem;        /* 8px —— 基准 */
  --space-3: 0.75rem;       /* 12px */
  --space-4: 1rem;          /* 16px */
  --space-5: 1.25rem;       /* 20px */
  --space-6: 1.5rem;        /* 24px */
  --space-8: 2rem;          /* 32px */
  --space-10: 2.5rem;       /* 40px */
  --space-12: 3rem;         /* 48px */
  --space-16: 4rem;         /* 64px */
  --space-18: 4.5rem;       /* 72px —— 桌面页边距 */

  /* ============ 阴影阶梯（克制使用） ============ */
  --shadow-1: 0 1px 2px rgba(15, 15, 15, 0.04);
  --shadow-2: 0 1px 3px rgba(15, 15, 15, 0.06), 0 1px 2px rgba(15, 15, 15, 0.04);
  --shadow-3: 0 4px 6px rgba(15, 15, 15, 0.05), 0 2px 4px rgba(15, 15, 15, 0.04);
  --shadow-4: 0 1px 2px rgba(15, 15, 15, 0.04), 0 8px 24px rgba(15, 15, 15, 0.04);
  --shadow-5: 0 1px 2px rgba(15, 15, 15, 0.06), 0 12px 36px rgba(15, 15, 15, 0.08);

  /* ============ 字体家族 ============ */
  --font-family-sans: 'Inter', system-ui, sans-serif;
  --font-family-serif: 'Fraunces', Georgia, serif;
  --font-family-mono: 'JetBrains Mono', monospace;

  /* ============ 行高阶梯 ============ */
  --leading-tight: 1.2;
  --leading-snug: 1.35;
  --leading-normal: 1.5;
  --leading-relaxed: 1.65;
  --leading-loose: 1.8;

  /* ============ 字重阶梯 ============ */
  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  /* ============ 动效 ============ */
  --duration-fast: 150ms;
  --duration-normal: 200ms;
  --duration-slow: 300ms;
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
  --ease-out: cubic-bezier(0, 0, 0.2, 1);
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);

  /* ============ z-index ============ */
  --z-dropdown: 1000;
  --z-sticky: 1100;
  --z-modal: 1300;
  --z-popover: 1400;
  --z-toast: 1500;
}
```

#### Layer 2: Semantic（明色）

```css
/* packages/shared-tokens/src/tokens/semantic-light.css */
:root {
  /* ============ shadcn 标准令牌（纸感调校） ============ */
  --background: 40 20% 98%;             /* 纸面米白，非纯白 */
  --foreground: 25 3% 15%;              /* 深炭灰，非纯黑 */
  --card: 40 20% 99%;                   /* 卡片略亮于背景（克制使用） */
  --card-foreground: 25 3% 15%;
  --popover: 40 20% 99%;
  --popover-foreground: 25 3% 15%;
  --primary: 220 60% 35%;               /* 学术深蓝，唯一强调 */
  --primary-foreground: 40 20% 98%;
  --secondary: 40 15% 96%;
  --secondary-foreground: 25 3% 15%;
  --muted: 40 12% 94%;
  --muted-foreground: 30 5% 45%;        /* 暖灰次文 */
  --accent: 220 60% 35%;
  --accent-foreground: 40 20% 98%;
  --destructive: 0 70% 45%;             /* 砖红 */
  --destructive-foreground: 40 20% 98%;
  --border: 30 10% 90%;                 /* 细线灰 */
  --input: 30 10% 90%;
  --ring: 220 60% 35%;
  --radius: 0.375rem;                   /* 6px，偏小圆角，纸感 */

  /* ============ 扩展语义令牌 ============ */
  --background-elevated: 40 20% 99%;    /* 浮层略亮 */
  --background-sunken: 40 15% 96%;      /* 凹陷区略暗 */
  --text-primary: 25 3% 15%;
  --text-secondary: 30 5% 45%;
  --text-tertiary: 30 5% 65%;
  --border-strong: 30 10% 75%;
  --border-subtle: 30 10% 95%;

  /* ============ 业务语义 ============ */
  --diff-add: 142 50% 35%;
  --diff-add-bg: 142 50% 35% / 0.1;
  --diff-remove: 0 70% 45%;
  --diff-remove-bg: 0 70% 45% / 0.1;

  /* ============ 状态色（仅状态语义） ============ */
  --status-success: 142 50% 35%;
  --status-warning: 38 80% 45%;
  --status-danger: 0 70% 45%;

  /* ============ sidebar ============ */
  --sidebar-background: 40 15% 96%;
  --sidebar-foreground: 25 3% 25%;
  --sidebar-primary: 220 60% 35%;
  --sidebar-primary-foreground: 40 20% 98%;
  --sidebar-accent: 40 12% 92%;
  --sidebar-accent-foreground: 25 3% 15%;
  --sidebar-border: 30 10% 88%;
  --sidebar-ring: 220 60% 35%;
}
```

#### Layer 2: Semantic（暗色）

```css
/* packages/shared-tokens/src/tokens/semantic-dark.css */
.dark {
  --background: 220 13% 8%;             /* 深炭蓝黑，非纯黑 */
  --foreground: 40 15% 92%;
  --card: 220 13% 10%;
  --card-foreground: 40 15% 92%;
  --popover: 220 13% 10%;
  --popover-foreground: 40 15% 92%;
  --primary: 220 60% 60%;               /* 暗色下强调色提亮 */
  --primary-foreground: 220 13% 8%;
  --secondary: 220 10% 15%;
  --secondary-foreground: 40 15% 92%;
  --muted: 220 10% 15%;
  --muted-foreground: 40 8% 65%;
  --accent: 220 60% 60%;
  --accent-foreground: 220 13% 8%;
  --destructive: 0 70% 55%;
  --destructive-foreground: 40 15% 92%;
  --border: 220 10% 18%;
  --input: 220 10% 18%;
  --ring: 220 60% 60%;
  --radius: 0.375rem;

  --background-elevated: 220 13% 11%;
  --background-sunken: 220 13% 6%;
  --text-primary: 40 15% 92%;
  --text-secondary: 40 8% 65%;
  --text-tertiary: 40 8% 45%;
  --border-strong: 220 10% 35%;
  --border-subtle: 220 10% 14%;

  --diff-add: 142 50% 55%;
  --diff-add-bg: 142 50% 55% / 0.15;
  --diff-remove: 0 70% 60%;
  --diff-remove-bg: 0 70% 60% / 0.15;

  --status-success: 142 50% 55%;
  --status-warning: 38 80% 55%;
  --status-danger: 0 70% 55%;

  --sidebar-background: 220 13% 6%;
  --sidebar-foreground: 40 12% 85%;
  --sidebar-primary: 220 60% 60%;
  --sidebar-primary-foreground: 220 13% 8%;
  --sidebar-accent: 220 10% 15%;
  --sidebar-accent-foreground: 40 15% 92%;
  --sidebar-border: 220 10% 15%;
  --sidebar-ring: 220 60% 60%;
}
```

#### Layer 3: Tailwind 暴露

```css
/* packages/shared-tokens/src/tokens/tailwind-theme.css */
@theme inline {
  /* shadcn 标准颜色 */
  --color-background: hsl(var(--background));
  --color-foreground: hsl(var(--foreground));
  --color-card: hsl(var(--card));
  --color-card-foreground: hsl(var(--card-foreground));
  --color-popover: hsl(var(--popover));
  --color-popover-foreground: hsl(var(--popover-foreground));
  --color-primary: hsl(var(--primary));
  --color-primary-foreground: hsl(var(--primary-foreground));
  --color-secondary: hsl(var(--secondary));
  --color-secondary-foreground: hsl(var(--secondary-foreground));
  --color-muted: hsl(var(--muted));
  --color-muted-foreground: hsl(var(--muted-foreground));
  --color-accent: hsl(var(--accent));
  --color-accent-foreground: hsl(var(--accent-foreground));
  --color-destructive: hsl(var(--destructive));
  --color-destructive-foreground: hsl(var(--destructive-foreground));
  --color-border: hsl(var(--border));
  --color-input: hsl(var(--input));
  --color-ring: hsl(var(--ring));

  /* 扩展语义 */
  --color-background-elevated: hsl(var(--background-elevated));
  --color-background-sunken: hsl(var(--background-sunken));
  --color-text-primary: hsl(var(--text-primary));
  --color-text-secondary: hsl(var(--text-secondary));
  --color-text-tertiary: hsl(var(--text-tertiary));
  --color-border-strong: hsl(var(--border-strong));
  --color-border-subtle: hsl(var(--border-subtle));

  /* 状态色 */
  --color-status-success: hsl(var(--status-success));
  --color-status-warning: hsl(var(--status-warning));
  --color-status-danger: hsl(var(--status-danger));

  /* diff */
  --color-diff-add: hsl(var(--diff-add));
  --color-diff-add-bg: hsl(var(--diff-add-bg));
  --color-diff-remove: hsl(var(--diff-remove));
  --color-diff-remove-bg: hsl(var(--diff-remove-bg));

  /* 字体 */
  --font-sans: var(--font-family-sans);
  --font-serif: var(--font-family-serif);
  --font-mono: var(--font-family-mono);

  /* 字号 */
  --text-size-0: var(--font-size-0);
  --text-size-1: var(--font-size-1);
  --text-size-2: var(--font-size-2);
  --text-size-3: var(--font-size-3);
  --text-size-4: var(--font-size-4);
  --text-size-5: var(--font-size-5);
  --text-size-6: var(--font-size-6);
  --text-size-7: var(--font-size-7);
  --text-size-8: var(--font-size-8);
  --text-size-9: var(--font-size-9);

  /* 圆角 */
  --radius-sm: calc(var(--radius) - 2px);
  --radius-md: var(--radius);
  --radius-lg: calc(var(--radius) + 2px);
  --radius-xl: calc(var(--radius) + 4px);
  --radius-full: 9999px;

  /* 阴影 */
  --shadow-1: var(--shadow-1);
  --shadow-2: var(--shadow-2);
  --shadow-3: var(--shadow-3);
  --shadow-4: var(--shadow-4);
  --shadow-5: var(--shadow-5);

  /* 动效 */
  --duration-fast: var(--duration-fast);
  --duration-normal: var(--duration-normal);
  --duration-slow: var(--duration-slow);

  /* z-index */
  --z-dropdown: var(--z-dropdown);
  --z-sticky: var(--z-sticky);
  --z-modal: var(--z-modal);
  --z-popover: var(--z-popover);
  --z-toast: var(--z-toast);
}
```

### 3.4 TS 常量导出

```typescript
// packages/shared-tokens/src/constants.ts
/** 间距常量（px，用于 canvas/SVG 等无法用 CSS 变量的场景） */
export const SPACE = {
  0: 0,
  0_5: 2,
  1: 4,
  1_5: 6,
  2: 8,
  3: 12,
  4: 16,
  5: 20,
  6: 24,
  8: 32,
  10: 40,
  12: 48,
  16: 64,
  18: 72,
} as const;

/** 字号常量（px） */
export const FONT_SIZE = {
  0: 9,
  1: 12,
  2: 13,
  3: 13.5,
  4: 14,
  5: 16,
  6: 18,
  7: 20,
  8: 24,
  9: 32,
} as const;

/** z-index 常量 */
export const Z_INDEX = {
  dropdown: 1000,
  sticky: 1100,
  modal: 1300,
  popover: 1400,
  toast: 1500,
} as const;
```

### 3.5 强制规则

- ✅ **做**：业务代码使用 `bg-background` / `text-foreground` / `font-serif` 等 Tailwind 类
- ✅ **做**：需要动态计算时使用 `hsl(var(--primary))`
- ❌ **不做**：TSX/TS 中出现 `#hex` 颜色字面量
- ❌ **不做**：出现 `'Inter'` / `'Fraunces'` / `'JetBrains Mono'` 字面量，统一用 `var(--font-family-*)`
- ❌ **不做**：出现 `font-size: 14px`，统一用 `var(--font-size-4)`
- ❌ **不做**：使用 `w-[137px]` 任意值，映射到 `--space-*` 或 Tailwind 默认阶梯
- ❌ **不做**：业务代码直接引用 Primitive 层（`--color-paper-*` / `--font-size-*`）

**豁免场景**（需 `// eslint-disable-next-line no-restricted-syntax -- <reason>` 注释）：
- PWA manifest
- 邮件 HTML 内联样式
- 图表 SVG 固定画布尺寸
- loading.tsx 占位骨架

---

## 4. 布局系统

### 4.1 三栏布局（核心工作台）

用户明确偏好：**左侧结构树 + 中间纸面内容区 + 右侧详情编辑面板**。

```mermaid
flowchart LR
    subgraph LEFT["左侧 · 结构树"]
        L1["学科/章节/课时<br/>树形导航"]
        L2["右键上下文菜单<br/>新增/删除/AI 辅助"]
    end

    subgraph CENTER["中间 · 纸面内容区"]
        C1["单一视觉锚点<br/>当前编辑的内容"]
        C2["Fraunces serif 主文<br/>纸感米白底"]
        C3["最少干扰<br/>无工具条悬浮"]
    end

    subgraph RIGHT["右侧 · 详情编辑面板"]
        R1["节点属性<br/>表单/元数据"]
        R2["AI 辅助建议<br/>侧边抽屉"]
        R3["关联资源<br/>列表式"]
    end

    LEFT -->|选中节点| CENTER
    CENTER -->|编辑属性| RIGHT
    RIGHT -->|应用变更| CENTER
```

**三栏比例建议：**

| 栏 | 比例 | 内容 | 字体 |
|----|------|------|------|
| 左侧结构树 | 20%（260px 固定） | 树形导航、章节列表 | Inter 14px |
| 中间纸面 | 50%（自适应） | 主编辑区、阅读区 | Fraunces 16px |
| 右侧详情 | 30%（380px 固定） | 属性面板、AI 辅助 | Inter 13px |

**做与不做：**

- ✅ **做**：左侧树用细线分隔，节点用文字而非图标
- ✅ **做**：中间纸面用 `--background` 纸感米白，主文用 Fraunces
- ✅ **做**：右侧面板用 `--background-elevated` 略亮区分
- ✅ **做**：所有节点支持右键上下文菜单
- ❌ **不做**：三栏都用卡片包裹（破坏纸感）
- ❌ **不做**：左侧树用图标代替文字（过度图标化）

### 4.2 全画布英雄区（Landing 页）

Landing 页采用 frontend-skill 的全画布英雄区模式：

```mermaid
flowchart TB
    subgraph HERO["全屏英雄区（100vh）"]
        H1["单一视觉锚点<br/>一句标题 + 一段引文"]
        H2["错峰揭示动画<br/>staggered reveals"]
    end

    subgraph BODY["正文区"]
        B1["分栏叙事<br/>非卡片网格"]
        B2["滚动联动<br/>scroll-linked 透明度/位移"]
    end

    subgraph CTA["收尾区"]
        C1["单一行动召唤<br/>不堆砌多个按钮"]
    end
```

**做与不做：**

- ✅ **做**：英雄区全屏占满，单一焦点（一句标题）
- ✅ **做**：标题用 Fraunces，副标用 Inter
- ✅ **做**：英雄区文字左对齐或编辑式排版，不居中堆砌
- ❌ **不做**：居中标题 + 居中副标 + 居中按钮的 SaaS 模板
- ❌ **不做**：英雄区放渐变背景或装饰图形

### 4.3 卡片克制

**默认无卡片，用以下手段替代：**

| 替代手段 | 适用场景 |
|----------|----------|
| 分栏（columns） | 并列内容 |
| 分隔线（border-b） | 列表项 |
| 留白（padding/margin） | 区块划分 |
| 缩进 + 左竖线 | 引文、注脚 |
| 表格 | 结构化数据 |

**何时可用卡片：**

- 仅当内容需要"被拎出来强调"时（如成绩报告卡打印）
- 卡片用 `--card` 略亮背景 + 1px `--border` 边框，**不加阴影**

**做与不做：**

- ✅ **做**：列表用 `divide-y divide-border` 分隔线
- ✅ **做**：引文用 `border-l-2 border-border pl-4` 左竖线
- ❌ **不做**：每个内容块都包 `rounded-2xl shadow-lg border`
- ❌ **不做**：卡片用 `shadow-2xl` 装饰性阴影

### 4.4 网格与间距

- 基准：8px（`--space-2`）
- 所有间距必须是 4px 的整数倍
- 桌面页边距：72px（`--space-18`）
- 移动页边距：16px（`--space-4`）
- 段落间距：20px（`--space-5`）
- 区块间距：32px~48px（`--space-8` ~ `--space-12`）

---

## 5. 组件规范

### 5.1 基础组件库

基于 shadcn/ui，但做纸感调校：

| 组件 | 来源 | 纸感调校 |
|------|------|----------|
| Button | shadcn | 默认 `variant=ghost`，强调用 `variant=default`（学术深蓝） |
| Input | shadcn | 边框 `--border`，聚焦 `--ring` 学术深蓝 |
| Dialog | shadcn | 圆角 `--radius`，无阴影或仅 `--shadow-2` |
| Sheet | shadcn | 右侧抽屉用于详情面板 |
| DropdownMenu | shadcn | 右键上下文菜单基础 |
| Tooltip | shadcn | 仅用于图标按钮，文字按钮不加 |
| Tabs | shadcn | 下划线式，非胶囊式 |
| Table | shadcn | 细线分隔，数据用 JetBrains Mono |
| Toast | shadcn | 极简，无图标 |

**做与不做：**

- ✅ **做**：Tabs 用 `underline` 模式
- ✅ **做**：数据表格用 `font-mono` 对齐数字
- ❌ **不做**：所有按钮都加图标
- ❌ **不做**：Dialog 用大圆角 + 大阴影

### 5.2 组件组合模式

结合 composition-patterns 与 shadcn，使用以下模式：

#### 5.2.1 Compound Components（复合组件）

用于三栏布局等"一组协同组件"：

```tsx
// 示例：三栏工作台复合组件
import { createContext, useContext } from "react";

type WorkbenchContextValue = {
  selectedNodeId: string | null;
  onSelectNode: (id: string) => void;
};

const WorkbenchContext = createContext<WorkbenchContextValue | null>(null);

function useWorkbench(): WorkbenchContextValue {
  const ctx = useContext(WorkbenchContext);
  if (!ctx) {
    throw new Error("Workbench 子组件必须用在 <Workbench> 内");
  }
  return ctx;
}

// 根组件：提供 Context，不渲染具体布局
export function Workbench({
  children,
  selectedNodeId,
  onSelectNode,
}: {
  children: React.ReactNode;
  selectedNodeId: string | null;
  onSelectNode: (id: string) => void;
}): React.ReactElement {
  return (
    <WorkbenchContext.Provider value={{ selectedNodeId, onSelectNode }}>
      <div className="grid h-screen grid-cols-[260px_1fr_380px]">
        {children}
      </div>
    </WorkbenchContext.Provider>
  );
}

// 子组件：各自渲染自己的栏，通过 Context 协同
export function WorkbenchTree({ children }: { children: React.ReactNode }): React.ReactElement {
  return (
    <aside className="border-r border-border bg-sidebar overflow-y-auto">
      {children}
    </aside>
  );
}

export function WorkbenchPaper({ children }: { children: React.ReactNode }): React.ReactElement {
  return (
    <main className="bg-background overflow-y-auto px-18 py-12">
      {children}
    </main>
  );
}

export function WorkbenchDetail({ children }: { children: React.ReactNode }): React.ReactElement {
  return (
    <aside className="border-l border-border bg-background-elevated overflow-y-auto">
      {children}
    </aside>
  );
}

// 使用方式：组合而非配置
<Workbench selectedNodeId={id} onSelectNode={setId}>
  <WorkbenchTree>
    <StructureTree />
  </WorkbenchTree>
  <WorkbenchPaper>
    <LessonEditor />
  </WorkbenchPaper>
  <WorkbenchDetail>
    <NodeProperties />
  </WorkbenchDetail>
</Workbench>
```

#### 5.2.2 Render Props（渲染函数）

用于列表项的灵活定制：

```tsx
// 示例：节点列表，渲染逻辑由调用方决定
type TreeNode = {
  id: string;
  title: string;
  type: "chapter" | "lesson" | "activity";
};

export function NodeList({
  nodes,
  renderItem,
}: {
  nodes: TreeNode[];
  renderItem: (node: TreeNode, isSelected: boolean) => React.ReactNode;
}): React.ReactElement {
  const { selectedNodeId, onSelectNode } = useWorkbench();
  return (
    <ul className="divide-y divide-border-subtle">
      {nodes.map((node) => (
        <li
          key={node.id}
          onClick={() => onSelectNode(node.id)}
          className="cursor-pointer px-4 py-2 hover:bg-muted"
        >
          {renderItem(node, node.id === selectedNodeId)}
        </li>
      ))}
    </ul>
  );
}

// 调用方决定渲染
<NodeList
  nodes={lessons}
  renderItem={(node, isSelected) => (
    <span className={isSelected ? "font-serif text-primary" : "font-serif"}>
      {node.title}
    </span>
  )}
/>
```

#### 5.2.3 Context Providers（状态共享）

用于右键菜单的统一管理：

```tsx
// 右键菜单 Context
type ContextMenuState = {
  open: (event: React.MouseEvent, nodeId: string) => void;
};

const ContextMenuContext = createContext<ContextMenuState | null>(null);

export function ContextMenuProvider({
  children,
}: {
  children: React.ReactNode;
}): React.ReactElement {
  const [state, setState] = useState<{
    x: number;
    y: number;
    nodeId: string | null;
  } | null>(null);

  const open = useCallback((event: React.MouseEvent, nodeId: string) => {
    event.preventDefault();
    setState({ x: event.clientX, y: event.clientY, nodeId });
  }, []);

  return (
    <ContextMenuContext.Provider value={{ open }}>
      {children}
      {state && (
        <DropdownMenu open onOpenChange={() => setState(null)}>
          <DropdownMenuContent style={{ left: state.x, top: state.y }}>
            {/* 菜单项由业务通过插槽注入 */}
          </DropdownMenuContent>
        </DropdownMenu>
      )}
    </ContextMenuContext.Provider>
  );
}
```

### 5.3 业务组件清单

| 组件 | 微前端 | 说明 |
|------|--------|------|
| StructureTree | teacher / admin | 左侧结构树，支持右键菜单 |
| LessonEditor | teacher | 中间纸面编辑器，Fraunces 主文 |
| NodeProperties | teacher | 右侧节点属性面板 |
| ClassCard | teacher / admin | 班级卡片，**克制使用**（默认列表式） |
| AssignmentCard | teacher / student | 作业卡片，**克制使用** |
| GradeTable | teacher / admin / parent | 成绩表，mono 数字对齐 |
| StudentRoster | teacher / admin | 学生名册，列表式 |
| ScheduleGrid | all | 课表网格 |
| ReportCard | teacher / parent | 成绩报告卡，**唯一允许卡片 + 打印样式** |

### 5.4 右键上下文菜单（强制）

**所有节点支持右键菜单**，菜单项包括：

- 新增子节点
- 编辑节点
- 删除节点
- 复制/移动
- AI 辅助（生成内容/优化措辞/关联推荐）

```tsx
// 节点右键菜单统一封装
export function NodeWithContextMenu({
  node,
  children,
}: {
  node: TreeNode;
  children: React.ReactNode;
}): React.ReactElement {
  const { open } = useContextMenu();
  return (
    <div onContextMenu={(e) => open(e, node.id)}>
      {children}
    </div>
  );
}
```

### 5.5 状态管理

- **React 19 `useOptimistic`**：用于乐观更新（节点编辑、批注提交）
- **zustand 细粒度选择器**：用于跨组件状态共享（选中节点、编辑模式）
- ❌ **不做**：用 Context 共享高频更新状态（导致重渲染）
- ❌ **不做**：用 useState 管理跨组件状态

```tsx
// zustand 细粒度选择器示例
import { create } from "zustand";
import { shallow } from "zustand/shallow";

type EditorStore = {
  selectedNodeId: string | null;
  editingMode: "view" | "edit" | "ai-assist";
  setSelectedNode: (id: string | null) => void;
  setEditingMode: (mode: EditorStore["editingMode"]) => void;
};

export const useEditorStore = create<EditorStore>((set) => ({
  selectedNodeId: null,
  editingMode: "view",
  setSelectedNode: (id) => set({ selectedNodeId: id }),
  setEditingMode: (mode) => set({ editingMode: mode }),
}));

// 组件中：只订阅需要的字段
function TreeView() {
  const selectedNodeId = useEditorStore((s) => s.selectedNodeId);
  // ...
}
```

---

## 6. 动效规范

### 6.1 动效原则

> **动效用于建立层级和氛围，不是噪音。**

| 原则 | 含义 |
|------|------|
| 有目的 | 每个动效必须传递信息或引导注意力 |
| 克制 | 同屏动效不超过 1 个主要 + 2 个次要 |
| 快速 | 交互反馈 ≤ 200ms，页面过渡 ≤ 300ms |
| 可关闭 | 遵守 `prefers-reduced-motion` |

### 6.2 入场序列（Staggered Reveals）

英雄区元素错峰揭示，建立阅读顺序：

```tsx
// 错峰揭示：标题 → 副标 → 正文 → CTA
function HeroSection() {
  return (
    <section className="h-screen flex flex-col justify-center px-18">
      <h1
        className="font-serif text-size-9 text-foreground"
        style={{ animation: "reveal-up 0.6s var(--ease-out) 0ms both" }}
      >
        Edu
      </h1>
      <p
        className="font-sans text-size-5 text-text-secondary mt-4"
        style={{ animation: "reveal-up 0.6s var(--ease-out) 120ms both" }}
      >
        一张可以编辑的学术纸
      </p>
      <p
        className="font-sans text-size-4 text-text-tertiary mt-2 max-w-prose"
        style={{ animation: "reveal-up 0.6s var(--ease-out) 240ms both" }}
      >
        面向 K12 教学的微前端工作台
      </p>
    </section>
  );
}
```

```css
/* 全局关键帧 */
@keyframes reveal-up {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
```

### 6.3 滚动联动（Scroll-Linked）

```tsx
// 使用 useScroll + useTransform
import { useScroll, useTransform } from "framer-motion";

function ScrollContent() {
  const { scrollYProgress } = useScroll();
  const opacity = useTransform(scrollYProgress, [0, 0.3], [1, 0]);
  const y = useTransform(scrollYProgress, [0, 0.3], [0, -20]);

  return (
    <motion.div style={{ opacity, y }}>
      {/* 滚动时淡出上移 */}
    </motion.div>
  );
}
```

### 6.4 交互反馈

| 交互 | 动效 | 时长 |
|------|------|------|
| hover 按钮 | `bg-muted` 渐显 | 150ms |
| 选中节点 | 左侧 2px 强调色竖线滑入 | 200ms |
| 打开 Dialog | 淡入 + 轻微下移 | 200ms |
| 切换 Tab | 下划线滑动 | 200ms |
| 提交表单 | 按钮 loading spinner | - |
| 乐观更新 | 内容淡入替换 | 150ms |

### 6.5 微前端场景

| 场景 | 动效 |
|------|------|
| 微前端页面切换 | 顶部 2px 进度条 + 内容淡入 |
| 模块加载占位 | 骨架屏（skeleton），不用 spinner |
| Module Federation 加载失败 | 错误边界 + 重试按钮 |

**做与不做：**

- ✅ **做**：Landing 页错峰揭示建立阅读顺序
- ✅ **做**：选中节点用竖线滑入暗示焦点
- ✅ **做**：加载用骨架屏，不用居中 spinner
- ❌ **不做**：每个元素都加入场动画
- ❌ **不做**：卡片 hover 时 `scale-105` 放大
- ❌ **不做**：装饰性的持续动画（脉冲、闪烁）

---

## 7. 微前端架构下的 UI 一致性

### 7.1 架构总览

```mermaid
flowchart TB
    subgraph SHELL["Shell 宿主"]
        ROUTER["统一路由<br/>+ 主题切换"]
        FONT["字体加载<br/>next/font"]
    end

    subgraph SHARED["共享层"]
        TOKENS["@edu/shared-tokens<br/>令牌包"]
        UI["@edu/shared-ui<br/>基础组件"]
        UTILS["@edu/shared-utils<br/>cn/权限/类型"]
    end

    subgraph MF["4 个微前端（Module Federation）"]
        T["teacher-portal<br/>教师工作台"]
        S["student-portal<br/>学生学习台"]
        P["parent-portal<br/>家长查看台"]
        A["admin-portal<br/>管理后台"]
    end

    SHELL -->|暴露| T
    SHELL -->|暴露| S
    SHELL -->|暴露| P
    SHELL -->|暴露| A

    TOKENS --> T
    TOKENS --> S
    TOKENS --> P
    TOKENS --> A

    UI --> T
    UI --> S
    UI --> P
    UI --> A
```

### 7.2 shared-tokens 包集成

各微前端 `package.json` 依赖：

```json
{
  "dependencies": {
    "@edu/shared-tokens": "workspace:*"
  }
}
```

各微前端 `globals.css`：

```css
@import "tailwindcss";
@import "@edu/shared-tokens/tokens"; /* 汇总入口 */
@plugin "tailwindcss-animate";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:where(.dark, .dark *));

@layer base {
  body {
    @apply bg-background text-foreground;
    font-feature-settings: "rlig" 1, "calt" 1;
  }
}
```

### 7.3 共享字体加载策略

**问题**：Module Federation 下，各微前端独立加载字体会导致重复请求与 FOUT。

**方案**：Shell 宿主统一加载字体，通过 CSS 变量共享；微前端只引用变量。

```tsx
// apps/shell/app/layout.tsx
import { Inter, Fraunces, JetBrains_Mono } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-inter",
});
const fraunces = Fraunces({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-fraunces",
});
const jetbrainsMono = JetBrains_Mono({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-jetbrains-mono",
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN" suppressHydrationWarning>
      <body className={`${inter.variable} ${fraunces.variable} ${jetbrainsMono.variable}`}>
        {children}
      </body>
    </html>
  );
}
```

**shared-tokens 中映射：**

```css
/* packages/shared-tokens/src/tokens/primitive.css */
:root {
  --font-family-sans: var(--font-inter), system-ui, sans-serif;
  --font-family-serif: var(--font-fraunces), Georgia, serif;
  --font-family-mono: var(--font-jetbrains-mono), monospace;
}
```

**做与不做：**

- ✅ **做**：Shell 加载字体，微前端引用变量
- ✅ **做**：`display: swap` 避免 FOIT
- ❌ **不做**：每个微前端各自 `next/font/google` 加载

### 7.4 Module Federation UI 协调

| 协调点 | 策略 |
|--------|------|
| 设计语言 | 统一 shared-tokens，禁止微前端自定义令牌 |
| 组件库 | shared-ui 提供基础组件，微前端可扩展业务组件 |
| 主题切换 | Shell 持有 `class="dark"` 状态，所有微前端响应 |
| 路由 | Shell 统一路由表，微前端注册子路由 |
| 加载态 | 统一骨架屏样式（来自 shared-ui） |

### 7.5 主题切换

```tsx
// apps/shell/components/ThemeProvider.tsx
"use client";

import { useEffect, useState } from "react";

type Theme = "light" | "dark";

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>("light");

  useEffect(() => {
    const saved = localStorage.getItem("edu-theme") as Theme | null;
    const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
    const initial = saved ?? (prefersDark ? "dark" : "light");
    setTheme(initial);
    document.documentElement.classList.toggle("dark", initial === "dark");
  }, []);

  const toggle = () => {
    const next: Theme = theme === "light" ? "dark" : "light";
    setTheme(next);
    localStorage.setItem("edu-theme", next);
    document.documentElement.classList.toggle("dark", next === "dark");
  };

  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}
```

**做与不做：**

- ✅ **做**：主题状态由 Shell 持有，`class="dark"` 挂在 `<html>`
- ✅ **做**：所有微前端通过 `@edu/shared-tokens` 响应主题
- ❌ **不做**：微前端各自实现主题切换

---

## 8. 可访问性

### 8.1 对比度（WCAG AA）

| 文本类型 | 对比度要求 | 验证 |
|----------|-----------|------|
| 正文（< 18px） | ≥ 4.5:1 | `--foreground` on `--background` |
| 大文本（≥ 18px） | ≥ 3:1 | `--text-secondary` on `--background` |
| 交互元素边框 | ≥ 3:1 | `--border-strong` |
| 焦点指示器 | ≥ 3:1 | `--ring`（学术深蓝） |

**当前令牌对比度验证：**

- 主文 `hsl(25, 3%, 15%)` on 纸面 `hsl(40, 20%, 98%)`：约 14:1 ✅
- 次文 `hsl(30, 5%, 45%)` on 纸面：约 4.6:1 ✅
- 强调 `hsl(220, 60%, 35%)` on 纸面：约 7:1 ✅

### 8.2 键盘导航

- 所有交互元素必须可 `Tab` 聚焦
- 焦点可见：`focus-visible:ring-2 focus-visible:ring-ring`
- 右键菜单支持键盘打开：`Shift + F10` 或 `Menu` 键
- 三栏布局支持 `Tab` 在栏间流转
- 树形结构支持方向键导航（↑↓←→）

```tsx
// 焦点可见样式
<button className="focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">
  按钮
</button>
```

### 8.3 屏幕阅读器

- 语义化 HTML：`<nav>` `<main>` `<aside>` `<article>`
- 图标按钮必须有 `aria-label`
- 动态内容用 `aria-live="polite"` 通告
- 树形结构用 `role="tree"` + `role="treeitem"` + `aria-expanded`
- 表单字段必须有 `<label>` 或 `aria-label`

```tsx
// 树形节点无障碍示例
<li
  role="treeitem"
  aria-expanded={hasChildren}
  aria-selected={isSelected}
  className="focus-visible:ring-2 focus-visible:ring-ring"
>
  {node.title}
</li>
```

### 8.4 焦点可见性

- ❌ **不做**：`outline: none` 后不提供替代焦点样式
- ✅ **做**：用 `focus-visible:ring-2` 替代 `outline`
- ✅ **做**：焦点指示器对比度 ≥ 3:1

---

## 9. 反模式清单

### 9.1 禁止清单（强制）

| # | 反模式 | 原因 | 正确做法 |
|---|--------|------|----------|
| 1 | 通用 SaaS 卡片网格作为首屏 | 缺乏视觉锚点，AI slop | 单一视觉锚点 + 分栏叙事 |
| 2 | 紫色渐变白底 | 通用 SaaS 感 | 纸感米白纯色底 |
| 3 | Inter 单字体全场 | 失去学术质感 | Fraunces 主文 + Inter UI + Mono 数据 |
| 4 | 过度图标化 | 视觉干扰，违背克制 | 文字优先，图标仅用于无文字场景 |
| 5 | 居中布局堆砌 | SaaS 模板感 | 编辑式左对齐排版 |
| 6 | 均匀圆角 `rounded-2xl` | 单调，无层级 | 按层级分级圆角，纸感用小圆角 |
| 7 | 装饰性阴影 `shadow-2xl` | 视觉噪音 | 默认无阴影，必要时用 `--shadow-1` |
| 8 | 多个竞争强调色 | 视觉焦点散乱 | 单一学术深蓝强调 |
| 9 | `#hex` 颜色字面量 | 绕过令牌系统 | `hsl(var(--*))` 或 Tailwind 类 |
| 10 | `font-size: 14px` 硬编码 | 绕过令牌系统 | `var(--font-size-4)` |
| 11 | `w-[137px]` 任意值 | 绕过间距系统 | `--space-*` 或 Tailwind 默认阶梯 |
| 12 | 每个元素加入场动画 | 动效噪音 | 仅英雄区错峰揭示 |
| 13 | 卡片 hover `scale-105` | 装饰性动效 | 用 `bg-muted` 反馈 |
| 14 | 持续脉冲/闪烁动画 | 注意力干扰 | 仅用于真正需要警告的场景 |
| 15 | `dangerouslySetInnerHTML` 未清洗 | XSS 风险 | 先 DOMPurify 清洗 |
| 16 | `role === "xxx"` 硬编码 | 权限散落 | `usePermission().hasPermission()` |
| 17 | 业务代码引用 Primitive 层 | 破坏分层 | 仅引用 Semantic 层 |
| 18 | 微前端自定义令牌 | 破坏一致性 | 统一 `@edu/shared-tokens` |

### 9.2 推荐清单

| # | 推荐模式 | 适用场景 |
|---|----------|----------|
| 1 | 三栏工作台 | 编辑类页面（备课、批注、详情） |
| 2 | 全画布英雄区 | Landing 页 |
| 3 | 列表 + 分隔线 | 数据列表（成绩、名册、通知） |
| 4 | 表格 + mono 数字 | 结构化数据 |
| 5 | 左竖线引文 | 引用、批注、AI 建议 |
| 6 | Tabs 下划线式 | 同级内容切换 |
| 7 | Sheet 右侧抽屉 | 详情面板、AI 辅助 |
| 8 | Compound Components | 一组协同组件 |
| 9 | Render Props | 列表项灵活定制 |
| 10 | zustand 细粒度选择器 | 跨组件状态共享 |
| 11 | `useOptimistic` | 乐观更新 |
| 12 | 骨架屏 | 加载占位 |

---

## 10. 实施清单

### 10.1 shared-tokens 包结构

```mermaid
flowchart TB
    subgraph PKG["@edu/shared-tokens"]
        P1["src/tokens/primitive.css"]
        P2["src/tokens/semantic-light.css"]
        P3["src/tokens/semantic-dark.css"]
        P4["src/tokens/module-lesson.css"]
        P5["src/tokens/module-grade.css"]
        P6["src/tokens/tailwind-theme.css"]
        P7["src/tokens/index.css"]
        C1["src/constants.ts<br/>SPACE/FONT_SIZE/Z_INDEX"]
        T1["src/types.ts"]
        E1["src/index.ts"]
    end

    subgraph EXPORTS["包导出"]
        E_CSS["./tokens → index.css"]
        E_TS["". → index.ts"]
        E_PKG["package.json exports 字段"]
    end

    P1 --> P7
    P2 --> P7
    P3 --> P7
    P4 --> P7
    P5 --> P7
    P6 --> P7
    P7 --> E_CSS
    C1 --> E1
    T1 --> E1
    E1 --> E_TS
```

**package.json exports 配置：**

```json
{
  "name": "@edu/shared-tokens",
  "exports": {
    ".": "./src/index.ts",
    "./tokens": "./src/tokens/index.css",
    "./constants": "./src/constants.ts"
  }
}
```

### 10.2 各微前端集成方式

每个微前端（以 teacher-portal 为例）：

1. **依赖声明**：`package.json` 添加 `@edu/shared-tokens`、`@edu/shared-ui`、`@edu/shared-utils`
2. **全局样式**：`app/globals.css` 导入 `@edu/shared-tokens/tokens`
3. **字体引用**：不加载字体，引用 Shell 注入的 CSS 变量
4. **Tailwind 配置**：依赖 shared-tokens 的 `@theme inline`，无需自定义 `theme.extend`
5. **主题响应**：监听 `<html>` 的 `class="dark"`，自动切换语义令牌

**集成检查清单：**

- [ ] `package.json` 依赖 `@edu/shared-tokens`
- [ ] `globals.css` 导入 `@import "@edu/shared-tokens/tokens"`
- [ ] 不出现 `next/font/google` 字体加载
- [ ] 不出现 `#hex` 颜色字面量
- [ ] 不出现 `'Inter'` / `'Fraunces'` / `'JetBrains Mono'` 字面量
- [ ] 不出现 `font-size: Npx` 硬编码
- [ ] 不出现 `w-[Npx]` 任意值

### 10.3 组件库抽取策略

```
packages/shared-ui/
├─ package.json                     # @edu/shared-ui
├─ src/
│  ├─ primitives/                   # shadcn 基础组件（纸感调校）
│  │  ├─ button.tsx
│  │  ├─ input.tsx
│  │  ├─ dialog.tsx
│  │  ├─ sheet.tsx
│  │  ├─ dropdown-menu.tsx
│  │  ├─ tabs.tsx
│  │  ├─ table.tsx
│  │  └─ tooltip.tsx
│  ├─ patterns/                     # 组合模式组件
│  │  ├─ workbench.tsx              # 三栏工作台
│  │  ├─ context-menu-provider.tsx  # 右键菜单
│  │  └─ node-list.tsx              # 节点列表
│  ├─ skeleton/                     # 骨架屏
│  │  ├─ tree-skeleton.tsx
│  │  ├─ paper-skeleton.tsx
│  │  └─ table-skeleton.tsx
│  └─ index.ts
└─ README.md
```

**抽取原则：**

- ✅ **做**：4 个微前端都用到的组件才抽取到 shared-ui
- ✅ **做**：业务专属组件留在各自微前端
- ❌ **不做**：提前抽取"可能复用"的组件
- ❌ **不做**：shared-ui 依赖任何微前端

### 10.4 设计审查清单

每次 PR 提交前，对照以下清单审查：

#### 视觉与美学

- [ ] 当前屏有且仅有一个视觉焦点
- [ ] 无卡片堆砌（默认无卡片，必要时才用）
- [ ] 无装饰性阴影（仅 `--shadow-1` 用于浮层）
- [ ] 无多个强调色竞争（仅学术深蓝）
- [ ] 字体差异化正确（serif 主文 / sans UI / mono 数据）

#### 令牌合规

- [ ] 无 `#hex` 颜色字面量
- [ ] 无字体字面量（`'Inter'` 等）
- [ ] 无 `font-size: Npx` 硬编码
- [ ] 无 `w-[Npx]` 任意值
- [ ] 业务代码不引用 Primitive 层

#### 布局

- [ ] 三栏布局比例正确（260 / 1fr / 380）
- [ ] 页面边距 `--space-18`（桌面）/ `--space-4`（移动）
- [ ] 留白充足（段落 `--space-5`，区块 `--space-8` 以上）

#### 交互

- [ ] 所有节点支持右键上下文菜单
- [ ] 交互反馈 ≤ 200ms
- [ ] 动效遵守 `prefers-reduced-motion`
- [ ] 无装饰性持续动画

#### 可访问性

- [ ] 对比度 ≥ WCAG AA
- [ ] 所有交互元素可 `Tab` 聚焦
- [ ] 焦点可见（`focus-visible:ring-2`）
- [ ] 图标按钮有 `aria-label`
- [ ] 树形结构有 `role="tree"` 等语义

#### 微前端一致性

- [ ] 使用 `@edu/shared-tokens`，不自定义令牌
- [ ] 不加载字体，引用 Shell 变量
- [ ] 主题切换响应 `<html>` 的 `class="dark"`

---

## 附录：与原 CICD 项目的差异

| 维度 | CICD（原） | Edu（新） |
|------|-----------|-----------|
| 架构 | Next.js 单应用 | 微服务 + 4 微前端 Module Federation |
| 令牌分发 | 应用内 `src/app/styles/tokens/` | `@edu/shared-tokens` npm 包 |
| 字体加载 | 应用内 `next/font` | Shell 加载，微前端引用变量 |
| 强调色 | Indigo（lp-interaction） | 学术深蓝 `hsl(220, 60%, 35%)` |
| 背景色 | 纯白 `hsl(0, 0%, 100%)` | 纸感米白 `hsl(40, 20%, 98%)` |
| 主文色 | `hsl(240, 10%, 3.9%)` 偏蓝黑 | `hsl(25, 3%, 15%)` 暖炭灰 |
| 圆角 | `0.5rem`（8px） | `0.375rem`（6px，更纸感） |
| 卡片使用 | 较多 | 克制，默认无卡片 |
| 字体差异化 | Fraunces 用于备课 | 全局三字体差异化 |

---

**文档版本**：v1.0
**最后更新**：2026-07-07
**维护者**：Edu 团队
