界面设计系统

design

源自 You Don't Have a Design System 及与 AI 展开的方法论

大多数人以为自己有「设计系统」,其实他们只有一个组件库和 design token。就像没有图纸的积木,每次 AI 搭出的结果都可能完全不同。

问题的核心是将已有的决策固定,Atomic Design 十年前就提出了一套自下而上的设计结构来实现:

  1. atoms: 组件库
  2. molecules: 原子组件加行为。ConfirmableSwitch = switch + 有重要代价需要确认
  3. organisms: 布局关系,molecules 的摆放方式
  4. templates: 页面骨架,organisms 的摆放方式
  5. pages: 完整的页面

这套结构过去让团队不需要从零判断界面长什么样,放到现在,可以更新为两点:

  1. 固定好决策的高层组件。规定每个部分该是什么样子
  2. 配套规则和 lint。限制 AI 绕过组件自己拼

高层组件的设计原则只有一个:尽可能少的 props。每少一个 prop,就少一个 AI 要做的决策,结果也更可控。

命名上必须遵守:命名即文档。ValidatedInput 代表会自

以 Next.js 为例,文件夹结构如下:

text
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 的内容如下:

tsx
/**
 * 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 代码如下:

tsx
// 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 为什么会绕过现成组件,主要原因有三:

  1. AI 不知道有组件
  2. AI 不知道什么时候用
  3. AI 觉得自己重新拼更省事

前两个是信息问题,第三个是约束问题。

信息问题要靠规则和导航,Build it, then say it。单凭代码无法指导 AI 如何使用组件,需要在 AI 必经之路定好规则。

项目根 AGENTS.md 要精简,只负责导航:

md
// AGENTS.md
 
## UI/UX
 
生成 UI 前阅读 `docs/UI.md`

docs/UI.md 放通用规则和各页面导航。

md
// docs/UI.md
生成设置页前,阅读 `components/patterns/settings/AGENTS.md`
生成工具页前,阅读 `components/patterns/tools/AGENTS.md`

细分规则只出现在最底层 AGENTS.md:这个页面用哪些组件、什么关系、禁止什么。上层只留导航。

约束问题靠 lint。AGENTS.md 只能引导,lint 负责拦截。AI 无视规则,直接 import 组件库的 button,报错会给出正确做法:

js
"no-restricted-imports": ["error", {
  paths: [{
    name: "@/components/ui/button",
    message: "Settings pages disallow custom button. See components/patterns/settings/AGENTS.md"
  }]
}]

界面设计本来就是模式识别:绝大多数界面问题早有现成答案,要做的是在既有模式里选对形式、用对地方,极少情况才需要创造新模式1。十年前 Atomic Design 的思路就是可以直接拿来的答案。

Footnotes

  1. @floguo