界面设计系统
源自 You Don't Have a Design System 及与 AI 展开的方法论
大多数人以为自己有「设计系统」,其实他们只有一个组件库和 design token。就像没有图纸的积木,每次 AI 搭出的结果都可能完全不同。
问题的核心是将已有的决策固定,Atomic Design 十年前就提出了一套自下而上的设计结构来实现:
- atoms: 组件库
- molecules: 原子组件加行为。
ConfirmableSwitch= switch + 有重要代价需要确认 - organisms: 布局关系,molecules 的摆放方式
- templates: 页面骨架,organisms 的摆放方式
- pages: 完整的页面
这套结构过去让团队不需要从零判断界面长什么样,放到现在,可以更新为两点:
- 固定好决策的高层组件。规定每个部分该是什么样子
- 配套规则和 lint。限制 AI 绕过组件自己拼
高层组件的设计原则只有一个:尽可能少的 props。每少一个 prop,就少一个 AI 要做的决策,结果也更可控。
命名上必须遵守:命名即文档。ValidatedInput 代表会自
以 Next.js 为例,文件夹结构如下:
components/
ui/ # atoms:shadcn/ui,原子组件库
patterns/ # 高层组件 + 页面特有规则
confirmable-delete.tsx # 全局 molecules:全产品都适用的行为决策
confirmable-switch.tsx
validated-input.tsx
settings/ # 设置页这一类的决策
settings-frame.tsx # template:页面骨架
setting-section.tsx # organism:分组
setting-row.tsx # organism:行
xxx.tsx # 专有 molecules
AGENTS.md # 只写设置页特有的规则
docs/
UI.md # 通用 UI 规则 + 各页面规则导航
app/settings/page.tsx # 业务页面只写"有哪些设置",不写"怎么排"
AGENTS.md一个设置页的 template 的内容如下:
/**
* SettingsFrame — every settings page uses this.
* - Group with SettingSection. Do not use Tabs or one card per setting.
* - Identity first, "Danger zone" last.
* - Save on change. No Save/Cancel buttons.
* The ticket decides which settings exist. This file decides how they appear.
*/
export function SettingsFrame({
title,
description,
children,
}: {
title: string
description: string
children: React.ReactNode
}) {
/* 宽度、间距、标题层级全部写死 */
}最后得到的 page 代码如下:
// app/settings/page.tsx
export default function SettingsPage() {
return (
<SettingsFrame title="Settings" description="Manage your workspace.">
<SettingSection title="Workspace">
<SettingRow
label="自动备份"
description="每晚快照所有项目"
control={
<ConfirmableSwitch
checked={b}
onConfirmChange={save}
confirmTitle="关闭自动备份?"
/>
}
/>
<SettingRow
...
/>
</SettingSection>
<SettingSection title="Danger zone">
{' '}
{/* 永远最后 */}
<SettingRow
label="删除工作区"
description="不可恢复"
control={<ConfirmableDelete name="Acme Labs" onConfirm={del} />}
/>
</SettingSection>
</SettingsFrame>
)
}高层组件本身就是强有力的规则,比文档更能控制行为。但只写高层组件,AI 会绕过并自建,设计规则和 lint 就是来约束这种行为。
先弄清 AI 为什么会绕过现成组件,主要原因有三:
- AI 不知道有组件
- AI 不知道什么时候用
- AI 觉得自己重新拼更省事
前两个是信息问题,第三个是约束问题。
信息问题要靠规则和导航,Build it, then say it。单凭代码无法指导 AI 如何使用组件,需要在 AI 必经之路定好规则。
项目根 AGENTS.md 要精简,只负责导航:
// AGENTS.md
## UI/UX
生成 UI 前阅读 `docs/UI.md`docs/UI.md 放通用规则和各页面导航。
// docs/UI.md
生成设置页前,阅读 `components/patterns/settings/AGENTS.md`
生成工具页前,阅读 `components/patterns/tools/AGENTS.md`细分规则只出现在最底层 AGENTS.md:这个页面用哪些组件、什么关系、禁止什么。上层只留导航。
约束问题靠 lint。AGENTS.md 只能引导,lint 负责拦截。AI 无视规则,直接 import 组件库的 button,报错会给出正确做法:
"no-restricted-imports": ["error", {
paths: [{
name: "@/components/ui/button",
message: "Settings pages disallow custom button. See components/patterns/settings/AGENTS.md"
}]
}]界面设计本来就是模式识别:绝大多数界面问题早有现成答案,要做的是在既有模式里选对形式、用对地方,极少情况才需要创造新模式1。十年前 Atomic Design 的思路就是可以直接拿来的答案。