关于 tokens
tokens 是用于审计硬编码设计值并建立分层设计令牌系统的编码 Agent 技能,涵盖令牌映射、替换、视觉检查与结果报告。
主要功能
- 01审计硬编码颜色、间距和字体
- 02组织基础、语义及组件令牌
- 03将代码中的值替换为令牌
- 04对比修改前后的截图
- 05报告令牌清单与未解决值
适用场景
- 01统一代码库中分散的样式值
- 02将重复硬编码值改为命名令牌
- 03检查桌面端与移动端的视觉变化
- 04记录令牌化进度及遗留值
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dawitlabs/ ui-skills --skill tokens安装后在 Claude Code 中输入/tokens确认已加载。
附带文件SYSTEMS.md
详细介绍
/tokens — 设计令牌系统
审计代码库中所有硬编码的设计值,构建或强制实施三层令牌系统(原始值 → 语义 → 组件),并在整个代码库中用令牌替换硬编码值。
令牌架构和命名规范:SYSTEMS.md
如果项目根目录中存在 DESIGN-BRIEF.md,请在开始前阅读。设计简报中的 Color System 和 Typography 字段提供了原始令牌的确切起点——直接使用其中的十六进制颜色值和字体名称,不要从头推导。
阶段 0 — 环境设置
0a. 查找源码根目录
find . -type d -name "src" \
-not -path "*/node_modules/*" \
-not -path "*/.next/*" \
-not -path "*/dist/*" \
-not -path "*/.turbo/*" \
| head -1
如果没有 src/:find . -type d -name "app" -not -path "*/node_modules/*" -not -path "*/.next/*" | head -1
在下文中始终将结果用作 <SRC>。
0b. 检测 CSS-in-JS
grep -r "styled-components\|@emotion\|@stitches\|vanilla-extract\|linaria" package.json packages/*/package.json apps/*/package.json 2>/dev/null | head -5
如果找到,则阶段 1 中搜索颜色和间距的命令也必须扫描 .ts 文件和模板字面量——阶段 1 的命令会在需要时包含这些内容。记录正在使用的库。
0c. 检测现有令牌系统
运行以下所有命令,并记录哪些命令有结果:
# CSS custom properties (most common)
find . -name "globals.css" -o -name "variables.css" -o -name "tokens.css" | grep -v node_modules
# Tailwind config
find . -name "tailwind.config.*" -not -path "*/node_modules/*"
# Style Dictionary / tokens.json
find . -name "tokens.json" -o -name "design-tokens.json" | grep -v node_modules
find . -type d -name "tokens" -not -path "*/node_modules/*"
# shadcn/ui pattern (CSS vars in globals.css under :root)
grep -rn ":root" . --include="*.css" -l | grep -v node_modules | head -5
# Existing token usage (CSS vars already in use)
grep -rn "var(--" "$SRC" --include="*.tsx" --include="*.css" | head -10
将结果对应到以下其中一个令牌存放位置——所有修复都在此处进行:
- CSS 自定义属性 →
globals.css的:root块 - Tailwind
theme.extend→tailwind.config.ts - Style Dictionary →
tokens/目录中的 JSON 文件 - 无 → 创建包含
:root块的globals.css(参见 SYSTEMS.md)
0d. 记录当前令牌集
读取在 0b 中找到的令牌存放位置文件。列出每个现有令牌名称及其值。这是基准——新令牌不得与这些令牌冲突或重复。
0e. 检查/启动开发服务器(用于视觉验证)
curl -s --max-time 2 -o /dev/null -w "%{http_code}" http://localhost:3000
如果状态不是 200,启动开发命令并等待:
until curl -s --max-time 2 http://localhost:3000 > /dev/null 2>&1; do sleep 1; done && echo "ready"
0f. 编写截图脚本
写入项目根目录(需要 node_modules)。在阶段 4 中删除。
_tokens_screenshot.mjs:
import { chromium } from '@playwright/test';
import { mkdirSync } from 'fs';
const get = (f) => { const i = process.argv.indexOf(f); return i !== -1 ? process.argv[i+1] : null; };
const url = get('--url');
const output = get('--output') ?? '/tmp/tokens-screenshot.png';
const width = parseInt(get('--width') ?? '1280');
const height = parseInt(get('--height') ?? '900');
const auth = get('--auth');
if (!url) { console.error('--url required'); process.exit(1); }
mkdirSync('/tmp/tokens', { recursive: true });
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width, height },
...(auth ? { storageState: auth } : {}),
});
const page = await context.newPage();
try {
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
} catch {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
}
await page.screenshot({ path: output, fullPage: true });
await browser.close();
console.log('saved:', output);
每次截图后:使用 Read 工具读取图片。
0g. 处理受身份验证保护的屏幕
ls /tmp/uiux-auth-state.json /tmp/animate-auth-state.json /tmp/copy-auth-state.json /tmp/a11y-auth-state.json 2>/dev/null | head -1
如果找到,则对所有受保护路由的截图调用传入 --auth <path>。
阶段 1 — 令牌审计
1a. 查找所有硬编码颜色
SRC=<your source root>
# Hex colors in TSX/JSX (inline styles, arbitrary Tailwind values)
echo "=== Hex colors in TSX ===" && \
grep -rn "#[0-9a-fA-F]\{3,8\}" "$SRC" --include="*.tsx" --include="*.jsx" \
| grep -v "//.*#[0-9a-fA-F]" | grep -v "\.md"
# Hex colors in CSS
echo "=== Hex colors in CSS ===" && \
grep -rn "#[0-9a-fA-F]\{3,8\}" "$SRC" --include="*.css" --include="*.scss" \
| grep -v "^.*:.*var(--"
# rgb/hsl inline values
echo "=== RGB/HSL values ===" && \
grep -rn "rgba\?\|hsla\?" "$SRC" --include="*.tsx" --include="*.css" | grep -v "var(--"
# Arbitrary Tailwind color values (hex)
echo "=== Arbitrary Tailwind colors (hex) ===" && \
grep -rn "\(bg\|text\|border\|ring\|shadow\|fill\|stroke\)-\[#" "$SRC" --include="*.tsx"
# Named Tailwind palette colors that should be semantic tokens
# e.g. bg-blue-500 should become bg-primary if blue-500 is the primary color
echo "=== Named Tailwind palette colors ===" && \
grep -rn "\(bg\|text\|border\|ring\|fill\|stroke\)-\(slate\|gray\|zinc\|neutral\|stone\|red\|orange\|amber\|yellow\|lime\|green\|emerald\|teal\|cyan\|sky\|blue\|indigo\|violet\|purple\|fuchsia\|pink\|rose\)-[0-9]\{2,3\}" "$SRC" --include="*.tsx" \
| grep -v "hover:\|focus:\|dark:\|disabled:" | head -40
# Note: filter hover/focus/dark variants — those are less likely to be primary brand usage
# High-frequency named colors (>5 occurrences) are strong tokenization candidates
如果检测到 CSS-in-JS(styled-components / emotion): 还要运行:
# Hex colors in template literals
grep -rn "#[0-9a-fA-F]\{3,8\}" "$SRC" --include="*.ts" --include="*.tsx" \
| grep "styled\.\|css\`\|createGlobalStyle\|keyframes" | grep -v "//.*#"
# CSS property values inside template literals
grep -rn "color:\s*#\|background:\s*#\|border.*:\s*#" "$SRC" --include="*.ts" --include="*.tsx"
1b. 查找所有硬编码间距
# Off-grid pixel values in CSS (not on 4px scale)
echo "=== Off-grid spacing in CSS ===" && \
grep -rn "\b[0-9]*[13579]\b\s*px\|[0-9]\+\.\s*[0-9]\+\s*px" "$SRC" --include="*.css" \
| grep -v "border\|outline\|shadow\|translate"
# Arbitrary Tailwind spacing values
echo "=== Arbitrary Tailwind spacing ===" && \
grep -rn "\(p\|m\|gap\|space\|w\|h\|inset\|top\|left\|right\|bottom\)-\[[0-9]" "$SRC" --include="*.tsx"
# Inline style pixel values
echo "=== Inline style px values ===" && \
grep -rn "style={{" "$SRC" --include="*.tsx" | grep "[0-9]px\|[0-9]rem" | head -20
1c. 查找所有硬编码排版值
# Font sizes in CSS not from scale
echo "=== Hardcoded font sizes ===" && \
grep -rn "font-size:" "$SRC" --include="*.css" | grep -v "var(--"
# Arbitrary Tailwind font sizes
echo "=== Arbitrary Tailwind font sizes ===" && \
grep -rn "text-\[[0-9]" "$SRC" --include="*.tsx"
# Font weights not from scale
echo "=== Hardcoded font weights ===" && \
grep -rn "font-weight:\s*[0-9]" "$SRC" --include="*.css" | grep -v "var(--"
# Line heights
echo "=== Hardcoded line heights ===" && \
grep -rn "line-height:\s*[0-9]" "$SRC" --include="*.css" | grep -v "var(--"
1d. 查找所有硬编码阴影、圆角和 z-index
# Box shadows not from token
echo "=== Hardcoded shadows ===" && \
grep -rn "box-shadow:" "$SRC" --include="*.css" | grep -v "var(--" && \
grep -rn "shadow-\[" "$SRC" --include="*.tsx"
# Border radii not from token
echo "=== Hardcoded border radii ===" && \
grep -rn "border-radius:" "$SRC" --include="*.css" | grep -v "var(--" && \
grep -rn "rounded-\[[0-9]" "$SRC" --include="*.tsx"
# Magic z-index numbers
echo "=== Magic z-index ===" && \
grep -rn "z-index:\s*[0-9]\|z-\[[0-9]" "$SRC" --include="*.css" --include="*.tsx"
1e. 查找重复和不一致的值
# Same color used with different names (group by hex value to find duplicates)
grep -rh "#[0-9a-fA-F]\{3,8\}" "$SRC" --include="*.tsx" --include="*.css" \
| grep -oE "#[0-9a-fA-F]{3,8}" | sort | uniq -c | sort -rn | head -20
高频值 = 使用次数最多的硬编码颜色。这些值应优先转换为令牌。
1f. 截取每个主要屏幕的截图(变更前)
为每个屏幕分别截取桌面端和移动端的截图——间距和排版方面的令牌变更可能会破坏移动端布局,而这些布局在 1280px 下看起来正常。
node _tokens_screenshot.mjs --url http://localhost:3000 --output /tmp/tokens/home-before-desktop.png
node _tokens_screenshot.mjs --url http://localhost:3000 --output /tmp/tokens/home-before-mobile.png --width 375 --height 812
选取 3–5 个视觉差异最明显的屏幕。**读取每张图片。**这些是变更前的基准状态。
继续之前,按类别展示完整的审计结果。
阶段 2 — 令牌映射
编写任何代码之前,先建立令牌映射——将每个硬编码值对应到建议的令牌名称。
格式:
Hardcoded value | Occurrences | Proposed token name | Layer
#3b82f6 | 23 | --color-primary | semantic
#1d4ed8 | 8 | --color-primary-dark | semantic
#f9fafb | 31 | --color-surface | semantic
13px | 4 | --spacing-3-5 | primitive
命名规则(来自 SYSTEMS.md):
- 原始令牌:
--color-{hue}-{shade}、--space-{n}、--radius-{size} - 语义令牌:
--color-primary、--color-surface、--color-text、--color-border - 组件令牌:
--button-bg、--card-shadow(仅限特定组件) - 不要按视觉外观命名:
--blue或--dark-gray→ 使用语义名称 - 语义名称在颜色变更后依然适用:品牌色从蓝色变为绿色时,
--color-primary仍然合适
按令牌层分组:
- 新增原始令牌 — 系统中尚未定义的原始值
- 新增语义令牌 — 映射到原始令牌、基于含义命名的别名
- 要使用的现有令牌 — 可映射到已定义令牌的值
请用户确认令牌映射后,再开始阶段 3。
第 3 阶段 — 递归令牌循环
按类别逐一处理:颜色 → 间距 → 排版 → 阴影 → 圆角 → z-index。 每个类别最多 3 次迭代。
3a. 定义新令牌
在替换任何用法之前,先将所有新的原始令牌和语义令牌添加到令牌主文件中。
CSS 自定义属性模式:
/* In globals.css */
:root {
/* Primitives */
--color-blue-500: #3b82f6;
--color-blue-700: #1d4ed8;
--space-1: 4px;
--space-2: 8px;
/* Semantic */
--color-primary: var(--color-blue-500);
--color-primary-dark: var(--color-blue-700);
--color-surface: #f9fafb;
--color-text: #111827;
--color-border: #e5e7eb;
}
/* Dark mode — same semantic names, different values */
.dark {
--color-primary: var(--color-blue-400);
--color-surface: #111827;
--color-text: #f9fafb;
--color-border: #374151;
}
Tailwind 模式:
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
primary: 'var(--color-primary)',
surface: 'var(--color-surface)',
border: 'var(--color-border)',
},
spacing: {
'3.5': 'var(--space-3-5)',
},
},
},
}
3b. 替换组件中的硬编码值
按文件逐一处理,优先处理出现次数最多的值。对每个文件:
- 读取文件
- 将硬编码值替换为令牌
- 切勿更改逻辑、布局或组件结构——只替换值
CSS 替换:
/* Before */
color: #3b82f6;
/* After */
color: var(--color-primary);
Tailwind 类替换:
/* Before */
<div className="bg-[#3b82f6] text-[#111827]">
/* After */
<div className="bg-primary text-text">
内联样式替换(最后手段——优先移至 CSS/Tailwind):
/* Before */
style={{ backgroundColor: '#3b82f6' }}
/* After — move to CSS class if possible */
className="bg-primary"
/* Or if dynamic: */
style={{ backgroundColor: 'var(--color-primary)' }}
3c. 每个类别处理完后截取屏幕截图
sleep 3 # hot reload
node _tokens_screenshot.mjs --url http://localhost:3000 --output /tmp/tokens/home-after-<category>-desktop.png
node _tokens_screenshot.mjs --url http://localhost:3000 --output /tmp/tokens/home-after-<category>-mobile.png --width 375 --height 812
读取两张图片。 对照两个视口下的处理前截图进行比较。
两个尺寸下的 UI 都应当 完全一致。如果有任何变化:
- 颜色错误 → 令牌值有误,修正令牌定义
- 移动端布局偏移 → 间距/尺寸令牌解析出的值与硬编码值不同,修正令牌值
3d. 令牌检查清单
进入下一个类别之前:
- 此类别中的所有值都已替换为令牌
- 替换过程中没有引入新的硬编码值
- 处理前后的桌面端截图在视觉上完全一致
- 处理前后的移动端截图(375px)在视觉上完全一致——间距/排版令牌不会导致布局偏移
- 令牌名称遵循原始 → 语义层级
- 暗色模式代码块为所有语义令牌定义了覆盖值(如果存在暗色模式)
- 没有两个令牌具有相同的值但语义不同(合并重复项)
- 令牌名称不描述外观(
--blue)——只描述其作用(--color-primary)
任一项未通过 → 返回 3b。第 3 次迭代后:标记问题并继续。
第 4 阶段 — 清理与报告
删除临时脚本:
rm -f _tokens_screenshot.mjs
每个类别处理完后:
✓ Colors (iteration 1) — 47 hardcoded values → 8 tokens
✓ Spacing (iteration 2) — 12 hardcoded values → 4 tokens
最终摘要表:
| 类别 | 处理前的硬编码值 | 创建的令牌 | 已替换的值 | 迭代次数 | 状态 |
|---|---|---|---|---|---|
| 颜色 | ... | ... | ... | ... | ✓ / ⚠ |
| 间距 | ... | ... | ... | ... | ✓ / ⚠ |
| 排版 | ... | ... | ... | ... | ✓ / ⚠ |
| 阴影 | ... | ... | ... | ... | ✓ / ⚠ |
| 圆角 | ... | ... | ... | ... | ✓ / ⚠ |
| Z-index | ... | ... | ... | ... | ✓ / ⚠ |
令牌清单 — 列出所有已定义的令牌、其值及使用次数。
列出所有无法解决的硬编码值并标记,同时说明原因。
规则
- 替换前先定义。 任何组件引用令牌之前,令牌必须已存在于令牌目录中。
- 先定义原始值,再定义语义值。 先定义原始值,再定义语义别名,只有必要时才定义组件令牌。
- 按角色命名,绝不按外观命名。
--color-primary可以适应品牌重塑,--color-blue则不行。 - 截图是回归测试。 替换前后必须在视觉上完全一致。如果存在差异,说明令牌值不正确。
- 深色模式是检验语义层是否良好的标准。 如果切换
.dark后颜色能正确变化,且无需修改组件,那么令牌就是正确的。 - 每种含义只用一个令牌。 两个令牌的值和含义相同 → 合并。
- 不要把所有内容都令牌化。
border: 1px solid中的1px不需要令牌。重点关注会随主题变化或在多个位置出现的值。 - 每个类别最多迭代 3 次。 标记后继续处理。
- 绝不更改逻辑。 只修改设计值,不改条件、组件结构或功能。
令牌架构、命名约定和完整代码示例,请参阅 SYSTEMS.md。