Hex 设计系统¶
Hex 的窗口、会议胶囊、转录选择器和新手引导均使用 Rune 以 OpenCode 的设计语言绘制:
rune-kit提供通用组件,命名沿袭@opencode/ui:Button、IconButton、Switch、SegmentedControl、Select、Menu、Popover、TextInput、TextArea、Badge、Progress,以及其余组件。rune-theme-opencode提供所有颜色、阴影层级、圆角、字体样式和动效 token(rune_theme_opencode::kit(dark)),即@opencode/ui的 token 数据化表示。Hex 跟随系统外观,因此每个视图都在浅色和深色下工作。
窗口遵循 OpenCode Desktop 的布局:一个深色背景的侧边栏和一个凸起的圆角内容面板。Hex 保留了自己的信息、区块和文案;外观则来自 kit。
听写 HUD(rune_hud.rs)在系统之外:Dot 作用域是一个着色器(rune_hud.wgsl),由 Rune 叠加层的合成器在每个 OS 上绘制,并在 HUD Lab 中手动调优。
规则¶
- 仅使用 kit token。 视图通过
Paint(rune_ui/controls.rs)指定 kit token:p.k.c.*颜色、p.k.ty.*字体、p.k.radius.*、p.k.elevation.*、p.k.motion.*,或Paint简写(text、muted、faint、accent、danger、success)。视图中剩余的数字是布局度量:行高、列宽和间距。绝不要在主题 crate 之外编写颜色字面量。 - kit 组件优先。 只要有合适的
rune-kit组件就使用它。当 Hex 需要更多能力时,扩展下方完成相应工作的 Hex 构件,或在旁边新增一个;绝不要为同一工作保留两个辅助实现。 - 每个屏幕只有一个强调操作。 kit 的对比按钮(
ButtonKind::Primary)在每个屏幕上至多标记一个操作:即用户前来要做的事。会议胶囊是一个独立的屏幕。状态不是操作:进行中的状态(如正在录制、有可用更新)是带status_dot的 Secondary 按钮。ButtonKind::Danger仅用于破坏性和已武装的确认;成功颜色仅标记就绪和进行中的状态。 - 可见操作要少。 详情头部显示一到三个最常见的操作;其余放入溢出菜单,破坏性选项放在最后。破坏性选项以 "…" 结尾,并保留两步确认。空格按下聚焦的按钮;Return 只按下
.default_button()。将其中一个设为对话框、导览步骤或可恢复确认出现时的确认操作;绝不要设为面板的常驻操作,也绝不要用于不可逆操作(清除全部历史、确认删除某个模式)。 - 悬停即时生效,且只作用于单个目标。 设置行从不绘制悬停或按下填充;只有其中的控件会响应(可切换的行是
pressable_row)。悬停填充属于本身就是一个可点击目标的元素:列表行、选择器行、侧边栏项。每个.hover(…)都不需要额外设置:窗口根节点设置了.hover_motion_within(Anim::INSTANT, Anim::INSTANT)(每个叠加层窗口的根节点也如此),因此其下所有悬停,包括 kit 控件和弹出层,都会无淡入淡出地即时切换。绝不要给元素单独设置hover_motion。按下和选中的动效保持 kit 默认绘制,列表除外:list_row或列表 + 详情item在指针按下时选择,且不显示按下叠层;选中填充和详情内容在同一帧内切换(select_at_once)。 - 通过目视验证。 用
--snap场景以浅色和深色渲染你接触过的每个视图(参见 验证)。
Hex 构件¶
这些构件位于 rune_ui/controls.rs 和 rune_ui/lists.rs 中,由 kit 组件组成:
| 用途 | 构件 |
|---|---|
| 设置页面(留白与各板块之间的节奏) | page(卡片会超出留白 row_px,使行文本与标题边缘对齐)或 bare_page(在留白上的独立内容);它们的数值是 METRICS,即唯一的布局 token 集合 |
| 设置卡片、带标题的卡片、标签栏上带有控件的卡片 | card、section、section_with(高级选项、状态、添加按钮) |
| 设置行(标题、说明、控件) | setting_row、described_row、link_row(标题下方的一行链接,使用普通行高)、toggle_row;可切换的行会显示 row_switch;其他任何整行按下的行使用 pressable_row |
| 少数互斥选项 / 一个下拉菜单 | segmented、select |
| 其选项在 sheet 中打开而非菜单中的值 | sheet_button(显示值和尾部箭头) |
| 主要、其他和破坏性操作 | button(…, ButtonKind::{Primary, Secondary, Danger}, …);行内使用 small_button;仅图标(带有可访问标签)使用 icon_button |
| 模态框进入和退出的方式 | 表面使用 modal_transition(缩放 0.96 和淡入淡出),遮罩使用 modal_scrim_transition |
| 快捷键键帽 | keycaps(凹槽托盘中的小键,与录音器同高);一个按下时会下沉的活动键:keycap 位于 key_tray 内;录音器使用 settings::hotkey_control |
| 状态(不可点击) | tone_tag;进行中或已录制的状态:status_dot |
| 控件下方的短消息 | note |
| 输入暂停时保存的文本输入 | text_field |
| 搜索弹出层中的键盘高亮 | Highlight |
| 列表 + 详情(命令、会议、历史) | list_detail:list_column(在行上方搜索,位于标题边缘,无分隔线)与 item 行(无悬停填充,时间在右侧,通过 row_action 在相应位置显示悬停操作),按 group 分组;选中的项目位于 body 的凸起卡片中,带有 card_header、card_title、meta_text、ghost_icon 操作、card_scroll 和 facts;无内容可显示时使用 empty_state |
| 设置卡片、带标题的卡片、标签栏上带有控件的卡片 | card、section、section_with(高级选项、状态、添加按钮) |
| 设置行(标题、说明、控件) | setting_row、described_row、link_row(标题下方的一行链接,使用普通行高)、toggle_row;可切换的行会显示 row_switch;其他任何整行按下的行使用 pressable_row |
| 少数互斥选项 / 一个下拉菜单 | segmented、select |
| 其选项在 sheet 中打开而非菜单中的值 | sheet_button(显示值和尾部箭头) |
| 主要、其他和破坏性操作 | button(…, ButtonKind::{Primary, Secondary, Danger}, …);行内使用 small_button;仅图标(带有可访问标签)使用 icon_button |
| 模态框进入和退出的方式 | 表面使用 modal_transition(缩放 0.96 和淡入淡出),遮罩使用 modal_scrim_transition |
| 快捷键键帽 | keycaps(凹槽托盘中的小键,与录音器同高);一个按下时会下沉的活动键:keycap 位于 key_tray 内;录音器使用 settings::hotkey_control |
| 状态(不可点击) | tone_tag;进行中或已录制的状态:status_dot |
| 控件下方的短消息 | note |
| 输入暂停时保存的文本输入 | text_field |
| 搜索弹出层中的键盘高亮 | Highlight |
| 列表 + 详情(命令、会议、历史) | list_detail:list_column(在行上方搜索,位于标题边缘,无分隔线)与 item 行(无悬停填充,时间在右侧,通过 row_action 在相应位置显示悬停操作),按 group 分组;选中的项目位于 body 的凸起卡片中,带有 card_header、card_title、meta_text、ghost_icon 操作、card_scroll 和 facts;无内容可显示时使用 empty_state |
| 列表 + 详情(Activity) | 由 list_row、group_label、detail_header、detail_title、section_label、fact 组成的 split |
| 键盘可选择的列表(侧边栏、列表 + 详情、语言选择器) | 在由 list_row 或 source_item 构成的行外围使用 source_list |
| 无内容可显示、错误 | message、placeholder、error_panel |
| 权限的 Allow / Allowed 控件 | permission_control、allowed |
| 滑块 | slider |
窗格专属组件保留在其窗格模块中:监听器卡片和模型卡片位于 rune_ui/settings.rs 和 rune_ui/picker.rs,引导组件位于 rune_ui/onboarding_kit,胶囊位于 rune_ui/meeting_capsules.rs。platform::Capabilities 决定每个 OS 上存在哪些窗格和行;缺少某项能力时会省略对应的行,而不是去模仿它。
文案¶
- 所有标签、按钮、标题和分区标签均使用句首大写(sentence case):"Open settings"、"Stop & transcribe"、"Permissions needed"。专有名词保留大写:Hex、OpenCode、Apple Intelligence、Finder、Zoom。
- 产品名称统一为 "Hex",文案、注释和文档中均如此。
HEX_*环境变量是标识符,不是文案。 - 省略号统一使用 "…"(单个字符),既用于进行中状态("Saving…"),也用于会打开进一步选择界面的按钮("Choose a file…")。
- 行描述是一个不带句末句号的短语("Hold to dictate, release to transcribe")。若为两句话,则保留各自的句号。
- 每个控件保持一行直截了当的说明。不要在卡片上方添加引言段落、不要在每行重复字段标签、不要在开关旁边放置状态文字,也不要在两步确认旁提示"无法撤销"。
- 每个已翻译的字符串(
tr、tr_fill、tr_named)都需要在desktop/i18n.rs中包含全部五种语言;every_translated_string_has_every_language会扫描每个 UI 源文件。仅限开发者使用的窗格(Activity、HUD Lab)保持英文。 - 例外:原生 macOS 菜单(
macos_rune/status_item.rs以及macos_rune.rs中的应用菜单)遵循 macOS 标题大写(title-case)惯例("Check for Updates…"、"Paste Last Dictation"、"Stop & Transcribe")。
验证¶
# Headless renders of named fixture scenes (`SCENES` in src/rune_ui.rs):
cargo run -- app -- --list-scenes
cargo run -- app -- --snap /tmp/hex-modes.png --scene modes
cargo run -- app -- --snap /tmp/hex-modes-light.png --scene modes --theme light
cargo run -- app -- --snap /tmp/hex-mac-settings.png --scene mac-settings
# Both themes of several scenes into one folder:
OUT=/tmp/hex-snaps scripts/snap-set.sh modes mac-settings
# Goldens (light and dark) and pane behavior:
scripts/check rune_ui
# After an intended visual change, rewrite the goldens (Linux only):
HEX_UPDATE_GOLDENS=1 cargo test --locked --bin hex rune_ui
在没有 GPU 的机器上,使用 lavapipe 渲染,并设置 VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json。golden 基准图存放在 tests/golden/ 中,并在较小的逐像素容差范围内进行比较。golden 测试仅在 Linux 上运行;在 Mac 上,在该分支上触发 check-linux.yml,并获取其 golden-actual 产物。请查看每一张更改过的图像,并将其与导致该更改的变更一起提交。场景是一种测试夹具,而非原生端到端的证明:会议胶囊和 HUD 没有对应场景,真实的窗口、覆盖层和输入仍然需要在每个 OS 上由真人测试。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 hex19/hex