设计 — 空间构图
适用场景
当你需要做空间构图设计时使用此技能。
模型无法信任自己的 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,找出肇事者:
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 修正前,这些都是拍脑袋的猜测):
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 的判断。不要让一个几何对齐指标去阻止一次光学修正。
局限性
- 仅当任务与上游来源和本地项目上下文明确匹配时使用此技能。
- 在应用任何更改之前,请验证命令、生成的代码、依赖、凭据以及外部服务的行为。
- 不要把示例当作环境特定测试、安全审查,或对破坏性 / 高成本操作的用户审批的替代。