主题
Lampio — UI 设计规范
本文件定义 Lampio 桌面端的视觉设计语言、组件规范和交互约定。 与
coding-standards.md并列:前者管代码工程,本文件管界面设计。 修改时间以git log -1 -- docs/ui-standards.md为准(手写"最后更新"必然漂移,已移除)。配套参考:
docs/ui-component-inventory.md记录每个 ui/ 组件的实际调用点、业务实体映射、0 调用的候选废弃、未抽象但重复的 pattern。本文档说"应该用什么",inventory 说"当前用在哪里"。
目录
- 设计原则
- Token 层级
- 背景层级系统
- 色彩语义
- 字号与排版
- 间距与密度
- 圆角与阴影
- Z-index 层级
- 图标尺寸
- 按钮体系
- 输入框
- Toggle / Active 态
- 列表模式
- 弹层与模态
- 空态 / Loading / 错误
- 交互状态与可访问性
- 动效
- Microcopy 语气
- Review 域专属
- 违规速查
- 落地路径
1. 设计原则
- 内容优先,Chrome 让路。ReadBrowser 是阅读工具,用户视觉焦点应在内容区,chrome(工具栏/导航栏)要稳、要安静,不抢戏。
- Accent 是信号,不是装饰。
primary主色只用在需要用户注意的地方(CTA、当前选中项、focus ring)。普通状态(hover、开关、chrome 按钮)用中性灰。 - 分层靠深浅,不靠边框。不同层级的面板、卡片、浮层通过背景色明度区分(4 层 surface),边框只在需要明确边界时用(如 input)。
- Dark mode 是平等公民,不是"顺手加 dark: 前缀"。每个颜色 token 必须同时在明暗模式下验证对比度。
- 规范优先于自由发挥。组件作者不应凭手感决定 padding/radius/color —— 从本文件选型。
2. Token 层级
项目采用三层 token 架构:
原始 token(CSS 变量) → @theme in src/index.css
↓
语义 token(JS 常量) → src/lib/design-tokens.ts
↓
组合 variants(class 串) → src/lib/ui-variants.ts(待建)2.1 原始 token(只此一处定义)
位置:src/index.css @theme。所有 --color-*、--text-*、--animate-* 在此声明。禁止在组件里内联定义 CSS 变量或 hex 字面量。
2.2 语义 token
位置:src/lib/design-tokens.ts。导出可在 TS 中引用的常量(如 CHART_COLORS、CEFR_PALETTE)。新增本规范要求的以下几组:
SEMANTIC_COLORS(success / warning / error / destructive / bookmark / review-hard / review-easy)ICON_SIZE(xs=12、sm=14、md=16、lg=20)Z_INDEX(base=0、toolbar=10、dropdown=40、overlay=50、modal=60、toast=70)
2.3 组合 variants
位置:src/lib/ui-variants.ts(新建)。导出可直接贴到 className 的字符串常量:
ts
export const BUTTON = {
primary: 'px-3 py-1.5 text-sm bg-primary text-white rounded-md hover:bg-primary-hover disabled:opacity-50',
secondary: '...',
ghost: '...',
destructive: '...',
iconOnly: '...',
};
export const LIST = { dense: '...', normal: '...', card: '...' };
export const INPUT = { default: '...' };
export const TOGGLE = { active: '...', inactive: '...' };组件用法:<button className={BUTTON.primary}>Save</button>。
3. 背景层级系统
3.1 四层基础 + 两层补充
| Token | Light | Dark | 用途 |
|---|---|---|---|
bg-surface-content | #ffffff | #121212 | 内容区(webview、文档) |
bg-surface | #f5f7fa | #1c1c1c | 面板底色、页面底 |
bg-surface-toolbar | #e5e9f0 | #161616 | Chrome(TitleBar active tab / AddressBar / Toolbar / 左右面板外壳 / 阅读画布) |
bg-surface-secondary | #e6eaf0 | #303030 | Chrome 凹陷(TitleBar inactive / StatusBar)、section card、metric card |
bg-surface-raised | #ffffff | #2a2a2a | 浮层、弹层、输入框(需与父层拉开对比) |
bg-surface-card | #eef1f6 | #242424 | 首页/列表卡片(取代 /50 alpha) |
暗色自 2026-07-04 起整族去色相(中性灰):原 slate/navy 蓝调对阅读产品偏冷、与浅色页面产生色相对撞、品牌松绿在蓝底上显含混(Kindle/Apple Books 同路线)。暗色文字/边框同步去蓝:
text #ececec/text-secondary #9a9a9a/border #3a3a3a。权威值以src/index.css为准。
两种模式下,surface-content 始终与相邻 chrome 形成最大对比(light 最亮、dark 最暗),保证内容焦点。secondary 在两种模式下的视觉角色不变(都是"比主层再退一步"),只是绝对明度反了。
3.2 Chrome 堆栈约定
TitleBar(非激活 tab 区) bg-surface-secondary
↓
TitleBar 激活 tab → AddressBar → Toolbar bg-surface-toolbar (← 连成一体)
↓
内容区 / 面板 bg-surface-content / bg-surface
↓
StatusBar bg-surface-secondary激活 tab 与 AddressBar/Toolbar 同层是有意设计(形成视觉连续感),不要改。
3.3 面板约定
- 独立面板(Right Panel 右侧内容):面板容器
bg-surface-toolbar,面板内容组件bg-surface,不同层。 - Embedded 面板(如
<HistoryPanel embedded />放在 LeftPanel 内):也必须自带 bg(bg-surface或bg-transparent明确),不要依赖父层渗透。
3.4 Hover 背景 —— 🔴 修 bug
当前 hover:bg-hover 在 36 处使用,但 --color-hover 未定义,全部失效。修复:在 src/index.css @theme 加:
css
--color-hover: rgba(0, 0, 0, 0.04); /* light: 黑 4% */
/* dark override in .dark body { --color-hover: rgba(255, 255, 255, 0.05); } */好处:hover 是"在当前 bg 上叠一层半透明",不绑定任何 surface 色,在任何父层上都工作。
统一约定:所有 hover 态只用 hover:bg-hover。废弃 hover:bg-surface-secondary(除非是"选中态"而非"悬浮态")、hover:bg-black/10 dark:hover:bg-white/15(TitleBar close button)。
3.5 禁忌
- ❌ 使用
/50等 alpha 修饰 surface 色(父 bg 变化时结果不可预测) - ❌ 输入框背景与父面板相同 bg(靠 border 硬撑对比)
- ❌
bg-white/bg-gray-*硬编码(只有 3 处合法例外:EPUB 阅读区的文档 bg,因为要复现纸面)
4. 色彩语义
4.1 中性色(text / border)
| Token | 用途 |
|---|---|
text-text | 主要文字、标题 |
text-text-secondary | 次要文字、辅助说明、非激活 chrome |
text-border | 仅用作 disabled 态文字(罕见) |
border-border | 标准边框 |
border-border/20 border-border/15 border-border/30 | 柔和分隔线(列表 divider / 段落分隔) |
4.2 Accent(primary)
bg-primary / text-primary 只用在以下 4 种场景:
- CTA 按钮(
bg-primary text-white) - 当前选中的导航项(TocPanel 当前章节、segmented control 激活项
bg-primary text-white) - Focus ring(
focus-visible:ring-primary) - 进度/加载指示(进度条填充、加载条)
禁止用在:普通 toggle active 态(用 §12 的规则)、状态指示(用 §4.3 semantic 色)。
4.3 Semantic 色(进 design-tokens.ts)
每个语义可选的子键:
| 子键 | 用途 | 示例 |
|---|---|---|
bg | 浅色背景(状态块、tag 背景) | bg-red-50 dark:bg-red-900/20 |
text | 主文字(状态块内段落、错误消息) | text-red-700 dark:text-red-400 |
iconText | 图标线条色 / 强调小字 | text-red-600 dark:text-red-400 |
border | 输入框错误态边框 | border-red-500 |
focusRing | 错误态 focus ring 覆盖 | focus-visible:ring-red-500 ... |
hoverBg / hover | 可交互元素的 hover | hover:bg-red-100 dark:... |
不是每个语义都需要全部子键。当前定义:
ts
// src/lib/design-tokens.ts
export const SEMANTIC = {
success: { bg, text, iconText },
warning: { bg, text, iconText, border, focusRing },
error: { bg, text, iconText, border, focusRing },
destructive: { bg, text, border, hover },
bookmark: { text },
reviewHard: { bg, text, hoverBg },
reviewEasy: { bg, text, hoverBg },
} as const;规则:组件里不再直接写 bg-red-* / text-emerald-* 等语义色,必须引用 SEMANTIC.xxx.yyy。如需新子键,扩 SEMANTIC 后再用,不要新起一组硬编码。
4.4 禁用的颜色模式
| ❌ 禁止 | 原因 | ✅ 改用 |
|---|---|---|
text-yellow-500 表"收藏" | 语义硬编码 | SEMANTIC.bookmark.text |
bg-red-50 text-red-600 表"Hard" | 跨组件不一致(ReviewCardFace 用 amber,WordPopup 用 red) | SEMANTIC.reviewHard |
text-emerald-600/70 dark:... 表"Review tab" | 硬编码 + 透明度 | 改用 text-text-secondary("当前在 Review 模式"靠 tab 位置表达,不靠色) |
bg-green-500 / bg-blue-500 表"CEFR 等级" | 与 CEFR_PALETTE 重复 | cefrBarClass(level) |
5. 字号与排版
5.1 字号 scale
| Class | px | 用途 |
|---|---|---|
text-badge | 9px | 角标数字(角标≥1位数) |
text-caption | 10px | 图例、微型 label、section header(uppercase) |
text-xs | 12px | 主要小字:列表项、按钮文本、辅助说明 |
text-sm | 14px | 正文、面板 header 标题、input 文本 |
text-base | 16px | 重点正文、卡片词头 |
text-lg | 18px | 首页问候语、ReportOverlay 标题、指标数值(中) |
text-2xl | 24px | 大指标数值、复习卡词头 |
禁止直接写 text-[15px] 等任意字号。
5.2 Weight 约定
font-normal(默认):正文、说明font-medium:导航 label、按钮文本、激活态 tabfont-semibold:section header、重点数字font-bold:复习卡词头、大指标、badge
禁止 font-extrabold / font-black(设计基调偏克制)。
5.3 排版层级图(单面板示例)
Panel Header text-sm font-medium
├─ Section Label text-caption font-semibold uppercase tracking-wider
├─ List Item Title text-xs font-medium
├─ List Item Meta text-caption text-text-secondary
└─ Action Button text-xs font-medium6. 间距与密度
6.1 Spacing scale
不新造 spacing token,直接用 Tailwind 的 0.5/1/1.5/2/2.5/3/4/6/8/12 档。约束是组合,不是尺度本身。
6.2 面板容器 padding
| 层 | 约定 | 示例 |
|---|---|---|
| 面板根容器 | px-3 py-1.5 | 左侧/右侧面板顶部区 |
| 面板 section(如搜索栏、tab 条) | px-3 py-2 | 和根容器 X 对齐 |
| 列表容器(内嵌式) | mx-1.5 my-1 rounded-lg | 微微缩进,形成卡片感 |
| 列表容器(全宽式) | 无 margin,直接贴边 | 当列表是面板主内容时 |
| 卡片内部 | px-3 py-2 或 px-3 py-2.5 | 与面板 section 对齐 |
6.3 列表项密度三档
| 档位 | py | 适用 |
|---|---|---|
| Dense | py-1.5 | 高密度(词汇列表、目录树、filter options) |
| Normal | py-2 | 常规列表(笔记、历史记录、RSS 条目) |
| Comfort | py-2.5 | 重点卡片(首页站点、Review overview group) |
全项目只能用这三档,禁止 py-1.75 py-3 等随意值。
6.4 列表项左内容 slot 约定
列表项左侧固定留一个 leading slot(即使为空也要占位),防止有/无 icon 时文字基线错位:
tsx
<div className="flex items-center gap-2 px-3 py-2">
<div className="w-5 h-5 flex-shrink-0 flex items-center justify-center">
{icon || null} {/* 可选 */}
</div>
<div className="flex-1 min-w-0">...</div>
</div>解决 VocabPanel Excluded vs Browse 看起来缩进不同的问题。
6.5 Gap 约定
- 面板内 section 之间:不同 section 通过背景层级或
border-b border-border/20分隔,不用space-y-* - 段内元素(按钮组、badge 组):
gap-1(紧)、gap-1.5(常规)、gap-2(松)、gap-3(卡片之间)
7. 圆角与阴影
7.1 Radius scale
| Class | 用途 |
|---|---|
rounded / rounded-sm | icon 按钮内部 hover bg、tag |
rounded-md | 按钮、input、segmented control 内部项、行内 IconButton |
rounded-lg | 列表项卡片(默认)、popover、dropdown、弹窗内框、section card |
rounded-xl | 首页卡片(大卡片、搜索框)、最外层 overlay modal 面板 |
rounded-full | chrome 胶囊层(见 7.1.1)+ 天然圆形(徽章、头像、进度条 track、toggle switch、pill 标签) |
禁止:rounded-2xl 及以上、rounded-[Npx] 任意值。
7.1.1 Radius = 层级语言(2026-07-04 成文)
圆角不是逐个组件的审美选择,而是层级的语法标记——新组件先判断自己属于哪一层,radius 随层级而定:
| 层 | Radius | 实例 | 语法来源 |
|---|---|---|---|
| 系统 chrome | rounded-full 胶囊 | SegmentedGroup 工具组、ChromeButton 圆钮、地址栏 pill、面板头部 × | macOS/Safari 胶囊 chrome |
| 内容卡片 | rounded-xl / lg | 精读句子卡、笔记卡、首页瓷砖、modal | 卡片=独立内容实体 |
| 表单控件 | rounded-md | Button、Input、行内 IconButton、TabButton | 控件族统一,含面板内搜索框(输入控件跟控件层不跟 chrome 层) |
| 天然圆形 | rounded-full | Switch、进度轨、计数圆点、头像盘、pill 标签 | 各自的通用 idiom,与胶囊 chrome 无关 |
判断违和的标准是"同层混用",不是"全局风格数量>1":chrome 说 chrome 的话、内容说内容的话(Safari 同款分层语法)。胶囊语法禁止漏进内容层(内容区按钮不得 rounded-full,天然圆形除外);反之内容控件也不进 chrome 胶囊组。配套前提:chrome 层共享一个连续底色场(面板/间隔/阅读画布同色,见 §3),胶囊浮在统一场上才成立。
7.2 Shadow 层级
| Class | 用途 |
|---|---|
shadow-sm | Segmented control 激活项、TitleBar 激活 tab |
shadow-md | ConfirmDialog(warning bar)、tooltip |
shadow-lg | SearchDropdown、AccountPopover、下拉菜单 |
shadow-xl | WordPopup、SelectionToolbar、ReportOverlay 主体 |
禁止:自定义 shadow-[1px_0_3px_rgba(0,0,0,0.06)] 等内联。如果需要侧边阴影(LeftPanel 右边缘),在 index.css 里定义专用 class(如 .shadow-panel-right)。
8. Z-index 层级
进 design-tokens.ts:
ts
export const Z_INDEX = {
base: 0, // 默认
toolbar: 10, // Chrome(TitleBar active tab 需要叠在 AddressBar 上方一点)
dropdown: 40, // SearchDropdown、TocPanel 下拉
overlay: 50, // Popover、浮动工具栏(WordPopup / SelectionToolbar / AccountPopover)
modal: 60, // 全屏模态(ReportOverlay、ConfirmDialog)← 当前 ConfirmDialog 是 z-10,🔴 要修
toast: 70, // Toast 通知(暂未实现)
} as const;Tailwind 写法:z-[var(--z-modal)] 或直接 z-10/z-40/z-50/z-60/z-70 保持同步。
当前 bug:ConfirmDialog:23 用 z-10,会被 WordPopup(z-50) 遮住。必须改 z-60。
9. 图标尺寸
9.1 Scale
ts
export const ICON_SIZE = {
xs: 12, // 微角标、内联 label icon
sm: 14, // ✨ 主要尺寸 ✨(Toolbar / AddressBar / StatusBar / 列表项 trailing icon)
md: 16, // 重点按钮、ReviewCardFace 内 icon、首页卡片 leading icon
lg: 20, // 大型 CTA(ThemeToggle 放宽到 18 也可以,但更推荐 20)
xl: 24, // 大弹层 header icon、空态插图
} as const;禁止:11/13/15/17/19/22/26 这些奇数值。TitleBar 当前 width="11"/"13" 混用,全部规约到 14。
9.2 Icon 按钮最小点击区
Icon-only 按钮必须有足够点击区:外层 p-1.5(内部 14px icon → 总 26px)或 p-2(内部 16px icon → 总 32px)。
当前问题:TitleBar :77 tab close button w-4 h-4(16px)+ 零 padding,点击区不足。
10. 按钮体系
10.1 五种变体
ts
export const BUTTON = {
// 主 CTA:登录、保存、确认
primary: 'px-3 py-1.5 text-sm font-medium bg-primary text-white rounded-md hover:bg-primary-hover disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',
// 次级:Settings 里的"浏览"、"测试"按钮;次要 CTA
secondary: 'px-3 py-1.5 text-sm font-medium bg-surface border border-border text-text rounded-md hover:bg-hover disabled:opacity-50',
// 幽灵:工具栏 icon+文字按钮(非 toggle);Cancel
ghost: 'px-2 py-1 text-sm text-text-secondary rounded-md hover:text-text hover:bg-hover',
// 破坏性:删除、清空、登出
destructive: 'px-3 py-1.5 text-sm font-medium bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400 border border-red-200 dark:border-red-800 rounded-md hover:bg-red-100 dark:hover:bg-red-900/30',
// 纯 icon:工具栏 icon 按钮、close 按钮
iconOnly: 'p-1.5 rounded-md text-text-secondary hover:text-text hover:bg-hover cursor-pointer disabled:opacity-50',
} as const;10.2 使用规则
- 每个面板最多 1 个 primary 按钮(视觉焦点唯一)
- 破坏性操作必须走 destructive,不得用
text-red-500手写 - ConfirmDialog 的 amber 主题:保留(amber = 警告,不是破坏;"确认关闭笔记本"不是删除)
10.3 现存违规(清理清单)
| 文件 | 行 | 问题 | 改为 |
|---|---|---|---|
settings/common.ts | - | 定义了 btnClass 但未被其他面板复用 | 删除,全员用 BUTTON.secondary |
StorageSettings:225 | delete 按钮 | text-red-500 bg-surface border | BUTTON.destructive |
AuthPanel:178 | 登录按钮 | bg-primary text-white 内联 | BUTTON.primary |
ReportOverlay:162 | 时段按钮 | 硬编码 | 改用 segmented control pattern |
11. 输入框
11.1 标准样式
ts
export const INPUT = {
default: 'w-full px-2.5 py-1.5 text-sm bg-surface-raised border border-border rounded-md text-text placeholder:text-text-secondary focus:outline-none focus-visible:ring-1 focus-visible:ring-primary focus-visible:border-primary',
search: '... 带放大镜 icon 的搜索框(基于 default)',
};11.2 关键规则
- 背景用
bg-surface-raised(新增 token),不要用bg-surface(与面板同色无对比) - Focus ring 统一:
focus-visible:ring-1 focus-visible:ring-primary focus-visible:border-primary - Label 放上方(顶对齐,
text-xs text-text-secondary mb-1),除非是 inline 横向表单 - 错误态:border 改
border-red-500,下方mt-1 text-xs text-red-600说明
11.3 当前违规
| 文件 | 问题 |
|---|---|
VocabPanel:186/333 | 两个 search 框 padding 不一致(px-2.5 py-1.5 vs px-2 py-1) |
AuthPanel:146/158 | bg-surface 与面板同色;只有这里有 focus:ring-1(其他 input 都没 focus) |
HomePage:256 | 用了 bg-surface-toolbar rounded-xl(特例,因首页是低密度欢迎屏可保留,但 rounded-xl 要说明) |
12. Toggle / Active 态
12.1 四种语义分层
同样是"某种激活",但语义不同,视觉也要不同:
| 语义 | 场景 | 视觉 |
|---|---|---|
| Navigation active(当前所在位置) | TocPanel 当前章节、LeftPanel 当前选中模式、TabBar 当前 panel | bg-primary/10 text-primary |
| Selection active(单选选中值) | Settings CEFR level picker、ReportOverlay 时段、AuthPanel mode 切换 | bg-primary text-white(或 segmented control 风格 bg-surface shadow-sm on bg-surface-secondary 壳) |
| Toggle on/off(状态开关) | Settings 里"开启 xxx 功能" | 物理 switch: w-8 h-5 rounded-full bg-primary(开)/ bg-border(关)+ 圆形滑块 |
| Trigger state(按下即生效的触发,如"阅读模式开着") | Toolbar 里的 ReaderMode / Bilingual / Highlight 开关 | 🔴 改用 neutral:bg-surface-secondary text-text(当前是 bg-primary/10 text-primary,用户反馈不协调) |
12.2 关键变更
用户的诉求"Toolbar 选中色调不协调"根因是语义混用:Toolbar 里的 toggle 属于"trigger state"而非"navigation active",不该用 accent。
ts
export const TOGGLE = {
// Navigation active(导航/标签选中)
navActive: 'bg-primary/10 text-primary',
navInactive: 'text-text-secondary hover:text-text hover:bg-hover',
// Trigger state(工具栏开关)
triggerOn: 'bg-surface-secondary text-text',
triggerOff: 'text-text-secondary hover:text-text hover:bg-hover',
// Switch(物理开关)
switchOn: 'bg-primary',
switchOff: 'bg-border',
};Toolbar.tsx:45/77 改走 TOGGLE.triggerOn / TOGGLE.triggerOff。
12.3 AddressBar 收藏按钮
⭐收藏的 text-yellow-500 属于语义色(bookmark),保留黄色但走 SEMANTIC.bookmark.text,不再硬编码。
13. 列表模式
13.1 四种 row shape(<ListItem> 统一)
Sprint 9.5(S5,2026-04-18)起,所有列表项走 src/components/ui/ListItem.tsx — 用 cva 驱动的 listItemVariants(见 ui-variants.ts):
| variant | 视觉 | 用途 |
|---|---|---|
simple(单行密集) | flex items-center gap-2 border-b border-border/15,hover bg-hover | 同质流(词汇 / 目录 / filter options) |
double(双行 stack) | leading · {title / subtitle} · meta · trailing | 历史、站点、RSS、Review overview |
rich(卡片独立实体) | bg-surface-raised border border-border rounded-lg hover:border-primary/30 | 笔记、独立创作物 |
tree(缩进) | depth 控制 paddingLeft,同 simple 视觉 | TocPanel、RssPanel drilldown(S7 长尾) |
density: dense (py-1.5) / normal (py-2) / comfort (py-2.5)。active + activeTone: soft=bg-primary/10,strong=bg-primary/15(多选态)。
13.2 选型依据 + 实体强制映射表
以"列表项是不是独立实体"为准,不以面板作者偏好:
- 历史、站点、RSS 条目 = 有双字段(标题 + meta)的同质流 →
double - 词汇、filter options = 紧凑同质词条流 →
simple+ dense - 笔记 = 独立创作物(有 title、cover、metadata) →
rich - TocPanel / drilldown = 层级结构 →
tree
实体 → variant 强制映射(防作者自由发挥)
每个已知实体只有唯一正确 variant + density。新增实体前必须在此表登记,不登记不迁移:
| 实体 | variant | density | 典型使用位置 |
|---|---|---|---|
| 访问记录(resource_history) | double | normal | HistoryPanel / HomePage Recently Opened |
| 站点(sites / bookmarks)行展示 | double | normal | MySitesPanel |
| 站点(sites / bookmarks)发现页两栏 | double | normal | HomePage 我的收藏站点 / 推荐站点(DiscoverRow) |
| 词汇条目(vocabulary / excluded / browse) | simple | dense | VocabPanel 三 tab |
| 笔记本(reading_notes) | rich | normal | NotesPanel |
| 复习分组(review_groups) | rich | normal | review/ReviewOverview |
| RSS 订阅源(feeds) | simple | normal | RssPanel feed 列表 |
| RSS 订阅源(feeds)发现页两栏 | double | normal | HomePage 我的订阅 / 推荐订阅(DiscoverRow) |
| RSS 文章(feed_items) | double + titleLines=2 + tone | normal | RssPanel 文章列表 |
| 内容推荐(recommended articles) | double | normal | HomePage Recommended |
| TOC 目录项 | tree | dense | TocPanel + 按 depth 缩进 |
| 搜索 Related Articles | simple | dense | SearchDropdown |
| 词典搜索结果(words) | — | — | 非列表(字典词条卡片,字段组合复杂),保持自写 |
| 翻译时间线(TimelineEntry) | — | — | 非列表(可展开详情卡片),保持自写 |
| WordPopup 释义 | — | — | 非列表(内联语义排版) |
扩表规则:新增业务实体 → 先在此表登记 variant + density 选择,给出理由;无对应 variant → 评估是否添加新 variant;实在不适合列表 → 归入"非列表"组,加入豁免清单。
ListItem API 刚性约束
- 不暴露
titleClassName/subtitleClassName:typography 完全由 variant 决定 leading不应自由 ReactNode:richvariant 使用avatar专用 slot(固定 28×28 圆角);其他 variant 只放 1-2 个 icon 小元素- 需要 2 行标题 → 用
titleLines={2}而非 ReactNode 包<span className="line-clamp-2"> - 需要"已读/已归档"灰化态 → 用
tone="muted"而非 ReactNode 改 className - 需要"选中/激活"态 → 用
active+activeTone而非外层 className override
13.3 迁移进度
| Panel | variant | 状态 |
|---|---|---|
NotesPanel 笔记本列表 | rich normal | ✅ S5 完成 |
HistoryPanel 资源历史 | double normal | ✅ S5 完成 |
MySitesPanel 站点 | double normal | ✅ S5 完成 |
VocabPanel Notebook / Excluded / Browse | simple dense | ✅ S5 完成 |
TocPanel | tree dense | ✅ S6.1 完成 |
RssPanel 订阅列表 | simple normal | ✅ S6.1 完成 |
RssPanel 文章列表 | double normal | ✅ S6.1 完成 |
review/ReviewOverview group | double | ⏳ S7 长尾 |
永久豁免(非列表 Panel,不走 ListItem):
StatsPanel— 图表分组 + metric cardDiscoveryPanel— CEFR filter chip + 按 level 分组的 3 列词汇网格CefrPanel— 纯展示 grid
/ui-check U1 基线 2(豁免 StatsPanel / DiscoveryPanel);/arch-check S18 基线 3(多算 CefrPanel,因 S18 比 U1 松)。
13.4 列表容器 wrapper
当列表需要"微缩进卡片感"(与面板 bg 拉开):mx-1.5 my-1 rounded-lg overflow-hidden 包裹整体。
当列表需要"全宽贴边":不加 margin。
13.5 Tile 组件 —— 已退役(2026-08-13)
ui/Tile.tsx 曾覆盖 HomePage 的"瓷砖"模式(Sites / Feeds 两栏),已删除,4 处调用全部 迁到 ListItem variant="double"(由 HomePage.DiscoverRow 承载)。
为什么取消这条分野:Tile 的存在理由是"2-col grid 与垂直列表不同",但发现页里 Tile 与 ListItem 同屏(「继续阅读 / 最近打开」是 ListItem),两套行语法并存的 代价大于网格感的收益;且 §13.2 里 double 的实体映射原本就写着 sites / feeds。 两栏的 grid 布局保留——变的是"栏里放什么行",不是"分不分栏"。
迁移中恢复的行为(对 2026-07-18「Tile 仅标题可点」P8 的有意反转,见 CHANGELOG): 整行可点 + 星标 trailing(子按钮 stopPropagation),与 MySitesPanel 对同一实体的 现有处理一致。星标显隐刻意不对称:已收藏态 hover 显现,可收藏态常显(后者是该栏主要动作)。
13.6 逃生舱
ListItem 支持 titleClassName / subtitleClassName / className prop 覆写默认 typography。如 VocabPanel 需要 text-sm text-text(默认是 text-xs font-medium):
tsx
<ListItem variant="simple" density="dense" titleClassName="text-sm text-text" ... />ListItem 不需要覆盖业务 layout 时,禁止再手写 <div className="flex items-center gap-2 px-3 py-1.5 ..."> 的列表行(由 /ui-check U1 + /arch-check S18 扫描)。
14. 弹层与模态
14.1 四种类型
| 类型 | 触发方式 | 位置 | Backdrop | 示例 |
|---|---|---|---|---|
| Popover(小弹层) | 点击触发器 | 锚定触发器下方/侧方 | 无 backdrop,点外关闭 | AccountPopover、SearchDropdown、WordPopup |
| Floating toolbar | 选区/悬浮 | 动态定位 | 无 | SelectionToolbar |
| Inline banner | 条件显示 | 嵌入式 | 无 | ConfirmDialog(警告条) |
| Modal overlay | 主动触发 | 居中 | bg-black/40 | ReportOverlay |
14.2 规范
所有 Popover:
- 容器:
bg-surface-raised border border-border rounded-lg shadow-lg - Max width:
max-w-xs(词汇卡)/max-w-sm(账户)/max-w-md(搜索下拉) - Z-index:
z-50(Z_INDEX.overlay) - Entry animation:
animate-fade-in(200ms) - 关闭:点外 + ESC 都必须支持
所有 Modal:
- Backdrop:
bg-black/40(light/dark 通用,或 dark 下bg-black/60) - 容器:
bg-surface rounded-xl shadow-xl(比 popover 更大 radius 和 shadow) - Z-index:
z-60(Z_INDEX.modal) - Entry animation:
animate-fade-in+animate-scale-in(200ms) - 必须:点 backdrop 关闭 + ESC 关闭 + × 按钮 + focus trap
14.3 现存 bug
ConfirmDialog:23用z-10→ 真 bug,会被其他弹层遮住,改z-60(Sprint 7.4 已修)- WordPopup 无 ESC 关闭 → 补上(Sprint 7.4 已加 useEscapeKey)
- 所有 modal 都无 focus trap → 补上(用
focus-trap-react或自实现)(Sprint 7.4 useFocusTrap 已加)
14.4 Tauri multi-webview 架构限制(Sprint 8.1 已解除)
ReadBrowser 采用 Tauri 2.x 的 multi-webview 架构:content tab 的 webview 是 main webview 的兄弟窗体(不是 DOM 子节点)。因此:
- React portal 渲染的 Modal 属于 main webview 的 DOM 树
- Modal 的
z-index只在 main webview 内部生效 position: fixed的全屏遮罩在 content webview 覆盖区域会被遮住
Sprint 8.1 解决方案(2026-04-18):useHideContentWebviewsWhileOpen hook 在 Modal 打开时把所有 content webview 移至屏幕外(通过已有的 hide_content_webview Rust 命令),关闭时触发 window.dispatchEvent ('resize') 让 BrowserPage 的 ResizeObserver 恢复 active tab 位置。
ui/Modal默认调用该 hook →ui/ConfirmDialog自动受益ReportOverlay也迁移到该 hook(替换了原有的 ad-hoc 仅覆盖 web tab 的实现, 现在覆盖所有持有 content webview 的 tab —— 判据是webviewLabel而非tab.type白名单:白名单每新增一个 TabType 就静默漏一类,'text'落地时就漏过一次)src/hooks/useHideContentWebviewsWhileOpen.ts为单一实现
适用边界:
| 触发场景 | 推荐形态 |
|---|---|
| 标签驱逐、inline 提示 | components/ConfirmDialog.tsx(inline banner) |
| Settings / Account 内二次确认 | ui/ConfirmDialog(Modal,自动 hook) |
| 全屏 Report / 复杂编辑 | ui/Modal 或继承 Modal 的 overlay 组件 |
| 小弹层(ESC + click-outside) | ui/Popover(仍可能被 content 遮挡,小范围 ok) |
| 菜单形下拉(点选列表/单选/开关组) | 原生 popup menu(见下"双轨规范") |
Popover 注意:Popover 当前不调用该 hook(小锚点 + chrome 层触发居多)。 若具体 Popover 有遮挡问题,单点调用 useHideContentWebviewsWhileOpen(open) 即可。
双轨规范(2026-07-12,ReadModeMenu 落地):会伸进 content 区域的 chrome 浮层按 UI 形态分轨——
- 菜单形 UI(点选列表、单选、开关组)→ 原生 popup menu:
Window::popup_menu_at(tauri 2.x 内建,muda 跨平台,无需 cfg 分叉),CheckMenuItem承担选中态。系统渲染在所有 webview 之上,遮挡在该形态下 不存在,附带系统级键盘导航/a11y。模板:reading.rs::show_read_mode_menu(React 传 i18n labels + 窗口逻辑坐标;item id<domain>|<arg>|…编码路由 上下文;lib.rs setup 的全局on_menu_event统一分发)。代价:系统外观不可 自定义、亮暗跟系统。 - 富内容浮层(输入框/滑杆/富文本)→ 维持 panel 形态(find/fontSize 先例) 或 Modal + hide hook。原生菜单放不下的交互不要硬塞。
历史包袱到此为止:过去"所有 chrome 下拉都被迫 panel 化"的根因(DOM 浮层 翻不过原生兄弟视图)对菜单形 UI 已根治;未来排序选择、tab 右键菜单、更多 操作菜单等一律走原生 popup menu,不再新增被遮挡的 DOM 下拉。
15. 空态 / Loading / 错误
15.1 空态 — 走 <EmptyState>
Sprint 9.4(S4)起,所有空态走 src/components/ui/EmptyState.tsx:
tsx
import { EmptyState } from '@/components/ui';
<EmptyState primary="暂无笔记本" secondary="阅读时保存生词即可开始。" />
// 带 icon + 操作
<EmptyState
icon={<Icon icon={CircleAlert} size="lg" />}
primary={errorMsg}
action={<Button variant="link" onClick={retry}>刷新</Button>}
density="compact" // compact=py-6 / normal=py-8(默认)
/>slot:icon / primary / secondary / action + density。
禁止:手写 <div className="py-12 text-center..."> 模板。/ui-check U2 扫描此类违规。
当前未迁:TranslationTimeline.tsx、review/ReviewOverview.tsx(S7 长尾)。
15.2 Loading
- 短任务(< 500ms):不显示(闪烁反而扰人)
- 中等任务:小 spinner + 文字
<Spinner /> 加载中… - 长任务:skeleton placeholder(列表项灰块动画)
禁止:<div>Loading...</div> 裸文本。Loading 也可用 <EmptyState primary="加载中..." />(NotesPanel 已这么用)。
15.3 错误 — 三档反馈
Sprint 9.6(S6)起,按反馈时长与上下文分三档:
| 场景 | 机制 | 样式 |
|---|---|---|
| 全局 / 瞬时反馈(mutation 成功、删除确认、复制成功、网络失败) | toast (sonner) | import { toast } from '@/lib/toast'; toast.success('已复制') / toast.error('...') |
| 面板级持久错误(Auth 登录失败、翻译解析失败) | inline 错误 div | <div className={`${SEMANTIC.error.bg} ${SEMANTIC.error.text} px-3 py-2 rounded-md text-xs`}>...</div> |
| 字段级错误(表单验证) | <Input error="..." /> / <Textarea error="..." /> / <Select error="..." /> | ui/Input 的 error prop 自动渲染 helperText |
Toast 配置:App.tsx 已挂载 <Toaster position="bottom-center" richColors closeButton />。Z_INDEX.toast = 70。
禁止:alert() / window.confirm()(native 弹窗不符合设计语言)、console.error 代替用户反馈。
16. 交互状态与可访问性
16.1 每个可交互元素的 5 态清单
| 状态 | 视觉 |
|---|---|
| Default | 基础色 |
| Hover | hover:bg-hover 或 hover:text-text,必须有反馈 |
| Focus-visible | focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2,键盘可达 |
| Active(按下瞬间) | active:scale-[0.98] 或 active:bg-*/20(可选) |
| Disabled | disabled:opacity-50 disabled:cursor-not-allowed |
16.2 最低可访问性要求
- 所有 icon-only 按钮必须有
aria-label(当前仅 2 处有) - 所有 input 必须关联 label(用
<label>或aria-label) - 所有 Modal 必须有 focus trap
- 所有可交互元素必须 keyboard reachable(有
tabIndex或本身是<button>/<a>/<input>) - color contrast:text-text 对 bg-surface ≥ 4.5:1(通过 WCAG AA)
16.3 键盘快捷键约定
Esc:关闭当前最上层弹层/模态Cmd/Ctrl + K:打开全局搜索(未来)↑ / ↓:列表导航Enter:确认选中- 快捷键显示用
<kbd>样式(见 GeneralSettings:180)
17. 动效
17.1 已定义(index.css)
| Animate | 时长 | 用途 |
|---|---|---|
animate-slide-in-right | 200ms | 右侧面板入场 |
animate-slide-out-right | 150ms | 右侧面板退场 |
animate-slide-up / slide-down | 200ms/150ms | 底部弹层 |
animate-fade-in | 150ms / 300ms | 弹层、页面切换 |
animate-card-flip | 400ms | 复习卡翻转 |
17.2 规则
- Entry 稍长于 exit(200ms vs 150ms),符合"进场要显眼、退场要快"
- 禁止 > 500ms 动效(除了复习卡翻转,它是体验核心)
- Transition 统一:
transition-colors(hover/focus)、transition-transform(translate/scale)、transition-all(只在必须时,因为性能) - 禁止滥用 bounce / elastic 缓动
18. Microcopy 语气
18.1 语言选择
- UI 本身(button label / menu / empty state / error):中文(主用户群为中文用户)
- 学习内容(词汇定义、IPA、例句):英文(保留原语言)
- 账户/同步技术提示(如 "Never synced"):当前混用,逐步统一为中文
18.2 Tone 约定
- 短、直接、不卖萌
- ✅ "没有更多内容了" / "今天的复习已完成"
- ❌ "哎呀,这里空空如也~" / "你真棒!快去学习吧"
- 错误信息说清楚原因 + 给下一步建议:
✅ "登录失败:密码错误。请重试或通过邮件找回。"
❌ "Error"
18.3 当前待统一清单
ReviewSession"Session done!" → "今日复习完成"ReviewOverview"All caught up!" → "暂无到期复习"WordPopup"No definition found" → "未找到释义"AuthPanel"Never synced" → "尚未同步"
19. Review 域专属
19.1 Hard / Easy 按钮
全项目统一走 SEMANTIC.reviewHard / SEMANTIC.reviewEasy(§4.3 定义)。
形状:w-full py-2 rounded-lg font-semibold transition-colors
现存不一致:
ReviewCardFace:110-117用 amber/emerald ✓(与规范一致)WordPopup:179-185用 red/green ❌ → 迁到SEMANTIC.reviewHard/Easy
19.2 复习卡
- 卡片尺寸:按面板可用空间自适应
- 翻转动效:400ms 3D rotateY(已实现
animate-card-flip) - 提示文字 "Tap to reveal":
text-xs text-text-secondary,改中文 "点击显示答案"
19.3 快捷键
Space/Enter:翻卡1/H:Hard2/E:EasyEsc:退出当前会话(带未保存提示)
20. 违规速查(arch-check + ui-check 落地对照)
以下模式在项目中出现即违规。机械规则走 /arch-check(< 30s),语义规则走 /ui-check(< 60s):
| 违规模式 | 规则 | 改为 |
|---|---|---|
#[0-9a-fA-F]{6} in .tsx/.ts | /arch-check H1(硬) | design-tokens.ts |
bg-(red|green|blue|yellow|amber|emerald|purple|orange|...)-\d+ + bg-white | /arch-check S12 | SEMANTIC.xxx / surface token |
text-(red|green|...)-\d+ | /arch-check S12 | SEMANTIC.xxx |
hover:bg-(surface-secondary|black/|white/|gray-/...) | /ui-check U3 | hover:bg-hover(两类例外见下) |
z-\d+ not in | /arch-check S13 | Z_INDEX.xxx |
rounded-2xl / rounded-3xl / rounded-[.*] | /arch-check S14 | scale:rounded / md / lg / xl / full |
shadow-\[.*\] | /arch-check S16 | scale:sm / md / lg / xl 或 index.css class |
text-\[\d+px\] | /arch-check S15 | text-badge / caption / xs / sm / base / lg / 2xl |
p(x|y)-(1.75|3.5|5|7|9|...) 非 scale spacing | /arch-check S11 | §6 scale |
裸 <button> / <input> in src/components/(非 ui/ 下) | /arch-check S10 | ui/Button / ui/IconButton / ui/Input,或同行加 arch-r1: 豁免 |
U3 的两类例外——共同判据:
bg-hover(4% 黑) 在这个面上还看不看得见。
- chrome 层:坐在
surface-toolbar灰底上 → 用实色surface-muted(§22.3)。- 实色底钮:静止态本身就有实心填充(如
bg-surface-muted的圆钮,用填充暗示可点) → hover 必须变深成另一档实色,4% 黑叠在实色上等于没有反馈。例外要在 U3 实际扫描的那一行(即写
hover:bg-*的那行)标arch-r1: <理由>—— 标在<button开标签行上不算数,U3 是逐行 grep,隔行的注释它看不见。 | chrome 文件内裸写尺寸类(h-7/h-8/p-1.5)或size=覆盖 |/arch-checkS25 |CHROME.*(§22.2 物化点) | | chrome 基元缺focus-visible:ring|/arch-checkS26 |CHROME.focus(§22.3) | | chrome 层用bg-primary*做激活态 |/arch-checkS27 |TOGGLE.triggerNav.active(§22.3:accent 退出 chrome) | |SegmentedGroup包了非互斥的命令簇 / 混装簇 |/ui-checkU7 | §22.4:rail 只表示互斥选择器 | | chrome 段控缺图标 / 命令按钮多余文字 / 三级筛选套了 rail |/ui-checkU8 | §22.5 层级表 | | 新 toolbar 未复用三区骨架、侧区宽未对齐面板 |/ui-checkU9 | §22.6 | | 组件中散落<svg>(非 ui/、charts/ 下) |/arch-checkS17(计数) | lucide +<Icon icon={X} size="sm" />| | Panel 列表未走<ListItem>|/arch-checkS18 //ui-checkU1 | ui/ListItem(§13) | | 空态模板手写 |/ui-checkU2 | ui/EmptyState(§15.1) | |alert()/window.confirm()|/ui-check(文档约定,尚未纳入 skill) | sonnertoast.error/success(§15.3) | | Icon-only<button>无aria-label|/rb-code-review(语义层) | 补 aria-label | |<input>无关联 label 且无aria-label|/rb-code-review(语义层) | 补 label |
豁免机制:同行添加 // arch-r1: <理由> 或 /* arch-r1: <理由> */ 可跳过对应 arch-check 行级扫描(S10-S17)。ui-check U1/U2 基于文件级 import 判断,无需豁免注释;StatsPanel 类"非列表 Panel"在基线表内标注接受。
21. 落地路径
Sprint A(半天):规范立起来
- [x] 本文件定稿
- [x] 追加到
coding-standards.md§2-§3 的引用链接 - [x] 在
CLAUDE.md§12 加一行"UI 规范见 docs/ui-standards.md"
Sprint B(1-2 天):Token 层扩展
- [x]
src/index.css加--color-hover/--color-surface-raised/--color-surface-card(含 dark override) - [x]
src/lib/design-tokens.ts加SEMANTIC/ICON_SIZE/Z_INDEX常量 - [x] 新建
src/lib/ui-variants.ts:导出BUTTON/LIST/INPUT/TOGGLE组合 class
Sprint C(2-3 天):紧急 bug + 扎眼违规
- [x] 修
ConfirmDialogz-10 → z-60 - [x] 修
bg-hover失效(36 处 —— 定义 token 即自动生效,无需大改代码) - [x] 修
VocabPanel两个 search 框 padding 对齐 - [x] 修
ToolbarToggleBtn / PanelBtn 改用TOGGLE.triggerOn(灰黑风格) - [x] 修
WordPopupHard/Easy 颜色统一到SEMANTIC.reviewHard/Easy - [x] 修
TitleBar的 emerald Review tab /bg-black/10hover - [x] 修
AddressBar的text-yellow-500→SEMANTIC.bookmark.text - [x] 修
StatusBar的bg-green-500/blue-500/purple-500→cefrBarClass
Sprint D(持续):组件迁移
- [x] 每个 panel 的 PR 顺手迁移至 ui/ 基础组件与 LIST/BUTTON 变体(Sprint 7.5 + 7.6 完成)
- [x] 基础组件补
aria-label/focus-visible/ focus trap(Sprint 7.4)
Sprint E(持续):/arch-check 扩展
- [x] 加 §20 违规速查的所有规则(S10/S11/S12 已纳入,见 arch-check v1.2.0)
- [x] 新 PR 提交前必须通过
Sprint F(低优):Dark mode 专项 + Microcopy 统一
- 按模块走对比度检查 + 中文化
Sprint G(深化 S1-S6,2026-04-18 全部完成)
见 docs/plans/archive/ui-standards-deepening-plan.md 详细进度:
- [x] S1 视觉割裂修复 + arch-check 扩 S13/S14/S15/S16
- [x] S2 lucide-react 迁移 +
ui/Icon封装(Top 5 文件,~43 处 svg → Icon;S17 基线 54) - [x] S2.5 Radix Primitives 档位 1:Modal/Popover/Tooltip 迁 Radix + 删 3 个自研 hook(useFocusTrap / useEscapeKey / useClickOutside)
- [x] S3 cva + tailwind-merge:
buttonVariants/inputVariants/toggleVariantscva 化,新增lib/cn.ts - [x] S4
ui/EmptyState+ui/Divider(迁 8 处 empty state + 2 处 section divider) - [x] S5
ui/ListItem+listItemVariants(4 variant × 3 density × cva),迁 NotesPanel / HistoryPanel / MySitesPanel / VocabPanel - [x] S6 sonner toast +
lib/toast.ts封装,App.tsx挂<Toaster>;新建/ui-checkskill v1.0(U1/U2/U3 + 基线追踪)
Sprint H(长尾,持续)
- [ ] 剩余 ~50 处
<svg>→ lucide(S17 基线 50 → 0) - [ ] 剩余 ListItem 迁移(TocPanel tree / RssPanel 双列表 / DiscoveryPanel / ReviewOverview)— S18 基线 5 → 1
- [ ] 剩余 hover 一致性(U3 基线 9 → 0,Toolbar / EpubNavBar / AddressBar 等)
- [ ] Radix 档位 2:Switch / Checkbox / Select / DropdownMenu 随调用点变化时迁
- [ ]
/ui-checkU4-U6 规则(列表风格语义 / 按钮语义选型 / spacing-字号灰区) - [ ] ESLint
no-restricted-syntax升级阻断裸<button>/<input> - [ ] PR 模板(UI 变更 6 项自查)
22. Chrome / Toolbar 体系
2026-08-01 立。审计与批次见
docs/plans/toolbar-unification-plan.md。 物化点 =design-tokens.ts的CHROME常量——本节所有数值以它为准,改数值改那里, 别在组件里各写一遍(/arch-checkS25 就是靠这个前提才能只扫"有没有人绕过它")。
22.1 什么算 chrome
窗口边框级的操作条:Read 的 AddressBar、Review focus 态的 AddressBarReviewSegment、 以及下沉到内容顶部的模块级/活动级段控(词汇↔笔记、复习↔重温)。
不算 chrome:面板内部的筛选 chip(CEFR 药丸、RSS 全部/未读)、列表行内联动作、 统计的 range picker——它们是「三级筛选」,走 §22.5。
22.2 一条带高:32px
band(整条 toolbar) 48 = CHROME.band → h-12 = py-1.5(6) + row 36 + py-1.5(6)
row(内容行) 36 = CHROME.row → min-h-9
rail(SegmentedGroup) 32 = CHROME.rail → h-8 p-0.5 gap-1
图标按钮 28 = CHROME.button + CHROME.iconPad → h-7 p-1.5 + 16px 图标
文字按钮 28 = CHROME.button + CHROME.textPad → h-7 px-2.5 text-xsband / row 决定纵向绝对位置:rail 中心恒在带顶下方 24px,加上 36px 高的标题栏 = 距窗口顶 60px,切模块时段控不跳。Read 的 AddressBar 天然是这个尺寸(URL 输入框 h-9 撑起内容行);没有输入框的 toolbar / 内容顶部段控必须显式吃 CHROME.band 或 CHROME.row, 否则会按内容高塌陷成 44/46px,跟 Read 一高一低。
- 28px 是点击靶下限,不再压到 24:rail 靠
p-0.5而不是缩小按钮来凑 32。 - URL 输入框
h-9(36px) 不属于本体系——输入域比图标按钮高一档是浏览器惯例(Chrome 即如此)。 - 面板 header 里的
ChromeButton size="sm"(24px) 也不属于——那是面板内联档,不是 chrome 带高。
22.3 形状与激活态
| 项 | 规则 |
|---|---|
| 形状 | 一律 CHROME.radius(rounded-full)——圆(图标)或药丸(文字),与 rail 自身同族 |
| 激活底色 | 一律 neutral TOGGLE.triggerNav.active(bg-surface-secondary) |
primary 色 | 🔴 退出 chrome。accent 只留给 CTA 与"数据选择"(CEFR 筛选) |
| hover | 一律 TOGGLE.triggerNav.inactive(实色 surface-muted) |
| 禁用 | CHROME.disabled,由基元内建;调用点不写 disabled: 覆盖串,更不另起裸 <button disabled> |
| 焦点 | CHROME.focus,由基元内建(ChromeButton / TabButton / PageToggleButton 三者都必须有) |
hover 是 §20 U3 的显式例外:U3 要求
hover:bg-hover(4% 黑),但 chrome 坐在surface-toolbar灰底上,4% 黑几乎不可见。chrome 层用实色surface-muted。 别按 U3 "修正"回去。
22.4 rail 的语义:只表示互斥选择器
<SegmentedGroup> 这个视觉(浮起白壳)只用于"同时只有一个成员激活"的选择器: 左区面板入口、右区面板选择器、复习↔重温、词汇↔笔记。全仓 rail 就这 4 组(批 2 收敛后)。
不该套 rail 的两类——它们走 CHROME.cluster(裸按钮串,gap-0.5):
| 类型 | 例子 | 为什么不能套 rail |
|---|---|---|
| 命令簇 | back / forward / reload | 根本没有激活态,白壳会谎称"这里同时只有一个生效" |
| 开关簇 | 净度菜单 · 尺寸% | 各自独立、可同时生效,不是二选一 |
分组改由间距对比表达:串内 gap-0.5(2px) vs 区间 gap-2(8px) = 4 倍反差, 够读出成组,又不多画一层壳。
反例(批 2 前):原「当前页」rail 里挤了四种行为——★收藏(站点 toggle) / 阅读净度(菜单触发器) / 尺寸%(复位命令) / 双语(页面 toggle)。判据是问一句 "这四个能同时生效吗":能 → 不是选择器 → 不该套 rail。 拆法见 22.4.1。
22.4.1 混装簇怎么拆:按「作用对象」而不是按「控件形态」
★收藏最后没有留在 chrome 里,而是进了 URL 输入框内右端(与「打开文件」并排)。 理由不是它长得像别的按钮,而是它作用于「这个地址 / 站点」(按域名存 favorite_sites), 不是「页面怎么显示」——Chrome / Safari / Edge 的星标也都在地址域内。
于是中区右端分成两簇,各自的面归属清晰:
| 簇 | 成员 | 面 | hover |
|---|---|---|---|
| 地址域动作 | ★收藏 · 打开文件 | 输入框内(surface-raised) | hover:bg-hover(4% 黑,输入框自己的面) |
| 页面呈现 | 净度菜单 · 尺寸% | chrome(surface-toolbar) | 实色 surface-muted(§22.3 例外) |
一般规则:拆混装簇时先问「它作用于什么」(地址 / 页面 / 面板 / 会话), 再问「它是什么形态」(toggle / 命令 / 菜单)。作用对象决定它落在哪个面上, 形态只决定用哪个基元。
22.5 图标 / 文字策略(按层级)
| 层级 | 例子 | 形态 |
|---|---|---|
| 一二级段控(我在哪) | 词汇↔笔记、复习↔重温 | 图标 + 文字,在 rail 内,TabButton size="chrome" |
| 命令与开关(我要做什么) | 前进后退、收藏、面板开关 | 纯图标 + tooltip,ChromeButton / PageToggleButton |
| 三级筛选(缩小范围) | CEFR、全部/未读、7/30/90 天 | 纯文字、无 rail、小一档,TabButton size="xs|sm" |
纯图标按钮必须有 aria-label(ChromeButton 自动 title ?? aria-label)。
22.6 三区骨架
toolbar 一律 左区 / 中区 / 右区(AddressBar.tsx 是参考实现):
- 侧区宽 = 它所控制 / 切换的那一栏的宽度(那一栏关闭也保留宽度,地址栏位置恒定):
- read → 左区
leftPanelWidth - focus review → 左区
reviewCardWidth(复习卡侧栏,跟随拖动) - 右区恒
rightPanelWidth
- read → 左区
- 侧区内
justify-center——按钮组正对那一栏 - 🔒 侧区就是真侧区,不许在中区里自画。批 2③ 前 focus review 把左区置 0、由
AddressBarReviewSegment在中区内style={{width: reviewCardWidth}}自画了一个"假左区", 结果两套坐标系嵌套:真左区的居中 / 溢出 / 窄窗降级规则对它一概无效,段控还被区间gap-2推离侧栏左缘 8px。新 toolbar 态要占侧区,就把侧区宽度改成它的宽度。 - 内容顶部段控同规则:Library 的「词汇/笔记」外层取
w-80(= 下方ListPanel左栏宽)justify-center——段控正对它所切换的那一栏,而不是贴左边缘
- 中区
flex-1语义(实为flexBasis: CHROME.zone.centerBasis+ grow,见 §22.8),严格夹在 左右侧区之间;命令簇贴中区左缘、页面呈现簇贴右缘 - 中区里若要放贴底的细装饰(review 的位置发丝条、URL 输入框的加载条),一律 绝对定位到底缘、不进流——进流会把中区撑高几像素,三区
items-stretch下侧区 内容就被推离主行中心。
间距四档:区间 gap-2 / 区内 gap-1.5 / rail 内 gap-1(CHROME.rail 自带)/ 簇内 gap-0.5(CHROME.cluster 自带)。
22.8 窄窗降级:优先级用 shrink 权重编码,不用 overflow-hidden
侧区不许写 width + flex-shrink-0 + overflow-hidden——那样窗口一窄,整行溢出、 右区被推出窗口,或按钮被从中间切掉半个(批 2④ 前就是这个状态)。
优先级不靠测量,靠 flex 的 shrink 权重:
侧区 flexBasis: 那一栏的宽度 flexGrow: 0 flexShrink: CHROME.zone.sideShrink (100)
中区 flexBasis: centerBasis flexGrow: 1 flexShrink: 1 minWidth: centerFloor侧区权重 100 : 中区 1 → 收缩必然先吃侧区;侧区被 min-width:auto(= min-content) 冻结在内容宽之后,剩下的才轮到中区。得到四档阶梯:
| 档 | 发生什么 | 代价 |
|---|---|---|
| T1 | 中区吃掉自己 grow 出来的余量 | 无 |
| T2 | 侧区从"那一栏的宽度"收到内容宽 | 放弃"正对面板"的对齐,按钮一个不少 |
| T3 | 中区从 centerBasis 收到 centerFloor | 地址栏变窄 |
| T4 | 溢出窗口右缘 | 退化视口(≈560 以下),不再保证 |
实测(4+3 按钮的侧区,Chromium):1104 三区满宽对齐 → 732 侧区触底而中区仍 477 → 560 全部触底。
🔒 两条铁律:
- 侧区不能带
overflow-hidden—— 那会让min-width:auto失效,直接跳过 T2 硬裁按钮。 - 三区所在的行也不能带
overflow-hidden—— 会裁掉SearchDropdown这类溢出到 toolbar 之外的浮层。宁可 T4 溢出。
22.7 基元归属
| 组件 | 用途 |
|---|---|
ui/ChromeButton | chrome 图标按钮(默认 size="md" = 28px);panel header 关闭按钮用 size="sm" |
ui/PageToggleButton | 页面功能开关(带 badge 数字 / 呼吸圆点槽位) |
ui/TabButton size="chrome" | rail 内文字段控 |
ui/SegmentedGroup | 互斥选择器外壳(§22.4,全仓 4 组) |
CHROME.cluster(非组件) | 命令簇 / 开关簇的裸按钮串 —— 没有外壳,靠间距分组 |
ui/IconButton | ❌ 不用于 chrome——它是面板/列表内联动作(rounded-md + 4% hover) |
附录 A:快速查找
| 我要做… | 去看 |
|---|---|
| 加一个按钮 | §10 按钮体系 |
| 动 toolbar / chrome 按钮 | §22 Chrome / Toolbar 体系 |
| 加一个列表 | §13 列表模式 |
| 写个弹层 | §14 弹层与模态 |
| 用颜色 | §4 色彩语义 |
| 定 padding | §6 间距与密度 |
| 选 icon 尺寸 | §9 图标尺寸 |
| 设置 z-index | §8 Z-index 层级 |
| 处理空态 | §15 空态 / Loading / 错误 |
| 做动效 | §17 动效 |
附录 B:变更日志
- v1.5(2026-08-02):全页双语退役(
docs/plans/archive/llm-server-unification-plan.mdWP4b), §22.4 / §22.4.1 / §22.5 里以「双语」为例的开关簇条目相应收敛为「净度菜单 · 尺寸%」。 §22.4 的反例段与 v1.4 条目保留原文——它们描述的是批 2 之前的历史状态,不随功能下线改写。PageToggleButton现仅剩ReadModeMenu一个消费者。 - v1.4(2026-08-01):§22 批 2 结构层落地(见
docs/plans/toolbar-unification-plan.md)。- §22.4 rail 语义收敛到位:全仓
SegmentedGroup只剩 4 组互斥选择器;命令簇 (back/forward/reload)与开关簇(净度/尺寸%/双语)脱 rail,改CHROME.cluster裸按钮串 + 间距对比分组。间距从三档扩到四档(加簇内gap-0.5)。 - §22.4.1 新增「混装簇怎么拆」——按作用对象而不是按控件形态。★收藏据此迁进 URL 输入框内(作用于地址/站点,非页面呈现),随之换到输入框自己的 4% 黑 hover。
- §22.6 三区骨架加铁律「侧区就是真侧区,不许在中区里自画」(Review 段控迁进真左区, 左区宽在 focus review 下 =
reviewCardWidth),并加「贴底细装饰一律绝对定位」。 - §22.8 新增窄窗降级四档阶梯——优先级用 flex shrink 权重编码(侧区 100 : 中区 1), 替代
overflow-hidden硬裁;两条铁律:侧区与三区行都不许带overflow-hidden。 - skill 基线更新:ui-check U7 待收敛 2→0、U9 嵌套 1→0(arch-check S24-S27 基线不变)。
- §22.4 rail 语义收敛到位:全仓
- v1.3(2026-08-01):新增 §22 Chrome / Toolbar 体系(见
docs/plans/toolbar-unification-plan.md)。- chrome 带高统一 32px(rail)/ 28px(按钮),物化进
design-tokens.ts的CHROME常量。 - 形状一律
rounded-full;激活态一律 neutral(primary退出 chrome);hover 统一实色surface-muted(§20 U3 的显式例外,已在速查表标注)。 - 禁用态 / 焦点环内建进
ChromeButton(删AddressBar.disabledNavClass与调用点的disabled:覆盖串);TabButton新增size="chrome"。 - rail 语义收紧为"只表示互斥选择器";图标/文字按三层级定;三区骨架成文。
- 新规则:arch-check S25/S26/S27(v1.6.0)+ ui-check U7/U8/U9(v1.1.0)。
- chrome 带高统一 32px(rail)/ 28px(按钮),物化进
- v1.2(2026-04-18):Sprint G 深化 S1-S6 闭环(见
docs/plans/archive/ui-standards-deepening-plan.md)。- §13 列表重写:引入
<ListItem>四 variant(simple/double/rich/tree)× 三 density × cva。 - §15.1 空态:改走
<EmptyState>(icon/primary/secondary/action slot)。 - §15.3 错误:加 sonner toast 三档反馈模型(toast / inline / field)。
- §20 违规速查:加 arch-check (H1/S10-S18) + ui-check (U1-U3) 规则编号映射列;豁免机制(
arch-r1:注释)说明。 - §21 加 Sprint G(S1-S6 完成)+ Sprint H(长尾)。
- 引入依赖:lucide-react、class-variance-authority、tailwind-merge、clsx、sonner、@radix-ui/react-dialog/popover/tooltip。
- 新 skill:
/ui-checkv1.0(U1/U2/U3)。arch-check v1.3.0 → v1.5.0(加 S13-S18)。
- §13 列表重写:引入
- v1.1(2026-04-18):Sprint 7 闭环。
- 7.4 完成 ui/ 基础组件库(Modal / Popover / ConfirmDialog / Button / IconButton / Input / Textarea / Toggle / Switch / Card / SectionCard)+ useFocusTrap / useEscapeKey / useClickOutside 三个 hook。
- 7.5 批量迁移 15+ 面板(History / MySites / Rss / Discovery / Cefr / Stats / Notes / AccountPopover / TocPanel / AuthPanel / 3 Settings 子面板 / SelectionToolbar / 6 chrome 组件 / VocabPanel / HomePage / 4 review 组件 / ThemeToggle / LeftPanel / EpubNavBar / TranslationTimeline)。
- 7.6 Token 扩展(TOGGLE.triggerNav/filterPill、SEMANTIC 13 个新子键、 BUTTON.wrapper、INPUT.xs/soft、Button xs/lg/link variants、IconButton xs、 Spinner/Select/Checkbox/ProgressBar 新 base 组件、SEMANTIC.tile 实色替代 alpha、text-badge 下探到 8px、SEMANTIC.grammarRole / typeBadge 新增)。 Retrofit 批次 1-10 清零 §7.5 的 35+ R1 账本。
- 新增
/arch-checkS10/S11/S12 规则(禁裸 button/input、禁非 scale spacing、 禁 Tailwind 语义色硬编码)。 - 新增 §14.4 Tauri multi-webview Modal 架构限制说明。
- 死代码清理:SiteCard / EpubPage / StatsPage。
- v1(2026-04-18):基于 3 轮 UX 审计整理初稿。覆盖 21 节规范。立即需要修的 bug:
bg-hover失效、ConfirmDialogz-index。