mcpField
U
uicolor

uicolor

github.com/dawitlabs设计与 UI740 次安装24 周前更新
代码仓库

关于 uicolor

用于审查并调整 Web 界面颜色的技能,面向正在改进界面的开发者。它通过截图和调色板覆盖率分析每个屏幕,并依据 HSB 规则修改颜色令牌后复核效果。

主要功能

  1. 01提取屏幕调色板及覆盖率
  2. 02检查颜色角色与层级
  3. 03审查色温和饱和度
  4. 04按优先级修正颜色令牌
  5. 05截图复核并生成报告

适用场景

  1. 01定位界面主色使用过多
  2. 02统一中性色的色温
  3. 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. 眯眼测试——颜色层级

截取最复杂屏幕的截图。眯眼直到画面模糊,然后询问:

  1. 你的目光首先落在哪种颜色上?
  2. 那是否是屏幕上最重要的元素?
  3. 是否有背景元素在色彩权重上与前景元素争抢注意力?

如果答案不正确 → 颜色层级存在问题。

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. 诊断根本原因

每个颜色问题都有以下五种根本原因之一:

  1. 取值错误 — 十六进制值不正确(饱和度过高、色温不对)
  2. 角色错误 — 颜色用于了错误的用途(主色用于装饰)
  3. 缺少变化 — 某种颜色只有一种深浅,因此无法区分不同状态
  4. 缺少细微色相倾向 — 中性色为纯灰,导致调色板缺乏生气
  5. 缺少阴影色调 — 阴影为纯黑色,而非带色相的阴影

编写任何修复方案前,先确定根本原因。

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%?
  • 中性色的色相是否朝正确方向偏移?
  • 所有中性色的色温是否一致?

再次进行眯眼测试。询问:

  1. 与之前相比,屏幕感觉更暖、更冷,还是更均衡?
  2. 主色现在是否能在背景上突出显示?
  3. 阴影是否有层次感,还是看起来仍像是贴上去的?

如果任何一项不符合预期:返回 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✓

修复前/后的调色板 — 列出所有变更的令牌、旧值和新值。

在底部列出所有已标记的问题。

规则

  1. 先查看截图。始终如此。 颜色问题是先感受到,再测量出来的。先看图像,再看数字。
  2. 修复令牌,而非组件。 如果有 40 处颜色不对,问题就在于某个令牌定义有误。
  3. 使用 HSB 工具。 切勿靠猜测十六进制值来调整颜色。先转换为 HSB,应用规则,再转换回来。
  4. 60-30-10 是视觉定律,不是建议。 如果主色覆盖率只有 25%,其他颜色就会与之争夺注意力。降低其他颜色的覆盖率。
  5. 中性色从来不是真正中性。 纯灰色用于暖色品牌时会显冷,用于冷色品牌时会显暖——始终加入一丝色相。
  6. 阴影也有颜色。 纯黑阴影会让 UI 显得扁平。有色调的阴影能增加层次感。
  7. 色温是系统层面的决定。 绝不要混用暖色和冷色中性色。选择一个方向并坚持下去。
  8. 语义颜色不可随意更改。 绿色 = 成功。红色 = 错误。将绿色用于品牌和错误状态会破坏信号。
  9. 每个屏幕最多迭代 3 次。 标记问题,然后继续。
  10. 绝不更改逻辑、布局或组件结构。 只更改颜色值。

参阅 COLOR-THEORY.md,了解 HSB 参考、色相温度图表、阴影构建公式,以及完整的 60-30-10 指南。

评论

支持 Markdown