From 65e4049fb145bd5362af429d2486633d28da3c8c Mon Sep 17 00:00:00 2001 From: DoodleBear Date: Thu, 1 Oct 2026 15:14:14 +0900 Subject: [PATCH] fix: the stage's controls track was eating the field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported twice now as "Column Drop shows nothing". The previous fix was correct and insufficient: the motion WAS rendering — 25 cells, animating, one column lit at a time. It was being clipped out of the viewport. The stage is `grid-template-columns: auto minmax(0, 1fr)`, and an `auto` track is sized by max-content. The refusal caption — "this motion is a highlight moving down the field…", shown only when the CSS renderer refuses the chosen motion — is a sentence, so that one caption grew the controls track from 264px to 602px and collapsed the field's track to ZERO. Measured, before the fix: spiral (caption hidden) stage 648 controls 264 field 338 columns (caption shown) stage 648 controls 602 field 0 The stage is `overflow: hidden`, so a zero-width track does not squash the field, it removes it from the render. The DOM held all 25 cells with correct `fill-opacity`; the page showed nothing. An `auto` track cannot express "as wide as it needs, but never at the field's expense", and `minmax(0, auto)` is the same thing — the track is capped instead, and `min-width: 0` lets the caption wrap inside that cap rather than widening it. The fix is one declaration and it is verified in a browser, where the numbers above come from. NO AUTOMATED TEST FOR THIS ONE, DELIBERATELY. jsdom computes no layout — `getComputedStyle (el).gridTemplateColumns` on a real grid rule returns the empty string — so nothing in this repo can catch a collapsed track. The first attempt was worse than none: it regexed the stylesheet for the rule and matched a DIFFERENT component's `repeat(auto-fill, minmax(280px, 1fr))`, so it went green while the bug was present and would have gone green forever after. A test that can assert about the wrong rule is not a slow test, it is a wrong one, and a green wrong test costs more than no test because it is believed. AGENTS.md now records the class instead: layout has to be verified in a real browser over CDP, with a dedicated `--user-data-dir` (reusing the default profile makes the instance exit immediately, which cost more time than the bug) and screenshots, never by matching CSS text. 215 tests (was 215), oxlint 0, registry ok, both changelog ledgers. --- AGENTS.md | 1 + CHANGELOG.md | 3 +++ CHANGELOG.zh.md | 3 +++ apps/site/src/styles/site.css | 20 +++++++++++++++++++- 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 082fe7c..484cfe4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,6 +60,7 @@ If Astro ships TS 7 support, wire `astro check` back in rather than leaving this - **Preserve the size algebra.** `cell = size / ((n - 1)(1 + gap) + dotSize)` is solved, not clipped, which is what keeps the field landing exactly on the box at any `dotSize` — including above 1, where the dots touch. If you change it, the invariant tests in `packages/core/test/dot-matrix.test.ts` are the specification. +- **Verify layout in a browser, not in jsdom.** jsdom computes no layout at all — `getComputedStyle(el).gridTemplateColumns` on a real grid rule returns the empty string — so no unit test here can catch a box that has collapsed. A stage whose controls column was an `auto` track (sized by max-content) grew from 264px to 602px on a single long caption and squeezed the field's track to zero; the stage is `overflow: hidden`, so the field was clipped out of existence while the DOM still held all 25 cells and every source-level check passed. Drive Chrome over CDP (`--remote-debugging-port`, `Runtime.evaluate`, `Page.captureScreenshot`) with a **dedicated `--user-data-dir`** — reusing the default one makes the instance exit immediately — and assert on measured geometry and on screenshots. Regexing the stylesheet for the rule is not a substitute: a test written that way matched a different component's `repeat(auto-fill, …)` and stayed green with the bug present. - **Direction is per-order AXES, not a global "reverse".** `direction` is a list of axis tokens (`["counterClockwise", "insideOut"]`), and each order reads only its own — `ORDER_DIRECTION_AXES` is the contract a UI reads to know what to offer. Offer from that table and never from a hardcoded list: a diff --git a/CHANGELOG.md b/CHANGELOG.md index 86a7e52..499a564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,9 @@ ### Fixed +- **Fixed: `Column Drop` still showed nothing, and this time the field was being clipped rather than missing.** The previous fix made the motion render, and it _was_ rendering — 25 cells, animating, one column lit at a time. The stage's controls column was an `auto` track, and `auto` is sized by max-content, so the refusal caption ("this motion is a highlight moving down the field…", shown only when the CSS renderer refuses the chosen motion) grew that track from **264px to 602px** and squeezed the field's own track to **zero**. The stage is `overflow: hidden`, so the field was not squashed — it was clipped out of existence. The tracks are capped now, and the caption wraps inside the cap instead of widening it. +- **This one has no automated test, on purpose.** jsdom computes no layout, so no test in this repo can catch a collapsed track, and the first attempt at one was worse than nothing: it regexed the stylesheet, matched a **different** component's `repeat(auto-fill, …)`, and stayed green with the bug present. A test that asserts about the wrong rule is not a slow test, it is a wrong one. `AGENTS.md` now records that this class of check needs a real browser over CDP, with the `--user-data-dir` detail that cost the most time to work out. + - **Fixed: picking `Column Drop` left the field blank.** A motion the CSS renderer cannot express — one that MOVES a highlight down a column, which a per-dot `animation-delay` cannot do — was still being rendered through it, and `buildCss` returns null for exactly those. The playground disabled the CSS switch and said so, then passed `renderer="css"` anyway, so the stage came up **empty**. Disabling a control without honouring it is the worst of both answers: the switch explains itself while the field shows nothing. The field now renders the one way it can, and the readout and the copied snippet say `svg` rather than claiming a mode the engine is not in. The engine normalises the same thing for direct callers, so `handle.options.renderer` reports what is actually painted instead of leaving the element empty. - **The CSS renderer no longer publishes keyframes it cannot honour.** It emitted `@keyframes botui-dm-columns`, and that rule animated _nothing_: `spike` is a gaussian centred on the cycle seam, so sampling it at the two positions its stop table names — 0 and 1 — yields full brightness at both ends, i.e. a static fully-lit field. Shipping that in every consumer's stylesheet was a lie about what the motion looks like, not merely dead weight. Only presets the renderer actually accepts get a rule, which is the same rule `buildCss` applies. diff --git a/CHANGELOG.zh.md b/CHANGELOG.zh.md index 35b4240..537f968 100644 --- a/CHANGELOG.zh.md +++ b/CHANGELOG.zh.md @@ -51,6 +51,9 @@ ### 修复 +- **修复:`Column Drop` 仍然什么都看不到,而这次场是被裁掉了而不是缺失。** 上一个修复让这个动效真的渲染出来了,而且它**确实**在渲染 —— 25 个格子、在动、每次点亮一列。舞台的控件列是 `auto` 轨道,而 `auto` 按 max-content 定尺寸,于是那条拒绝提示("这个动效是高亮沿着场移动…" —— 只在 CSS 渲染器拒绝当前动效时显示)把该轨道从 **264px 撑到 602px**,把场自己的轨道挤到 **0**。舞台是 `overflow: hidden`,所以场不是被压扁,而是被整个裁掉。轨道现在设了上限,提示在上限内换行,而不是把列撑宽。 +- **这一条故意没有自动化测试。** jsdom 完全不算布局,所以本仓库里没有任何测试能抓住塌掉的轨道;而我写的第一版比没有更糟:它用正则去匹配样式表,匹配到了**另一个**组件的 `repeat(auto-fill, …)`,于是在 bug 存在时依然全绿。能断言到错误规则的测试不是慢的测试,是错的测试。`AGENTS.md` 现在记下了这类检查需要用 CDP 驱动真实浏览器,并写下了最耗时间才弄明白的 `--user-data-dir` 细节。 + - **修复:选中 `Column Drop` 后场是空白的。** CSS 渲染器无法表达的动效 —— 那种把高亮**移动**到下一列的动效,而每格 `animation-delay` 做不到这件事 —— 仍然被交给它渲染,而 `buildCss` 对这些 preset 恰好返回 null。Playground 把 CSS 开关禁用了并说明了原因,**然后照样传 `renderer="css"`**,于是舞台**什么都没有**。禁用一个控件却不遵守它是最糟的答案:开关解释了自己,而场什么也不显示。现在场会用它唯一能的方式渲染,readout 和复制出的片段都如实写 `svg`,而不是声称一个引擎并未处于的模式。引擎侧对直接调用方做同样的归一化,于是 `handle.options.renderer` 报告真正被绘制的东西,而不是留一个空元素。 - **CSS 渲染器不再发布自己无法兑现的 keyframes。** 它一直在输出 `@keyframes botui-dm-columns`,而那条规则**什么都不动**:`spike` 是一个以周期接缝为中心的 gaussian,所以按它的 stop 表所列的两个位置 —— 0 和 1 —— 采样,两端都是全亮度,也就是一块静止的全亮场。把这个规则塞进每个使用者的样式表里,是对那个动效外观的**谎称**,而不只是无用代码。现在只有渲染器真正接受的 preset 才有规则,与 `buildCss` 用的是同一条判据。 diff --git a/apps/site/src/styles/site.css b/apps/site/src/styles/site.css index ebaf3d6..9db479b 100644 --- a/apps/site/src/styles/site.css +++ b/apps/site/src/styles/site.css @@ -304,6 +304,15 @@ h3 { grid-template-columns: 1fr; gap: 6px; font-size: 13px; + /* without this the caption's intrinsic width is what the track is sized from */ + min-width: 0; +} + +.stage-toggle-label em { + /* the refusal caption is a sentence, not a control label: let it wrap inside the + controls column instead of pushing the column wider */ + display: block; + line-height: 1.5; } .stage-controls label { @@ -632,7 +641,16 @@ footer { .playground-stage { display: grid; - grid-template-columns: auto minmax(0, 1fr); + /* Bounded, not `auto`. The controls column holds a sentence — "this motion is a + highlight moving down the field…" — and an `auto` track is sized by max-content, so + that one caption grew the track from 264px to 602px and squeezed the field's own track + to ZERO. The stage has `overflow: hidden`, so the field was not squashed, it was + clipped out of existence: the DOM had all 25 cells and the page showed nothing. + + An `auto` track cannot express "as wide as it needs, but never at the field's + expense", and `minmax(0, auto)` is the same thing. The track is capped instead, and + `min-width: 0` lets the caption wrap inside that cap rather than widening it. */ + grid-template-columns: minmax(0, 264px) minmax(0, 1fr); border: 1px solid var(--line); border-radius: var(--radius); background: var(--card);