界面规范
本文是 code-association 工作台的唯一权威视觉与交互规范。 界面实现映射 记录本文到文件、类名与数据契约的对应关系; UiContractTest 把本文的关键契约固化成断言。
改 UI 之前先读本文,改完之后同步本文。 规范与实现漂移比没有规范更糟: 下一个人会照着错的规范改,而测试会替这个错误背书。
目录
| 章 | 内容 | 什么时候看 |
|---|---|---|
| 1 | 定位、视觉方向、四条签名特征 | 第一次接触,或怀疑”这个界面到底想做成什么” |
| 2 | 双主题、三态偏好、画布取色 | 改配色或主题相关逻辑 |
| 3 | 令牌分组、文字三级、语义色、对比度实测 | 选颜色 |
| 4 | 字体栈、字号阶梯、字重字距、行高 | 加文字或调层级 |
| 5 | 骨架、栅格、尺寸表、断点、滚动 | 加区块或改宽度 |
| 6 | 圆角四级、阴影四级 | 新建容器 |
| 7 | 26 类组件的结构/变体/状态/禁忌 | 最常查的一章 |
| 8 | 五种状态、错误分级、破坏性操作、进度 | 写异步逻辑 |
| 9 | 令牌、允许与禁止、降低动效 | 加动画 |
| 10 | 焦点、键盘、ARIA、触屏、对比度 | 加交互控件 |
| 11 | 语气、用词、标签映射、句式、禁忌 | 写界面文字 |
| 12 | 可勾选的自检表 | 提交前 |
| 13 | 规范与现状的差异清单 | 改到相关组件时 |
| 14 | 相关文档 | — |
1. 定位
1.1 这是什么界面
一个证据审阅台,不是仪表盘,也不是数据录入系统。
使用者打开它时心里有一个具体问题:「我这次改动,别的程序要不要跟着动?」 界面上的每一块都必须服务于这个判断,或者服务于”为什么我该相信这个判断”。
1.2 视觉方向
低饱和薰衣草色工作台 + 白色业务卡片 + 可折叠单侧栏 + 任务优先的非对称布局。
- 画布是浅紫的,内容是白的。 紫色属于外壳(背景、侧栏), 白色属于信息(卡片、表格、抽屉)。这条边界让”哪里是结构、哪里是内容”一眼可分。
- 低饱和:所有紫色都压过明度,没有一处高饱和的纯色块。界面要在屏幕上被盯很久。
- 一条可折叠侧栏:图标与文字菜单整合在同一列;展开时显示标签、搜索与分组细节, 收起时只留图标列,把宽度让给工作区。两种状态是同一个面板,不卸载、不换页。
1.3 是 / 不是
| 是 | 不是 |
|---|---|
| 审阅台:判断、证据、路径、置信度 | 监控大屏:滚动数字与实时曲线 |
| 任务优先:主任务占更大宽度 | 等分布局:每块一样大 |
| 克制的紫:只做品牌与选中态 | 装饰性渐变与彩色卡片 |
| 语义色只表达语义 | 用红绿做美化 |
| 信息密度高但分层清晰 | 大留白 + 大图标 + 少信息 |
1.4 四条签名特征
这四条是识别本产品的视觉锚点,改动它们等于换产品:
- 可折叠的紫色单侧栏:图标与文字菜单在同一列,展开显示标签与分组,收起只剩图标列。
- 页头方块徽章 + 27px 页面标题:每页开头一个渐变紫方块图标 + 大标题 + 一行副标题。
- 风险色阶驱动的模块地图:节点大小=风险分,颜色=风险等级,紫色描边=本次变更模块。
- 逐跳路径(path)组件:从变更起点开始的一列圆点 + 竖线,每跳标注使用方式。
2. 主题
2.1 双主题,单一令牌源
浅色与深色共用同一套布局规则,只在 :root[data-theme="dark"] 里覆盖颜色令牌。
深色块不新增任何选择器、不修改任何尺寸。
这条纪律的收益是双向的:加一个组件只需要写一次布局;改一次深色只需要改令牌。
2.2 三态偏好
| 偏好 | 行为 |
|---|---|
system(默认) |
跟随操作系统 prefers-color-scheme,系统切换实时生效 |
light |
固定浅色 |
dark |
固定深色 |
- 存储在
localStorage的ca.theme。 index.html在样式表之前内联解析偏好并写入data-theme, 因此深色会话首帧即深色,不会闪白。data-theme-pref始终记录三态原值,data-theme="dark"只表示解析后的结果。 两者分开,否则无法区分”用户选了深色”与”用户跟随系统且系统恰好是深色”。
2.3 画布颜色必须运行时读取
依赖图(Cytoscape)把颜色烘进 SVG 场景,无法继承 CSS 变量。
因此 app.js 的 themeColor(name, fallback) 读令牌计算值再传给图实例,
主题切换时重建图。
禁忌:在 JS 里硬编码颜色。fallback 参数只用于令牌缺失的兜底,不是第二套调色板。
2.4 切换动效
调色板切换必须像调光,不能像闪光。参与主题切换的容器
(body、.app、.sidebar、.card、.stat、.modal、.drawer、.toast、.code、.map)
统一挂 background-color / border-color / color 的 --t-base 过渡。
3. 色彩
3.1 令牌分组
颜色只有一个来源:style.css 的 :root。禁止在组件规则里写十六进制字面量。
| 分组 | 令牌 | 用途 |
|---|---|---|
| 表面 | --app-bg --app-bg-tint --rail-bg --nav-bg --surface --surface-2 --surface-3 --surface-hover |
画布、侧栏、卡片、卡内嵌块、悬停 |
| 品牌 | --primary --primary-600 --primary-700 --primary-50 --primary-100 |
主操作、选中、链接、品牌填充 |
| 文字 | --text --text-2 --text-3 --text-inverse |
正文 / 次要 / 弱化 / 反白 |
| 线条 | --border --border-strong |
分隔线 / 控件描边 |
| 状态 | --success --warning --danger --danger-strong 及各 -bg |
语义反馈 |
| 风险色阶 | --risk-critical --risk-high --risk-medium --risk-low --risk-info |
只用于风险等级 |
| 依赖图 | --map-edge --map-arrow --map-edge-label --map-label-bg |
只被 themeColor() 读取 |
| 代码 diff | --code-add-fg/bg --code-del-fg/bg |
增删行 |
| 浮层 | --overlay-bg --shadow-drawer --toast-* --scrollbar-* |
遮罩、抽屉、轻提示、滚动条 |
完整取值见界面实现映射 · 令牌映射。
3.2 文字三级纪律
| 令牌 | 用在什么上 | 不能用在什么上 |
|---|---|---|
--text |
正文、标题、表格主值、被选中的项 | — |
--text-2 |
次要说明、标签、副标题、未选中的导航项 | 正文段落主体 |
--text-3 |
元信息、时间戳、占位符、大写小标签、图标 | 任何需要被读懂的正文 |
--text-3 是”可以看不见”的层级。把解释性文字放在这里,等于让用户看不见它。
3.3 语义色纪律
- 语义色只表达语义:红=危险/破坏性,黄=需要注意,绿=通过/就绪,紫=品牌与选中。
- 一屏之内,红只出现在真正需要停下判断的地方。 满屏红色等于没有红色。
- 风险等级用风险色阶,不用语义色。二者数值相近但语义不同:风险色阶表达”这条影响有多严重”, 语义色表达”这个操作/状态是什么性质”。混用会让图例与徽章对不上。
- 状态徽章统一走
pill(tone):success/warning/danger/neutral/brand/outline。
3.4 对比度实测
下表是当前令牌对的实际对比度(WCAG 2.1,相对亮度公式实算):
文字对表面(4.5:1 为 AA 正文门槛)
| 组合 | 浅色 | 深色 |
|---|---|---|
--text on --surface |
13.41 ✅ | 13.41 ✅ |
--text-body on --inset-bg |
8.66 ✅ | 10.97 ✅ |
--nav-item-fg on 侧栏 |
7.19 ✅ | 8.48 ✅ |
--primary-700 on --primary-50 |
6.44 ✅ | 8.58 ✅ |
--text-2 on --surface |
4.58 ✅ 临界 | 6.78 ✅ |
--text-2 on --surface-3 |
4.07 ❌ | 6.43 ✅ |
--primary on --surface |
4.57 ✅ 临界 | 6.63 ✅ |
--on-primary on --primary |
4.57 ✅ 临界 | 7.37 ✅ |
--text-3 on --surface |
3.30 ⚠️ 仅大字 | 4.66 ✅ |
--text-3 on --surface-2 |
3.10 ❌ | 4.42 ❌ 临界 |
--text-3 on --surface-3 |
2.93 ❌ | 4.14 ❌ |
语义色对语义底
| 组合 | 浅色 | 深色 |
|---|---|---|
--warning-fg on --warning-bg |
4.53 ✅ | 8.66 ✅ |
--danger-strong on --danger-bg |
4.21 ❌ 临界 | 7.60 ✅ |
--danger-strong on --btn-danger-bg |
4.53 ✅ | — |
--success on --success-bg |
3.57 ❌ | 6.64 ✅ |
--danger on --danger-bg |
3.20 ❌ | 5.93 ✅ |
--warning on --warning-bg |
2.79 ❌ 最低 | 6.64 ✅ |
风险色阶(圆点与图形,按 3:1 非文本门槛)
| 令牌 | 对 --surface(浅色) |
|---|---|
--risk-critical |
3.70 ✅ |
--risk-low |
4.04 ✅ |
--risk-medium |
3.11 ✅ 临界 |
--risk-high |
2.73 ❌ |
--risk-info |
2.02 ❌ 最弱 |
结论:深色主题整体优于浅色。 浅色的 --text-3、--warning、--success、--danger
在小字号(10.5–11px,正是 pill 与标签的字号)下不满足 AA 4.5:1;
--risk-high 与 --risk-info 连非文本 3:1 都不到。
处理原则(不要假装达标):
- 正文与关键数据必须 ≥ 4.5。任何承载判断依据的文字(影响路径、符号名、文件行号、
置信度)不得使用
--text-3,也不得放在--surface-2/--surface-3上。 --text-3只用于装饰性元信息与全大写小标签。这些位置对比度不足是可接受的已知债务, 但新增用法时不要再往里塞新信息。- 语义文字优先用
-strong/-fg变体:警告文案用--warning-fg(4.53)而不是--warning(2.79)。这是现有代码已经在做的事,规范把它固定下来。 - 风险等级不能只靠颜色区分。
--risk-high与--risk-info对白底只有 2.73 / 2.02, 低于非文本 3:1。因此风险等级必须同时有文字标签(严重/高/中/低/信息), 颜色只做辅助。图例也必须带文字,不能只有色点。 - 修
--text-3与--warning的对比度是一次独立改动,需要同时改浅色与深色两套值, 并重跑本文的实测表。不要顺手改。
4. 排版
4.1 字体栈
--font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
--mono: "SFMono-Regular", "JetBrains Mono", "Menlo", Consolas, monospace;
- 界面统一
--font;Inter 由 Google Fonts 外链加载,必须保留系统字体兜底(离线可用)。 --mono只用于标识符:符号名、文件路径、SQL、配置 key、提交 SHA、版本号、diff。 不用于普通文案。- 数字对齐:所有会被纵向比较的数字加
.tabular(font-variant-numeric: tabular-nums), 包括计数、百分比、版本号、表格数值列。
4.2 字号阶梯
实际取值(来自样式表,不是设计稿):
| 字号 | 用途 | 代表选择器 |
|---|---|---|
| 30px | 版本号大数字 | .upd-version-num |
| 27px | 页面标题(≤1180px 降为 22px) | .page-title |
| 21px | 统计块数值 | .stat .value |
| 20px | 页头指标值、进度环百分比 | .head-metric .value .ring-label .pct |
| 18px | 弹窗标题 | .modal-title |
| 16px | 侧栏标题 | .sidebar-context h2 |
| 15px | 卡片标题、流程卡值 | .card-title .flow-value |
| 13px | 正文基准 | body、.row-title |
| 12.5px | 控件、按钮、导航项、副标题 | .btn .nav-item .page-sub |
| 12px | 次要行文字、行描述 | .row-desc、.sidebar-foot .nav-item |
| 11.5px | 元信息、卡片副标题、标签值 | .card-sub .stat .label .meta-row |
| 11px | 徽章、chip、标签 | .pill .chip .tag-chip |
| 10.5px | 全大写小标签、导航分组标题、计数 | .eyebrow .nav-group-title .head-metric .label |
| 10px | 键盘提示、极弱计数 | .search kbd .ring-label .cap |
| 9.5px | 合并标记(唯一一处) | .commit-merge |
规则:新增字号必须落在上表里。上表已经够密,再插一个 12.25px 只会让层级失效。
4.3 字重与字距
- 常规 400,控件与行标题 500,强调 550,标题 600,极少数 650/700(头像、Logo)。
- 标题统一
letter-spacing: -0.01em起,页面标题-0.025em,越大越收紧。 全大写小标签(eyebrow 家族) 是界面里最特别的一族排版:10.5px、大写、正向字距、弱色。 它标记的是结构(分组、列头、区块),不是内容。
主配方(六处,字距略有差异):
| 选择器 | 字距 |
|---|---|
.eyebrow |
0.09em |
.nav-group-title |
0.08em |
.section-label |
0.07em |
.flow-label |
0.06em |
.tr.head |
0.06em |
.nav-tag-head |
0.04em |
统一为:10.5px + uppercase + font-weight: 600 + --text-3。
两个变体(尺寸或字重不同,因为所处位置不同):
| 选择器 | 差异 | 为什么 |
|---|---|---|
.head-metric .label |
0.04em、无 600 | 页头指标标签紧挨 20px 数值,加粗会与数值抢注意力 |
.ev-type |
10px、等宽、0.05em、--primary |
证据类型是标识符不是结构标签,用品牌色区分 |
- 中文不加大写变换,全大写只对拉丁字符有意义——这类标签只用于短英文/数字场景。
- 字距必须为正。大写字距为 0 会挤成一团,这是这类标签唯一的硬性排版要求。
4.4 行高
body基准line-height: 1.5。- 标题收紧到 1.2(
.page-title)或 1.1(.upd-version-num)。 - 多行说明文字放宽到 1.55~1.7(
.row-desc1.55、.code1.65、.release-notes1.7)。 收紧行高会让人读错路径与符号名。
5. 布局
5.1 主骨架
.app ── grid: var(--sidebar-w) minmax(0, 1fr)
├─ nav.sidebar 288px 紫色渐变单侧栏(展开:图标 + 文字 + 分组)
└─ main.workspace 滚动容器,内边距 26px 30px 48px
└─ section.page max-width 1460px,居中
.app高度calc(100vh - 28px),圆角 22px,overflow: hidden。 页面本身不滚,只有.workspace滚。- 收起侧栏 =
.app.nav-collapsed把侧栏轨道换成--sidebar-w-collapsed(图标宽度); 侧栏本身仍在文档里,图标始终可点,focus 不会落进被隐藏的面板。 - 侧栏轨道由
--sidebar-w驱动,断点改它而不是另写一套grid-template-columns。
5.2 卡片栅格
| 类 | 列定义 | 用途 |
|---|---|---|
.grid-2 |
minmax(0,1.5fr) minmax(0,1fr) |
主任务 + 辅助栏(非对称,任务优先) |
.grid-2-even |
repeat(2, minmax(0,1fr)) |
两栏同权 |
.grid-3 |
repeat(3, minmax(0,1fr)) |
三个并列块 |
.stats |
repeat(4, minmax(0,1fr)) |
统计块带 |
.flow |
repeat(3, minmax(0,1fr)) |
三段式结论(变更 → 传播 → 影响) |
.rv-layout |
minmax(0,1fr) minmax(0,1.05fr) |
影响审核:发现列表 + 证据面板(≤1180px 折单列) |
.stack |
单列 + 16px 间距 | 纵向卡片序列 |
.grid-2-even与.grid-3目前没有使用者(只有定义与断点降级规则)。 它们是备用的等分栅格:需要两栏同权或三栏并列时可以直接用,但不要为了”用上新类”而套一层栅格。 当前主任务页都用非对称比例(.grid-2或影响审核页的.rv-layout),这是有意的——任务优先,主栏更宽。
minmax(0, …) 不是可选项。 少了它,网格项的最小内容宽度会撑破容器,
表现为”卡片里文字不换行、整列被裁掉”。
5.3 尺寸表
| 量 | 值 |
|---|---|
| 侧栏展开宽 | 288px(≤1180px 248px,≤860px 236px) |
| 侧栏收起宽 | 72px |
| 工作区内边距 | 26px 30px 48px(≤1180px 20px 20px 40px) |
| 页面最大宽 | 1460px |
| 卡片内边距 | 20px(紧凑 15px) |
| 卡片间距 | 16px(统计块 14px) |
| 页头下间距 | 22px |
| 控件高度 | 按钮 30px 级、输入框 34px 级、小按钮 26px 级 |
5.4 响应式断点
断点是固定尺寸的对应物,必须与上表同改。断点写死数值是有意的: 它们是”这些尺寸加起来超了”的临界点,不是设备分类。
五个断点,分两档:布局断点(1400 / 1180 / 860)改变骨架,局部断点(1024 / 720)只调整单个组件。
| 断点 | 档 | 改动 |
|---|---|---|
| ≤1400px | 布局 | 统计块 4→2 列;.grid-2 主栏比例 1.5→1.35 |
| ≤1180px | 布局 | 侧栏宽 288→248;.app 加 min-width: 0(否则 1180px 下限把右列裁掉且无滚动条);.grid-2/-even/-3、.flow 与复核页 .rv-layout 全部单列(粘性证据栏恢复文档流);页面标题 27→22px;.tr-history 与 .tr-sources 收缩固定轨道;代码源列表隐藏标签与同步时间两列;剩余溢出在卡片内横向滚动 |
| ≤1024px | 局部 | 版本号 30→25px;发布记录行改为纵向堆叠 |
| ≤860px | 布局 | 侧栏宽 248→236;统计块单列;.tr .cell.optional 隐藏;.tr-changes .tr-history .tr-commits .tr-sources 各自减少轨道;版本台账保留六列改为横向滚动;步骤条改两列换行;标签单元与筛选条收窄 |
| ≤720px | 局部 | 提交范围行去掉 meta 列;更新横幅动作组占满整行 |
降级原则:先砍列,再收缩,最后在卡片内横向滚动。永远不要靠 body { overflow: hidden } 藏掉内容——
用户没有任何办法把被裁掉的右列拿回来。
5.5 滚动约定
- 外壳不滚,
.workspace滚。 - 卡片内长列表(
.peer-picker168px、.commit-list190px、.picker-list320px、#an-commits360px、.code280px、.release-notes320px)自己滚,不把外层撑高。 - 宽表格在窄屏放进
.upd-ledger-scroll/.card.flush > .table这类横向滚动容器, 而不是压缩到不可读。 - 滚动条统一 9px、圆角 6px、
--scrollbar-thumb,轨道透明。
6. 几何与阴影
6.1 圆角四级
| 令牌 | 值 | 用途 |
|---|---|---|
--r-card |
16px | 卡片 |
--r-tile |
14px | 统计块 |
--r-control |
10px | 按钮、输入框、下拉 |
--r-pill |
999px | 徽章、标签、分段控件 |
另有固定的中间档:12px(行卡片、流程卡、callout、时间线块)、10px(代码块)、
9px(行内图标块、证据行)、8px(小按钮、导航项)、6px(chip)、4px(合并标记)。
外壳与弹窗更大:.app 22px、.modal 18px、.title-badge 13px、.sidebar-logo 12px。
层级对应:越大越外层的容器圆角越大。把卡片做成 8px 而按钮做成 16px,层级会读反。
6.2 阴影四级
| 令牌 | 用途 |
|---|---|
--shadow-soft |
极轻:选中态导航项、激活 tab |
--shadow-card |
卡片与统计块默认 |
--shadow-hover |
行卡片悬停 |
--shadow-pop |
弹窗(唯一的大阴影) |
另有 --shadow-drawer(抽屉,向左投射)、--app-shadow(外壳)、
--brand-shadow*(品牌按钮与徽章的彩色投影)、--toast-shadow。
纪律:
- 阴影表达层级,不表达装饰。同一层级的元素阴影必须一致。
- 只有弹窗与抽屉可以有明显的大阴影,卡片一律克制。
- 不要给卡片加彩色阴影;彩色投影只属于品牌元素(主按钮、Logo、页头徽章)。
7. 组件
组件按”从外到内”排列。每项给出结构 / 变体 / 状态 / 禁忌。
7.1 应用外壳 .app
结构:nav.sidebar + main.workspace。
- 背景是径向渐变(
--app-grad-a→--app-bg→--app-grad-b), 这是全站唯一的大面积渐变画布。 - 圆角 22px 加外壳阴影,让工作台像一张浮在桌面上的纸。
- 状态:默认收起(
.nav-collapsed)/ 展开;收起是初始态,展开是用户选择。
7.2 侧栏 .sidebar
结构:.sidebar-top(Logo + 品牌名 + 折叠按钮)→ .sidebar-context(eyebrow + 页面标题 + 添加)→
.search → .nav-scroll(主功能 + 代码源 / 状态 / 最近分析)→ .sidebar-foot(历史 / 设置 /
已忽略 / 主题 / 头像)。
- 整条侧栏是紫色渐变(
--rail-grad-*),与画布同属外壳;搜索框与选中行是白色内容。 - 顶部 Logo 40×40、圆角 12px、品牌渐变、白字 “CA”;品牌名与副标题只在展开态出现。
- 主功能 6 项(总览 / 代码源 / 影响审核 / 分析详情 / 契约地图 / 复核队列)用
.nav-item:图标 15px、文字 12.5px、右侧计数 pill;border-radius: 9px。 active 用--primary实心 + 白字 + 品牌阴影。 - 代码源 / 状态 / 最近分析是分组细节,只出现在展开态;分组标题 10.5px 全大写。
- 代码源条目每行带一枚 22px 品牌磁贴(
.row-icon,圆角 6px、图标 15px)。磁贴自成一格, 不跟随 active 行的白色图标:底色固定--primary-50、字形--primary-700(规范 3.4 实测 6.44:1 / 8.58:1);失败代码源换--danger-bg+--danger-strong。active 行只把条目主图标 染成--on-primary——否则白字形落在浅色磁贴上会直接消失,深色主题下反过来是深字形落在深色磁贴上。 - 计数默认半透明白底;
.alert用红底红字。未读点位于条目右侧,收起态改为右上角绝对定位。 - 折叠按钮放在常驻的
.sidebar-top里;它是侧栏的一部分,收起后仍可见并负责展开。
7.3 侧栏收起态 .nav-collapsed
这是默认态。 首次打开就是图标列;展开后写入 localStorage 的 ca.nav,下次首帧保持展开。
- 轨道换成
--sidebar-w-collapsed(72px),但面板不卸载:图标仍然可点, 搜索、标签、分组标题与计数退出布局(display: none),不会进入 Tab 序列。 - 主功能与底部条目收成 40×40 圆形按钮,图标 18px;未读点改为右上角绝对定位。
- 折叠 / 展开按钮的
aria-expanded同步反转;#nav-expand只在收起态出现。 - 收起是同一个面板的密度变化,不是换一套导航:用户不需要重新学习入口位置。
7.4 页头 .page-head
面包屑(工作区 / 页面)
[渐变方块徽章 40px] 页面标题 27px [状态 pill]
副标题一行(次要信息,可含分隔点)
…右侧:指标组 + 操作按钮组
.head-metric:10.5px 全大写标签 + 20px 数值,可带small单位。- 页头右组在窄屏换行到下一行(
flex-wrap: wrap),不挤压标题。 - 每页必须有且只有一个
.page-title。
7.5 卡片 .card
- 白底、
--card-border、--r-card、--shadow-card、内边距 20px。 .card.flush:零内边距 + 隐藏溢出,用于表格直接贴边。.card-head= 标题区(.card-title15px +.card-sub11.5px)+ 右侧动作组。- 禁忌:卡片内不要再套一张有阴影的卡片。需要分组用
.flow-card/.callout这类无阴影的浅底块。
7.6 统计块 .stats / .stat
- 4 列,
.stat内为 34px 图标块 + 数值 21px + 标签 11.5px。 - 图标块色调变体:默认品牌紫、
.danger、.success、.warning。 - 标签单行省略号,数值
.tabular。 - 禁忌:统计块只放能一眼比较的量。需要解释的内容放卡片。
7.7 徽章与 chip
| 组件 | 尺寸 | 用途 |
|---|---|---|
.pill |
11px / 3px 9px / --r-pill |
状态、风险等级、模式 |
.pill .dot |
5px 圆点 | 与文字同色的状态点 |
.chip |
11px / 2px 7px / 6px 圆角 |
键值元信息 |
.tag-chip |
11px / 550 字重 / 品牌浅底 | 可删除的标签 |
.tag-pill |
10.5px | 行内标签(可点击过滤);.active = 当前筛选,实心紫底 |
变体:success warning danger neutral brand outline。
禁忌:不要用徽章表达长句;超过 6 个字的判断放正文。
7.8 按钮 .btn
| 变体 | 外观 | 用途 |
|---|---|---|
| 默认 | 白底 + 描边 | 次要操作 |
.btn-primary |
紫底白字 + 品牌阴影 | 每个视图只应有一个主操作 |
.btn-ghost |
透明底无描边 | 弱化的第三级操作 |
.btn-danger |
浅红底 + 红字 | 破坏性操作 |
.btn-sm |
11.5px / 5px 10px / 8px 圆角 |
卡片头、行内 |
.btn-block |
100% 宽 | 表单主提交 |
状态:默认 / hover(浅紫底 + 紫字)/ active(下移 1px)/ disabled(opacity: .5 + not-allowed)/ loading(内嵌 .spinner,11px)。
- 图标槽 14px(
.btn-sm内 spinner 缩到 11px 才不撑破行高)。 - 禁忌:不要用纯色块做”卡片式按钮”;不要给次要按钮加阴影。
7.9 表单
.field= 标签(11.5px--text-2)+ 控件 +.hint(11px--text-3)。- 必填标记
.req用--danger-strong。 - 输入框默认
--surface-3无描边;focus 时变白底 + 浅紫描边 +--focus-ring。 select去掉原生箭头,用内联 SVG 背景图 + 右侧 28px 内边距。- 只读派生值(如自动推导的名称):透明底 + 虚线描边 +
--text-2+ 无 focus 环。 虚线是”这是算出来的,不是填的”的唯一视觉线索。 - checkbox 用
accent-color: var(--primary)。 input[type="date"]与文本输入共用同一套控件外观,只有原生日历图标(鼠标样式为 pointer); 日期就是日期,不另造一套日历浮层。- 同一行里的字段会随模式显隐时,用
.field-row.single让剩下的字段占满整行。 留半列空位会被读成”少了一个控件”,比换行更费解(提交范围就靠它收起基线输入框)。 - 禁忌:不要用 placeholder 代替标签。placeholder 在输入后消失,用户会忘记字段含义。
7.10 列表与行卡片
| 组件 | 结构 | 用途 |
|---|---|---|
.list-row |
行内分隔线、12px 圆角 | 连续列表 |
.card-row |
独立卡片、12px 圆角 | 发现列表(每条是独立判断单元) |
.tr |
网格行、10px 圆角 | 数据表格 |
.tr.head |
10.5px 全大写表头 | 列标题 |
分段控件
.tabs/.tab目前只用于影响审核页的发现分组(#rv-tabs:待审核 / 已确认 / 已忽略)。 它是胶囊底 + 白色激活块的形态,.tab .n承载计数。不要为页面切换另造一套 tab—— 页面切换由左侧导航负责。影响审核页只有一处影响清单。 对比程序不单独占卡:发现列表标题下用一行
.peer-strip汇总(每个程序一枚.pill:程序 · 契约数 · 影响处数),逐条影响只在下方出现一次; 右侧证据面板在未选中发现时承载完整契约命中与对方使用点,选中后切换为证据链。 总览的合并卡复用同一个.peer-strip。影响清单被截断时必须说明。 一次大变更可能产生数十万条跨程序影响,报告按风险只保留前 N 条 (
code-association.max-report-impacts,默认 5000)。发现列表上方用一块.callout.warning(#rv-impacts-note,图标#i-alert-circle,文案取report.impactsNote)说明截断了多少; 未截断时整块.hidden,不占位。页头副标题的「N 项影响」用全量的report.impactsTotal, 列表条数只用于标签计数与分页——截断而不说,等于让用户以为影响只有这么多。
- 行卡片 hover:描边转浅紫 +
--shadow-hover;选中:浅紫底 + 浅紫描边。 .row-icon32px 圆角 9px,色调变体同统计块。.row-desc两行截断(-webkit-line-clamp: 2),完整内容在详情里。- 行内破坏性动作(删除):默认
opacity: 0,行 hover 或按钮 focus 时显现;@media (hover: none)下常驻——触屏没有 hover,藏起来等于没有这个功能。 - 操作列的最后一轨必须容得下动作本身:
.tr-sources的 76px 是编辑 26 + 删除 24 + 箭头 14 + 两个 4px 间距(共 72px)之后的余量。.cell.actions右对齐(flex-end), 轨道偏窄不会裁切而是向左溢出,压住前一列——「同步时间」曾被盖住,编辑按钮看起来 像根本没渲染出来。 - 表格栅格模板必须显式声明(
.tr-*)。不声明会退化成单列纵向堆叠, 一行能长到 260px 高。 - 图标只能引用 sprite 里已定义的
#i-*:<use href="#…">命中不到<symbol>时 不报错、不占位,按钮直接渲染成一块看不见的空白——比缺一个按钮更难发现。 新增图标先加<symbol id="i-…">,再在app.js里icon('i-…')引用;UiContractTest会逐个核对,漏定义即失败。
栅格模板族(列数 = 单元格数,断点降级时同步改):
| 模板 | 默认列 | 用途 |
|---|---|---|
.tr-sources |
76px / 1.7fr / 96 / 150 / 128 / 92 / 76(7 列) | 代码源列表 |
.tr-history |
48px / 1.4fr / 96 / 150 / 130 / 58(6 列) | 分析历史 |
.tr-changes |
124px / 1fr / 168 / 26(4 列) | 变更清单 |
.tr-commits |
116px / 1fr / 130 / 110(4 列) | 提交范围 |
.tr-ledger |
110px / 1fr ×5(6 列,可横向滚动) | 版本台账 |
.tr-impacts |
已废弃(无使用者) | 见第 13 章 #10 |
降级开关 .cell.optional:≤860px 时统一隐藏。标 .optional 等于声明”这一列可以为了宽度让路”。
必须保留的列不要标 optional——分析历史的状态列就是例子:失败原因才是打开这个列表的理由。
断点里的轨道数必须恰好等于该行剩余单元格数。多一条轨道会把尾列推到第二行, 整行变成两行高;少一条轨道则最后一格跨列。这是这类表格最常见的错法。
7.11 分页 .pager
- 上分隔线 + 左”第 x / y 页 · 共 n 项” + 右页码按钮组(
.btn.btn-sm,最小 30px)。 - 当前页用
.btn-primary,首/尾页的上一页/下一页按钮disabled。 - 页码最多 7 个,超出用
.pager-gap省略号,flex-wrap折行。 - 只有一页时整条隐藏:没有可翻的页,控件只占地方。
- 每页 50 条。页码控件由脚本动态插入列表之后,静态骨架里没有这个节点。
7.12 进度、步骤条与环
.progress:6px 高、999px 圆角,.thin4px;填充色默认品牌紫,变体 success/danger/warning;宽度过渡 400ms。- 总量未知时用
.indeterminate:斜纹滑动条纹(1.1s linear 无限),不编造百分比。 条纹在prefers-reduced-motion下改为静止的浅色实心条——直接压掉动画会把条纹停在半周期, 看起来像坏了。
- 总量未知时用
.ring:92px 环形,stroke-dashoffset过渡 500ms,中心百分比 20px + 说明 10px。.stepper:等分列,每步 24px 圆点 + 标题 + 元信息。- 状态:待办(灰)/ 进行中(品牌)/ 完成(绿 + 勾)/ 失败(红)
- 失败不是”卡住的进度条”:必须把原因完整可读地展示出来,不截断。
- ≤860px 改两列换行。
7.13 三段式结论 .flow
[变更] → [传播] → [影响]
标签 10.5px 全大写 / 值 15px 600 / 说明 11.5px
- 底色
--surface-2+--border;.accent用品牌浅底;.ok用成功浅底。 - 值可带
.danger/.success语义色。 - 禁忌:三段必须对应同一条链路。混进无关指标会让”变更 → 传播 → 影响”的读法失效。
7.14 时间线与路径
两个组件共享同一套”圆点 + 竖线”语法,但语义不同:
| 组件 | 圆点 | 语义 |
|---|---|---|
.timeline / .tl-* |
9px,浅紫/红/绿 | 事件先后顺序 |
.path / .path-* |
9px + 3px 光晕,起点红色 | 传播链路,逐跳因果 |
.path-sym用等宽字体 11.5px,.path-rel11px 说明关系,其中b用--primary-700强调关系类型。- 路径是本产品的核心证据形态:必须能从起点读到终点,不允许折叠中间跳。
7.15 依赖图 .map
- 高 360px,
--inset-bg底、12px 圆角、内描边。 - 图例在下方,7px 圆点对应五个风险等级。
- 节点大小=风险分,颜色=风险等级,紫色描边=本次变更模块;实线=调用/引用/接口/SQL/配置,虚线=Maven 依赖。
- 颜色全部来自
themeColor(),主题切换必须重建实例。 - 容器宽度变化(侧栏收起)后需要重新 layout,否则图还停在旧尺寸。
7.16 弹窗 .overlay / .modal
- 遮罩:全屏、
--overlay-bg、backdrop-filter: blur(3px)、z-index 90。 - 弹窗:宽
min(560px, 100%)(文件选择器min(620px, 100%))、圆角 18px、--shadow-pop、 最大高calc(100vh - 64px)。 - 结构:
.modal-head(38px 图标块 + 18px 标题 + 12px 副标题 + 右上关闭)/.modal-body(可滚)/.modal-foot(分隔线 + 左侧说明 + 右侧动作)。 - 进场:220ms,
translateY(10px) scale(0.985)→ 原位。 - 关闭方式:右上按钮 / 点遮罩 /
Escape。带草稿的弹窗(添加代码源)关闭时要重置草稿。 - 语义:
role="dialog"+aria-modal="true"+aria-labelledby。
7.17 抽屉 .drawer
- 从右侧滑入,宽
min(720px, 94vw),全高,--shadow-drawer向左投射。 - 用于影响详情:在保留列表上下文的前提下看证据链。
- 进场 220ms,
translateX(24px)+ 透明度 0.6 → 原位。 - 禁忌:抽屉不用于表单提交。需要用户”停下来做决定”的用弹窗,需要”边看边对照”的用抽屉。
7.18 轻提示 .toast
- 右下角固定(
bottom: 22px; right: 22px),z-index 120,纵向堆叠,右对齐。 - 12.5px、深色底(
--toast-bg)、10px 圆角、最大宽 380px。 - 图标:成功绿 / 失败红;无图标 = 中性提示。
- 生命周期:2600ms 后淡出 200ms 再移除。
- 禁忌:toast 只报告”刚刚发生了什么”,不承载需要阅读的内容。 错误原因、失败详情必须同时留在页面上。
7.19 空态 .empty
- 虚线描边 +
--surface-2底 + 居中 + 12.5px--text-3。 strong一行 13px 说明”为什么这里是空的”,下一行给”接下来做什么”。- 每个列表、每个页面都必须有空态。没有空态的列表会被误读成加载失败。
7.20 提示块 .callout
- 12px 圆角、12px 内边距、图标 15px + 文字 12px / 1.6 行高。
- 变体:默认(中性)、
.warning、.danger、.brand。 - 用途:解释规则、说明边界、警告不可逆操作。
- 禁忌:callout 不是装饰。一屏超过两个 callout 说明信息架构需要重做。
7.21 代码与 diff .code
- 等宽 11.5px / 1.65 行高、
--inset-bg底、10px 圆角、最大高 280px、可滚。 .add绿字绿底、.del红字红底、.ctx--text-3。整行着色(display: block)。- 保留
white-space: pre:代码换行会破坏可读性。
7.22 证据行 .evidence / .ev-*
- 9px 圆角小卡 +
--surface-2底。 .ev-type10px 等宽全大写品牌紫(证据类型),.ev-main正文,.ev-meta10.5px 等宽元信息。- 长路径与 URL 必须
word-break: break-word,不允许撑破卡片。 - 未选中发现时,复核页的证据面板用
.evidence展示本轮契约命中;选中后同一位置切换为路径与证据。 - 证据行与路径跳点下方内联引用行所在的整个代码块(
.code-snippet:.code-line/.code-n/.code-t, 引用行加.hl);片段自身横向滚动,不撑破卡片。 - 片段上方一行 10.5px 灰色小字(
.code-snippet-caption)写明范围:「方法 · 第 12–48 行」「类型 · 第 1–80 行」「元素 select · 第 5–20 行」「SQL 语句 · 第 3–24 行」「代码块 · 第 5–20 行」, 没有代码块边界的文件写「引用行上下 10 行」。读者先要知道自己在看一个完整方法还是一段被裁过的代码, 才知道这段代码能信多少;这行只讲定位,「分析版本不在本地仓库」「文件只到第 N 行」这类异常说明 仍由note单独一行承担,两者不合并。 - 代码块超过 160 行时只展示引用行附近 160 行,并把「共 N 行,展示第 a–b 行」写进上面那行小字: 不设上限的话,一个自动生成的大类会让单条证据撑满整屏,请求体也会涨到几百 KB。
7.23 AI 审核卡片 .ai-*(可选功能)
AI 审核默认关闭。关闭时页面上不出现任何新元素——一个不启用它的安装不应看到它的痕迹。
这个组件在视觉上要解决一个特殊问题:让读者一眼分清”证据”与”解释”。
| 元素 | 类名 | 规则 |
|---|---|---|
| 分析页卡片 | #an-ai-card |
未配置时整卡 .hidden;有报告且已配置才出现 |
| 生成正文 | .ai-text |
12.5px / 1.7 行高 / --text-body / pre-wrap |
| 溯源行 | .ai-foot |
11px --text-3——这是 --text-3 唯一正当的使用场景:模型名与时间是可忽略的元信息 |
| 「待验证」标注 | .ai-verified-note |
警告色底 + 警告色字 + 13px 图标,贴在卡片头里,紧邻标题 |
| 动作组 | .ai-actions |
右对齐;生成按钮 .btn-sm |
| 设置页状态行 | .ai-status-row |
状态 pill + 11.5px 说明;GitLab 私有仓库卡片(7.25)复用同一配方,不另造类名 |
| 快捷填充 | .ai-presets |
三个 .btn-sm,刻意用普通按钮——它们是输入捷径,不是决策,不能和「保存」抢主位 |
三条硬规则:
- 「AI 生成 · 待验证」必须贴着内容,不能放脚注。 标注放在卡片头、紧邻标题, 让警告随内容一起移动。放在页面底部的免责声明没有人会读到。
- AI 正文用
--text-body(8.66 对比度),不用--text-3。 它是最长的一段可读文字, 行高放宽到 1.7 也是为此——行距紧会让人读错评审要点。 - 快捷填充不改变 Key。 三个预设只填接口地址与模型;Key 是账号凭据,不该被模板覆盖。
失败态:AI 失败时只影响这张卡片(就地显示原因 + 重试),分析结论与证据链完全不受影响。 界面上必须让读者看到这一点——AI 是解释层,不是证据层。
.ai-verified-note 使用 --warning-fg(4.53)而非 --warning(2.79),
符合 3.4 的对比度纪律。
7.24 提交选择 .commit-list / .commit-row / .commit-actions
「新建分析」选到 COMMIT_RANGE 时,表单用它回答”这次到底分析哪些提交”。它刻意不是多选下拉:
一次要审的常是十几个提交,用户要看清标题、作者与时间才决定勾谁。
结构
- 一个
.field:标签「选择要分析的提交」+.req必填标记,.hint常驻计数, 其下是.commit-actions(全选/清空)与.commit-list。 - 行是
<label class="commit-row selectable">:checkbox +.commit-sha(短 SHA,--mono+--primary).commit-subject(标题,单行截断)+.commit-meta(作者 · 时间,10.5px--text-3)。 整行都是勾选热区——三十行的 14px 小方框逐个点是折磨。
.commit-list上限 190px、自己滚(5.5):范围可能上百个提交,弹窗不能被它撑高。 底--surface-2、描边--border、圆角 10px;列表为空时整块隐藏(:empty),不显示空框。.commit-actions放在滚动区之上:列表再长,全选/清空也不会被滚走。
状态
- hover 行
--surface-hover;已勾选行--primary-50底,标题提到--text+ 字重 500。 - 筛选(换分支 / 改日期)后不在新列表里的提交同时取消勾选:选择属于屏幕上的列表, 不属于历史;看不见的提交不能跟着请求走。
全选只作用于当前列表,不是整个分支历史;清空清掉所有勾选。- 空态与读取失败只用
.hint一句话说明(”这个时间范围内没有提交。”/ 服务端note),不出 toast。 - 一个都没勾就点「开始分析」:用弹窗内的错误条说明「请至少勾选一个提交」,不启动分析(见 8.2)。
日期窗口(同属该模式的字段)
- 两个
input[type="date"],标签写明「起始日期(含)」「结束日期(含)」——括号里的”含” 是契约:服务端按提交时间取整天,两端都包含。 - 留空即不设边界;「不填日期时列出该分支最新的 100 个提交。」作为常驻
.hint, 让”为什么只有 100 行”在用户开口之前就有答案。 - 起始晚于结束时,
.hint直接说「起始日期不能晚于结束日期」,不发请求、不留一个空列表。 - 列表被截断时
.hint必须说明”只列出最新的 100 个,可用时间范围缩小”: 截断而不说,等于让用户以为分支就这么多提交。
禁忌
- 不要用带
multiple的select代替:看不到作者与时间,几十行时无法核对。 - 不要在列表为空时保留
全选/清空:作用于空集合的按钮只会让人怀疑状态没刷出来。 - 不要把日期窗口当成”范围”:它只缩小候选,起止两端的分析属于
GIT_DIFF。 - 不要预勾选任何提交。勾选就是这次分析的范围,替用户默认打勾等于替他决定评审什么。
7.25 GitLab 私有仓库卡片(设置页)#card-gitlab
设置页在 AI 卡片 #card-ai 与导入导出卡片 #card-transfer 之间多一张卡片,用来填写 GitLab
个人访问令牌(PAT)。它不新增组件类,全部由既有组件拼成:
| 元素 | 复用类名 / id | 规则 |
|---|---|---|
| 卡片 | .card #card-gitlab |
与 AI 卡片同宽(grid-column: 1 / -1) |
| 状态行 | .ai-status-row / .ai-status-note #gl-status #gl-status-note |
与 AI 卡片共用「pill + 11.5px 说明」配方(7.23),不另造类名 |
| 令牌输入 | .field .path-field #gl-token |
HTML 标准密码输入框;永远不预填明文,占位符只说明「已保存 <掩码>」或「未设置」,留空 = 保持已保存的值掩码> |
| 清除 | .btn.btn-sm #gl-clear |
与输入框同排,放在 .path-field 内;清除前必须确认 |
| 保存 | .btn.btn-sm.btn-primary #gl-save |
卡片头动作组,沿用 AI 卡片「一个主操作」的排法 |
| 说明 | .callout .hint .ai-foot |
scope、保存位置、优先级与配置文件路径 |
纪律:
- 回填只写 placeholder(掩码)与配置文件路径,任何情况下都不把令牌写入 DOM。
- 表单只在首载 / 保存 / 清除后回填;轮询刷新只更新状态行——与 7.23 的 AI 表单同一条规则。
- 「清除」是破坏性操作(8.3):先确认,再发清除请求;空输入保存等于”保持不变”,不是”删除”。
- 卡片只在设置页出现;添加 / 编辑代码源时,凭据环境变量留空才回落到这张卡片保存的令牌。
7.26 配置导入导出卡片(设置页)#card-config
设置页在 GitLab 卡片 #card-gitlab 与代码源导入导出卡片 #card-transfer 之间多一张卡片:
一个 JSON 带走设置项(AI 审核、GitLab 令牌)与全部代码源。两张导入导出卡片并存不是重复——
一张搬”这台机器的全部配置”,一张只搬代码源;合成一张会逼着只想换代码源的人连带接受设置覆盖。
同样不新增组件类,全部由既有组件拼成:
| 元素 | 复用类名 / id | 规则 |
|---|---|---|
| 卡片 | .card #card-config |
与相邻卡片同宽(grid-column: 1 / -1) |
| 导出 | .btn.btn-sm #cfg-export data-config-export |
卡片头动作组左侧;window.location 直接下载,不经 fetch——导出文件可能很大,不该先读进 JS 内存 |
| 导入 | .btn.btn-sm.btn-primary #cfg-import data-config-import |
卡片头唯一主操作;点它只是打开隐藏的文件输入 |
| 文件输入 | input[type=file] #cfg-file |
accept="application/json,.json" + hidden,不占位 |
| 密钥开关 | .checkbox #cfg-secrets |
文案「包含密钥(API Key / GitLab 令牌,明文)」;永远不预勾选 |
| 规则提示 | .kv-list #cfg-transfer-hint |
四行:导出内容 / 不导出 / 导入规则 / 密钥规则 |
| 结果宿主 | #cfg-result |
成功、部分成功、失败都渲染在这里,不另开弹窗 |
密钥规则(三条一起读):
- 默认不勾选。未勾选导出的文件不含任何密钥;勾选后才写入 API Key 与 GitLab 令牌的明文。
- 明文导出是一次明确的决定,必须由用户当次打勾——替用户记住上次的勾选,等于替他决定。
- 导入的文件不含密钥时,保留本机已保存的 Key 与令牌,不是清空。文件里没有的东西不能删掉文件外的东西。
结果呈现(8.1 的部分成功):
| 结果 | 呈现 |
|---|---|
| 设置项 | .import-counts 两行:已应用的设置项(AI 审核 / GitLab 令牌)+ 密钥是否随文件导入 |
| 代码源 | .import-counts 四行:共读取 / 新建 / 更新(合并标签)/ 跳过 |
| 失败项 | .import-errors 逐条列出:设置项以「设置」+ 段名 + 原因,代码源以 #序号 + 名称 + 原因 |
| 文件本身非法 | 4xx 的中文消息进 .callout.danger + 一条错误 toast(「导入失败:…」) |
- 部分成功不得显示成「导入失败」:设置项被拒而代码源照常合并是正常结果,计数与逐条原因要同时给出。
- 设置项与代码源的失败放在同一份逐条列表里,但各带来源标识——一句话的 toast 说不清是哪个段没生效。
导入后刷新:
- 设置项已落盘,客户端缓存必须整体作废:清
state.ai.loaded/state.ai.formFilled、state.gitlab.loaded/state.gitlab.formFilled与state.health,再renderSettings()重填 AI 与 GitLab 表单。 否则两张卡片会继续显示导入前的值,直到用户刷新页面——这正是 7.23 / 7.25「表单只在首载 / 保存后回填」的例外情形: 导入等价于一次保存。 - 代码源同步刷新:
refreshAll(false)→loadTags()→renderNavSources()→renderSourcesPage(), 与代码源导入导出卡片走同一串调用,侧栏分组与列表不会各自停在旧数据上。 - toast 要点名生效了什么(「AI 审核、GitLab 令牌 已应用;代码源 新建 2 / 更新 1」),而不是只说「导入完成」。
禁忌:
- 不要预勾选密钥开关,也不要把勾选状态记进
localStorage。 - 不要把设置项与代码源的失败合并成一句「部分内容未导入」。
- 不要在导入成功后只重绘结果区:设置表单与侧栏是另外两处缓存,必须一起刷新。
8. 反馈与状态
8.1 五种状态,缺一不可
任何异步区域都必须实现全部五种:
| 状态 | 表现 |
|---|---|
| 加载中 | 骨架或 .spinner + 一行”正在…”;不要只放一个转圈 |
| 空 | .empty 说明原因 + 下一步动作 |
| 成功 | 内容本身;必要时一条 toast |
| 失败 | 就地显示原因(不截断、不折叠)+ 重试入口 |
| 部分成功 | 计数 + 逐条错误列表,不因一条失败中断整体 |
8.2 错误呈现分级
| 级别 | 呈现 |
|---|---|
| 字段级 | 字段下方 .hint 红色文字 |
| 区块级 | 该区块内 .callout.danger,其他区块照常可用 |
| 全局 | 顶部横幅或页面级 callout,仍保留可操作入口 |
错误必须说明三件事:发生了什么、为什么、下一步怎么办。 “操作失败”不是错误信息。
8.3 破坏性操作
- 必须二次确认,且确认方式要与破坏程度匹配: 删除代码源要求输入名称才能点确认(不可撤销且级联删分析记录); 清除 GitLab 令牌一句话确认即可(随时可以重新填写,不级联删除任何数据)。
- 确认按钮用
.btn-danger,位置在右侧主位。 - 禁止用 toast 做”已删除 + 撤销”这种可逆承诺,除非真的实现了撤销。
8.4 进度可见性
- 长任务必须显示阶段名而不只是百分比。 “54%” 说不出它在哪一步;”3 / 8 步 · 索引目标版本 · 54%” 可以。
- 总大小未知时不编造百分比:走不确定态条纹,只显示已接收量。
- 运行中最多显示 99%,100% 只属于真正结束的那一步。
- 轮询只在任务活跃时进行,结束后立即停止。
9. 动效
9.1 令牌
--ease: cubic-bezier(0.32, 0.72, 0.28, 1); /* 快出慢入,收尾柔和 */
--t-fast: 150ms; /* 颜色、悬停、焦点 */
--t-base: 180ms; /* 布局轨道、主题切换、页面进场 */
9.2 允许的动画
| 动画 | 时长 | 用途 |
|---|---|---|
| 颜色/描边/阴影过渡(32 处 transition 的主体) | --t-fast 150ms |
所有 hover/focus 反馈 |
grid-template-columns(侧栏收起) |
--t-base 180ms |
侧栏轨道由 288px 变到 72px |
页面进场 page-in |
--t-base |
opacity + translateY(4px) |
遮罩进场 overlay-in |
--t-base |
opacity |
弹窗进场 modal-in |
220ms | translateY(10px) scale(.985) |
抽屉进场 drawer-in |
220ms | translateX(24px) + 透明度 |
toast 进场 toast-in |
200ms | translateY(8px) |
| 进度条宽度 | 400ms | 数值变化 |
| 环形进度 | 500ms | stroke-dashoffset |
spinner spin |
800ms linear 无限 | 加载 |
不确定进度条纹 progress-slide |
1.1s linear 无限 | 总量未知 |
| 主题按钮图标 | --t-base |
hover 旋转 -18° |
| 按钮按下 | transform 80ms | translateY(1px) |
| 开关滑块 | --t-fast |
translateX(11px) |
入场时长的分层是有意的:页面与遮罩用 --t-base,浮层(弹窗/抽屉/toast)用 200–220ms
的裸值。浮层比基础时长略慢,让人感到”它浮起来了”。这三个裸值尚未令牌化,
新加入场动画请沿用 200–220ms 这个区间,不要另起一个 150 或 400。
9.3 禁止
- 超过 300ms 的交互反馈动画(进场动画除外)。
- 位移动画超过 24px。
- 自动播放的循环动画(spinner 除外)。
- 弹跳、回弹、旋转超过 90°。
- 用动画掩盖加载延迟。
9.4 降低动效
prefers-reduced-motion: reduce 时,全部动画与过渡压到 0.01ms、迭代 1 次。
这是全局规则,新增动画自动继承,大部分情况不需要单独处理。
但有一个例外必须单独处理:无限循环的动画被压到 0.01ms 后会停在周期的某个随机位置,
看起来像坏了。不确定进度条纹已经单独补了 animation: none + 静止条纹(语义不变)。
新增无限循环动画时,必须同时写一条 reduced-motion 覆盖,给出静止状态。 一次性入场动画不需要。
10. 无障碍
10.1 焦点
- 键盘焦点必须始终可见:
:focus-visible统一outline: 2px solid var(--primary)+outline-offset: 2px+border-radius: 8px。 - 鼠标点击不显示焦点环(
:focus:not(:focus-visible) { outline: none })。 - 输入框另有 focus 环(
--focus-ring),属于控件自身状态,与键盘焦点并存。
10.2 键盘
| 操作 | 键 | 行为 |
|---|---|---|
| 收起 / 展开侧栏 | Ctrl + B / ⌘ + B |
在输入控件内让位 |
| 聚焦全局搜索 | Ctrl + K / ⌘ + K |
⚠️ 当前未让位(见第 13 章 #8) |
| 关闭弹窗 / 抽屉 | Escape |
关闭所有可见浮层;带草稿的表单同时重置 |
| 文件夹选择器跳转 | Enter(路径框内) |
— |
- 全局快捷键在
input/textarea/select/contenteditable内自动让位。 这是硬要求:一个吃掉用户输入字符的快捷键,比没有这个快捷键更糟。 - 收起侧栏只把文字标签、搜索与上下文分组移出布局(
display: none),图标仍然可见可点; Tab 顺序与视觉顺序一致,不会停在 0px 宽的隐藏控件上。折叠/展开按钮始终留在侧栏顶部。 - Tab 顺序必须与视觉顺序一致;不要用
tabindex正数重排。
焦点管理的现状:打开浮层不自动聚焦首个控件(只有表单字段用 60ms 定时聚焦), 关闭后不归还焦点,也没有焦点陷阱。这三点是已知缺口,见第 13 章 #7。
10.3 ARIA 契约
| 元素 | 必需属性 |
|---|---|
主导航 .sidebar |
aria-label="主导航" |
| 图标按钮 | title + aria-label(两者都要:前者给鼠标,后者给读屏) |
| 折叠/展开按钮 | aria-expanded + aria-controls="sidenav" |
| 主题按钮 | aria-pressed 反映解析后是否为深色 |
| 弹窗 | role="dialog" + aria-modal="true" + aria-labelledby |
| 列表型浮层 | role="listbox" + aria-label |
| 进度条 / 更新条 | aria-live="polite" |
| 全局横幅 | role="status"(非打断式播报) |
| 标签筛选 pill | aria-pressed 反映是否处于筛选 |
| 纯装饰图标 | aria-hidden="true";雪碧图容器整块 aria-hidden |
收起态必须同步两个按钮的 aria-expanded。 隐藏按钮上残留过期的展开状态,
比不写这个属性更糟。
已知缺口:当前导航项只靠 .active 类表示当前页,没有 aria-current="page"。
新增导航时请补上——视觉选中态对读屏用户不存在。
10.4 触屏
- 所有依赖 hover 才显现的动作(行内删除、编辑)必须在
@media (hover: none)下常驻。 - 可点击区域不小于 24×24px;行内图标按钮 24px,工具栏图标按钮 30px。
10.5 对比度
见 3.4 对比度实测。不要新增 --text-3 承载信息的用法。
11. 文案
11.1 语气
- 陈述事实,不推销。 “本次改动影响 2 个其他程序”,不是”发现 2 个潜在风险!”
- 不确定就说不确定。 “置信度 0.42(偏低)”优于含糊的”可能影响”。
- 说明边界,不回避。 盲区、未解析的调用点必须出现在界面上,不能只进日志。
- 中文为主,标识符保留原文。中英文之间加空格。
11.2 用词表
| 概念 | 界面用词 |
|---|---|
| 代码源 | 代码源(不用”项目”“仓库”混指) |
| 其他程序 | 其他程序 / 对比程序(不用”peer”) |
| 契约 | 契约(接口/数据库/配置/Maven/消息) |
| 影响 | 影响(不用”风险”指代影响本身) |
| 风险 / 置信度 | 两个独立概念,永不混称 |
| 确认 / 忽略 | 复核动作(不用”通过/拒绝”) |
11.3 中文标签映射
风险:CRITICAL 严重 / HIGH 高 / MEDIUM 中 / LOW 低 / INFO 信息
(色调:danger / warning / warning / success / neutral)
状态:DONE 已就绪 / RUNNING 分析中 / PENDING 排队中 / FAILED 失败
(色调:success / brand / neutral / danger)
分析模式:GIT_DIFF 提交对比 / WORKTREE_DIFF 未提交改动 / PULL_REQUEST PR / MR / COMMIT_RANGE 提交范围(勾选要分析的提交)
COMMIT_RANGE 的界面动作是「勾选要分析的提交」(分支 + 日期窗口,见 7.24);按「起始..结束」
两端分析走 GIT_DIFF。命令行不受影响,仍按两端解析(起止提交都必填)。
变更类型、影响类型见 app.js 的 CHANGE_LABEL / TYPE_LABEL 映射表——
新增枚举必须同时补映射,否则界面会显示原始常量名。
11.4 句式
按钮:动词开头,2–4 字。现网用词:新建分析 / 开始分析 / 导出 / 导入 / 立即检查 / 确认 / 忽略 / 取消。
空态:emptyBlock(title, sub) 固定两段——第一段说状态(strong),第二段说下一步。
现网样例:
| 场景 | title | sub |
|---|---|---|
| 复核已清空 | 没有待处理的影响 | 当前分析中其他程序的所有影响项都已复核。 |
| 无分析记录 | 这个代码源还没有分析记录 | 点击「新建分析」为 <名称> 运行一次分析。 |
| 无时间线 | 还没有事件 | 运行一次分析后,这里会出现时间线。 |
| 未选中条目 | 尚未选择发现 | 左侧列表中的每一条都带有变更点、传播路径与代码位置。 |
第二段永远回答”那我现在能做什么”,不重复第一段。
错误:现象 + 原因 + 下一步。”目录不存在:/work/alpha(来源类型为本地目录)→ 检查路径或改选其他目录”。
计数:数字在前,单位在后,用千分位。”共 1,204 条发现”。
11.5 AI 生成内容的标注
AI 审核的输出永远带「AI 生成 · 待验证」标注,并且与静态分析结论分开存放。
| 要求 | 说明 |
|---|---|
| 标注措辞固定 | 「AI 生成 · 待验证」,不要改写成”智能建议”“AI 洞察”这类更讨喜的说法 |
| 标注位置 | 卡片头、紧邻标题;不放脚注 |
| 不用 AI 的口吻 | 模型输出原样展示(pre-wrap 保留编号行),界面不替它加”我们建议” |
| 不隐藏失败 | AI 失败时就地显示原因与重试;不静默降级成”没有建议” |
| 不影响结论 | 界面文案要让读者明白:AI 是解释层,影响清单与风险由静态分析决定 |
一个用户如果分不清哪句话是引擎算出来的、哪句话是模型写的,这个功能就是净损失。
11.6 禁忌
- 不用感叹号。
- 不用”我们”。
- 不写”请稍候”这类没有信息量的等待文案——说明在等什么。
- 不把技术异常直接抛给用户(
NullPointerException),但也不隐藏可操作的关键细节(路径、退出码)。
12. 验收清单
改完界面后逐条勾选。
视觉
- 颜色全部来自令牌,没有新增十六进制字面量
- 深色主题下没有看不清的文字,没有一处”白底白字”
- 字号落在 4.2 的阶梯里
- 圆角与阴影符合 6.1 / 6.2 的层级
- 一屏之内主操作按钮只有一个(
.btn-primary)
布局
- 在 1440 / 1280 / 1024 / 860 / 720 宽度下都没有横向被裁的内容
- 侧栏收起后内容区正常回流,依赖图重新 layout
- 长列表在卡片内滚动,没有把整页撑高
- 表格栅格显式声明了
.tr-*模板
状态
- 加载 / 空 / 成功 / 失败 / 部分成功五种状态都实现
- 失败原因完整可读,有重试入口
- 破坏性操作有匹配强度的二次确认
- 设置页表单只在首载 / 保存后回填,轮询刷新不覆盖正在输入的内容
- 配置导入导出:密钥默认不导出、勾选才含;设置项与代码源的成败分段呈现,部分成功不写成「导入失败」
- 配置导入成功后 AI / GitLab 表单与代码源列表一起刷新,不停留在导入前的值
- 长任务显示阶段名而不只是百分比
无障碍
- 纯键盘可以走完主流程,焦点环始终可见
- 快捷键在输入框内让位
- 弹窗有
role="dialog"/aria-modal/aria-labelledby,Escape可关 - 折叠按钮的
aria-expanded与视觉状态一致 - 仅 hover 显现的动作在触屏上常驻
- 承载信息的文字没有用
--text-3
一致性
- 新增枚举补了中文标签映射
- 新引用的图标 id 都在
index.html的 sprite 里定义 - 文案符合 11 的语气与句式
- 影响清单被截断时显示了
.callout.warning说明,页头「N 项影响」用全量计数 - 界面实现映射 已同步
- 第 13 章「已知偏差」没有变长;修掉的那条已删除
mvn test通过(含UiContractTest)
13. 已知偏差:规范与现状不一致
规范描述的是应该怎样。下面这些是核对源码后确认的现状与规范不一致的地方, 逐条可复现。它们不是被认可的写法,而是待偿还的债:
- 修它们时顺手删掉本条目;
- 不要为了让实现”符合规范”而改规范去迁就 bug;
- 新增代码不要模仿下表的任何一条。
| # | 现状 | 规范要求 | 影响 |
|---|---|---|---|
| 1 | 侧栏标签分组折叠失效:渲染写 data-navtag-group,事件委托查 data-tag-group-toggle |
数据属性名必须与委托选择器一致 | 多标签时侧栏无法折叠分组 |
| 2 | .step-body(分析进度步骤容器)无对应 CSS |
每个输出到 DOM 的类名都要有样式归属 | 步骤内容靠 .step-dot 的 margin 撑开,布局脆弱 |
| 3 | .flash(更新卡高亮)无对应 CSS |
同上 | 点「查看」跳转后没有位置提示 |
| 4 | toast 传入 'danger' 语气,但样式只有 .toast.success / .toast.error |
语气枚举与样式必须一一对应 | 危险类 toast 与中性 toast 视觉相同 |
| 5 | 输入控件无 :disabled 样式(分支下拉读取中被禁用) |
禁用态必须与可用态可区分 | 用户以为下拉坏了 |
| 6 | 无字段级错误态(无 .field.error / aria-invalid) |
见 8.2 错误分级 | 表单校验失败只能靠 toast,字段定位不到 |
| 7 | 浮层无滚动锁定、无焦点陷阱 | 见 7.16 | 弹窗打开时背景仍可滚动,Tab 会跑到弹窗外 |
| 8 | ⌘K 未做输入框让位(Ctrl/⌘+B 做了) |
见 10.2 | 两个全局快捷键策略不一致 |
| 9 | 「记住上次打开的页面」勾选框未接线 | 见 8.1 五种状态 | 控件存在但无效 |
| 10 | .tr-impacts / .meta-row / .flow-arrow 有样式无使用者 |
样式表不留死规则 | 阅读样式表时误导(以为有用) |
| 11 | .empty-hint 只在 .picker-row 作用域下有样式,却在别处使用 |
组件样式应与其使用范围一致 | 部分空态提示无样式 |
| 12 | .grid-2-even / .grid-3 有定义无使用者 |
样式表不留死规则 | 见 5.2 的说明 |
| 13 | data-theme-pref 只写不读(CSS 与 JS 都无消费方) |
属性要有消费方,否则是死状态 | 目前靠 localStorage + data-theme 已足够;该属性是预留 |
| 14 | 9 处 transition 未带 --ease,回落浏览器默认缓动;同类交互配方不一致(.row-edit 带、.row-delete 不带) |
见 9.1 令牌纪律 | 相同交互的手感略有差异 |
| 15 | 滚动条只有 ::-webkit-scrollbar,无标准 scrollbar-* 属性 |
— | Firefox 显示系统默认滚动条 |
这份清单怎么用:改到相关组件时顺手修一条,并在同一次提交里删掉对应行。 清单长度应该单调下降;如果它在变长,说明新增代码在制造新债。
14. 相关文档
| 文档 | 内容 |
|---|---|
| 界面实现映射 | 本文规范到文件、类名、数据契约的映射 |
| 设计取舍 | 为什么这些边界不做得更多 |
| 核心概念 | 契约、传播、风险与置信度 |
| 架构设计 | 分层与数据流 |
| README · 界面 | 面向使用者的功能说明 |