Agent Skillshenjicc/Henji-AI › henji-ui-surface

henji-ui-surface

GitHub

规范前端界面层级与表面样式,解决卡片嵌套、条带重叠等问题。提供五级容器词汇表、两条铁律及决策树,指导页面骨架、弹窗、侧栏等UI的结构设计与美化。

.codex/skills/henji-ui-surface/SKILL.md henjicc/Henji-AI

Trigger Scenarios

新建或改造界面/页面骨架/面板/弹窗/侧栏 调整按钮层级、分隔线、颜色、图标、动画 界面美观度优化(如太挤、像卡片套卡片) 布局不合理或标题栏合并需求 统一UI配色、动画或修复主题变色问题

Install

npx skills add henjicc/Henji-AI --skill henji-ui-surface -g -y
More Options

Non-standard path

npx skills add https://github.com/henjicc/Henji-AI/tree/main/.codex/skills/henji-ui-surface -g -y

Use without installing

npx skills use henjicc/Henji-AI@henji-ui-surface

指定 Agent (Claude Code)

npx skills add henjicc/Henji-AI --skill henji-ui-surface -a claude-code -g -y

安装 repo 全部 skill

npx skills add henjicc/Henji-AI --all -g -y

预览 repo 内 skill

npx skills add henjicc/Henji-AI --list

SKILL.md

Frontmatter
{
    "name": "henji-ui-surface",
    "description": "Henji-AI 新建或改造任何界面\/页面骨架\/面板\/弹窗\/侧栏\/设置分区\/节点 UI,或调整按钮层级、分隔线、颜色、图标、毛玻璃、动画、层级时使用。主文件涵盖页面骨架的横向条带上限、表面层级(surface\/elevation)铁律、五级容器词汇表、动作按钮三档、分隔线准入、选项集合静息态与选中态词汇表;颜色\/材质、动效、图标、排版令牌、性能分层、静默失效坑拆在 references\/ 按需读。触发场景:用户要求\"做一个 XX 面板\/页面\/弹窗\"、\"这个界面不好看\/太挤\/像卡片套卡片\"、\"顶部堆了好几行\/几个条\/布局不合理\"、\"标题栏和工具栏能不能合并\"、\"这块儿怎么像张卡片\"、\"帮我美化一下这个界面\"、\"加一个设置分区\"、\"统一一下 UI\/配色\/动画\/模糊\"、\"这个动画太快\/太慢\/很生硬\"、\"这个界面卡顿\/拖动掉帧\"、\"切换主题后有些地方没变色\"、\"为什么有的按钮有边框有的没有\"、\"这里要不要加分隔线\"、\"图标不一致\"。"
}

Henji-AI 界面表面与层级规范

为什么需要这份规范

项目的 Ui* primitives 已经组件化,但组件化 ≠ 界面好看。实测本仓库最常见的三类问题:

  1. 卡片套卡片AssistantSidebarbg-panel + border)里放 AssistantConversation 的消息块(又是 bg-panel + border);Settings/index.tsxbg-panel 弹窗)→ SectionCard(又一层 bg-panel)→ 内部行(第三层 bg-surface-dark)。三层边框叠在一起,视觉上就是"一张卡里弹一张卡又套一张"。
  2. 条带叠条带:工具箱 → 图片编辑,从窗口顶到画布之间横着切了 4 刀(标题带 / "打开图片"带 / 工具带 / 样式带),分别来自 3 个文件,其中两条同底色的带中间还夹了一条透明带。
  3. 该扁平的地方带了壳UiPanel/UiIconButton/UiOptionButton默认值自带 border + bg,每个组件都假设自己是最外层独立卡片。在容器内部使用时,就会多出一层不该有的边框背景。

前两类是同一个根因在两个方向上的表现:每一层壳都以为自己是最外层,于是纵向各画一圈边框、横向各加一条头带。第 3 类则是表面 token 把 borderbg 打包绑死(见 styleTokens.tsUI_PANEL_SURFACE_CLASS / UI_FIELD_SURFACE_CLASS),且没有"只分组、不画框"的官方写法。本 skill 提供这几条缺失的规则。

参考文档(按需读,不要一次全读)

本文件只放每次改 UI 都要用的骨架规则。细分主题拆在 references/

什么时候读 文件
调颜色、写 .css、加毛玻璃、改对比度 references/color-and-material.md
写任何过渡/动画,或用 setTimeout 卸载动画组件 references/motion.md
用到任何图标 references/icons.md
定字号/圆角/阴影/层级/间距 references/typography-and-tokens.md
界面卡顿、拖动掉帧、长列表 references/performance.md
"我改了但没生效" references/pitfalls.md

两条铁律(先记住这两句)

纵深:同一层视觉深度,只画一次边框/背景。 进入一个已经有边框或背景的容器后,内部分组必须改用留白 / 分隔线 / 更暗的底色,不得再叠一层 border + bg + rounded

水平:一个视图只画一条命令带。 返回、标题、文件上下文、工具、导出动作全部进这一条;随工具变化的参数用紧贴其下的从属带,且与命令带共用同一块底色和同一条下边框

参考 Atlassian 的表述:能用边框或留白区分时,就不要用抬升(卡片)来分组。成熟设计系统普遍只保留 4~6 个层级并刻意克制。水平方向同理——桌面编辑器(VS Code、Figma、Photoshop)顶部一律是一条命令带加一条可选的上下文带,不会因为壳换了一层就多长一条。

五级容器词汇表(先背这张表)

写任何界面前,先确定"我在第几级",然后只用那一级允许的东西。 从 Region 往下选,能停在哪级就停在哪级——不要一上来就用 Card。

概念 组件 边框 背景 阴影 用途
1 Region 页面区域 <UiRegion> 页面主区,只管外边距与最大宽度
2 Group 分组 <UiGroup title=…> 普通内容分组的默认选择:标题 + 间距
3 Divided 分隔 <UiGroup divided> 仅一条线 需要明确切分时
4 Surface 内嵌面 <UiPanel variant="inset"> 更暗底 代码块、只读预览、列表项(父级已是卡片时)
5 Card 卡片 <UiPanel> 浮层/弹窗/侧栏/画布节点
<UiPanel>                  {/* 5 卡片:border + bg-panel + shadow-panel + rounded-xl */}
<UiPanel variant="inset">  {/* 4 内嵌:仅 bg-app/40 + rounded-lg,无边框无阴影 */}
<UiPanel variant="bare">   {/* 4 纯容器:只有圆角 */}
<UiGroup title="基础设置">  {/* 2 分组:零装饰,标题 + 间距 */}
<UiGroup divided>          {/* 3 分隔:上方一条线 */}

方向铁律:内层背景只能比外层更暗,不能更亮。 比父级亮 = 视觉上"浮起来" = 卡片。 本项目 bg-app(10) < bg-panel(23) < bg-surface-dark(38) < bg-layer(64)。 在 bg-panel 的弹窗里用 bg-surface-dark 做分区,就是在造卡片。

卡片准入条件(四条全中才允许)

  1. 有独立交互或独立状态
  2. 可被单独移动 / 关闭 / 拖拽
  3. 与兄弟元素是并列实体(列表项、画布节点)
  4. 脱离页面上下文仍能被理解

卡片嵌套上限:1 层。 卡片内部一律用 Group / Surface。

决策树:这个容器要不要边框背景?

我正在写的这个 div,它的父级链上已经有 border 或 表面 bg 了吗?
├─ 没有(我就是最外层浮层/弹窗/侧栏/画布节点)
│    → <UiPanel>。不要手写 border + bg-panel + rounded。
│
└─ 有(我在某个 panel 内部)
     ├─ 只是想把几个字段归成一组   → <UiGroup title="…">   ← 默认走这条
     ├─ 需要明确切分               → <UiGroup divided>
     ├─ 要让这块"沉下去"(代码块/只读预览/列表项)→ <UiPanel variant="inset">
     └─ 想不出理由,只是"看着空"    → 什么都不加。留白就是设计。

"想不出理由就不加" 是本规范最重要的执行细节 —— 绝大多数丑陋的套娃,都来自"这里看着空,加个卡片吧"。

页面骨架:横向条带(做整页/工具页之前先看这节)

上面几节全部在管纵深。这一节管水平:从窗口顶到内容区之间,横着切了几刀。

组件级审查(每个按钮用没用对 primitive)发现不了这类问题——每一条带单独看都合规,丑的是它们摞在一起。所以看整页时的第一个动作是数条带,不是看按钮。

三类条带与数量上限

名称 每视图允许 装什么 视觉
A 命令带 恰好 1 条 返回、标题、文件上下文、主工具组、导出/保存动作 h-10~h-11 + border-b border-border-dark + bg-surface-dark + px-2
B 从属参数带 0~1 条,必须紧贴 A 下方 只随当前工具变化的参数(颜色、线宽、字号) 不自带底色、不自带边框,与 A 在同一个容器里,共用 A 那条 border-b
C 状态带 0~1 条,页面底部 只读状态、进度、计数 无边框,text-text-muted

连续操作条带上限 = 2(A + B)。 出现第三条就是骨架错了,不是间距问题。

决策树:这东西要不要新开一条带?

我要往页面顶部加一个东西,它是什么?
├─ 返回 / 标题 / 文件名 / 主动作 / 工具        → 进现有命令带(没有就建**一条**)
├─ 只随当前工具变化的参数                      → 从属带,紧贴命令带,不另画底色与边框
├─ 只读状态、进度、计数                        → 命令带右端 `ml-auto`,或页面底部状态带
└─ "它跟上面那些不是一类,单独放一行吧"        → 停。先问它是不是上面三类之一,
                                                 99% 的情况是,只是懒得往已有带里塞。

外层壳已有命令带时:注入,不要再嵌一层壳

功能组件被塞进一个已经有命令带的外壳里时,不能自己再长一条头带。正确做法是把内容作为 props 注入外层那一条带——项目里已经有这个出口,ImageEditortoolbarActions 就是(复制 / 加入资产库 / 另存为三个按钮就是这么进到工具带右端的)。

// ❌ 外壳已经有一条命令带了,功能组件又开一条自己的行
<div className="p-4">
  <div className="flex items-center gap-2">   {/* 第二条带:只为了放一个按钮 */}
    <UiButton variant="ghost">打开图片</UiButton>
    <span>{fileName}</span>
  </div>
  <Editor />
</div>

// ✅ 注入到已有的那条命令带里
<Editor
  toolbarLeading={<><UiButton variant="ghost">打开图片</UiButton><span>{fileName}</span></>}
  toolbarActions={<UiButton variant="primary">另存为…</UiButton>}
/>

返回入口的三种形态(不要为返回单开一条带)

返回按钮的位置由这个页面长什么样决定,不由"哪个文件画的"决定:

页面类型 返回落点 例子
有页面标题的二级页面 <UiPageHeader onBack backLabel>,渲染在标题左侧 3D 镜头参考工程列表、图片编辑空态、资产库工作区
自带命令带的全屏工作面 那条命令带的左端 3D 场景编辑器、图片编辑器
没有命令带的全屏工作面 浮在内容上的玻璃返回按钮 画布项目内

禁止为"返回 + 页面名"单画一条 h-10 横带。 它会和应用标题栏叠成"双标题栏", 而且页面名通常和下面的页面标题重复一遍。

实测踩过:ToolboxWorkspace 曾统一画一条「← 工具名」带,于是 3D 镜头参考列表页 纵向出现两遍"3D 镜头参考";为了让编辑器形态只剩一条带,又加了 ownsCommandBarview !== 'editor' 两个开关逐个工具关掉。判据换成"页面有没有标题/有没有命令带" 之后,那两个开关连同整条带一起删掉了。

同一批返回入口当时长成四种样子:外层条带图标、工具自绘条带图标、命令带左端图标、 画布上的玻璃文字按钮,以及资产库放在标题右侧动作区的文字按钮。用户在应用里 换一个页面就要重新找返回在哪——这是"每处各自决定"的必然结果,不是审美问题。

实测:同一个工具箱里,好例子和坏例子并存

视图 条带数 情况
工具箱 → 3D 镜头参考 1 条 CameraStageEditorh-11 带里塞下了返回 + 撤销 + 快捷添加 + 视口工具 + 中间路径上下文 + 右端状态/徽标/设置
工具箱 → 图片编辑 4 条 ❌(已修)ToolboxWorkspace 标题带 → ImageMarkTool 的"打开图片"裸行 → ImageEditorShell 工具带 → 样式带;外层标题带已按上一节删除,返回改进页面标题

更值得注意的是:ToolboxWorkspace 里曾有个 showToolHeader 开关,专门为 3D 镜头参考关掉外层标题带,好让它只剩一条。也就是说这个问题早就被撞见过,但当时是给单个工具开特例躲过去的,没有沉淀成规则——于是下一个工具(图片编辑)原样又撞了一次。现在外层标题带整条删除,那个开关也不复存在。

结论:特例是规则缺失的信号。 再看到"为某个页面单独关掉某段骨架"的开关时,先问它是不是该反过来变成默认。

横向 padding 必须对齐

图片编辑那 4 条带的横向 padding 分别是 px-2 / p-4 / px-3,于是返回箭头、"打开图片"、工具组的左端落在三个不同位置。同一视图内所有条带与其下的内容区用同一个横向 padding,改一处就该一起改。

全屏工作面不套卡片

画布、编辑区、预览区这类铺满剩余空间的工作面不是卡片——对照「卡片准入条件」四条:不能单独移动、不是并列实体、脱离上下文无意义,一条都不中。

// ❌ MarkCanvas 现状:工作面自带卡片外观,外层再给 p-4 空白,于是整个编辑器浮成一张卡
<div className="rounded-xl border border-veil-subtle bg-bg-dark/85">

// ✅ 铺满,边界由它和命令带之间的那条 border-b 表达
<div className="bg-bg-dark/85">

判据:这块区域会不会随窗口一起长大? 会,就不是卡片。要给它一个更暗的底以便和 chrome 区分是可以的,但不要 rounded + border + 外层留白三件套——那三样凑齐就是卡片。

动作层级:视觉重量 = 动作的重要性

上面几节管容器和骨架,这一节管按钮本身该有多重

主流设计系统都是同一个阶梯(Material 的 filled/outlined/text、Apple 的 prominent/bordered/plain、Fluent 的 primary/default/subtle),本项目对应三档:

UiButton variant 图标版 用途
primary(实底) —— 一个表面只允许一个,这一屏的主动作
ghost / muted(描边) UiIconButton(默认带边框) 常用但非唯一的动作
plain(无边框,hover 出底) UiIconButton showBorder={false} appearance="hover-only" 工具栏、行内辅助动作

两条硬规则

  1. 一个表面只有一个主动作。 出现第二个实底按钮,用户就不知道该点哪个。
  2. 同一组、同一层级的动作必须同档。 不能因为"这里太挤了"把其中一个降档——那是拿视觉语言解决布局问题, 用户读到的信息会变成"这个按钮没那么重要",而事实不是。

实测踩过:图片编辑命令带右侧的「打开」和「复制 / 加入资产库」同属文件类次级动作, 为了给工具组腾 44px 宽度被降成了无边框图标,一眼就看出不对。宽度问题要用缩短文案、 图标化整组、或接受轻微偏移来解决,不能只降其中一个。

⚠️ ghostmuted 目前视觉等价(都是描边 + 底色),是历史命名, 不要按字面理解成"无边框"——真正的无边框档是 plain

动作 ≠ 模式

工具栏里的工具(选择/标注/矩形…)不是按钮,是"我现在处于哪个模式", 点它改变的是"接下来会发生什么",不是"立刻发生一件事"。 它属于选中态语言,不属于动作层级:静息不描边,选中用中性层底 + 强调文字 (UiChipButton selectionRole="navigation"),把实底强调色让给那个唯一的主动作。

同理,参数面板里的"当前值是什么"(形状、比例、档位)是单选, 用 UiOptionButton active(强实底 + 白字)——详见「选中态词汇表」。

分隔线:分组的第二手段,不是第一手段

分组有三级,按顺序往下选,能停在上一级就别用下一级

  1. 间距(格式塔邻近律)—— 首选,零视觉成本
  2. 分隔线 —— 间距不够用或空间紧张时
  3. 容器 / 边框 —— 最后手段(回到「五级容器词汇表」)

准入判据(只有一条)

两侧的交互语义根本不同,用户不会把它们当成一串连续操作。

场景 判定
工具组 ┃ 撤销/重做/清空 模式 vs 动作——点工具是改变后续行为,点撤销是立刻发生一件事
打开 ┃ 复制/加入资产库/另存为 ❌ 都是动作,只是输入 vs 输出。差异远小于上一行,间距就够
两组同类按钮,只是"感觉该分开" ❌ 加大间距

数量上限

一条 bar 上最多一条分隔线。 第二条会把它切成三段,而右端的主动作实底本身 已经是"终点"标志,再加竖线的信息增量接近零。

选项集合的静息态:不描边

上面的决策树管容器,这一节管容器里那一堆并列的可点项(菜单项、模型网格、分辨率格子、列表行)。

边框表达的是"边界",不是"可点击"。 一屏里几十个选项各自描边时,边框互相抵消、不再传递任何信息,只剩视觉重量。 可点击性由 hover 反馈 + 排布规律表达,不需要静息态的框。

UiOptionButtonvariant="menu" 就是这条规则的落点:静息态无边框无底色,hover 出 bg-layer,选中态才是实底。

判据(两条都要满足才用 menu

  1. 是同质选项的集合:≥3 个由 map 渲染的并列 peer,或语义上明确的二选一分段。孤立的单个按钮不算 —— 那种情况下框才真的在划定边界。
  2. 去掉框之后形状还在。满足任一即可:
    • 已被可见容器圈住(浮层面板、弹窗左栏、下拉列表)——容器已经画过一次边界了
    • 每项自带足以撑出形状的内容(缩略图、图标块、多行文本、比例示意图)
    • 是二维网格 —— 此时补一层 bg-veil-faint 撑格子,但仍然不描边(底色已经表达过一次边界,边框是多余的第二次)

反例:这些要保留边框

场景 为什么
纯文字 chip 组(筛选 chips、数值 marks、CompositeRadio 直接落在面板底色上,去框后变成裸文字,点击可供性丢失
内容入口卡(工具箱、工程列表) 是内容卡不是选项,走 Card
表单单选 RadioInput 框就是命中区域
动作按钮("上传音频"、"选择文件") 是按钮不是选项,走 variant="flat"

选中态词汇表:先判断语义,再选强度

“选中”不是一种视觉,而是四种不同语义。业务层优先传递 active / checkedselectionRole,由 Ui* primitive 消费 styleTokens.ts 中的状态令牌,不要在调用点 复制蓝底、蓝框或强调文字。

语义 表达 通用落点
导航:正在看哪里 中性层底 + 强调文字 + 方向指示条 UiNavButton active;横向 chip 用 selectionRole="navigation"
单选:当前值是什么 强品牌实底 + 白字 UiOptionButton active
多选/标签:集合中哪些已选 强调描边 + 中性层底 + 强调文字 UiChipButton active
布尔:功能是否开启 强调色只进入开关轨道或复选框本体,整行保持静息 UiSwitch checked / UiCheckbox checked

默认态不是第五种选中态:它保持当前表面的中性视觉。不要用整行实底表达“已启用”, 也不要把筛选 chip 的多选语义画成单选项的强实底。

状态展示统一走这三个

页面不要自己写空/加载/错误块(历史上因此出现同一状态在不同页面长得不一样):

<UiEmpty title="还没有供应商" description="先添加一个吧。" />
<UiLoading message="生成中…"><ProgressBar progress={p} /></UiLoading>
<UiError message={err} onRetry={retry} />

状态块不画卡片——它已经在某个容器里了。

布局与表单

<UiRegion maxWidthClassName="max-w-3xl">
  <UiPageHeader title="生成历史" description="共 128 条" actions={<UiButton>清空</UiButton>} />
  <UiGroup title="基础设置">
    <UiFormRow label="语言" hint="影响界面与模型提示词">
      <Dropdown … />
    </UiFormRow>
    <UiFormRow label="启用快速下载" inline>
      <UiSwitch … />
    </UiFormRow>
  </UiGroup>
</UiRegion>

分区之间的间距用 UI_SECTION_STACK_CLASS,不要每处自己定 space-y-*

硬性禁止清单

禁止 正确做法
业务组件手写 rounded-xl border border-border-dark bg-panel <UiPanel>
UiPanel 内部再放一个 border + bg 的 div variant="inset" / "bare" / 纯留白
UiIconButton 默认态(自带边框)做工具栏密集图标 showBorder={false}appearance="hover-only"
容器内的同质选项集合逐项描边 UiOptionButton variant="menu",见"选项集合的静息态"
UiOptionButton 调用点手写 !border-transparent !bg-transparent hover:!bg-layer variant="menu",别再复制这串
面板/弹窗内部再叠一层自己的底色(bg-zinc-900/40 这类) 表面由外壳统一提供;要切分用分隔线,要下沉用 inset
panelClassName 覆盖 PanelTrigger / Dropdown 的外壳表面 不传即可;同级浮层长得不一样多半就是这么来的
zinc-* / gray-* 等固定调色板 语义色,见「颜色必须跟随主题」
自己拼 backdrop-blur-* + bg-black/xx + border-white/xx ui-glass;且先确认这个浮层真的压在媒体/画布上
text-zinc-600 dark:text-zinc-400 双分支 直接写最终值,dark: 的基础值是死代码
给已经带边框的控件外面再包一层框 去掉外层框
为"填充空白"添加卡片、边框、阴影 留白 / 调整间距
同一屏出现 3 层以上叠加边框 重新走上面的决策树
复制其他文件的 border + bg class 串当模板 先判断目标位置在第几层
外层壳已有命令带,内层功能组件再长一条自己的头带 把内容作为 props 注入外层那条带(toolbarActions 那种出口)
为一个"打开文件 / 新建"按钮单独占一整行 塞进命令带左端,文件名跟在按钮后面
两条同底色条带中间夹一条透明带 合并成一条;确实要分就让中间那条也归属同一块底色
全屏工作面(画布/编辑区/预览区)套 rounded + border + 外层留白 铺满,边界交给上方那条 border-b
同一视图里各条带用不同的横向 padding 统一到同一个值,与其下内容区对齐
业务组件手写 inline <svg> 画图标 用 lucide-react;确属图形则加入 check-icon-tokens.cjs 豁免并写明理由
在调用点自己从 lucide 挑业务概念图标 @/core/theme/icons 的登记常量
建一个「本目录自己的图标模块」 删掉,调用点直接用 lucide;私有图标集=又一套平行体系
一个表面出现两个 variant="primary" 只留一个主动作,其余降到 ghost
为了省宽度把同组动作里的一个降档 缩短文案 / 图标化整组 / 接受轻微偏移,不要只动一个
用分隔线分开两组同类动作 加大间距;分隔线只用于交互语义根本不同的两侧,一条 bar 最多一条
把工具/模式切换写成带边框的按钮 那是选中态语言:selectionRole="navigation",静息不描边

必须复用 vs 允许新增

先查表,再动手。已有实现的一律复用,不要另写一份:

需求 必须用 不要做
弹窗 UiModal 手写 fixed inset-0 + bg-black/… + 卡片(存量已全部清零,check:surface 规则 C 会拦,别再加)
分组 UiGroup 手写 border + bg 的 div
页面标题区 UiPageHeader 手写 h2 + p
表单行 UiFormRow 手写 label + 间距
空/加载/错误 UiEmpty / UiLoading / UiError 内联手写状态块
按钮/输入/开关等 @/components/uiUi* 原生 <button>/<input>
提示词编辑 PromptEditor 自己拼 textarea
文件上传/排序 FileUploader / useReorderDrag 重写拖拽
音频播放 @/components/AudioPlayer 再写一个播放器
长列表 react-virtuoso(已是依赖) 全量 map 渲染上百项

新增组件的门槛(三条全满足才允许):

  1. @/components/ui 与本表中确认没有可复用或可扩展的组件
  2. 优先改成扩展现有组件的枚举变体(像 UiPanel variant / UiGroup titleTone),而不是新开一个组件
  3. 动手前先向用户说明原因和替代方案,等确认

变体也要克制:新增变体必须是有限枚举,不要开放任意 className 覆盖视觉。

Agent 改 UI 的标准流程

1. 读需求 → 判断落在哪个工作区/页面
2. 整页体检(先横后纵,别直接进组件):
   a. 数条带:从窗口顶到内容区横着切了几刀?> 2 条就是骨架问题,先合并再谈别的
   b. 顺着 JSX 往上找外层壳:它是不是已经有一条命令带了?有就注入,不要再嵌
   c. 数边框层数、看有没有全屏工作面被套成卡片
   d. 数实底按钮(应恰好 1 个)、看同组动作是否同档、数分隔线(一条 bar 最多 1 条)
3. 定级:这块内容属于 Region / Group / Divided / Surface / Card 的哪一级
4. 查复用表:需要的组件是否已存在 → 存在就复用
5. 用排版令牌建立层级(标题/正文/元信息),先不加任何容器装饰
6. 只在四条准入条件全中时才用 Card
7. 写代码:颜色用语义类,字号/圆角/阴影/层级用登记令牌
8. 跑自检清单;高影响 UI 再运行真实 Electron 视觉场景并由 Agent 逐张目视截图
9. 完成前按项目规则检查开发环境:未运行就启动,需要重启就只重启当前仓库进程树

第 2 步不能跳。 审查界面时只做组件级检查(每个按钮用没用对 primitive)会漏掉所有骨架问题—— 条带叠条带、工作面套卡片、左边距三个位置,这些单看每一条都合规,丑的是它们摞在一起。 用户说"布局不合理""特别丑"而你只查出了几个 primitive 用错,那基本可以确定第 2 步跳了。

页面完成自检清单

  • 从窗口顶到内容区横着切了几刀? > 2 条(命令带 + 从属带)就是骨架错了
  • 外层壳是不是已经有命令带了?有的话内层不能再长一条,改成注入
  • 有没有为一个按钮单独占的一整行?塞进命令带
  • 返回入口放对形态了吗?有页面标题就进 UiPageHeader onBack,别为「← 页面名」单开一条带
  • 各条带与其下内容区的横向 padding 是同一个值吗?
  • 画布/编辑区/预览区被套成卡片了吗?会随窗口长大的区域不是卡片
  • 这个页面有几层边框叠加?> 2 层就回到决策树
  • 有没有"比父级更亮"的背景块?有就该是 inset 或 bare
  • 有没有为了填空白而加的卡片/边框/阴影?删掉
  • 容器里并列的可点项,静息态还在逐个描边吗?该是 UiOptionButton variant="menu"
  • 这个表面有几个实底按钮?超过一个就不知道该点哪个
  • 同一组里的同级动作是不是同档?有没有为了省宽度把其中一个降档
  • 加分隔线了吗?两侧交互语义真的不同吗?一条 bar 最多一条
  • 有手写 <svg> 吗?路径写死的就是图标,改用 lucide;路径算出来的才是图形
  • 用到跨界面的业务概念图标了吗?走 @/core/theme/icons 的登记常量,别在调用点自己挑
  • 有没有 zinc-* / gray-* / slate-*?改强调色或换主题预设时它们不会跟着动
  • accent 当文字色了吗?改用 text-brand-300;白字要压实心蓝的话底色用 bg-brand-500
  • 破坏性动作(删除/清空)是不是 variant="primary"?那会抢走主动作的视觉权重,应该静息中性、hover 才出危险色
  • 改了 .css 文件吗?里面不能有 #hexrgba(数字…),只能 rgb(var(--xxx-rgb) / a)
  • 新加的全局样式/变量放对文件了吗?懒加载的样式表里不能放全局主题变量
  • 同一个 className 里有没有两个类抢同一个 CSS 属性?改成互斥三元
  • 新面板有没有再叠一层自己的底色?表面应该由外壳统一提供
  • 加了模糊吗?只有压在图片/视频/画布上才该加,且只能用 ui-glass / ui-glass-scrim
  • 动效时长是否落在 150/200/300/500 四档?(缓动已是全局默认,不用每处写)
  • setTimeout 卸载动画组件吗?那个数字必须和 className 里的 duration-* 同档
  • 过渡的是 opacity/transform 吗?别过渡宽高间距,也别用裸 transition
  • 空/加载/错误三态是否都走了 UiEmpty/UiLoading/UiError
  • 字号是否全部来自登记档位(无 text-[Npx]
  • 圆角是否只用了 rounded-lg/xl/full,且内层不大于外层
  • 阴影是否只出现在浮层
  • z-index 是否用了语义 token
  • 同级元素间距是否统一(不要一行 mt-2 一行 mt-3
  • 高频状态(进度/hover/拖拽)是否放在独立 store 而非大列表 state
  • 列表超过 ~50 项是否考虑了虚拟化
  • 只有改动共享页面骨架、设计令牌或全局界面机制时才按 docs/rules/testing.md 运行相关 ui:tour -- --only ...;局部界面改动不默认跑全量场景

完成前按风险选择

npm run check:surface
npm run check:colors
npm run check:icons

只运行与本次改动直接相关的专项检查;验证级别和是否追加 lint、类型检查、构建由 docs/rules/testing.md 决定。

改了动效档位或 motion.ts 再补一条(它保证 ms 数值与 duration-* 类不漂移):

npx vitest run src/components/ui/motion.test.ts

只有共享页面骨架、设计令牌、浮层/滚动/溢出机制等高影响界面改动,才先构建并按场景缩小范围运行截图巡检与规则审计:

npm run ui:tour -- --only <受影响场景> --size <受影响尺寸>
npm run check:ui-visual

用户要求“真实运行环境”的视觉审查时,使用 npm run test:reality -- --suite ui|ui-audit --profile real --only ... 的正式 Electron 场景;默认只读,不传 --allow-writes。禁止用浏览器、ego-browser、Chrome、裸 Vite 或 temporary profile 代替。场景断言通过后,Agent 仍必须打开实际截图,检查对齐、裁切、层级、颜色、文案和面板开合状态;DOM 通过不等于视觉通过。巡检结束后退出巡检实例,并恢复或重启当前仓库的 electron:dev

局部样式、文案或叶子组件改动只运行直接相关的静态检查和精确测试,不追加完整 ui:tourui:tour 产出 .ui-tour/index.md 与截图,专门让人检查对齐、留白、视觉权重、hover / 聚焦 / 下拉 / 右键状态;check:ui-visual 只输出规则结论与 .ui-audit/audit.json, 不做像素差异。两者共用场景配置但职责分离,截图差异不作为 CI 门禁。

已知门禁缺口:条带数量还没进自动检查

「页面骨架:横向条带」那节的规则目前全靠人看——check:surface 只看单元素的 border + bg + rounded 组合,check:ui-visual 的十一条规则里也没有数条带的那条。 所以 4 条带的图片编辑页在两个门禁下都是全绿的。

这条其实可判定:在真实 DOM 里沿主轴找连续的 border-bottom 兄弟条带, 数量 > 2 即报错;同理可判「铺满剩余空间且带 border-radius 的容器」。 补这条规则前,看整页时必须人工数一遍,别因为门禁全绿就认为骨架没问题。

check:surface 报三类问题:

  • [A] 手写面板表面 → 改用 <UiPanel>
  • [B] 同文件多处卡片表面 → 疑似卡片套卡片,内层降级
  • [C] 手写弹窗(fixed inset-0 + 黑色遮罩但没用 UiModal/AlertDialog)→ 改用 UiModal

存量已全部清零,check:surface:strict 已接入 build / electron:build 与 CI,违规会直接让构建失败。改完必须跑一次确认通过。

确需例外时在该行上方加注释 ui-surface-allow 并写明理由;只允许行级豁免,禁止文件级 ui-surface-allow-file(否则该文件将来真正的套娃也会被放行)。

已确认的例外类别:全屏沉浸式媒体查看器(mediaViewer/ 三个 Modal)不套用 UiModal——UiModal 是居中卡片语义,与铺满视口的查看器不匹配。

npm run check:surface(告警式)可用于本地快速查看,构建链路走的是 --strict

相关规范

  • 组件复用与原生标签落点、颜色令牌三处入口:见 docs/rules/frontend-ui.mddocs/rules/architecture.md
  • 画布节点的行组件拼装:见 skill canvas-node-builder
  • 提示词编辑器、文件上传控件:必须复用 PromptEditor / FileUploader,不要重写

Version History

  • 19fcc12 Current 2026-08-28 23:13

Same Skill Collection

.claude/skills/canvas-node-builder/SKILL.md
.claude/skills/henji-ai-adaptation-assistant/SKILL.md
.claude/skills/henji-application-capability/SKILL.md
.claude/skills/henji-model-adaptation/SKILL.md
.claude/skills/henji-ui-surface/SKILL.md
.codex/skills/canvas-node-builder/SKILL.md
.codex/skills/henji-ai-adaptation-assistant/SKILL.md
.codex/skills/henji-application-capability/SKILL.md
.codex/skills/henji-model-adaptation/SKILL.md
resources/assistant-skills/三维镜头构图/SKILL.md
resources/assistant-skills/图片生成/SKILL.md
resources/assistant-skills/生成排障/SKILL.md

Metadata

Files
0
Version
19fcc12
Hash
e0ca56c7
Indexed
2026-08-28 23:13

trang chủ - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-09-03 23:26
浙ICP备14020137号-1 $bản đồ khách truy cập$