表面系统(深色模式)#
深色模式 UI 分为两个表面上下文。每个表面拥有各自的 交互语言,使眼睛能一眼分辨出当前悬停在应用的哪个 "层":导航层还是内容层。
两个表面#
─────────────────────────────────────────────────────────────────
surface background tone where it lives
─────────────────────────────────────────────────────────────────
deep near-black ``--bg`` / left sidebar, right
``--bg-secondary`` sidebar (branches /
worktrees / mini-DAG)
─────────────────────────────────────────────────────────────────
panel slightly lifted greyish chat stream, settings
``--bg-surface`` / panes, dialog content,
``--bg-tertiary`` function-card grid,
attach card, runtime
blocks
─────────────────────────────────────────────────────────────────
deep 与 panel 之间的抬升是有意为之的——它替代了 聊天内容列上显式的边框 / 阴影,使气泡区域看起来像一张 漂浮在导航之上的独立纸面。
各表面的交互语言#
Deep 表面(侧边栏)#
deep 表面上的组件是列表行——会话项、分支条目、 函数收藏。它们不应当表现得像按钮:
- 闲置状态下无边框、无描边、无填充
- 悬停 / 选中 → 背景切换为略浅的灰色
(
--bg-hover/--bg-selected),文本保持--text-primary或--text-secondary - 避免使用品牌色字形处理,唯一例外是极小的状态 /
活动指示器(
.indicator-dot)
理由:侧边栏密集且被频繁扫视。一片品牌色胶囊会显得 喧闹,并在视觉上与内容列竞争。悬停变灰让这一层保持 克制,同时仍为点击目标提供足够的反馈。
Panel 表面(聊天内容 + 对话框)#
panel 表面上的组件就是按钮 / 胶囊 / 卡片:
- 它们位于抬升的背景之上,因此"幽灵描边"模式 能干净地呈现
- 闲置状态——
--bg-surface背景,--text-primary文本,主操作则用品牌色文本 - 悬停——以品牌色填充,文本切换为其对比配对色
(
--text-on-accent) - 这种反转式悬停让一连串"操作"感觉像同一个设计 家族——用户知道这种颜色变化在各处都统一表示 "这会执行某个动作"的可供性
按钮变体指南#
web/components/ui/button.tsx 已经暴露了两种主要
模式:
无边框。 每个 Button 变体在闲置和悬停状态下都没有
边框。deep / panel 之间的表面抬升已经分隔了各层;在此
之上再加显式的 border-input 会给密集行增添视觉噪声,
并且与本应用在其他各处(function-card 网格、attach 卡片、
fn-form 胶囊)使用的轻抬升幽灵胶囊约定相比显得过时。
variant idle hover
─────────────────────────────────────────────────────────────────
default bg-background + text-primary bg-primary +
text-primary-foreground
─────────────────────────────────────────────────────────────────
outline bg-background + foreground bg-accent +
text-accent-foreground
─────────────────────────────────────────────────────────────────
ghost transparent bg-accent +
text-accent-foreground
─────────────────────────────────────────────────────────────────
secondary subtle grey fill darkens slightly
─────────────────────────────────────────────────────────────────
destructive bg-background + text-destructive bg-destructive +
text-destructive-foreground
─────────────────────────────────────────────────────────────────
按表面选择:
- Panel + 主操作(Run、Save、Test、Apply、Check)→
variant="default"。默认为品牌色文本,悬停时品牌色 填充。大多数聊天 / 设置 / 函数对话框操作都应使用它。 - Panel + 次要操作(Cancel、Close、Reset、Browse)→
variant="outline"(淡灰悬停)或ghost。 - Deep 表面——侧边栏行 → 不要使用 Button 原语。
使用由
sidebar.module.css设样式的普通锚点 / div, 因为行本身就是交互。 - 破坏性操作(Delete、Remove、Force)→
variant="destructive"。默认红色文本,悬停时红色填充。
要警惕的失效模式是:本该用 default 的地方用了 outline。
outline 是 shadcn 开箱即用的样子,作者会下意识地选它,
于是主操作拿到的是低调的悬停强调,而非品牌色填充。纠正它
只能逐个调用点做,因为一个操作属于主还是次要,只有人能判断。
尺寸系统——两套,套内无变体#
每个交互原语在两套尺寸中选其一。套内没有
sm / md / lg 的阶梯——一旦你选定 list 还是 button,
高度和圆角就被锁定。CSS 变量位于
web/app/styles/base.css:
set height radius css tokens
─────────────────────────────────────────────────────────────────
list 32 px 10 px --ui-list-h
--ui-list-radius
─────────────────────────────────────────────────────────────────
button 30 px (slightly 10 px --ui-button-h
shorter than list) --ui-button-radius
─────────────────────────────────────────────────────────────────
为什么 button 比 list 矮:panel 表面上的胶囊在视觉上
不应当压过它旁边的侧边栏行。两套共用同一个 10px 圆角
—— Claude 的形状语言把列表行和小按钮都放在 10px,把
12px(--radius-lg)留给卡片和面板。
为什么套内无变体:当设计允许一个槽位从 sm / md / lg 中挑选时,每位作者都会与设计讨价还价而不是遵循它,尺寸 随之分叉。两套固定尺寸是可强制执行的。
Button 的向后兼容:size="sm" | "lg" | "icon-sm" 作为
别名为现有调用点保留,但它们解析到与 default 相同的
高度。token 名称才是唯一可信来源。
禁止事项#
- 不要在未先于此处列出的情况下引入新的胶囊背景色。 三种风味(deep、panel、品牌填充)就是预算上限。
- 不要在 deep 表面使用品牌色填充——它与近黑背景的 对比会让品牌色胶囊看起来像一条警报,而非点击目标。
- 不要在任一表面添加悬停时位移(translate-y、scale-105) 效果。我们仅依赖背景切换;密集行内的运动会被读作 抖动,而非反馈。
- 不要给 Button 派生组件添加
border/ring/outline。表面抬升已经把它们与背景分隔开;在抬升 之上再加边框会被读作一个堆叠的警报对话框或聚焦光晕, 而非一个安静的点击目标。