# 设计令牌专项重构 · 实施计划 > **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
``` - [ ] **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: