关于 uicolor
用于审查并调整 Web 界面颜色的技能,面向正在改进界面的开发者。它通过截图和调色板覆盖率分析每个屏幕,并依据 HSB 规则修改颜色令牌后复核效果。
主要功能
- 01提取屏幕调色板及覆盖率
- 02检查颜色角色与层级
- 03审查色温和饱和度
- 04按优先级修正颜色令牌
- 05截图复核并生成报告
适用场景
- 01定位界面主色使用过多
- 02统一中性色的色温
- 03检查多屏颜色层级
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dawitlabs/ ui-skills --skill uicolor安装后在 Claude Code 中输入/uicolor确认已加载。
附带文件COLOR-THEORY.md
详细介绍
/uicolor — 颜色校正与平衡
色彩总管。读取渲染后的 UI,提取实际色彩分布,诊断每处失衡,并运用 HSB 理论精准校正。逐屏处理,并始终通过视觉验证。
完整色彩理论参考:COLOR-THEORY.md
阶段 0 — 环境设置
0a. 检查/启动开发服务器
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"
0b. 查找源代码根目录和令牌所在位置
find . -type d -name "src" -not -path "*/node_modules/*" -not -path "*/.next/*" \
-not -path "*/dist/*" -not -path "*/.turbo/*" | head -1
find . \( -name "globals.css" -o -name "variables.css" \) -not -path "*/node_modules/*" | head -3
find . -name "tailwind.config.*" -not -path "*/node_modules/*" | head -2
读取令牌所在文件,并记录其中已定义的每个颜色值。这些值是需要校正的对象,而不是要从头替换的内容。
0c. 如有 DESIGN-BRIEF.md,则读取
cat DESIGN-BRIEF.md 2>/dev/null | head -60
设计简报的 Color System 部分会说明预期方向(色温、饱和度、主色十六进制值)。以此为准绳——校正的方向应当符合这一意图,而不是偏离它。
0d. 编写颜色分析脚本
写入项目根目录(需要 node_modules)。此文件将在阶段 4 删除。
_uicolor_extract.mjs — 提取渲染后的调色板及其覆盖率:
import { chromium } from '@playwright/test';
import { mkdirSync, writeFileSync } from 'fs';
const get = (f) => { const i = process.argv.indexOf(f); return i !== -1 ? process.argv[i+1] : null; };
const url = get('--url') ?? 'http://localhost:3000';
const name = get('--name') ?? 'screen';
const width = parseInt(get('--width') ?? '1280');
const height = parseInt(get('--height') ?? '900');
const auth = get('--auth');
mkdirSync('/tmp/uicolor', { 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 });
}
// Screenshot for visual reading
await page.screenshot({ path: `/tmp/uicolor/${name}.png`, fullPage: true });
// Extract color distribution from rendered elements
const palette = await page.evaluate(() => {
const colorArea = new Map();
const totalArea = document.documentElement.scrollHeight * window.innerWidth;
document.querySelectorAll('*').forEach(el => {
const rect = el.getBoundingClientRect();
const absTop = rect.top + window.scrollY;
const area = rect.width * (rect.height || 1);
if (area < 100) return; // skip tiny elements
const style = getComputedStyle(el);
const props = ['backgroundColor', 'color', 'borderTopColor', 'outlineColor'];
props.forEach(prop => {
const val = style[prop];
if (!val || val === 'rgba(0, 0, 0, 0)' || val === 'transparent') return;
// Normalise to rgba string as key
colorArea.set(val, (colorArea.get(val) ?? 0) + (prop === 'backgroundColor' ? area : area * 0.1));
});
});
// Convert to sorted array with coverage %
return [...colorArea.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 20)
.map(([color, area]) => ({
color,
coverage: Math.round((area / totalArea) * 1000) / 10, // % of page area
}));
});
// Parse rgb values to hex for readability
const toHex = (rgb) => {
const m = rgb.match(/\d+/g);
if (!m || m.length < 3) return rgb;
return '#' + m.slice(0, 3).map(n => parseInt(n).toString(16).padStart(2, '0')).join('');
};
const result = {
url,
viewport: `${width}×${height}`,
palette: palette.map(p => ({ ...p, hex: toHex(p.color) })),
};
writeFileSync(`/tmp/uicolor/${name}-palette.json`, JSON.stringify(result, null, 2));
await browser.close();
console.log(`screenshot: /tmp/uicolor/${name}.png`);
console.log(`palette: /tmp/uicolor/${name}-palette.json`);
palette.slice(0, 10).forEach(p => console.log(` ${toHex(p.color).padEnd(10)} ${p.coverage}%`));
用法:
node _uicolor_extract.mjs --url http://localhost:3000 --name home
node _uicolor_extract.mjs --url http://localhost:3000/dashboard --name dashboard --auth /tmp/uiux-auth-state.json
node _uicolor_extract.mjs --url http://localhost:3000 --name home-mobile --width 375 --height 812
每次运行后:读取截图和调色板 JSON。
0e. 处理受身份验证保护的屏幕
ls /tmp/uiux-auth-state.json /tmp/animate-auth-state.json /tmp/copy-auth-state.json 2>/dev/null | head -1
如果发现受保护的路由,则在所有脚本调用中传入 --auth <path>。
阶段 1 — 颜色审计
1a. 对每个屏幕运行颜色提取
node _uicolor_extract.mjs --url http://localhost:3000 --name home
node _uicolor_extract.mjs --url http://localhost:3000 --name home-mobile --width 375 --height 812
对每个不同的屏幕重复此操作。读取每张截图和每个调色板 JSON。
对每个屏幕,记录:
- 覆盖率最高的 3 种颜色——分别是主色、次要色、强调色(应符合 60-30-10 原则)
- 完整调色板——留意覆盖率低于 1% 的颜色(装饰性噪声)或意外出现的颜色
- 是否有单一色相的覆盖率超过 40%(饱和度疲劳)
1b. 检查颜色角色
为调色板中的每种颜色确定其角色:
| 角色 | 应为 | 覆盖率 |
|---|---|---|
| 背景/主色 | 中性色表面 | 50–65% |
| 次要色 | 略微凸起的表面、侧边栏 | 20–30% |
| 文本 | 深色中性 | 10–20%(应用于文本元素) |
| 主色/强调色 | 品牌色 | ≤10% |
| 语义色 | 成功、错误、警告 | 总计 ≤5% |
立即标记:
- 主色/品牌色覆盖率 > 15% → 品牌稀释(所有元素都在高声呐喊,没有重点)
- 背景色调超过 3 种 → 表面混乱
- 使用纯黑色(#000000)作为阴影 → 效果生硬且刺眼
- 纯灰中性色缺少一丝色相 → 表面死板、毫无生气
- 所有悬停状态都与基础状态同色 → 没有反馈
1c. 读取令牌定义
将调色板中的每种颜色与令牌文件交叉比对。针对每种颜色:
- 是否有令牌名称?(良好)
- 是否硬编码?(标记以供 /tokens 处理)
- 令牌名称是否具有语义性?(
--color-primary与--blue的区别)——标记非语义名称
1d. 眯眼测试——颜色层级
截取最复杂屏幕的截图。眯眼直到画面模糊,然后询问:
- 你的目光首先落在哪种颜色上?
- 那是否是屏幕上最重要的元素?
- 是否有背景元素在色彩权重上与前景元素争抢注意力?
如果答案不正确 → 颜色层级存在问题。
1e. 色温审计
检查每种中性表面颜色和文本颜色:色温是否一致?
# Extract all CSS color values from token file
grep -E "color:|background:|border:" globals.css tailwind.config.* 2>/dev/null | grep -v "var(--" | head -30
将每种颜色转换为 HSB。检查:
- 所有中性色都应带有相同的色相倾向(暖色:H 20–50°;冷色:H 200–240°)
- 在同一屏幕上混用暖灰和冷灰 = 色温冲突
- 品牌色的色温方向应与中性色一致
继续之前,按屏幕分组呈现完整审计结果。
阶段 2 — 优先级排序
为每个问题评分:
| 问题 | 分数 |
|---|---|
| 品牌色/主色覆盖率 > 15%(造成稀释) | +4 |
| 色温冲突(暖色与冷色中性色混用) | +4 |
| 色彩层级错误(强调色用于背景,而非前景) | +4 |
| 阴影为纯黑色(没有色相倾向) | +3 |
| 背景色深浅超过 3 种 | +3 |
| 悬停状态 = 基础状态(没有颜色反馈) | +3 |
| 中性灰为纯灰(没有细微色相倾向) | +2 |
| 语义色用于装饰(绿色用于非成功状态) | +2 |
| 同一角色元素的饱和度不一致 | +2 |
| 文本颜色没有遵循明确的层级 | +2 |
| 位于高流量/第一印象屏幕 | +2 |
| 色盲风险(仅靠红/绿区分) | +2 |
按分数排序。分数最高的问题优先修复。进入阶段 3 前,请用户确认。
阶段 3 — 递归颜色修复循环
按排序后的顺序处理问题。每个屏幕最多进行 3 次迭代。
3a. 修复前截图
针对此屏幕运行 _uicolor_extract.mjs。读取截图和调色板 JSON。
用一句话描述当前颜色给人的感觉是什么?(冷淡平板?刺眼?浑浊?缺乏生气?)
3b. 诊断根本原因
每个颜色问题都有以下五种根本原因之一:
- 取值错误 — 十六进制值不正确(饱和度过高、色温不对)
- 角色错误 — 颜色用于了错误的用途(主色用于装饰)
- 缺少变化 — 某种颜色只有一种深浅,因此无法区分不同状态
- 缺少细微色相倾向 — 中性色为纯灰,导致调色板缺乏生气
- 缺少阴影色调 — 阴影为纯黑色,而非带色相的阴影
编写任何修复方案前,先确定根本原因。
3c. 修复 — 精确应用 HSB 调整
自动应用 COLOR-THEORY.md 中的规则:
Brand dilution (primary used everywhere):
→ Keep primary on: CTAs, links, focus rings, active nav items, badges
→ Replace on: decorative icons, background fills, section dividers
→ Replace with: neutral-600 for icons, surface colors for backgrounds
Temperature correction:
→ Convert current neutral hex to HSB
→ Adjust hue: toward 30° for warm, toward 220° for cool
→ Keep S ≤ 8% (barely perceptible — a whisper, not a tint)
→ Recheck against brand primary temperature — they should agree
Shadow construction:
→ Bad: box-shadow: 0 4px 6px rgba(0,0,0,0.1)
→ Good: take surface color HSB → increase S by 30% → decrease B by 60% → use at 0.12–0.20 opacity
→ Example: white surface (#ffffff) → shadow: 0 4px 6px rgba(0, 0, 40, 0.12)
→ Example: blue-50 surface (#eff6ff) → shadow: 0 4px 6px rgba(30, 60, 120, 0.12)
Hover state construction:
→ Hover (darker): S+10, B-15, hue toward 0°/120°/240°
→ Active/pressed: S+15, B-25 (more dramatic than hover)
→ Disabled: S-30, B+10, opacity 0.5
→ Focus ring: brand primary at full S+B, 2px offset
Neutral hue whisper:
→ Pure gray: hsl(0, 0%, 97%) → add whisper: hsl(220, 8%, 97%)
→ The hue should match the brand primary's temperature direction
→ S between 3–8% — visible in isolation, invisible when focused on content
Text color scale:
→ Primary text: brand-neutral-900 (or equivalent)
→ Secondary text: brand-neutral-500 (not pure gray — use hue-tinted neutral)
→ Disabled text: brand-neutral-400
→ Never more than 4 levels of text gray
60-30-10 correction:
→ Measure coverage from palette JSON
→ If primary > 10%: audit every use, keep only interactive elements
→ If no element < 5% coverage: the accent has no room to breathe — reduce secondary color
先在令牌文件中修复,再验证组件用法。 修改单个令牌即可修复 50 个组件。
3d. 修复后截图
sleep 3 # hot reload
node _uicolor_extract.mjs --url http://localhost:3000<ROUTE> --name <name>-after
读取截图和新的调色板 JSON。
比较修复前后的调色板 JSON:
- 主色覆盖率是否降至 ≤10%?
- 中性色的色相是否朝正确方向偏移?
- 所有中性色的色温是否一致?
再次进行眯眼测试。询问:
- 与之前相比,屏幕感觉更暖、更冷,还是更均衡?
- 主色现在是否能在背景上突出显示?
- 阴影是否有层次感,还是看起来仍像是贴上去的?
如果任何一项不符合预期:返回 3c(迭代次数加 1,最多 3 次)。
3e. 颜色检查清单
进入下一个屏幕前:
- 任意屏幕上的主色/品牌色覆盖率 ≤10%(根据调色板 JSON)
- 所有中性色都有一致的细微色相倾向 — 色温方向相同
- 任何表面上都没有纯灰中性色(HSB S = 0%)
- 所有阴影都带有色相,而非纯黑色
- 悬停状态在视觉上区别于基础状态(至少 S+10 B-15)
- 激活/按下状态在视觉上区别于悬停状态
- 文本遵循 ≤4 级灰度层级,且色相一致
- 没有将语义色(成功绿、错误红)用于装饰
- 没有仅用红/绿组合区分两种状态
- 眯眼测试:视线首先落在优先级最高的元素上
- 色温一致 — 单个屏幕内没有冷暖冲突
- 修复前后调色板 JSON 显示覆盖率朝正确方向变化
任何一项未通过 → 返回 3c。第 3 次迭代后:标记问题并继续。
阶段 4 — 清理与报告
删除临时脚本:
rm -f _uicolor_extract.mjs
每个屏幕处理完后:
✓ Screen name (rank N, iteration K) — primary coverage: 23% → 8%, shadows tinted, neutrals warmed
最终摘要:
| 屏幕 | 修复前主色覆盖率 | 修复后 | 色温 | 阴影 | 迭代次数 | 状态 |
|---|---|---|---|---|---|---|
| 首页 | 28% | 7% | ✓ 暖色 | ✓ 带色调 | 2 | ✓ |
修复前/后的调色板 — 列出所有变更的令牌、旧值和新值。
在底部列出所有已标记的问题。
规则
- 先查看截图。始终如此。 颜色问题是先感受到,再测量出来的。先看图像,再看数字。
- 修复令牌,而非组件。 如果有 40 处颜色不对,问题就在于某个令牌定义有误。
- 使用 HSB 工具。 切勿靠猜测十六进制值来调整颜色。先转换为 HSB,应用规则,再转换回来。
- 60-30-10 是视觉定律,不是建议。 如果主色覆盖率只有 25%,其他颜色就会与之争夺注意力。降低其他颜色的覆盖率。
- 中性色从来不是真正中性。 纯灰色用于暖色品牌时会显冷,用于冷色品牌时会显暖——始终加入一丝色相。
- 阴影也有颜色。 纯黑阴影会让 UI 显得扁平。有色调的阴影能增加层次感。
- 色温是系统层面的决定。 绝不要混用暖色和冷色中性色。选择一个方向并坚持下去。
- 语义颜色不可随意更改。 绿色 = 成功。红色 = 错误。将绿色用于品牌和错误状态会破坏信号。
- 每个屏幕最多迭代 3 次。 标记问题,然后继续。
- 绝不更改逻辑、布局或组件结构。 只更改颜色值。
参阅 COLOR-THEORY.md,了解 HSB 参考、色相温度图表、阴影构建公式,以及完整的 60-30-10 指南。