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(均为既有问题)
1537 lines
54 KiB
Markdown
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 内部步骤应独立验证,确保问题早发现。
|