feat(ui): UI 设计系统重构与依赖锁定 - #32
Merged
Merged
Conversation
把"前端该长什么样"从口头约定变成可执行的检查:三个零依赖脚本 + 一份
stylelint 配置 + 一个可复用的技能包。
ui/scripts/audit-css.mjs(1403 行)
对 .css 与 .vue 的 <style> 块出漂移分数:彩色种类、近白背景、圆角档位、
字号档位、:root 块数、重复声明的选择器、!important、token 覆盖率、
rgb 基色种类。有未通过项时 exit 1。
支持 --ignore <片段>:定义文件(tokens.css)里的字面量本来就合法,
不该污染分数;支持 --json 供 CI 消费。
ui/scripts/capture.mjs(454 行)
调本机 Chrome/Edge 的 headless 模式按多个宽度截图,本地页面实测约
0.7–3 s、公网站点 17–31 s。必须 spawnSync(..., { stdio: 'ignore' }):
受限沙箱下走管道会抛 EPERM。
ui/scripts/check-contrast.mjs(91 行)
解析 tokens.css 推导「前景 × 背景」配对,逐组算 WCAG 对比度。
当前 40 组,0 组不达标(门槛 4.5:1)。
ui/stylelint.config.mjs
stylelint-config-standard 默认只认 kebab-case,会判整套 BEM
(ui-button--sm、ui-select__field)违规,已放行 BEM。
no-descending-specificity 降级为 warning —— 它与本项目"状态必须显式排序
default→hover→focus→active→disabled"的写法直接冲突。
ui/package.json
新增 audit / audit:json / shots / lint:css / lint:css:all / check:contrast,
以及把上面几条串起来的 verify。迁移期 lint:css 只卡新代码,legacy
main.css 用 audit 跟踪收敛 —— 否则一开就是 600+ 条报错,人会习惯性忽略
输出,等于没开。
.agents/skills/frontend-craft/(20 文件)
仓库里已有 .agents/skills/ 约定(coding-agent-issue-writer),此技能遵循
同一约定。含 SKILL.md;bootstrap / build / screenshot-and-qa /
reference-sources / tokens-baseline / prompts 六份参考;anti-patterns 与
qa-checklist 两份模板;tokens.css 与 stylelint 配置模板;6 个 Vue 原语模板。
.gitignore
新增 docs/design/reference/:参照物截图是别人的版权素材、1.8 MB、
重跑 capture.mjs 就能再取一份。
一次连续的重构,`ui/src` 全量重写。分不开成多个提交:main.css 同时承载 token 收敛、原语迁移与视觉改版。下面按阶段记。 —— 阶段一 · 地基 ui/src/styles/tokens.css(新,272 行) 把散在 main.css 里的值收进唯一的 :root。改前有 3 个 :root 块、8 组重复 定义同名变量:--surface-soft 定义了 3 次(生效的在 main.css:1122,另两处 在 :14 与 :564 是死的),--accent-strong / --border / --muted / --sidebar-open / --sidebar-closed / --sidebar-width / --surface 各重复 2 次。 改文件开头那一行什么都不会发生,且不报错。 ui/src/api/types.js(新) 契约层唯一真相源。所有形状来自对真实后端的探测,不是按印象编的: Message 用的是 author 不是 role;createdAt/updatedAt 是毫秒数不是 ISO 串; repoFullName 可能是空字符串不是 null;MessageChunk 只有 text/run_activity/proposal 三种(渲染必须按 kind 分支,不能 if (chunk.text) 猜)。 4 个字段在现有数据中恒为 null、形状未观测,已显式标注而不是猜一个。 —— 阶段二 · 值收敛(类名与结构一个字不动) ui/src/styles/main.css 硬编码颜色 457 次 → 0;硬编码 px 539 → 4(全是合法的 1px 边框); token 覆盖率 2.9% → 99.6%;重复声明的选择器 108 → 0;!important 2 → 0; 彩色种类 166 → 0;字号 19 → 7;圆角 20 → 4;:root 3 → 1。 顺带清掉一整片死 CSS(1139 → 997 行)—— 窄化共享选择器列表会让不再被 任何元素命中的声明暴露出来,audit 的"冗余声明"检查就是个死代码探针。 —— 阶段三 · 原语与拆分 ui/src/components/ui/(新) UiButton / UiInput / UiSelect / UiTextarea / UiCard / UiBadge / UiEmptyState。 注意 .ui-btn[data-v-xxx] 的 specificity 是 (0,2,0),全局单类名 (0,1,0) override 不了原语 —— 可调项必须是 props,不能靠外部 CSS 覆盖。 ui/src/components/AgentWorkspace.vue 454 → 206 行,拆成 WorkspaceHeader / WorkspaceConversation / WorkspaceWelcome; ui/src/composables/ 新增 useSidebarPreferences / useProjectDialogs / useRelativeTime / useSmartScroll 收拢状态逻辑; ui/src/stores/agent.js 的 bootstrap() 改成 Promise.allSettled,新增 projectsError 状态与 retryProjects(),并补了测试。 —— 视觉改版(用户要求的两步) 第一步 · 暖色调 + 近黑主色 灰阶偏橙黄约 35° 但饱和度极低;主色 #1f7667 → #231e18;遮罩与阴影基色 从冷黑 rgba(20,28,25,…) 改成暖黑 rgba(31,26,21,…)。 --c-surface 刻意保持纯白:它与米白底的 4% 明度差就是"卡片浮在页面上"的 全部来源,调成同值所有卡片立刻失去边界。 hover 变亮而不是变暗:近黑没有变暗余地,再暗就跟正文分不开了。 代价是有意的 —— 主色不再是品牌识别载体。 对比度是真量的:40 组 WCAG 配对全过,正文/页面底 16.02,次级说明最差 6.78,弱化文字最差 5.04,反白/主色按钮 15.47,成功/警告/危险色在各自 浅底上 5.33 / 5.31 / 5.09。 换主色逼出一个真的语义问题:--c-success-subtle 一直在兼任"真正的成功"和 "随便一个浅色底"两个角色,被借用了 14 处 —— 旧主色本来就是绿的所以看不 出来。已按选中/普通次级/真成功三条重新归属,并把「已完成」胶囊本身中性化、 只留小圆点是绿的。 第二步 · 面包屑 / 消息头 / 工具胶囊 / 对齐 面包屑:Header 副标题原本拼「项目名 · 仓库名」,而真实数据里这两个字符串 经常完全相同,渲染出 clumsyspc/test_coding_repo · clumsyspc/test_coding_repo。 改成两级面包屑,且两者相同时整个隐掉仓库徽标 —— 这不是边界情况是常态。 消息头:名字 + 相对时间(刚刚 / N 分钟前 / 跨过一小时退回 HH:MM / 昨天 / M 月 D 日 / 跨年带年份),悬停 title 给精确时刻。相对时间用模块级共享的 一个 30 秒 interval + 消费者引用计数,消费者归零就 clearInterval。 可折叠「深度思考」:kind === 'think' 的事件原本和工具调用混在同一条平铺 列表里,现在单独成一层次级折叠,默认收起、标题给段数。 工具胶囊:.run-activity 原本是占满 1200px 的卡片,一条"运行了 4 秒"占的 版面跟一条消息一样大,四条叠起来把消息区全占了。改成贴着内容的胶囊。 对齐:消息列与输入框都从 --layout-content-max 反推 —— 那个 token 定义了 但从来没人用过;改前 1440 下差 30px。 —— 顺手修掉的 15 个真缺陷(不是"改好看",是原来就坏的) 侧栏「加载失败」冒充「空态」;加载中也冒充空态;键盘够不到「复制」; .thread-delete 是 <span role=button> 嵌在 <button class="thread-item"> 里, 违反 HTML 内容模型;仓库名省略号从不触发(display:flex 与 text-overflow 冲突);todo 长文本顶破卡片(缺 min-width: 0 与 overflow-wrap);只读仓库 展示长得像可点控件;15 处 outline: none 把焦点环杀了;.message-actions 用 visibility: hidden 让子元素无法聚焦,改成 opacity + pointer-events; audit-css.mjs 把 @Keyframes 内部步骤当成规则导致误报;等。 —— 验证 npm test → 9 files / 91 tests passed(改前 2 files / 10 tests) npm run audit → 全部通过(24 文件 · 3732 行 · 547 规则块 · 1076 处 var()) npm run lint:css → 0 errors / 19 warnings npm run check:contrast → 40 组 0 组不达标 npm run build → ✓ 约 0.97 s npm run shots → 3/3 —— 新增的安全网 RunActivityCard.test.js 的 8 条尤其重要:这个组件此前零测试,而"think 不能 混进时间线"这条不变式一旦被破坏,界面看起来仍然正常 —— 截图看不出来。 useRelativeTime.test.js 的 10 条在写的过程中撞出一个真 bug: new Date(null) 不是 Invalid Date 而是 1970-01-01,缺时间戳的消息会显示成 「1970 年 1 月 1 日」。 —— 诚实的边界 证据是 91 条测试 + 三宽度截图 + 7 格状态矩阵 + 16 格消息矩阵, 没有做完整的交互流程手动走查。
这次重构的判据、证据与理由。文档从 §1 到 §22,每一节记一次改动的
"改了什么 / 为什么这么改 / 怎么验证的"。
DESIGN.md
§1–§5 约束层:冻结参照物(Cursor 主 / Linear 次)、token 表、
原语白名单、反模式清单的落点。
§6 页面 × 状态矩阵。所有格子都已填满 —— 空态、加载态、错误态、
边界态各自被真的渲染过,不是"应该有"。
§10 迁移记录与「已知例外」表。
§11–§19 阶段三每一笔的记录。这些的验收标准是截图逐字节相同。
§20 视觉改版第一步 · 暖色调 + 近黑主色。含完整旧新值表、
--c-surface 保持纯白的理由、hover 变亮不是变暗的理由、
40 组对比度实测表、--c-success-subtle 的三条归属表。
§21 视觉改版第二步 · 面包屑 / 消息头时间 / 工具胶囊 / 消息列对齐。
§22 第二步补完 · 相对时间 + 可折叠深度思考。
从 §20 起验收标准变了:不再是"截图逐字节相同",而是"每个状态都重新看过、
且对比度真的量过"。前十九节的零视觉变化标准是整个重构的下限保证,但它
也压低了视觉上限 —— 信息层级、布局比例、视觉调性在那一阶段基本没碰。
anti-patterns.md 22 条视觉禁飞区,每条含「长什么样 · 为什么坏 · 正确做法 ·
机器可查」,锚定的是本项目真实踩过的坑。
qa-checklist.md 8 组验收清单,顺序不可变(对齐栅格 → 层级灰度 → 控件
一致性 → 间隔节奏 → 颜色 → 交互状态 → 文本健壮性 →
参照物对比)。
direction.md 参照物的「取 / 不取」表 —— "不取"比"取"更重要。
baseline/ 34 个文件
before-{375,768,1440}.png 重构前的界面,只增不改。
after-{375,768,1440}.png 每次验证过的改动后刷新。
audit-before.json audit-css 对重构前 src 的完整分数
(彩色 167 / 近白 17 / 圆角 20 / 字号 19 /
:root 3 块含 8 组重复变量 / 重复选择器 109 /
token 覆盖率 3.4%)。
phase3-*.png 阶段三每一笔的证据。
phase3-states-920.png 改版前的七格状态矩阵。
phase3-warm-states-920.png 同一份夹具、同一份数据、暖色改版后。
这两张可以直接并排比对。
未提交:docs/design/reference/(1.8 MB)—— 那是 Cursor / Linear / GitHub /
Vercel / Notion / Raycast 的产品截图,别人的版权素材,重跑 capture.mjs
就能再取一份。已在 .gitignore 里排除。
Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
本 PR 为 UI 设计系统的完整重构,包含设计 token 标准化、组件原语抽取和依赖锁定。
主要变更
1. 前端设计约束工具链 (6215689)
2. 前端重构 (943521b)
3. 设计系统文档 (70c6652)
4. 依赖锁定 (0c96b78)
测试
相关文档
Checklist
Made with Cursor