Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

桌面设计系统

本文档规定了 Electron 桌面应用(apps/desktop)的规范。在添加任何组件、覆盖层或样式之前,请先阅读本内容。核心原则为:一个功能对应一个来源,优先使用符号而非硬编码值,采用扁平结构而非嵌套结构。若你需要使用原始颜色、一次性阴影、定制按钮,或是为控件硬编码 px-* 值——请立即停止,因为系统早已提供了相应的基础元素。

本文件定义了视觉呈现与交互规则。有关架构、状态管理、解析机制、传输方式及测试规范,请参阅 AGENTS.md

本文档包含两类内容,其维护方式各不相同:

  • 设计原则(如扁平性、意图性、反馈机制、动画效果、取消操作等)是长期不变的。无论组件如何更替,这些原则始终有效。
  • 命名规范(如符号、Button 的不同变体、基础元素名称等)代表了设计系统当前的 API 接口。这类内容需与代码同步维护:一旦修改了某个基础元素、符号或变体,必须在同一份代码修改中更新此处的对应条目——若文件中的名称过时,就会像类型过时一样引发问题。

当规则与代码出现冲突时,应修正错误的部分,而非在调用处临时创建特殊处理方案。

设计原则

  1. 扁平结构,避免嵌套。不得使用卡片套卡片的布局,面板内也不允许有分隔边框。应通过空白区域和细边框进行分组,绝不能使用多层圆角嵌套结构。
  2. 浮动面板采用无边框凸起效果。覆盖层应通过 shadow-nous--stroke-nous 细边框实现悬浮效果,而非厚边框框定。面板内的结构可偶尔使用符号形式的细边框。
  3. 一个功能对应一个基础元素。一个 Button、一组控件变体、一个 SearchField、一个 Loader、一个 ErrorState——应直接使用这些标准元素,避免自行创建新元素。
  4. 优先使用符号,而非硬编码值。在组件中应引用 CSS 变量(如 --ui-*--shadow-nous--theme-*),严禁直接使用十六进制颜色值或临时指定的 rgba 值。
  5. 样式定义在基础元素中。各变体及尺寸类型会自行决定内边距、圆角大小、颜色及边框样式。调用方只需传递 variant/size 参数,无需通过 className 覆盖来重新指定这些属性。
  6. 先体现意图,再实现自动化。应首先展示有用的操作和预览内容,但不得仅因工具生成了某些结果就自动打开新面板、切换焦点或进行导航。
  7. 即时反馈。直接操作应首先更新界面显示。网络请求或磁盘持久化操作随后再进行同步处理,若失败则需明显地撤销之前的更改。

信息架构

  • 聊天界面是核心区域。对话记录和输入框始终处于主导地位;工具、预览内容、文件、审核功能以及终端窗口则用于补充对话流程。
  • 页面是长期存在的目标界面。聊天、技能应用、消息功能以及文档管理等功能都应保留在主界面框架内。不得将某个独立的产品功能隐藏在无关的页面中。
  • 路由覆盖层用于处理短期任务。设置选项、命令中心、定时任务、用户配置、智能体列表以及星图等功能均以 OverlayView 卡片形式呈现,关闭后会返回到之前的界面。模型/会话选择器及对话框则叠加在当前界面之上,不属于导航栈的一部分。
  • 面板用于承载当前工作内容。预览窗口、文件管理、审核功能以及终端窗口都应与当前任务保持关联。即使暂时隐藏这些面板或切换聊天界面,只要底层工具仍需保持状态,这些面板的状态也应保留。
  • 一个操作对应一个统一处理方式。某个命令可能同时提供键盘快捷键、调色板选项及可视化操作入口,但它们都应触发相同的操作并产生一致的状态。不得为不同的操作入口设计不同的行为逻辑。
  • 项目拥有独立的工作目录。建议通过侧边栏的“项目”功能来管理本地文件夹和工作树,无需再引入针对会话或右侧边栏的独立文件夹选择流程。

导航系统必须保持上下文一致性。后台会话结束、工具处理结果返回或项目刷新时,虽然可能会更新徽章和缓存数据,但绝不能替换当前显示的对话内容或抢占焦点。

界面与凸起效果

浮动面板(基础类型为 Dialog)、路由覆盖层、启动/安装/更新界面、模型选择器、引导界面、提示框以及通知等元素均采用以下设计方式:

shadow-nous           /* downward-weighted, layered contact→ambient falloff */
border-(--stroke-nous) /* currentColor hairline, theme-adaptive */

这两者都是 src/styles.css 中的 CSS 变量——只需在一处进行调整,所有元素都会自动继承该设置。
请勿为每个叠加层单独添加 shadow-[…]border-(--ui-stroke-secondary) 这类一次性配置;若需改变阴影深度,应直接修改对应的变量值。

菜单与弹出框会使用自己统一的 shadow-md--ui-stroke-secondary 规则。可拖动的控件则会采用基于变量的虚线目标及局部模糊效果。这些均为语义化的表面样式类,而非允许在任意位置自行创建阴影或边框的许可。

轮廓与颜色变量

变量名用途
--ui-stroke-primary…quaternary从强到弱的细线风格
--ui-stroke-tertiary面板内分隔线及列表细线的默认样式
--stroke-nous叠加层的细线(与 shadow-nous 搭配使用)
--ui-text-primary / -secondary / -tertiary文本层级配色
--ui-bg-quaternary软填充背景色(用于次要按钮)
--chrome-action-hover静态控件悬停时的填充效果
--theme-primary, --ui-accent品牌主色/强调色

切勿直接硬编码 border-gray-*bg-whitetext-black 等值。BrandMark 中的白色方块是唯一被允许的固定颜色(因为品牌标识需要固定的背景)。

按钮——单一组件

所有按钮的实现均来自 src/components/ui/button.tsx。只需选择 variant(样式变体)与 size(尺寸),不得传递 h-*px-*py-* 或图标尺寸相关的覆盖参数。

样式变体: default(主色风格)、destructivesecondary(软填充风格——默认的非主色外观)、outline(透明背景加 1px 内圈,无填充与阴影)、ghostlinktext(无边框的行内控件——“取消”、“清除”)、textStrong(加粗下划线的行内控件——“更改”、“打开日志”)。

尺寸: defaultxssmlginline(紧贴左侧,无边框——适用于嵌在标题或句子中的按钮;可替代 h-auto px-0 py-0)、micro(用于状态栏/表格底部),以及图标系列 icon / icon-xs / icon-sm / icon-lg / icon-titlebar

注意事项:

  • 文本按钮为正方形(无圆角),尺寸由内边距与行高决定(无固定高度)。仅有图标按钮会采用统一的 4px 圆角。
  • SVG 图标会继承 size-3.5(在 xs 尺寸下为 size-3),无需再次设置图标大小。
  • 当按钮需要以链接或 Slot 的形式渲染时,可使用 asChild 实现多态化。

表单控件

  • controlVariants(位于 src/components/ui/control.ts)是 InputTextareaSelectTrigger 等控件的通用样式基础。新的文本输入控件均基于此构建。
  • SearchField——无边框,聚焦时显示下划线,宽度自动调整。这是唯一的搜索输入框。请勿为其创建带边框的搜索栏,也不要将其包裹在带边框的方块中。列表为空时会隐藏该搜索框。
  • SegmentedControl——用于处理少量互斥选项的场景(如颜色模式、工具调用显示方式、使用周期等),可替代传统的单选按钮组或药丸状选项行。
  • Switch(尺寸为 xs)——仅包含基础元素及 aria-label 属性,无带边框的文本包装层。

布局

  • 内边距: 使用 PAGE_INSET_X(位于 src/app/layout-constants.ts)设置页面两侧的内边距;使用 PAGE_INSET_NEG_X 可将子元素向边缘偏移。请勿在页面中直接硬编码 px-6/px-8 等值。
  • 主内容/详情叠加层: 采用 OverlaySplitLayout 结合 OverlaySidebar / OverlayMain 的结构。任务调度、用户资料等功能均基于此布局实现,无需重新构建标题栏外壳。
  • 行结构: 使用 ListRow(定义于 primitives.tsx)来组织标签/描述/操作等元素。这类行为水平排列、紧贴左侧,不会因每行添加内边距而影响顶部标题的对齐。
  • 除非列表确实需要分隔线,否则不应在行之间添加分隔线;优先使用适当间距即可。若必须添加分隔线,仅使用一条 --ui-stroke-tertiary 风格的细线。

反馈及空状态/错误状态/加载状态

  • 加载中: 使用 Loader 组件(位于 src/components/ui/loader.tsx),通过动画化的数学曲线或 ASCII 图形(长时间操作时使用 lemniscate-bloom 动画)来展示加载状态。切勿直接显示“Loading…”等文字。
  • 错误状态: 使用 ErrorState 组件配合标准的 ErrorIcon(无背景色块),即可统一呈现 React 边界错误、对话框内错误以及启动失败提示。请传递标题与描述对应的节点,以便 Radix 的 DialogTitle/Description 组件能够正常渲染,从而满足无障碍访问需求。
  • 日志显示: 使用 LogView 组件——无背景色,仅有细线边框,内边距紧凑,字体为等宽体。所有需要展示原始日志的地方均应使用该组件。
  • 空状态: 空白的页面内容可使用 EmptyState;而带有图标与操作按钮的叠加层主内容/详情空状态则应使用 PanelEmpty。无需自行创建第三种居中的空状态组件。

聊天、工具及启动界面

  • 聊天记录与输入框功能均基于 @assistant-ui/react 构建。应在 src/components/assistant-ui 以及 src/app/chat/composer 下扩展现有组件,切勿为某个功能单独创建第二个 Markdown、消息、工具调用或审批渲染器。
  • 工具结果可展示用于打开预览的行内操作按钮,但不得自动弹出操作栏。
  • 安装、引导、连接、启动失败以及重新认证等状态虽各不相同,但共享相同的视觉基础元素。在统一外观设计时,请保留这些状态对应的恢复逻辑。
  • 需要尊重 AppShell 对叠加层的管理权限。持久化的终端/内容层、路由叠加层、对话框以及启动界面,均不得通过随意设置 z-index 来相互覆盖。

图标与品牌标识

  • Tabler 是默认的组件及图标集。请从 src/lib/icons.ts 中导入其精选的别名及 iconSize 缩放规则,切勿在功能代码中直接导入第三方图标包。
  • Codicon 提供了简洁的编辑器、工具及状态相关图标。如需使用 Tabler 风格的组件,则应调用 src/components/ui/codicon.tsx 及其中的 codiconIcon() 函数。
  • 应根据语义上下文选择合适的图标,并复用现有图标来表示相应操作。切勿在同一个控件组中引入第三套图标或混合使用不同风格的图标。
  • BrandMark(位于 src/components/brand-mark.tsx)是品牌标识——即白色方块上的 nous-girl 图标,具有柔和的圆角,深色与浅色版本外观一致。它已取代了旧版本中分散在各处的小星星图标,用于更新页面、引导流程及关于页面等场景。请在关键的品牌展示区域使用该图标,不要再引入装饰性的星星或闪光图标。

动画效果

  • 动画应快速且实用(控件动画时长约 100 毫秒)。除淡入淡出效果外,其他动画都应尊重用户的 prefers-reduced-motion 设置。
  • 复杂的退出动画(如引导流程中的“矩阵式”渐隐效果)应逐个元素依次动画,最后再整体定格——外部容器的淡出效果应适当延迟,以避免掩盖内部的动画内容。切勿让全局淡出动画抢在细节动画之前完成。
  • 动画应跟随状态变化而触发,绝不能因此延迟状态更新。选择、拖动目标、取消操作以及按下反馈等效果都应在当前帧立即呈现。
  • 在高频交互场景中,切勿使用 transition-all 来动画化布局几何属性。应为每个属性指定名称,避免在动画过程中触发背景模糊效果的重新绘制,并在出现性能问题时立即停止动画。

直接操作与性能优化

即使在负载较高的情况下,应用也应具备即时响应的特性——比如处理长篇聊天记录、多个分屏界面或实时流媒体内容。设计时应以此为目标:

  • 先进行直接操作并立即渲染结果;持久化存储功能会在之后进行同步处理,若出现错误则能清晰地回退到之前的状态。
  • 保持交互反馈的轻量级:高频交互的状态应保持在本地或通过简单方式计算得出,避免深入复杂的树结构;指针相关的操作也应在每一帧中集中处理。
  • 每个放置区域仅有一个视觉控制对象,且所有放置目标在不同文件、会话、标签页及分屏之间都使用统一的可操作语言。重叠的目标应优先显示当前活跃的那个,而非简单堆叠在一起。
  • 容错性更强的几何布局优于像素级精确的触发条件;边缘操作元素应靠近其实际位置,而非集中在中心区域。
  • 高成本的带状态界面在隐藏时仍应保持挂载状态。可见性并非决定组件生命周期的因素。

请通过真实的内容来证明应用的快速响应能力。一个快速的空状态演示无法体现应用在处理长篇聊天记录或复杂终端界面时的表现。

键盘操作与取消功能

  • 键盘控制权随焦点变化而转移。当前获得焦点的界面可占用所有键盘按键;标题栏中的快捷键不得抢占终端或编辑器的默认绑定。
  • 应通过统一的层级结构来注册全局快捷键,而非使用临时的监听器。
  • 一个取消手势仅能执行一项操作:要么取消当前的交互,要么关闭最顶层的可关闭界面——绝不能同时执行两项操作,也不得影响下方的控件。
  • 即使后续的清理操作是异步的,UI 层面的取消操作仍是同步进行的:叠加层、光标以及待处理的手势状态都会立即清除。
  • 那些明确设计为不可关闭的流程(如安装引导、破坏性操作确认)必须明确告知用户这一点。

国际化支持

  • 所有面向用户的字符串内容都必须经过 useI18n() 函数处理(位于 src/i18n/context.tsx),JSX 中不得出现硬编码的字符串。
  • 所有语言版本应同时更新——包括 enjazhzh-hant。如果在 en.ts 中修改了字符串却未同步更新其他语言文件,就会导致功能退化(如标点符号错位、标签过时)。请确保这四种语言版本的尾随标点及语气风格保持一致。

状态管理(TypeScript)

详细的 state 规范定义在 scoped 文件 [AGENTS.md] 中。视觉代码的实现应遵循以下原则:

  • 共享或跨组件使用的状态应通过小型 nanostores 管理,而非通过 prop 逐层传递。每个功能模块负责管理自己的状态原子,而共享的原子则存储在 src/store 中。
  • 渲染组件通过 useStore 订阅状态,而非渲染相关的操作则通过 $atom.get() 读取状态值。
  • 当组件无需显示状态的完整值时,应订阅派生出的粗粒度事实,而非高频更新的原始状态原子。
  • 操作相关模块应与通用钩子分开编写,每个钩子仅负责单一功能。
  • 状态持久化逻辑应与对应的状态原子放在一起,避免路由根节点变得过于臃肿。
  • 公共属性推荐使用 interface 定义;可基于 React 原生类型进行扩展(如 React.ComponentProps<'button'>Omit<…>)。

可操作性设计

  • 在基础组件层面应设置 cursor-pointer 属性(适用于按钮、下拉菜单/选择框等),无需在每个使用位置都硬编码该属性。
  • 全局焦点环应重置;标题栏中的操作按钮没有激活背景状态。
  • 按下 Esc 键可关闭所有可关闭的叠加层或对话框(安装引导场景除外);关闭操作应使用 X 图标,而非文字“关闭”。

添加新功能前的检查清单

  • 是否可以复用现有的基础组件(如 ButtonSearchFieldSegmentedControlListRowLoaderErrorStateLogView),而无需自行创建新组件?
  • 是否使用了变量(如 --ui-*shadow-nous--stroke-nous)?是否避免了直接使用原始颜色或一次性设置的阴影效果?
  • 是否没有通过 className 覆盖基础组件的内边距、尺寸、圆角或边框样式?
  • 叠加层是否使用了 shadow-nousborder-(--stroke-nous),而非硬编码的边框?
  • 布局是否简洁?是否避免了嵌套卡片结构以及不必要的行分隔线?
  • 是否没有自动导航、键盘焦点抢占或由背景事件触发的分屏打开行为?
  • 直接操作能否立即生效,且失败时能干净地回退到初始状态?
  • 高频交互是否避免了大规模的订阅操作、布局抖动以及 transition-all 动画?
  • 键盘控制权及 Esc 键的单一操作行为是否正确?
  • 所有四种语言版本的相关字符串是否都已更新?
  • cursor-pointer、焦点环以及 Esc 键关闭功能是否正常工作?
  • 是否修改了任何基础组件、变量或样式变体?相关的内容应在同一份代码变更中得到同步更新。