Skip to content

Lampio — UI 设计规范

本文件定义 Lampio 桌面端的视觉设计语言、组件规范和交互约定。 与 coding-standards.md 并列:前者管代码工程,本文件管界面设计。 修改时间以 git log -1 -- docs/ui-standards.md 为准(手写"最后更新"必然漂移,已移除)。

配套参考docs/ui-component-inventory.md 记录每个 ui/ 组件的实际调用点、业务实体映射、0 调用的候选废弃、未抽象但重复的 pattern。本文档说"应该用什么",inventory 说"当前用在哪里"。


目录

  1. 设计原则
  2. Token 层级
  3. 背景层级系统
  4. 色彩语义
  5. 字号与排版
  6. 间距与密度
  7. 圆角与阴影
  8. Z-index 层级
  9. 图标尺寸
  10. 按钮体系
  11. 输入框
  12. Toggle / Active 态
  13. 列表模式
  14. 弹层与模态
  15. 空态 / Loading / 错误
  16. 交互状态与可访问性
  17. 动效
  18. Microcopy 语气
  19. Review 域专属
  20. 违规速查
  21. 落地路径

1. 设计原则

  1. 内容优先,Chrome 让路。ReadBrowser 是阅读工具,用户视觉焦点应在内容区,chrome(工具栏/导航栏)要稳、要安静,不抢戏。
  2. Accent 是信号,不是装饰primary 主色只用在需要用户注意的地方(CTA、当前选中项、focus ring)。普通状态(hover、开关、chrome 按钮)用中性灰。
  3. 分层靠深浅,不靠边框。不同层级的面板、卡片、浮层通过背景色明度区分(4 层 surface),边框只在需要明确边界时用(如 input)。
  4. Dark mode 是平等公民,不是"顺手加 dark: 前缀"。每个颜色 token 必须同时在明暗模式下验证对比度。
  5. 规范优先于自由发挥。组件作者不应凭手感决定 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_COLORSCEFR_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 四层基础 + 两层补充

TokenLightDark用途
bg-surface-content#ffffff#121212内容区(webview、文档)
bg-surface#f5f7fa#1c1c1c面板底色、页面底
bg-surface-toolbar#e5e9f0#161616Chrome(TitleBar active tab / AddressBar / Toolbar / 左右面板外壳 / 阅读画布)
bg-surface-secondary#e6eaf0#303030Chrome 凹陷(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 内):也必须自带 bgbg-surfacebg-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 种场景

  1. CTA 按钮bg-primary text-white
  2. 当前选中的导航项(TocPanel 当前章节、segmented control 激活项 bg-primary text-white
  3. Focus ringfocus-visible:ring-primary
  4. 进度/加载指示(进度条填充、加载条)

禁止用在:普通 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可交互元素的 hoverhover: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

Classpx用途
text-badge9px角标数字(角标≥1位数)
text-caption10px图例、微型 label、section header(uppercase)
text-xs12px主要小字:列表项、按钮文本、辅助说明
text-sm14px正文、面板 header 标题、input 文本
text-base16px重点正文、卡片词头
text-lg18px首页问候语、ReportOverlay 标题、指标数值(中)
text-2xl24px大指标数值、复习卡词头

禁止直接写 text-[15px] 等任意字号。

5.2 Weight 约定

  • font-normal(默认):正文、说明
  • font-medium:导航 label、按钮文本、激活态 tab
  • font-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-medium

6. 间距与密度

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-2px-3 py-2.5与面板 section 对齐

6.3 列表项密度三档

档位py适用
Densepy-1.5高密度(词汇列表、目录树、filter options)
Normalpy-2常规列表(笔记、历史记录、RSS 条目)
Comfortpy-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-smicon 按钮内部 hover bg、tag
rounded-md按钮、input、segmented control 内部项、行内 IconButton
rounded-lg列表项卡片(默认)、popover、dropdown、弹窗内框、section card
rounded-xl首页卡片(大卡片、搜索框)、最外层 overlay modal 面板
rounded-fullchrome 胶囊层(见 7.1.1)+ 天然圆形(徽章、头像、进度条 track、toggle switch、pill 标签)

禁止rounded-2xl 及以上、rounded-[Npx] 任意值。

7.1.1 Radius = 层级语言(2026-07-04 成文)

圆角不是逐个组件的审美选择,而是层级的语法标记——新组件先判断自己属于哪一层,radius 随层级而定:

Radius实例语法来源
系统 chromerounded-full 胶囊SegmentedGroup 工具组、ChromeButton 圆钮、地址栏 pill、面板头部 ×macOS/Safari 胶囊 chrome
内容卡片rounded-xl / lg精读句子卡、笔记卡、首页瓷砖、modal卡片=独立内容实体
表单控件rounded-mdButton、Input、行内 IconButton、TabButton控件族统一,含面板内搜索框(输入控件跟控件层不跟 chrome 层)
天然圆形rounded-fullSwitch、进度轨、计数圆点、头像盘、pill 标签各自的通用 idiom,与胶囊 chrome 无关

判断违和的标准是"同层混用",不是"全局风格数量>1":chrome 说 chrome 的话、内容说内容的话(Safari 同款分层语法)。胶囊语法禁止漏进内容层(内容区按钮不得 rounded-full,天然圆形除外);反之内容控件也不进 chrome 胶囊组。配套前提:chrome 层共享一个连续底色场(面板/间隔/阅读画布同色,见 §3),胶囊浮在统一场上才成立。

7.2 Shadow 层级

Class用途
shadow-smSegmented control 激活项、TitleBar 激活 tab
shadow-mdConfirmDialog(warning bar)、tooltip
shadow-lgSearchDropdown、AccountPopover、下拉菜单
shadow-xlWordPopup、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 保持同步。

当前 bugConfirmDialog:23z-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:225delete 按钮text-red-500 bg-surface borderBUTTON.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 关键规则

  1. 背景用 bg-surface-raised(新增 token),不要bg-surface(与面板同色无对比)
  2. Focus ring 统一focus-visible:ring-1 focus-visible:ring-primary focus-visible:border-primary
  3. Label 放上方(顶对齐,text-xs text-text-secondary mb-1),除非是 inline 横向表单
  4. 错误态: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/158bg-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 当前 panelbg-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 开关🔴 改用 neutralbg-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。新增实体前必须在此表登记,不登记不迁移:

实体variantdensity典型使用位置
访问记录(resource_history)doublenormalHistoryPanel / HomePage Recently Opened
站点(sites / bookmarks)行展示doublenormalMySitesPanel
站点(sites / bookmarks)发现页两栏doublenormalHomePage 我的收藏站点 / 推荐站点(DiscoverRow
词汇条目(vocabulary / excluded / browse)simpledenseVocabPanel 三 tab
笔记本(reading_notes)richnormalNotesPanel
复习分组(review_groups)richnormalreview/ReviewOverview
RSS 订阅源(feeds)simplenormalRssPanel feed 列表
RSS 订阅源(feeds)发现页两栏doublenormalHomePage 我的订阅 / 推荐订阅(DiscoverRow
RSS 文章(feed_items)double + titleLines=2 + tonenormalRssPanel 文章列表
内容推荐(recommended articles)doublenormalHomePage Recommended
TOC 目录项treedenseTocPanel + 按 depth 缩进
搜索 Related ArticlessimpledenseSearchDropdown
词典搜索结果(words)非列表(字典词条卡片,字段组合复杂),保持自写
翻译时间线(TimelineEntry)非列表(可展开详情卡片),保持自写
WordPopup 释义非列表(内联语义排版)

扩表规则:新增业务实体 → 先在此表登记 variant + density 选择,给出理由;无对应 variant → 评估是否添加新 variant;实在不适合列表 → 归入"非列表"组,加入豁免清单。

ListItem API 刚性约束

  • 不暴露 titleClassName / subtitleClassName:typography 完全由 variant 决定
  • leading 不应自由 ReactNoderich variant 使用 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 迁移进度

Panelvariant状态
NotesPanel 笔记本列表rich normal✅ S5 完成
HistoryPanel 资源历史double normal✅ S5 完成
MySitesPanel 站点double normal✅ S5 完成
VocabPanel Notebook / Excluded / Browsesimple dense✅ S5 完成
TocPaneltree dense✅ S6.1 完成
RssPanel 订阅列表simple normal✅ S6.1 完成
RssPanel 文章列表double normal✅ S6.1 完成
review/ReviewOverview groupdouble⏳ S7 长尾

永久豁免(非列表 Panel,不走 ListItem):

  • StatsPanel — 图表分组 + metric card
  • DiscoveryPanel — 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/40ReportOverlay

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-50Z_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-60Z_INDEX.modal
  • Entry animation:animate-fade-in + animate-scale-in(200ms)
  • 必须:点 backdrop 关闭 + ESC 关闭 + × 按钮 + focus trap

14.3 现存 bug

  1. ConfirmDialog:23z-10真 bug,会被其他弹层遮住,改 z-60(Sprint 7.4 已修)
  2. WordPopup 无 ESC 关闭 → 补上(Sprint 7.4 已加 useEscapeKey)
  3. 所有 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 menuWindow::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.tsxreview/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基础色
Hoverhover:bg-hoverhover:text-text必须有反馈
Focus-visiblefocus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2键盘可达
Active(按下瞬间)active:scale-[0.98]active:bg-*/20(可选)
Disableddisabled: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-right200ms右侧面板入场
animate-slide-out-right150ms右侧面板退场
animate-slide-up / slide-down200ms/150ms底部弹层
animate-fade-in150ms / 300ms弹层、页面切换
animate-card-flip400ms复习卡翻转

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:Hard
  • 2 / E:Easy
  • Esc:退出当前会话(带未保存提示)

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 S12SEMANTIC.xxx / surface token
text-(red|green|...)-\d+/arch-check S12SEMANTIC.xxx
hover:bg-(surface-secondary|black/|white/|gray-/...)/ui-check U3hover:bg-hover(两类例外见下)
z-\d+ not in/arch-check S13Z_INDEX.xxx
rounded-2xl / rounded-3xl / rounded-[.*]/arch-check S14scale:rounded / md / lg / xl / full
shadow-\[.*\]/arch-check S16scale:sm / md / lg / xl 或 index.css class
text-\[\d+px\]/arch-check S15text-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 S10ui/Button / ui/IconButton / ui/Input,或同行加 arch-r1: 豁免

U3 的两类例外——共同判据:bg-hover(4% 黑) 在这个面上还看不看得见。

  1. chrome 层:坐在 surface-toolbar 灰底上 → 用实色 surface-muted(§22.3)。
  2. 实色底钮:静止态本身就有实心填充(如 bg-surface-muted 的圆钮,用填充暗示可点) → hover 必须变深成另一档实色,4% 黑叠在实色上等于没有反馈。

例外要在 U3 实际扫描的那一行(即写 hover:bg-* 的那行)标 arch-r1: <理由>—— 标在 <button 开标签行上不算数,U3 是逐行 grep,隔行的注释它看不见。 | chrome 文件内裸写尺寸类(h-7/h-8/p-1.5)或 size= 覆盖 | /arch-check S25 | CHROME.*(§22.2 物化点) | | chrome 基元缺 focus-visible:ring | /arch-check S26 | CHROME.focus(§22.3) | | chrome 层用 bg-primary* 做激活态 | /arch-check S27 | TOGGLE.triggerNav.active(§22.3:accent 退出 chrome) | | SegmentedGroup 包了非互斥的命令簇 / 混装簇 | /ui-check U7 | §22.4:rail 只表示互斥选择器 | | chrome 段控缺图标 / 命令按钮多余文字 / 三级筛选套了 rail | /ui-check U8 | §22.5 层级表 | | 新 toolbar 未复用三区骨架、侧区宽未对齐面板 | /ui-check U9 | §22.6 | | 组件中散落 <svg>(非 ui/、charts/ 下) | /arch-check S17(计数) | lucide + <Icon icon={X} size="sm" /> | | Panel 列表未走 <ListItem> | /arch-check S18 / /ui-check U1 | ui/ListItem(§13) | | 空态模板手写 | /ui-check U2 | ui/EmptyState(§15.1) | | alert() / window.confirm() | /ui-check(文档约定,尚未纳入 skill) | sonner toast.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.tsSEMANTIC / ICON_SIZE / Z_INDEX 常量
  • [x] 新建 src/lib/ui-variants.ts:导出 BUTTON / LIST / INPUT / TOGGLE 组合 class

Sprint C(2-3 天):紧急 bug + 扎眼违规

  • [x] 修 ConfirmDialog z-10 → z-60
  • [x] 修 bg-hover 失效(36 处 —— 定义 token 即自动生效,无需大改代码)
  • [x] 修 VocabPanel 两个 search 框 padding 对齐
  • [x] 修 Toolbar ToggleBtn / PanelBtn 改用 TOGGLE.triggerOn(灰黑风格)
  • [x] 修 WordPopup Hard/Easy 颜色统一到 SEMANTIC.reviewHard/Easy
  • [x] 修 TitleBar 的 emerald Review tab / bg-black/10 hover
  • [x] 修 AddressBartext-yellow-500SEMANTIC.bookmark.text
  • [x] 修 StatusBarbg-green-500/blue-500/purple-500cefrBarClass

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 / toggleVariants cva 化,新增 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-check skill 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-check U4-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.tsCHROME 常量——本节所有数值以它为准,改数值改那里, 别在组件里各写一遍(/arch-check S25 就是靠这个前提才能只扫"有没有人绕过它")。

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-xs

band / row 决定纵向绝对位置:rail 中心恒在带顶下方 24px,加上 36px 高的标题栏 = 距窗口顶 60px,切模块时段控不跳。Read 的 AddressBar 天然是这个尺寸(URL 输入框 h-9 撑起内容行);没有输入框的 toolbar / 内容顶部段控必须显式吃 CHROME.bandCHROME.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-raisedhover:bg-hover(4% 黑,输入框自己的面)
页面呈现净度菜单 · 尺寸%chrome(surface-toolbar实色 surface-muted(§22.3 例外)

一般规则:拆混装簇时先问「它作用于什么」(地址 / 页面 / 面板 / 会话), 再问「它是什么形态」(toggle / 命令 / 菜单)。作用对象决定它落在哪个面上, 形态只决定用哪个基元。

22.5 图标 / 文字策略(按层级)

层级例子形态
一二级段控(我在哪)词汇↔笔记、复习↔重温图标 + 文字,在 rail 内,TabButton size="chrome"
命令与开关(我要做什么)前进后退、收藏、面板开关纯图标 + tooltipChromeButton / PageToggleButton
三级筛选(缩小范围)CEFR、全部/未读、7/30/90 天纯文字、无 rail、小一档TabButton size="xs|sm"

纯图标按钮必须有 aria-labelChromeButton 自动 title ?? aria-label)。

22.6 三区骨架

toolbar 一律 左区 / 中区 / 右区AddressBar.tsx 是参考实现):

  • 侧区宽 = 它所控制 / 切换的那一栏的宽度(那一栏关闭也保留宽度,地址栏位置恒定):
    • read → 左区 leftPanelWidth
    • focus review → 左区 reviewCardWidth(复习卡侧栏,跟随拖动)
    • 右区恒 rightPanelWidth
  • 侧区内 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-1CHROME.rail 自带)/ 簇内 gap-0.5CHROME.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 全部触底。

🔒 两条铁律

  1. 侧区不能overflow-hidden —— 那会让 min-width:auto 失效,直接跳过 T2 硬裁按钮。
  2. 三区所在的行也不能overflow-hidden —— 会裁掉 SearchDropdown 这类溢出到 toolbar 之外的浮层。宁可 T4 溢出。

22.7 基元归属

组件用途
ui/ChromeButtonchrome 图标按钮(默认 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.md WP4b), §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 基线不变)。
  • v1.3(2026-08-01):新增 §22 Chrome / Toolbar 体系(见 docs/plans/toolbar-unification-plan.md)。
    • chrome 带高统一 32px(rail)/ 28px(按钮),物化进 design-tokens.tsCHROME 常量。
    • 形状一律 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)。
  • 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-check v1.0(U1/U2/U3)。arch-check v1.3.0 → v1.5.0(加 S13-S18)。
  • 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-check S10/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 失效、ConfirmDialog z-index。