# Design Spatial

> 设计 — 空间构图

- Skill: `kscz0000/design-spatial` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add kscz0000/design-spatial`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kscz0000/design-spatial/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: kscz0000 (https://skillmd.com/u/kscz0000)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/kscz0000/design-spatial

---


# 设计 — 空间构图
## 适用场景

当你需要做空间构图设计时使用此技能。


模型无法信任自己的 UI 输出。其他一切，都源自两个失败。

## 1. 它看不见自己做出来的东西

UI 是按 token 流生成的，不是按像素生成的——所以模型感知不到碰撞、重叠、失衡、错误的间距。它会写出一个标题撞进 hero 图里，还浑然不知。

**渲染出来再看图，而不是看代码。** 用任意静态服务器（例如 `python3 -m http.server` 或 `npx serve`）起服务，再用 Playwright 无头截图。在几个不同宽度下分别截图。

**用新鲜的眼光去批评——而不是自己的。** 给自己写的作品打分会被合理化；作者看着重叠的标题会觉得"没问题"（这正是真实碰撞在测试中溜过去的方式）。用一个独立的评审者——一个**没有**写过这个页面的子代理——让它专门挑**错**：碰撞、边缘相切、参差不齐的对齐、左右轻重失衡、没有明确的视觉焦点、在某些宽度下断裂。修，再渲染，再评审。

## 2. 它的第一个想法就是平均

无论它首先生成什么，都是训练数据的均值——而且这种均值不止一种：

- **通用 AI 均值**：Inter 字体、紫白渐变、居中单栏、三张等宽卡片；
- **设计师潮流均值**：超大压缩体大写、深色模式 + 颗粒感、等宽体的"vibe"微文字、贴纸徽章。

落到第二种也不是品味——它只是更讨喜的均值，所以会蒙混过关。**把你的第一直觉当作均值，然后有意识地向*这个产品的特定世界*偏离**（用 design-thinking 的领域 / 色彩世界 / 标志性元素来定向），**而不是去追另一种潮流。** 如果结果套到任何一家初创公司上都成立，那你交付的就是均值。

## 3. 所以不要规定风格

任何固定规则——12 列网格、8 点刻度、"等宽 = 数据"——都会**变成**下一个周期的均值，而且盲目的模型照样会执行成碰撞。规定**流程，而不是外观**：用新鲜的眼光去看，向领域方向推开均值。品味提供方向（design-thinking / design-philosophy）；本技能只坚持让你**看**，并且**不要交付均值**。

做迭代式空间调优时，本地页面 + 实时控件（滑块、选择器、拖动手柄）比一次性评审更有效。

## 4. 永远不要交付横向溢出——强制门禁，无例外

> **阻塞门禁。你只有在**本轮**用一个较窄宽度跑过下面的 `scrollWidth` 检查并看到结果为 `0` 之后，才能把任何 Web UI 称为"完成"、"能用"、"修好了"或"看起来不错"。**不是**"我加了 overflow-x:clip 所以没问题"。不是"我的宽度看着挺好"。**测量。窄宽度。每次都测。** 如果你没测，就不能说完成——应该说"还没检查溢出"。**

那种和内容对不上的横向滚动条，是**最常见、也最丢人**的布局失败，而且它**一而再、再而三地**溜过去——因为开发视口宽得足以掩盖它，溢出只有在窗口比某个元素窄的时候才出现。它**在桌面宽度下不可见**，所以 §1 的"渲染-评审"循环**根本**抓不到，除非你用窄宽度截图。独立的、显式的、不可商量的门禁。

**它反复出现，是因为布局在增长，而检查还停留在上次。** 每次你加一个导航标签、工具栏按钮、顶部控件、芯片、更宽的公式 / `<pre>`，或任何新的元素到 `flex`/`inline` 行里，上一次的溢出检查就失效了——昨天还放得下的那一行，今天多出来就在 ~720–1200px 之间挤出边界，而你的 1440px 开发窗口看不到任何问题。（2026-06 真实交付，**两次**：进度条的一个边缘标签溢出 23px；然后一个 `flex-wrap:nowrap` 的顶部栏加了 4 个标签，让整页在 720–1200px 之间横向滚动 309px——两者在开发宽度都不可见，都只有在窄宽度测量时才被发现。）**所以：任何给横向行增加元素的改动都会重新激活本门禁。重新测量。**

**默认防御（前置应用，让门禁从构造上就能通过）：**
- **顶部 / 导航 / 工具栏行：`flex-wrap: wrap`，永远不要 `nowrap`。** 不断增长的单行 flex 是这个 bug 的第一大来源。换行在放得下时是空操作，在放不下时救你一命。
- **`body { overflow-x: clip }`** 作为每个 app 的兜底（用 `clip` 而不是 `hidden`——保留 sticky / 锚定布局正常工作）。这是兜底，**不是**测量的替代品。

**检查——在任何页面被宣布完成前必跑：** `document.documentElement.scrollWidth - document.documentElement.clientWidth` 必须等于 `0`，在你的开发宽度**和**缩窄后的宽度（≤1024px，以及手机宽度 ~390px）下都测。如果 > 0，找出肇事者：
```js
document.querySelectorAll('*').forEach(el=>{const r=el.getBoundingClientRect();
  if(r.right>innerWidth+1||r.left<-1) console.log(Math.round(r.right), el);});
```

**安全网：** `body` 上加 `overflow-x: clip`（优先用 `clip` 而不是 `hidden`——它裁剪但不会创建滚动容器，因此不会破坏 `position:sticky` / 锚定布局）。但网不是解法——**找到根因并干掉它：**

- **`position:absolute` + `white-space:nowrap` 锚定到边缘**（`left:100%`、`right:0`）：一个*居中*的 nowrap 标签贴在右边缘会凸出视口。（2026-06 真实交付：进度条上 "300 · learned model" 里程碑标签用 `left:100%` + `translateX(-50%)`，溢出 23px → 在 <1180px 宽度下出现幽灵横向滚动。）**把边缘标签向内锚定**——右端用 `right:0; transform:none`，左端用 `left:0; transform:none`。
- **`100vw`**——包含滚动条宽度（~15px），所以在任何垂直滚动的页面上，它保证会产生 ~15px 的横向溢出。用 `100%`。
- **flex / grid 子元素没有 `min-width:0`**——它们拒绝收缩到内容以下，会撑爆轨道（flex 卡片里的长标题、grid 单元里的 `<pre>`）。加上 `min-width:0`。
- **长的不可断字符串**（URL、哈希、token）：用 `overflow-wrap:anywhere` 或 `word-break:break-word`。
- 比视口还宽的固定像素宽度；过大的负 margin；尺寸过大的 `position:absolute` 元素。

通用结论：**任何锚定到边缘或用视口单位定尺寸的元素都是横向溢出嫌疑——在窄宽度下测试，测量 `scrollWidth`，把 body 兜底裁剪，并把锚定到边缘的内容向内收。**

## 5. 按任务顺序排版——最小化跳转成本

放置元素之前，**走一遍用户完成该页操作的真实步骤序列**，然后把元素按同样的感知 / 视觉顺序排列。布局应该像任务本身一样可读：定向 → 操作 → 确认。任何无意义的鼠标移动或滚动都是缺陷。

- **顶部定向：** 把控件 / 选项放在顶部是好的——它们在用户投入阅读之前，先告诉用户这个页面是做什么的、它能做什么。
- **在工作结束的地方确认：** 如果任务是"审阅一个长列表，然后操作"（通过、标记、提交、保存），操作按钮**也必须**出现在底部——用户眼睛和光标停留的地方。原始的失败案例：一个删除审阅页面的确认按钮只在顶部工具栏——用户翻过 120 张图之后，又得滚回顶部去点"标记剩余"。在底部复制一份操作栏（或者让顶部栏 sticky）；两者都只多一行代码，往返滚动却是每个页面都要付出的。
- **启发式：节省用户移动时间。** 每次交互都有一条路径：步骤结束时眼睛 / 光标在哪里，下一步的控件在哪里。把这些距离加起来，缩短大的那个。这是整个页面的 Fitts 定律，不只是一个按钮的。
- **在"渲染-评审"循环里验证（§1）：** 让评审"追踪任务：用户在每一步结束时在哪里，下一个控件距离多远？"——一个布局可能对齐了、平衡了，却仍然强迫用户往返。

## 6. 平衡是可测的——不要目测（或相信 VLM 的眼睛）

§1 说的是渲染出来让新鲜眼睛评审。那一遍定性的检查能抓碰撞和参差的对齐，但**模型没有可靠的视觉平衡感**——问一个 VLM"这是不是居中 / 平衡？"它会编一个结论。修法是停止征求意见，**测量一个数字**，再用**独立的**检查保证数字诚实。两个都用：§1 的新鲜眼睛评审，加上下面的硬数字。（这把浏览器内实时盒模型 + 自动平衡器，跟离线的像素 oracle 重新测量渲染截图配对了——见本技能的 `layout-audit.js` 配套脚本。）

**原理。** 视觉平衡是*视觉重量的质心*。它是算术，不是品味——所以要算出来。

**光学中心，不是几何中心。** 目标 `x = 0.50`，`y ≈ 0.46`——稍高一点，因为字面 50% 的质心读起来会下沉。

**视觉重量 = 面积 × 墨水密度，不只是面积。** 同尺寸不等于同重量：纯黑标题很重；灰色 / ASCII / 浅色图像读起来远比面积轻；正文是稀疏的。校准的初始系数（来自 `asym.html`，按项目重新调——在被像素 oracle 修正前，这些都是拍脑袋的猜测）：
```js
const DENS = {portrait:0.34, h1:0.82, kicker:0.42, lead:0.22, body:0.16, meta:0.5};
```

**质心。** 每根轴上，`centroid = Σ(wᵢ·posᵢ) / Σwᵢ`；平衡 ⇔ 质心落在光学中心上。要**修正**失衡，按跷跷板来想：重要的是**力矩**= 重量 × 距轴距离，所以边缘的重量大元素，可以被（a）一个对侧的重量、（b）另一侧更大的元素、（c）把重元素往里拉（更短的力臂）、（d）缩小它 来配平。这正是 `asym.html` 里自动平衡器的递进顺序——先让对侧标题长大（最便宜），再加重量，再把重元素往里拉，最后缩小（最后手段）。

**两个模型——以及为什么你需要独立的那个：**
- **廉价的盒模型**（实时调参）：把每个元素的重量放在它的包围盒*中心*。即时，对拖滑块够用。但它有一个系统化 bug——左对齐文本的墨水落在盒子*左侧*，所以盒模型把重量位置算错了。跟布局自身假设共享的指标是**循环**的；它曾经报告"平衡"，而像素实测的偏离是 0.93。
- **真相像素 oracle**（`analyze.py`）：把*渲染后*的页面栅格化（Playwright 截图或 html2canvas），然后取实际非纸张像素的质心，每个像素按其离背景色的距离加权。它对布局的意图一无所知——它只数墨水。**当盒模型和像素不一致时，像素赢。**（`asym.html` 闭环：它回归盒 vs 像素的偏差，并提供 α 信任旋钮，向 oracle 方向混合。）
- **验收标准（可测）：** `|centroid_x − 0.50| < 0.03` 且 `|centroid_y − 0.46| < 0.04`，加上较低左右和上下失衡（`|w_left − w_right| / total`）。

**验证门禁（比 §4 的轻，思路相同）。** 在把一个平衡关键的布局称为"平衡"之前，**不要**从代码或 VLM 意见做断言——截一张*渲染后*的页面，算墨水质心到光学中心的偏移，并报告实际数字。这是本设计技能对 `verify-outputs-rule` 的应用：看真实的产物，让验证检查（像素）独立于你调过的东西（布局）。这是 §1 定性评审的定量补充。

## 7. 布局审计——指标**中介**眼睛，永远不替代眼睛

§6 谈的是平衡；本节把它推广成一次完整的确定性扫描，并修掉最重要的失败模式：**模型读了指标 / JSON 就再也不看截图，所以它无法用常识去判断指标本身是不是错了。**

`scripts/layout-audit.js` 是一个零依赖的扫描，通过 Playwright MCP 的 `browser_evaluate` 在渲染后的页面上跑。它用确定方式测六样东西——全是几何、颜色和像素，没有"看起来对不对？"：

| 检查 | 方式（确定性） | 等级 |
|---|---|---|
| **碰撞** | 内容矩形相交 ≥12% | 门禁 |
| **对比度** | 文本与有效背景的 WCAG 亮度比（<4.5，大字 <3） | 门禁 |
| **点击热区** | 可交互目标 <44×44（Apple HIG） | 门禁 |
| **溢出** | `scrollWidth − clientWidth`（§4 那个门禁） | 门禁 |
| **对齐** | 左边缘聚类 → 与共享基线相差 1–7px 的近失 | 信号 |
| **间距** | 容器子元素的 gap 变异系数 | 信号 |
| **平衡** | 墨水密度加权质心与光学中心（§6） | 信号 |

**让它"中介"而不是"替代"的关键：** 它不只是返回 JSON——它**把每条发现都画成 SVG 叠层盖到页面上**，所以下一次 `browser_take_screenshot` 是一张**带标注的截图**。数字告诉你往哪里看；然后你去看，再下判断。这是强制项，不是可选的：

```
browser_evaluate({ function: "() => { <paste scripts/layout-audit.js> ; return __audit({}); }" })
browser_take_screenshot()      // ← 现在叠层已经在页面上了。看它。围绕它做判断。
```

传 `{align:'.card .title,.card .price', space:'.feature-list'}` 来限定两个依赖选择器的检查；传 `{contentSelector:'…'}` 用于非语义化、碰撞需要帮助定位块的布局。

**这些是启发式，不是定律——而且它们分成两类，绝不能混为一谈：**

- **门禁 = 正确性**（溢出、对比度、点击热区）。这些测的是可访问性 / 可用性*事实*，不是品味。任何一项不过就是真缺陷。可以放心**阻塞**。（碰撞接近门禁：通常是真 bug，但也可能是故意的——所以用眼睛确认，不要自动失败。）
- **信号 = 惯例**（平衡、对齐、间距节奏）。这些测的是布局多大程度贴合*对称、规整、网格化*的美学——而这恰恰是 §2 让你推*离*的那种**通用均值**。**把布局优化到这些分数最高，会让它更平庸。** 一个偏离中心的平衡、一次故意的错位、一段不均匀的节奏，是核心的创作工具，也常常是页面上最好的东西。把信号当作"值得一看"，**永远不要**当作要去修的缺陷。

**纪律（这就是本节的全部要点——强烈倾向于此）：**
- **没看过的指标不要接受。** 一个标记是*提示你看的位置*，不是结论。读到 `collisions: 1` 却不去看那张带标注的截图，本节存在的目的就是为了消灭这种失败。
- **用信号抓意外，永远不要用它强制惯例。** 一次你没打算的 7px 对齐漂移、一个幽灵滚动条、一个 1.9:1 的标题比例——抓这些。但**同一个**平衡 / 对齐 / 间距信号也会在故意的不对称、刻意的重叠、表达性的节奏上触发。**当指标和那个有意思的选择冲突时，那个有意思的选择通常赢。** 眼睛没判定真的更糟之前，不要为了对称 / 均匀去"修"一个信号。把这些分数刷到最高的模型，设计出来的就是均值。
- **推翻眼睛判定为故意的标记。** 野兽派的标题叠在 banner 上、不对称的 hero——指标会标出来；常识去推翻。工作中的例子已经验证：野兽派样稿如果听碰撞检查的，会去掉那个重叠，设计反而变*平*。
- **门禁必要，不充分。** `gates_pass:true`（溢出 / 对比度 / 点击热区全为 0）只说明确定性地板过了——**不**代表布局好。一个平庸的居中模板能过所有门禁，但依然是均值。门禁过了之后，真正的判断（§1 的新鲜眼睛评审、品味、品牌契合）仍然要做。

**证据，落到了具体：** 做一个页面，把六种算法实时跑在几个真实风格的样例站上，每站都有一个开关切换布局**原貌**和**听指标**两个版本，再加上渲染时的叠层。它同时展示两半：听一个*门禁*，能修一个真 bug（低对比 CTA、过小点击热区、重叠卡片）；听一个*信号*，反而更糟（一个不对称的编辑 hero 是更有意思的布局；"纠正"一个刻意的野兽派重叠反而抹平了它）。`layout-audit.js` 就是从那个可交互演示里蒸馏出来的。

## 8. 光学工艺——感知胜过几何

审计里的对齐检查（§7）测的是*几何*边缘。眼睛不读几何，读取感知——所以有几种情况需要手动微调，指标做不出来。这些是眼睛判断，不是门禁。（出自 Web Interface Guidelines，`vercel-labs/web-interface-guidelines` @ `4e799d4`。）

- **光学对齐——测出来居中但*看起来*偏时，微调 ±1–2px。** 圆形按钮里的播放三角形必须向右偏离几何中心才看着居中（它的视觉质量偏左）。字形、箭头、不对称图标经常需要同样的处理。按盒指标垂直居中的文本常常会低一像素——把它往上提一点。几何是起点；眼睛才是裁判。
- **平衡图标 / 文字的组合。** 当图标和文字并排时，要让它们的*视觉重量*匹配——调整图标的描边粗细、大小、间距或颜色，避免任何一方压过另一方。细描边图标配中等字重文字看着弱；把描边加粗（或略微放大），让它们读成一个整体。目标是光学大小，不是像素大小一致。
- 这跟 §6 / §7 去偏的原理相同：数字把你带近；眼睛做最后那 1px 的判断。不要让一个几何对齐指标去*阻止*一次光学修正。

## 局限性

- 仅当任务与上游来源和本地项目上下文明确匹配时使用此技能。
- 在应用任何更改之前，请验证命令、生成的代码、依赖、凭据以及外部服务的行为。
- 不要把示例当作环境特定测试、安全审查，或对破坏性 / 高成本操作的用户审批的替代。
