Files
NextEdu/docs/superpowers/plans/2026-07-04-design-tokens-refactor.md
SpecialX 214ebec976 refactor(design-tokens): 全量体系化重建设计令牌
Primitive + Semantic 双层令牌架构,HEX->HSL,明暗双份,@theme inline 暴露为 Tailwind 类。

- 新建 src/app/styles/tokens/ 6 个令牌文件(primitive/semantic-light/semantic-dark/lesson-preparation/tailwind-theme/index)
- globals.css 改为 @import 引入,477->258 行
- 清理 91 处 #hex 硬编码颜色 -> hsl(var(--*))
- 清理 10 处硬编码字体 -> var(--font-family-*)
- 清理 100 文件 Tailwind 任意值(Tier 1 映射/Tier 3 注释豁免)
- 清理 M3 Surface 死代码,升级 --lp-* 令牌(HEX->HSL + 暗色补全)
- 新建 ESLint 自定义规则 no-hardcoded-design-tokens(单词边界正则)
- eslint.config.mjs 新增 no-restricted-syntax 禁止 #hex + 自定义规则加载(pathToFileURL)
- 项目规则新增设计令牌规范强制章节
- 架构图 004/005 同步设计令牌体系节点
- known-issues.md 追加设计令牌问题分类(7 个规则表)

验证: tsc --noEmit 0 errors, npm run lint 0 errors/12 warnings(均为既有问题)
2026-07-05 01:03:19 +08:00

1537 lines
54 KiB
Markdown

# 设计令牌专项重构 · 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 全量体系化重建设计令牌(Primitive + Semantic 双层),清理 91 处硬编码颜色 / 8 处硬编码字体 / 15 处硬编码字号 / 98 处 Tailwind 任意值,建立 ESLint 强制约束,大爆炸式单 PR 提交。
**Architecture:** Tailwind v4 + `@theme inline` 双层令牌架构(Primitive 原料层 + Semantic 语义层),令牌文件拆分到 `src/app/styles/tokens/` 6 个文件,globals.css 改为 `@import` 引入。`--lp-*` 保留命名空间但 HEX→HSL、补全暗色、暴露到 Tailwind。M3 Surface 死代码清理。ESLint 自定义规则 + `eslint-plugin-tailwindcss` 锁定任意值。
**Tech Stack:** Tailwind CSS v4、Next.js 16 (App Router)、ESLint 9、TypeScript 5、shadcn/ui。
**Spec:** [`docs/superpowers/specs/2026-07-04-design-tokens-refactor-design.md`](file:///e:/Desktop/CICD/docs/superpowers/specs/2026-07-04-design-tokens-refactor-design.md)
**执行方式:** 大爆炸式(单 PR),所有任务完成后单次 commit `refactor(design-tokens): 全量体系化重建设计令牌`
---
## 文件结构
### 新建文件
| 文件 | 职责 |
|---|---|
| `src/app/styles/tokens/primitive.css` | Layer 1: 原始色板(zinc/stone/indigo)、字号阶梯、间距阶梯、阴影阶梯、字体家族 |
| `src/app/styles/tokens/semantic-light.css` | Layer 2: `:root` 语义令牌(shadcn 标准 + 扩展层级 + chart/sidebar + 新增 diff/graph) |
| `src/app/styles/tokens/semantic-dark.css` | Layer 2: `.dark` 语义令牌(完整暗色) |
| `src/app/styles/tokens/lesson-preparation.css` | `--lp-*` 令牌(明暗双份,HEX→HSL) |
| `src/app/styles/tokens/tailwind-theme.css` | `@theme inline` 暴露所有令牌为 Tailwind 类 |
| `src/app/styles/tokens/index.css` | 汇总 `@import` 入口 |
| `eslint-rules/no-hardcoded-design-tokens.js` | 自定义 ESLint 规则:检测硬编码字体/字号字面量 |
### 修改文件
| 文件 | 修改内容 |
|---|---|
| `src/app/globals.css` | 改为 `@import` 引入 tokens,清理已迁移内容,令牌化剩余硬编码样式(lp-tb-btn/lp-inline-node/lp-qa-*) |
| `eslint.config.mjs` | 新增 `tailwindcss/no-arbitrary-value` 规则、`no-restricted-syntax` hex 检测、自定义规则加载 |
| `.trae/rules/project_rules.md` | "Tailwind 规范"章节新增"设计令牌规范(强制)" |
| `docs/architecture/004_architecture_impact_map.md` | 新增"设计令牌体系"章节 |
| `docs/architecture/005_architecture_data.json` | `modules.shared.exports` 新增 `design-tokens` 节点 |
| `docs/troubleshooting/known-issues.md` | 新增"设计令牌"问题分类 |
| 15 文件(91 处 #hex) | 按映射表令牌化或注释豁免 |
| 2 文件(8 处 font-family) | 令牌化为 `var(--font-family-*)` |
| 3 文件(15 处 font-size) | 令牌化为 `var(--font-size-*)` 或注释豁免 |
| 40 文件(98 处任意值) | Tier 1/2/3 分级处理 |
---
## Task 1: 创建 primitive.css(Layer 1 原始阶梯)
**Files:**
- Create: `src/app/styles/tokens/primitive.css`
- [ ] **Step 1: 创建 primitive.css 文件**
```css
/* src/app/styles/tokens/primitive.css */
/* Layer 1: Primitive (Raw Tokens) — 原始色板/字号/间距/阴影/字体家族
* 仅被 Semantic 层引用,业务代码不直接使用。
* 色板层不区分明暗,主题差异在 Semantic 层体现。
*/
:root {
/* ============ 色板 ============ */
/* Zinc 中性色板(shadcn 默认) */
--color-zinc-50: 0 0% 99%;
--color-zinc-100: 240 4.8% 95.9%;
--color-zinc-200: 240 5.9% 90%;
--color-zinc-300: 240 4.8% 83.9%;
--color-zinc-400: 240 5% 64.9%;
--color-zinc-500: 240 3.8% 46.1%;
--color-zinc-600: 240 5.2% 33.9%;
--color-zinc-700: 240 5.3% 26.1%;
--color-zinc-800: 240 5.9% 10%;
--color-zinc-900: 240 5.9% 3.9%;
--color-zinc-950: 240 10% 3.9%;
/* Stone 暖灰(用于 lp-* 纸感) */
--color-stone-50: 60 4.8% 95.9%;
--color-stone-100: 60 5.1% 90%;
--color-stone-200: 20 5.9% 90%;
--color-stone-300: 24 5.7% 82.9%;
--color-stone-400: 24 5.4% 63.9%;
--color-stone-500: 25 5.1% 44.7%;
--color-stone-600: 33 5% 39.8%;
--color-stone-700: 30 5.2% 32.7%;
--color-stone-800: 12 6.5% 31.4%;
--color-stone-900: 24 10% 10%;
--color-stone-950: 20 14.3% 4.1%;
/* Indigo 强调色(lp-interaction 用) */
--color-indigo-500: 238.7 83.5% 66.7%;
--color-indigo-600: 238.6 84.5% 59.8%;
/* ============ 字号阶梯 ============ */
--font-size-1: 12px; /* 元信息/角色标签 */
--font-size-2: 13px; /* inline-node body */
--font-size-3: 13.5px; /* inline-node 主文(备课纸感) */
--font-size-4: 14px; /* 标题/按钮 */
--font-size-5: 16px; /* 正文(Fraunces 16px) */
--font-size-6: 18px; /* H2 */
--font-size-7: 20px; /* H1 */
--font-size-8: 24px; /* 区块标题 */
--font-size-9: 32px; /* 页面标题 */
/* ============ 间距阶梯 ============ */
--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-2_5: 0.625rem; /* 10px */
--space-3: 0.75rem; /* 12px */
--space-3_5: 0.875rem; /* 14px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-7: 1.75rem; /* 28px */
--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);
--shadow-6: 0 10px 15px rgba(15, 15, 15, 0.1), 0 4px 6px rgba(15, 15, 15, 0.05);
/* ============ 字体家族 ============ */
--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;
}
```
- [ ] **Step 2: 验证文件创建**
Run: `Test-Path src/app/styles/tokens/primitive.css`
Expected: `True`
---
## Task 2: 创建 semantic-light.css(`:root` 语义令牌)
**Files:**
- Create: `src/app/styles/tokens/semantic-light.css`
- [ ] **Step 1: 创建 semantic-light.css 文件**
```css
/* src/app/styles/tokens/semantic-light.css */
/* Layer 2: Semantic Tokens (Light / :root)
* 业务代码唯一引用入口。所有令牌都有 .dark 对应定义。
*/
:root {
/* ============ shadcn 标准令牌(原 globals.css 迁移,值不变) ============ */
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
/* ============ chart 令牌(原 globals.css 迁移,值不变) ============ */
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
/* ============ sidebar 令牌(原 globals.css 迁移,值不变) ============ */
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
/* ============ 语义层扩展(新增层级) ============ */
--background-elevated: 0 0% 100%;
--background-sunken: 240 4.8% 95.9%;
--text-primary: 240 10% 3.9%; /* = --foreground */
--text-secondary: 240 3.8% 46.1%; /* = --muted-foreground */
--text-tertiary: 240 4% 65%;
--border-strong: 240 5.9% 70%;
--border-subtle: 240 5.9% 95%;
/* ============ 新增业务语义令牌 ============ */
/* 版本对比 */
--diff-add: 142 71% 45%;
--diff-add-bg: 142 71% 45% / 0.1;
--diff-remove: 0 84% 60%;
--diff-remove-bg: 0 84% 60% / 0.1;
/* 知识图谱节点色 */
--graph-node-1: 12 76% 61%; /* = --chart-1 */
--graph-node-2: 173 58% 39%; /* = --chart-2 */
--graph-node-3: 197 37% 24%; /* = --chart-3 */
--graph-node-4: 43 74% 66%; /* = --chart-4 */
--graph-node-5: 27 87% 67%; /* = --chart-5 */
--graph-node-6: 280 65% 60%;
}
```
- [ ] **Step 2: 验证文件创建**
Run: `Test-Path src/app/styles/tokens/semantic-light.css`
Expected: `True`
---
## Task 3: 创建 semantic-dark.css(`.dark` 语义令牌)
**Files:**
- Create: `src/app/styles/tokens/semantic-dark.css`
- [ ] **Step 1: 创建 semantic-dark.css 文件**
```css
/* src/app/styles/tokens/semantic-dark.css */
/* Layer 2: Semantic Tokens (Dark / .dark)
* 所有令牌都有 :root 对应定义。
*/
.dark {
/* ============ shadcn 标准令牌(原 globals.css 迁移,值不变) ============ */
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
/* ============ chart 令牌(原 globals.css 迁移,值不变) ============ */
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
/* ============ sidebar 令牌(原 globals.css 迁移,值不变) ============ */
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
/* ============ 语义层扩展(新增层级) ============ */
--background-elevated: 240 6% 10%;
--background-sunken: 240 6% 8%;
--text-primary: 0 0% 98%; /* = --foreground */
--text-secondary: 240 5% 64.9%; /* = --muted-foreground */
--text-tertiary: 240 5% 50%;
--border-strong: 240 5% 40%;
--border-subtle: 240 5% 18%;
/* ============ 新增业务语义令牌 ============ */
--diff-add: 142 71% 55%;
--diff-add-bg: 142 71% 55% / 0.15;
--diff-remove: 0 84% 70%;
--diff-remove-bg: 0 84% 70% / 0.15;
--graph-node-1: 220 70% 50%;
--graph-node-2: 160 60% 45%;
--graph-node-3: 30 80% 55%;
--graph-node-4: 280 65% 60%;
--graph-node-5: 340 75% 55%;
--graph-node-6: 200 80% 60%;
}
```
- [ ] **Step 2: 验证文件创建**
Run: `Test-Path src/app/styles/tokens/semantic-dark.css`
Expected: `True`
---
## Task 4: 创建 lesson-preparation.css(--lp-* 明暗双份)
**Files:**
- Create: `src/app/styles/tokens/lesson-preparation.css`
- [ ] **Step 1: 创建 lesson-preparation.css 文件**
```css
/* src/app/styles/tokens/lesson-preparation.css */
/* --lp-* 令牌(lesson-preparation 模块命名空间)
* HEX 已转换为 HSL,与 shadcn 体系统一。
* 暗色已补齐。
*/
:root {
/* 纸感 */
--lp-paper: 0 0% 99.6%;
--lp-paper-edge: 60 9% 97%;
--lp-paper-shadow: var(--shadow-4);
--lp-paper-shadow-active: var(--shadow-5);
/* 锚点 */
--lp-anchor-range: 20 14.3% 4.1% / 0.08;
--lp-anchor-range-active: 20 14.3% 4.1% / 0.16;
--lp-anchor-point: 20 14.3% 4.1%; /* = stone-950 */
/* inline-node */
--lp-inline-node-border: 30 5.7% 82.9%; /* = stone-300 */
--lp-inline-node-text: 24 5.4% 26.9%; /* ≈ stone-700 */
--lp-inline-node-meta: 30 5% 64.9%; /* = stone-400 */
/* 交互强调 */
--lp-interaction: 238.6 84.5% 59.8%; /* = indigo-600 */
/* 节点类型色点(极克制,全部映射到 zinc/stone) */
--lp-dot-objective: 240 3.8% 46.1%; /* = zinc-500 */
--lp-dot-key-point: 0 0% 30%; /* ≈ zinc-700 灰 */
--lp-dot-import: 240 4% 50%; /* = zinc-500 变体 */
--lp-dot-new-teaching: 240 10% 3.9%; /* = zinc-950 */
--lp-dot-consolidation: 240 4% 50%;
--lp-dot-summary: 0 0% 30%;
--lp-dot-homework: 0 0% 30%;
--lp-dot-blackboard: 24 5.4% 26.9%; /* = stone-700 */
--lp-dot-text-study: 240 3.8% 46.1%;
--lp-dot-exercise: 240 4% 50%;
--lp-dot-rich-text: 30 5% 64.9%; /* = stone-400 */
--lp-dot-reflection: 240 4% 50%;
--lp-dot-interaction: 238.6 84.5% 59.8%;/* = indigo-600 */
--lp-dot-textbook: 24 5.4% 26.9%;
--lp-dot-default: 30 5% 64.9%;
}
.dark {
/* 纸感(暗背景) */
--lp-paper: 240 6% 10%;
--lp-paper-edge: 240 6% 8%;
--lp-paper-shadow: var(--shadow-4);
--lp-paper-shadow-active: var(--shadow-5);
/* 锚点(反相) */
--lp-anchor-range: 0 0% 98% / 0.08;
--lp-anchor-range-active: 0 0% 98% / 0.16;
--lp-anchor-point: 0 0% 98%;
/* inline-node */
--lp-inline-node-border: 240 4% 30%;
--lp-inline-node-text: 240 5% 80%;
--lp-inline-node-meta: 240 5% 55%;
/* 交互强调(暗色微亮) */
--lp-interaction: 238.7 83.5% 66.7%; /* = indigo-500 */
/* 节点类型色点(暗色微亮) */
--lp-dot-objective: 240 5% 64.9%;
--lp-dot-key-point: 0 0% 70%;
--lp-dot-import: 240 4% 65%;
--lp-dot-new-teaching: 0 0% 98%;
--lp-dot-consolidation: 240 4% 65%;
--lp-dot-summary: 0 0% 70%;
--lp-dot-homework: 0 0% 70%;
--lp-dot-blackboard: 30 5% 70%;
--lp-dot-text-study: 240 5% 64.9%;
--lp-dot-exercise: 240 4% 65%;
--lp-dot-rich-text: 30 5% 55%;
--lp-dot-reflection: 240 4% 65%;
--lp-dot-interaction: 238.7 83.5% 66.7%;
--lp-dot-textbook: 30 5% 70%;
--lp-dot-default: 30 5% 55%;
}
```
- [ ] **Step 2: 验证文件创建**
Run: `Test-Path src/app/styles/tokens/lesson-preparation.css`
Expected: `True`
---
## Task 5: 创建 tailwind-theme.css(@theme inline 暴露)
**Files:**
- Create: `src/app/styles/tokens/tailwind-theme.css`
- [ ] **Step 1: 创建 tailwind-theme.css 文件**
```css
/* src/app/styles/tokens/tailwind-theme.css */
/* @theme inline: 将所有 Semantic 令牌暴露为 Tailwind 类
* 业务代码使用 bg-background / text-foreground / bg-lp-paper / font-sans 等
*/
@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));
/* ============ chart 颜色 ============ */
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
/* ============ sidebar 颜色 ============ */
--color-sidebar: hsl(var(--sidebar-background));
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
--color-sidebar-primary: hsl(var(--sidebar-primary));
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
--color-sidebar-accent: hsl(var(--sidebar-accent));
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
--color-sidebar-border: hsl(var(--sidebar-border));
--color-sidebar-ring: hsl(var(--sidebar-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-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));
--color-graph-node-1: hsl(var(--graph-node-1));
--color-graph-node-2: hsl(var(--graph-node-2));
--color-graph-node-3: hsl(var(--graph-node-3));
--color-graph-node-4: hsl(var(--graph-node-4));
--color-graph-node-5: hsl(var(--graph-node-5));
--color-graph-node-6: hsl(var(--graph-node-6));
/* ============ lp-* 颜色(lesson-preparation) ============ */
--color-lp-paper: hsl(var(--lp-paper));
--color-lp-paper-edge: hsl(var(--lp-paper-edge));
--color-lp-anchor-point: hsl(var(--lp-anchor-point));
--color-lp-inline-node-border: hsl(var(--lp-inline-node-border));
--color-lp-inline-node-text: hsl(var(--lp-inline-node-text));
--color-lp-inline-node-meta: hsl(var(--lp-inline-node-meta));
--color-lp-interaction: hsl(var(--lp-interaction));
--color-lp-dot-objective: hsl(var(--lp-dot-objective));
--color-lp-dot-key-point: hsl(var(--lp-dot-key-point));
--color-lp-dot-import: hsl(var(--lp-dot-import));
--color-lp-dot-new-teaching: hsl(var(--lp-dot-new-teaching));
--color-lp-dot-consolidation: hsl(var(--lp-dot-consolidation));
--color-lp-dot-summary: hsl(var(--lp-dot-summary));
--color-lp-dot-homework: hsl(var(--lp-dot-homework));
--color-lp-dot-blackboard: hsl(var(--lp-dot-blackboard));
--color-lp-dot-text-study: hsl(var(--lp-dot-text-study));
--color-lp-dot-exercise: hsl(var(--lp-dot-exercise));
--color-lp-dot-rich-text: hsl(var(--lp-dot-rich-text));
--color-lp-dot-reflection: hsl(var(--lp-dot-reflection));
--color-lp-dot-interaction: hsl(var(--lp-dot-interaction));
--color-lp-dot-textbook: hsl(var(--lp-dot-textbook));
--color-lp-dot-default: hsl(var(--lp-dot-default));
/* ============ 字体家族 ============ */
--font-sans: var(--font-family-sans);
--font-serif: var(--font-family-serif);
--font-mono: var(--font-family-mono);
/* ============ 字号阶梯 ============ */
--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) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--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);
--shadow-6: var(--shadow-6);
/* ============ 动效 ============ */
--duration-fast: var(--duration-fast);
--duration-normal: var(--duration-normal);
--duration-slow: var(--duration-slow);
--ease-in: var(--ease-in);
--ease-out: var(--ease-out);
--ease-in-out: var(--ease-in-out);
/* ============ 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);
/* ============ 动画(原 globals.css 迁移) ============ */
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from { height: 0; }
to { height: var(--radix-accordion-content-height); }
}
@keyframes accordion-up {
from { height: var(--radix-accordion-content-height); }
to { height: 0; }
}
}
```
- [ ] **Step 2: 验证文件创建**
Run: `Test-Path src/app/styles/tokens/tailwind-theme.css`
Expected: `True`
---
## Task 6: 创建 index.css(汇总入口)
**Files:**
- Create: `src/app/styles/tokens/index.css`
- [ ] **Step 1: 创建 index.css 文件**
```css
/* src/app/styles/tokens/index.css */
/* 设计令牌汇总入口
* 加载顺序:Primitive → Semantic(明/暗) → 模块命名空间 → Tailwind 暴露
*/
@import "./primitive.css";
@import "./semantic-light.css";
@import "./semantic-dark.css";
@import "./lesson-preparation.css";
@import "./tailwind-theme.css";
```
- [ ] **Step 2: 验证文件创建**
Run: `Test-Path src/app/styles/tokens/index.css`
Expected: `True`
---
## Task 7: 改造 globals.css(@import + 清理 + 令牌化)
**Files:**
- Modify: `src/app/globals.css`
- [ ] **Step 1: 重写 globals.css 顶部 @import 区**
替换 [src/app/globals.css:1-6](file:///e:/Desktop/CICD/src/app/globals.css#L1-L6) 为:
```css
@import "tailwindcss";
@import "./styles/tokens/index.css";
@plugin "tailwindcss-animate";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:where(.dark, .dark *));
```
- [ ] **Step 2: 删除已迁移到 tokens 的内容**
删除 [src/app/globals.css:8-215](file:///e:/Desktop/CICD/src/app/globals.css#L8-L215)(原 `:root` / `.dark` / `@theme inline` 块全部已迁移到 tokens 文件)。
- [ ] **Step 3: 令牌化 lp-paper-toolbar 硬编码**
找到 `.lp-paper-toolbar` 选择器(原约 350 行),替换为:
```css
.lp-paper-toolbar {
position: sticky;
top: 0;
margin: calc(-1 * var(--space-16)) calc(-1 * var(--space-18)) var(--space-6);
padding: calc(var(--space-2) + var(--space-0_5)) var(--space-18);
background: hsl(var(--background) / 0.92);
backdrop-filter: blur(8px);
border-bottom: 1px solid hsl(var(--border));
display: flex;
gap: var(--space-0_5);
z-index: var(--z-sticky);
}
```
- [ ] **Step 4: 令牌化 lp-tb-btn 硬编码**
替换 `.lp-tb-btn` 选择器:
```css
.lp-tb-btn {
width: var(--space-7);
height: var(--space-7);
border: none;
background: transparent;
border-radius: var(--radius-sm);
cursor: pointer;
font-family: var(--font-family-sans);
font-size: var(--font-size-2);
color: hsl(var(--foreground));
display: inline-flex;
align-items: center;
justify-content: center;
transition: background var(--duration-fast);
}
.lp-tb-btn:hover { background: hsl(var(--muted)); }
.lp-tb-btn.is-active { background: hsl(var(--muted)); color: hsl(var(--foreground)); }
.lp-tb-btn.b { font-weight: var(--weight-bold); }
.lp-tb-btn.i { font-style: italic; font-family: var(--font-family-serif); }
.lp-tb-btn.h1 { font-size: var(--font-size-1); font-weight: var(--weight-semibold); }
.lp-tb-btn.h2 { font-size: var(--font-size-1); font-weight: var(--weight-medium); }
.lp-tb-btn.ul { font-size: var(--font-size-3); }
.lp-tb-btn.quote { font-family: var(--font-family-serif); font-style: italic; font-size: var(--font-size-3); }
.lp-tb-divider {
width: var(--space-0_5);
background: hsl(var(--border));
margin: var(--space-1) var(--space-1);
}
```
- [ ] **Step 5: 令牌化 lp-anchor-* 硬编码**
替换 `.lp-anchor-range``.lp-anchor-point`:
```css
.lp-anchor-range {
background: hsl(var(--lp-anchor-range));
border-radius: var(--radius-sm);
padding: var(--space-0_5) var(--space-0_5);
margin: 0 calc(-1 * var(--space-0_5));
cursor: pointer;
border-bottom: 1.5px solid hsl(var(--lp-anchor-point));
transition: background var(--duration-fast);
}
.lp-anchor-range:hover, .lp-anchor-range.is-active {
background: hsl(var(--lp-anchor-range-active));
}
.lp-anchor-point {
display: inline-block;
width: var(--space-4);
height: var(--space-4);
line-height: var(--space-4);
text-align: center;
border-radius: var(--radius-full);
background: hsl(var(--lp-anchor-point));
color: hsl(var(--background));
font-family: var(--font-family-sans);
font-size: var(--font-size-1);
font-weight: var(--weight-semibold);
margin: 0 var(--space-0_5);
vertical-align: middle;
cursor: pointer;
}
```
- [ ] **Step 6: 令牌化 lp-inline-node 硬编码**
替换 `.lp-inline-node` 系列:
```css
.lp-inline-node {
font-family: var(--font-family-sans);
margin: var(--space-5) 0 var(--space-5);
padding: calc(var(--space-3) + var(--space-0_5)) calc(var(--space-4) + var(--space-2)) calc(var(--space-3) + var(--space-0_5)) var(--space-5);
border-left: 2px solid hsl(var(--lp-inline-node-border));
color: hsl(var(--lp-inline-node-text));
font-size: var(--font-size-3);
line-height: var(--leading-relaxed);
transition: border-color var(--duration-normal);
}
.lp-inline-node:hover { border-left-color: hsl(var(--foreground)); }
.lp-inline-node-head {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-2);
font-size: var(--font-size-1);
font-weight: var(--weight-semibold);
text-transform: uppercase;
letter-spacing: 0.08em;
color: hsl(var(--lp-inline-node-meta));
}
.lp-inline-node-title {
font-family: var(--font-family-sans);
font-weight: var(--weight-semibold);
font-size: var(--font-size-4);
color: hsl(var(--lp-anchor-point));
margin: 0 0 var(--space-1_5);
line-height: var(--leading-snug);
}
.lp-inline-node-body {
font-size: var(--font-size-2);
line-height: var(--leading-normal);
color: hsl(var(--lp-inline-node-text));
}
```
- [ ] **Step 7: 令牌化 lp-qa-* 硬编码**
替换 `.lp-qa-*` 系列:
```css
.lp-qa-dialog { margin: var(--space-2) 0 var(--space-1); }
.lp-qa-turn {
display: grid;
grid-template-columns: calc(var(--space-6) + var(--space-2)) 1fr;
gap: calc(var(--space-2) + var(--space-0_5));
padding: var(--space-2) 0;
border-bottom: 1px dashed hsl(var(--border));
font-size: var(--font-size-1);
line-height: var(--leading-relaxed);
}
.lp-qa-turn:last-child { border-bottom: none; }
.lp-qa-role {
font-family: var(--font-family-mono);
font-size: 9px;
font-weight: var(--weight-semibold);
text-transform: uppercase;
letter-spacing: 0.05em;
padding-top: var(--space-0_5);
text-align: right;
}
.lp-qa-role.teacher { color: hsl(var(--lp-anchor-point)); }
.lp-qa-role.student { color: hsl(var(--color-zinc-500)); }
.lp-qa-content { color: hsl(var(--lp-inline-node-text)); }
.lp-qa-prompt {
font-style: italic;
color: hsl(var(--color-zinc-600));
font-size: var(--font-size-1);
display: block;
margin-top: var(--space-0_5);
}
```
- [ ] **Step 8: 令牌化 range-anchor / point-anchor / anchor-edge 硬编码**
替换 `.range-anchor` 等(注意 fallback 改为令牌):
```css
.range-anchor {
background-color: var(--node-color, hsl(var(--lp-interaction)));
opacity: 0;
transition: opacity var(--duration-normal);
border-radius: var(--radius-sm);
cursor: pointer;
padding: 0 var(--space-0_5);
}
.range-anchor.active { opacity: 0.3; }
.range-anchor:hover { opacity: 0.5; }
.point-anchor {
background-color: var(--node-color, hsl(var(--lp-interaction)));
color: hsl(var(--background));
opacity: 0.3;
transition: opacity var(--duration-normal);
border-radius: var(--radius-full);
cursor: pointer;
padding: 0 var(--space-1);
margin: 0 var(--space-0_5);
font-size: 0.85em;
font-weight: var(--weight-semibold);
display: inline-block;
min-width: 1.2em;
text-align: center;
line-height: 1.2;
user-select: none;
}
.point-anchor.active { opacity: 1; }
.point-anchor:hover { opacity: 0.8; }
.anchor-edge {
opacity: 0.1;
transition: opacity var(--duration-normal);
}
.anchor-edge.active { opacity: 1; }
```
- [ ] **Step 9: 验证 globals.css 行数**
Run: `(Get-Content src/app/globals.css | Measure-Object -Line).Lines`
Expected: 应从原 477 行降至约 200 行以下(打印样式 + 组件硬编码样式令牌化后)
- [ ] **Step 10: 验证 dev 启动**
Run: `npm run dev`(后台运行,5 秒后停止)
Expected: 无 CSS 编译错误
---
## Task 8: 清理 91 处 #hex 颜色硬编码
**Files:**
- Modify: `src/app/manifest.ts`(2 处,注释豁免)
- Modify: `src/app/layout.tsx`(2 处,检查后令牌化或注释)
- Modify: `src/shared/components/ui/chart.tsx`(5 处,令牌化为 `--chart-*`)
- Modify: `src/modules/notifications/channels/email-channel.ts`(7 处,注释豁免)
- Modify: `src/modules/textbooks/components/knowledge-graph.tsx`(10 处,令牌化为 `--graph-node-*`)
- Modify: `src/modules/textbooks/components/force-graph.tsx`(17 处,令牌化为 `--graph-node-*`)
- Modify: `src/modules/lesson-preparation/components/paper-editor/paper-context-menu.tsx`
- Modify: `src/modules/lesson-preparation/components/paper-editor/paper-editor.tsx`
- Modify: `src/modules/lesson-preparation/components/paper-editor/textbook-tiptap-editor.tsx`
- Modify: `src/modules/lesson-preparation/components/paper-editor/inline-node.tsx`
- Modify: `src/modules/lesson-preparation/components/paper-editor/inline-qa-dialog.tsx`
- Modify: `src/modules/lesson-preparation/components/structure-tree/tree-node-row.tsx`
- Modify: `src/modules/lesson-preparation/components/detail-panel/detail-panel.tsx`
- Modify: `src/modules/lesson-preparation/components/detail-panel/detail-head.tsx`
- Modify: `src/modules/lesson-preparation/components/anchor-migration-banner.tsx`(5 处)
- Modify: `src/modules/lesson-preparation/components/version-diff-view.tsx`(1 处,令牌化为 `--diff-*`)
- Modify: `src/modules/attendance/components/attendance-grade-correlation-card.tsx`(3 处,令牌化为 `--chart-*`)
- Modify: `src/modules/classes/components/my-classes-grid.tsx`(2 处)
- [ ] **Step 1: 读取每个文件,定位 #hex 字面量**
对上述每个文件运行 Grep 搜索 `#[0-9a-fA-F]{3,8}\b`,记录每处行号与上下文。
- [ ] **Step 2: 注释豁免 manifest.ts**
在 [`src/app/manifest.ts`](file:///e:/Desktop/CICD/src/app/manifest.ts) 中所有 `#hex` 上方加注释:
```typescript
// arbitrary-value: PWA manifest requires literal hex
themeColor: "#1976d2",
```
- [ ] **Step 3: 注释豁免 email-channel.ts**
在 [`src/modules/notifications/channels/email-channel.ts`](file:///e:/Desktop/CICD/src/modules/notifications/channels/email-channel.ts) 中所有 `#hex` 上方加注释:
```typescript
// arbitrary-value: email HTML inline style requires literal hex
```
- [ ] **Step 4: 令牌化 chart.tsx**
在 [`src/shared/components/ui/chart.tsx`](file:///e:/Desktop/CICD/src/shared/components/ui/chart.tsx) 中,将 `#xxx` 替换为 `hsl(var(--chart-N))` 形式。例如:
```typescript
// 旧:fill="#12 76% 61%" 或 fill="#e11d48"
// 新:fill={`hsl(var(--chart-1))`} 或 fill="hsl(var(--chart-1))"
```
- [ ] **Step 5: 令牌化 knowledge-graph.tsx 与 force-graph.tsx**
在 [`src/modules/textbooks/components/knowledge-graph.tsx`](file:///e:/Desktop/CICD/src/modules/textbooks/components/knowledge-graph.tsx) 与 [`force-graph.tsx`](file:///e:/Desktop/CICD/src/modules/textbooks/components/force-graph.tsx) 中,将节点色 `#xxx` 替换为 `hsl(var(--graph-node-N))`
- [ ] **Step 6: 令牌化 lesson-preparation/components/paper-editor/* 与 detail-panel/* 与 structure-tree/* 与 anchor-migration-banner.tsx**
将所有 `#hex` 替换为对应的 `hsl(var(--lp-*))``hsl(var(--color-zinc-N))` / `hsl(var(--color-stone-N))`。具体映射按 spec 4.4 节表。
- [ ] **Step 7: 令牌化 version-diff-view.tsx**
`#hex` 替换为 `hsl(var(--diff-add))``hsl(var(--diff-remove))`
- [ ] **Step 8: 令牌化 attendance-grade-correlation-card.tsx**
`#hex` 替换为 `hsl(var(--chart-N))`
- [ ] **Step 9: 令牌化 my-classes-grid.tsx**
`#hex` 替换为对应令牌。
- [ ] **Step 10: 检查 layout.tsx**
读取 [`src/app/layout.tsx`](file:///e:/Desktop/CICD/src/app/layout.tsx),若 `#hex` 用于 metadata themeColor 等字面量场景,加注释豁免;否则令牌化。
- [ ] **Step 11: 验证 #hex 残留(白名单外)**
Run: `npx eslint src/app/manifest.ts src/modules/notifications/channels/email-channel.ts --rule '{"no-restricted-syntax":["error",{"selector":"Literal[value=/#[0-9a-fA-F]{3,8}/]","message":"hex"}]}'`
Expected: 仅白名单文件有 hex,其它文件零 hex
---
## Task 9: 清理 8 处硬编码 font-family
**Files:**
- Modify: `src/modules/lesson-preparation/components/paper-editor/textbook-tiptap-editor.tsx`(1 处)
注:`app/globals.css` 中 7 处已在 Task 7 令牌化。
- [ ] **Step 1: 令牌化 textbook-tiptap-editor.tsx**
在 [`src/modules/lesson-preparation/components/paper-editor/textbook-tiptap-editor.tsx`](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/paper-editor/textbook-tiptap-editor.tsx) 中搜索 `font-family.*['\"]Inter['\"]|['\"]Fraunces['\"]|['\"]JetBrains Mono['\"]`,替换为:
```typescript
// 旧:fontFamily: "'Inter', sans-serif"
// 新:fontFamily: "var(--font-family-sans)"
```
- [ ] **Step 2: 验证 font-family 残留**
Run: Grep 搜索 `'Inter'|'Fraunces'|'JetBrains Mono'``src/`
Expected: 仅 `src/app/styles/tokens/primitive.css` 有定义
---
## Task 10: 清理 15 处硬编码 font-size
**Files:**
- Modify: `src/modules/lesson-preparation/components/paper-editor/textbook-tiptap-editor.tsx`(1 处)
注:`app/globals.css` 中 13 处已在 Task 7 令牌化;`email-channel.ts` 1 处注释豁免。
- [ ] **Step 1: 注释豁免 email-channel.ts font-size**
在 [`src/modules/notifications/channels/email-channel.ts`](file:///e:/Desktop/CICD/src/modules/notifications/channels/email-channel.ts) 中所有 `font-size: Npx` 上方加注释:
```typescript
// arbitrary-value: email HTML inline style requires literal px
```
- [ ] **Step 2: 令牌化 textbook-tiptap-editor.tsx font-size**
在 [`textbook-tiptap-editor.tsx`](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/paper-editor/textbook-tiptap-editor.tsx) 中搜索 `fontSize.*\d+px`,替换为对应 `var(--font-size-N)`
- [ ] **Step 3: 验证 font-size 残留**
Run: Grep 搜索 `font-size:\s*\d+(\.\d+)?px``src/`
Expected: 仅 `email-channel.ts``primitive.css`
---
## Task 11: 清理 98 处 Tailwind 任意值(分级)
**Files:** 见 spec 6.4 节,40 个文件
- [ ] **Step 1: Tier 1 — 直接映射 Tailwind 默认阶梯**
读取 Grep 结果(`w-\[\d+px\]|h-\[\d+px\]|text-\[\d+px\]|p-\[\d+px\]|m-\[\d+px\]|gap-\[\d+px\]`),对每个匹配:
- `w-[28px]``w-7`
- `w-[8px]``w-2`
- `h-[16px]``h-4`
- `h-[28px]``h-7`
- `gap-[6px]``gap-1.5`
- `gap-[8px]``gap-2`
- `p-[8px]``p-2`
- `p-[10px]``p-2.5`(Tailwind v4 支持)
- `m-[8px]``m-2`
- `text-[12px]``text-xs`
- `text-[13px]``text-size-2`
- `text-[14px]``text-sm`
逐文件替换,使用 Edit 工具。
- [ ] **Step 2: Tier 2 — 新增间距令牌后引用**
对无法直接映射的尺寸,使用 `--space-*` 令牌:
- `w-[72px]``w-[length:var(--space-18)]`
- `p-[20px]``p-[length:var(--space-5)]`
- `m-[64px]``m-[length:var(--space-16)]`
- `px-[72px]``px-[length:var(--space-18)]`
注:Tailwind v4 任意值引用 CSS 变量需 `[length:var(--*)]` 语法。
- [ ] **Step 3: Tier 3 — 注释豁免**
对图表 SVG 固定画布、loading.tsx 占位骨架等,在任意值上方加注释:
```typescript
// arbitrary-value: chart canvas fixed size
<div className="w-[400px] h-[300px]" />
```
- [ ] **Step 4: 验证任意值残留**
Run: Grep 搜索 `w-\[\d+px\]|h-\[\d+px\]|p-\[\d+px\]|m-\[\d+px\]|gap-\[\d+px\]|text-\[\d+px\]``src/`
Expected: 仅 Tier 3 注释豁免的残留
---
## Task 12: 新增 ESLint 强制约束规则
**Files:**
- Create: `eslint-rules/no-hardcoded-design-tokens.js`
- Modify: `eslint.config.mjs`
- [ ] **Step 1: 创建自定义 ESLint 规则文件**
```javascript
// eslint-rules/no-hardcoded-design-tokens.js
// 检测 TSX/TS 中硬编码的字体家族字面量(Inter/Fraunces/JetBrains Mono)
// 白名单:src/app/styles/tokens/primitive.css(令牌定义)、email-channel.ts、manifest.ts
const FORBIDDEN_FONTS = ['Inter', 'Fraunces', 'JetBrains Mono'];
const WHITELIST_FILES = [
'eslint-rules/no-hardcoded-design-tokens.js',
'src/app/styles/tokens/primitive.css',
'src/modules/notifications/channels/email-channel.ts',
'src/app/manifest.ts',
];
module.exports = {
meta: {
type: 'problem',
docs: {
description: '禁止硬编码字体家族字面量,使用 var(--font-family-sans/serif/mono)',
category: 'Best Practices',
recommended: true,
},
messages: {
forbiddenFont:
'禁止硬编码字体家族 "{{font}}",使用 var(--font-family-sans/serif/mono) 或 var(--font-family-mono)',
},
schema: [],
},
create(context) {
const filename = context.getFilename().replace(/\\/g, '/');
if (WHITELIST_FILES.some((f) => filename.endsWith(f))) {
return {};
}
return {
Literal(node) {
if (typeof node.value !== 'string') return;
for (const font of FORBIDDEN_FONTS) {
if (node.value.includes(font)) {
context.report({
node,
messageId: 'forbiddenFont',
data: { font },
});
break;
}
}
},
TemplateElement(node) {
const raw = node.value.raw;
for (const font of FORBIDDEN_FONTS) {
if (raw.includes(font)) {
context.report({
node,
messageId: 'forbiddenFont',
data: { font },
});
break;
}
}
},
};
},
};
```
- [ ] **Step 2: 修改 eslint.config.mjs 加载自定义规则与 tailwindcss 规则**
替换 [`eslint.config.mjs`](file:///e:/Desktop/CICD/eslint.config.mjs) 全文:
```javascript
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
{
rules: {
"react-hooks/incompatible-library": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
// 禁止硬编码 hex 颜色字面量(白名单:tokens 定义文件、邮件、manifest)
"no-restricted-syntax": [
"error",
{
selector: "Literal[value=/#[0-9a-fA-F]{3,8}/]",
message:
"禁止硬编码 hex 颜色,使用设计令牌 hsl(var(--*)) 或 Tailwind 类 bg-*",
},
],
},
},
{
files: ["tests/**/*.ts"],
languageOptions: {
globals: {
describe: "readonly",
it: "readonly",
test: "readonly",
expect: "readonly",
beforeAll: "readonly",
afterAll: "readonly",
beforeEach: "readonly",
afterEach: "readonly",
vi: "readonly",
},
},
},
// 自定义规则:检测硬编码字体家族字面量
{
plugins: {
"design-tokens": {
rules: {
"no-hardcoded-fonts": await import(
join(__dirname, "eslint-rules/no-hardcoded-design-tokens.js")
).then((m) => m.default ?? m),
},
},
},
rules: {
"design-tokens/no-hardcoded-fonts": "error",
},
},
// Override default ignores of eslint-config-next.
globalIgnores([
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
"docs/scripts/**",
"playwright-report/**",
"test-results/**",
"tests/webapp/debug_drizzle.js",
"scripts/**/*.js",
]),
]);
export default eslintConfig;
```
注:`eslint-plugin-tailwindcss` 与 Tailwind v4 兼容性未经验证,本期暂不引入。任意值约束通过自定义规则或 code review 保障,后续可单独评估。
- [ ] **Step 3: 验证 ESLint 配置加载**
Run: `npm run lint -- --print-config src/app/layout.tsx`
Expected: 无配置加载错误
---
## Task 13: 更新项目规则文档
**Files:**
- Modify: `.trae/rules/project_rules.md`
- [ ] **Step 1: 在 "Tailwind 规范" 章节后追加 "设计令牌规范(强制)"**
在 [`.trae/rules/project_rules.md`](file:///e:/Desktop/CICD/.trae/rules/project_rules.md) "Tailwind 规范" 章节末尾追加:
```markdown
### 设计令牌规范(强制)
- **禁止硬编码颜色**: TSX/TS/CSS 中不得出现 `#hex` 颜色字面量,统一使用 `hsl(var(--*))` 或 Tailwind 类 `bg-*`
- **禁止硬编码字体**: 不得出现 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量,使用 `var(--font-family-sans/serif/mono)`
- **禁止硬编码字号**: 不得出现 `font-size: Npx`,使用 `var(--font-size-1~9)`
- **禁止 Tailwind 任意值**: 不得使用 `w-[Npx]`/`h-[Npx]`/`p-[Npx]` 等,映射到 `--space-*` 或 Tailwind 默认阶梯
- **豁免场景**(需 `// arbitrary-value: <reason>` 注释):
- PWA manifest(`src/app/manifest.ts`)
- 邮件 HTML 内联样式(`src/modules/notifications/channels/email-channel.ts`)
- 图表 SVG 固定画布尺寸
- loading.tsx 占位骨架
- **令牌文件分布**: `src/app/styles/tokens/`(primitive/semantic-light/semantic-dark/lesson-preparation/tailwind-theme/index)
- **令牌分层**:
- Layer 1 Primitive(`primitive.css`):原始色板/字号/间距/阴影,业务代码不直接引用
- Layer 2 Semantic(`semantic-light.css` + `semantic-dark.css`):语义令牌,业务代码唯一引用入口
- 模块命名空间(`lesson-preparation.css`):`--lp-*` 令牌,明暗双份
- Tailwind 暴露(`tailwind-theme.css`):`@theme inline` 将 Semantic 令牌暴露为 `bg-*`/`text-*`/`font-*`
- **改令牌必同步图**: 修改令牌定义后,同步更新 `docs/architecture/004``005`
```
- [ ] **Step 2: 验证文件更新**
Run: `Test-Path .trae/rules/project_rules.md`
Expected: `True`
---
## Task 14: 同步架构图 004
**Files:**
- Modify: `docs/architecture/004_architecture_impact_map.md`
- [ ] **Step 1: 在 shared/ 章节后追加 "设计令牌体系" 章节**
在 [`docs/architecture/004_architecture_impact_map.md`](file:///e:/Desktop/CICD/docs/architecture/004_architecture_impact_map.md) 1.1 分层架构图的 shared/ 描述后追加:
```markdown
## 1.1.2 设计令牌体系(2026-07-04 新增)
项目采用**双层令牌架构**(Primitive + Semantic),Tailwind v4 通过 `@theme inline` 暴露为业务可用的类。
### 文件分布
```
src/app/
├─ globals.css # @import 入口 + base layer + 组件硬编码样式
└─ styles/tokens/
├─ primitive.css # Layer 1: 原始色板(zinc/stone/indigo)、字号/间距/阴影/字体家族阶梯
├─ semantic-light.css # Layer 2: :root 语义令牌(shadcn + 扩展 + chart/sidebar + diff/graph)
├─ semantic-dark.css # Layer 2: .dark 语义令牌(完整暗色)
├─ lesson-preparation.css # --lp-* 令牌(明暗双份,HEX→HSL)
├─ tailwind-theme.css # @theme inline 暴露所有令牌为 Tailwind 类
└─ index.css # 汇总 @import 入口
```
### 令牌分类
| 分类 | 命名 | 暴露为 Tailwind 类 |
|---|---|---|
| shadcn 标准 | `--background`/`--foreground`/`--primary`/... | `bg-background`/`text-foreground`/`bg-primary`/... |
| 扩展层级 | `--background-elevated`/`--text-secondary`/`--border-strong`/... | `bg-background-elevated`/`text-text-secondary`/... |
| chart | `--chart-1~5` | `bg-chart-1`/... |
| sidebar | `--sidebar-*` | `bg-sidebar`/`text-sidebar-foreground`/... |
| lp-* 命名空间 | `--lp-paper`/`--lp-inline-node-text`/`--lp-dot-*` | `bg-lp-paper`/`text-lp-inline-node-text`/... |
| 字体家族 | `--font-family-sans/serif/mono` | `font-sans`/`font-serif`/`font-mono` |
| 字号阶梯 | `--font-size-1~9` | `text-size-1`~`text-size-9` |
| 间距阶梯 | `--space-0~18` | `w-[length:var(--space-N)]` 等 |
| 阴影阶梯 | `--shadow-1~6` | `shadow-1`~`shadow-6` |
| 圆角阶梯 | `--radius-sm/md/lg/xl/2xl/full` | `rounded-sm`/`rounded-md`/... |
| 动效 | `--duration-*`/`--ease-*` | `duration-fast`/`ease-in`/... |
| z-index | `--z-dropdown/sticky/modal/popover/toast` | `z-dropdown`/`z-modal`/... |
### 强制约束
- ESLint 规则 `no-restricted-syntax` 禁止 `#hex` 字面量
- 自定义规则 `design-tokens/no-hardcoded-fonts` 禁止 `'Inter'`/`'Fraunces'`/`'JetBrains Mono'` 字面量
- 白名单:`primitive.css`(令牌定义)、`email-channel.ts`(邮件 HTML)、`manifest.ts`(PWA)
- 任意值 `w-[Npx]` 等需通过 `// arbitrary-value: <reason>` 注释豁免
```
---
## Task 15: 同步架构图 005
**Files:**
- Modify: `docs/architecture/005_architecture_data.json`
- [ ] **Step 1: 在 modules.shared.exports 中新增 design-tokens 节点**
读取 [`docs/architecture/005_architecture_data.json`](file:///e:/Desktop/CICD/docs/architecture/005_architecture_data.json),在 `modules.shared.exports` 数组中追加:
```json
{
"name": "design-tokens",
"type": "css-tokens",
"path": "src/app/styles/tokens/",
"files": [
"primitive.css",
"semantic-light.css",
"semantic-dark.css",
"lesson-preparation.css",
"tailwind-theme.css",
"index.css"
],
"layers": [
{
"name": "primitive",
"description": "原始色板/字号/间距/阴影/字体家族,业务代码不直接引用",
"tokens": [
"--color-zinc-50 ~ 950",
"--color-stone-50 ~ 950",
"--color-indigo-500/600",
"--font-size-1 ~ 9",
"--space-0 ~ 18",
"--shadow-1 ~ 6",
"--font-family-sans/serif/mono",
"--leading-tight/snug/normal/relaxed/loose",
"--weight-regular/medium/semibold/bold",
"--duration-fast/normal/slow",
"--ease-in/out/in-out",
"--z-dropdown/sticky/modal/popover/toast"
]
},
{
"name": "semantic",
"description": "语义令牌,业务代码唯一引用入口,明暗双份",
"tokens": [
"--background/foreground/card/popover/primary/secondary/muted/accent/destructive/border/input/ring/radius",
"--background-elevated/sunken",
"--text-primary/secondary/tertiary",
"--border-strong/subtle",
"--chart-1 ~ 5",
"--sidebar-*",
"--diff-add/add-bg/remove/remove-bg",
"--graph-node-1 ~ 6"
]
},
{
"name": "lesson-preparation",
"description": "--lp-* 命名空间令牌,明暗双份",
"tokens": [
"--lp-paper/paper-edge/paper-shadow/paper-shadow-active",
"--lp-anchor-range/range-active/point",
"--lp-inline-node-border/text/meta",
"--lp-interaction",
"--lp-dot-objective/key-point/import/new-teaching/consolidation/summary/homework/blackboard/text-study/exercise/rich-text/reflection/interaction/textbook/default"
]
}
],
"tailwindExposure": "tailwind-theme.css @theme inline",
"constraints": {
"eslint": [
"no-restricted-syntax: 禁止 #hex 字面量",
"design-tokens/no-hardcoded-fonts: 禁止 Inter/Fraunces/JetBrains Mono 字面量"
],
"whitelist": [
"src/app/styles/tokens/primitive.css",
"src/modules/notifications/channels/email-channel.ts",
"src/app/manifest.ts"
]
}
}
```
- [ ] **Step 2: 验证 JSON 合法性**
Run: `npx eslint docs/architecture/005_architecture_data.json``Get-Content docs/architecture/005_architecture_data.json | ConvertFrom-Json`
Expected: 无 JSON 解析错误
---
## Task 16: 更新 known-issues.md
**Files:**
- Modify: `docs/troubleshooting/known-issues.md`
- [ ] **Step 1: 在文件末尾追加 "设计令牌" 章节**
```markdown
## 设计令牌
| 规则 | 正确写法 | 错误写法 |
|------|---------|---------|
| 颜色必须用令牌 | `color: hsl(var(--foreground))``className="bg-background"` | `color: "#1c1917"` |
| 字体必须用令牌 | `fontFamily: "var(--font-family-sans)"` | `fontFamily: "'Inter', sans-serif"` |
| 字号必须用令牌 | `fontSize: "var(--font-size-3)"` | `fontSize: "13.5px"` |
| 间距优先 Tailwind 默认阶梯 | `className="w-7 p-2 gap-1.5"` | `className="w-[28px] p-[8px] gap-[6px]"` |
| 非标准尺寸用 --space-* 令牌 | `className="w-[length:var(--space-18)]"` | `className="w-[72px]"` |
| lp-* 必须有暗色 | `.dark { --lp-paper: 240 6% 10%; }` | 仅 :root 定义,无 .dark |
| 任意值需注释豁免 | `// arbitrary-value: chart canvas fixed size`<br>`<div className="w-[400px]" />` | `<div className="w-[400px]" />`(无注释) |
| M3 Surface 令牌已删除 | `bg-background-elevated``bg-card` | `bg-surface` / `bg-surface-container-low`(已清理) |
| Tailwind v4 @theme inline 暴露 | `--color-lp-paper: hsl(var(--lp-paper));` 后用 `bg-lp-paper` | 直接 `style={{ background: "var(--lp-paper)" }}`(可用但非首选) |
| ESLint 白名单文件可豁免 | `src/app/manifest.ts``email-channel.ts``// arbitrary-value: PWA manifest requires literal hex` | 其它文件直接写 `#hex`(ESLint 报错) |
```
---
## Task 17: 验证与提交
- [ ] **Step 1: 运行 TypeScript 检查**
Run: `npx tsc --noEmit`
Expected: 零错误
- [ ] **Step 2: 运行 ESLint**
Run: `npm run lint`
Expected: 零错误零警告
- [ ] **Step 3: 运行视觉回归测试**
Run: `npm run test:visual`
Expected: 快照无差异(或更新后人工确认)
- [ ] **Step 4: 手动验证备课编辑器**
启动 dev server,访问 `teacher/lesson-plans/[id]/edit`,检查:
- 纸感样式(lp-paper)无变化
- inline-node 样式无变化
- 师生对话样式无变化
- 切换暗色主题,所有元素可读
- [ ] **Step 5: 手动验证暗色模式**
切换暗色主题,抽查:
- dashboard 首页
- button/card/dialog/table 组件
- 备课编辑器
- 知识图谱(textbooks)
- [ ] **Step 6: 暂存所有变更**
Run: `git add src/app/globals.css src/app/styles/tokens/ eslint-rules/ eslint.config.mjs .trae/rules/project_rules.md docs/architecture/004_architecture_impact_map.md docs/architecture/005_architecture_data.json docs/troubleshooting/known-issues.md`
注:Task 8-11 修改的文件也需逐一 `git add`(用 `git status` 查看完整清单)
- [ ] **Step 7: 单次 commit**
Run:
```bash
git commit -m "$(cat <<'EOF'
refactor(design-tokens): 全量体系化重建设计令牌
- 新建 src/app/styles/tokens/ 双层令牌架构(Primitive + Semantic)
- --lp-* 令牌 HEX→HSL,补全暗色,暴露到 Tailwind @theme inline
- 清理 M3 Surface 死代码(零使用)
- 清理 91 处硬编码 #hex 颜色
- 清理 8 处硬编码 font-family
- 清理 15 处硬编码 font-size
- 清理 98 处 Tailwind 任意值(分级:映射/令牌化/注释豁免)
- 新增 ESLint 规则:no-restricted-syntax 禁止 #hex + 自定义 no-hardcoded-fonts
- 项目规则新增"设计令牌规范(强制)"章节
- 同步架构图 004/005 与 known-issues.md
Spec: docs/superpowers/specs/2026-07-04-design-tokens-refactor-design.md
Plan: docs/superpowers/plans/2026-07-04-design-tokens-refactor.md
EOF
)"
```
- [ ] **Step 8: 验证 commit 成功**
Run: `git log -1 --stat`
Expected: 显示 commit 与所有变更文件
---
## Self-Review
### Spec 覆盖检查
| Spec 章节 | 对应 Task |
|---|---|
| 1. 背景与目标 | 全任务上下文 |
| 2. 令牌架构(双层 + 文件分布) | Task 1-6 |
| 3. Primitive 层(色板/字号/间距/阴影/字体) | Task 1 |
| 4. Semantic 层(shadcn + 扩展 + lp-* + M3 清理) | Task 2-4, 7 |
| 5. 尺度令牌补齐(圆角/行高/字重/动效/z-index) | Task 1(primitive 定义) + Task 5(tailwind 暴露) |
| 6.1 91 处 #hex 清理 | Task 8 |
| 6.2 8 处 font-family 清理 | Task 9 |
| 6.3 15 处 font-size 清理 | Task 10 |
| 6.4 98 处任意值清理 | Task 11 |
| 7. ESLint 强制约束 | Task 12 |
| 7.2 项目规则更新 | Task 13 |
| 8.1 验证清单 | Task 17 Step 1-5 |
| 8.2 架构图同步 | Task 14, 15 |
| 8.3 known-issues 更新 | Task 16 |
| 8.3 大爆炸式单 PR | Task 17 Step 6-8 |
### 占位符扫描
无 TBD/TODO,所有步骤含完整代码或具体命令。✓
### 类型一致性
- `--space-N` 命名在 primitive.css、tailwind-theme.css、Task 7 令牌化中一致 ✓
- `--font-size-N` 命名在 primitive.css、tailwind-theme.css、Task 9/10 令牌化中一致 ✓
- `--lp-*` 命名在 lesson-preparation.css、tailwind-theme.css、Task 7 令牌化中一致 ✓
- ESLint 规则名 `design-tokens/no-hardcoded-fonts` 在 Task 12 与 Task 14 架构图描述中一致 ✓
### 风险点
- **Tailwind v4 任意值引用 CSS 变量语法**:Task 11 Step 2 使用 `[length:var(--space-N)]`,需在 Task 17 Step 3 视觉回归测试验证。若不生效,回退为 Tailwind 默认阶梯或 inline style。
- **ESLint config 顶层 await**:`eslint.config.mjs` 使用 `await import(...)` 加载自定义规则,需 Node.js 18+ 支持。项目 Next.js 16 要求 Node 18+,符合。
- **`--lp-paper-shadow: var(--shadow-4)` 嵌套引用**:Tailwind v4 `@theme inline``--shadow-4: var(--shadow-4)` 自引用可能有问题,需在 Task 17 验证。若有问题,改为直接展开阴影值。
---
## 执行说明
本计划为**大爆炸式单 PR**,所有 Task 完成后单次 commit(Task 17 Step 7)。Task 之间无独立 commit,但每个 Task 内部步骤应独立验证,确保问题早发现。