feat(r3): compare — two picked rels side by side, linked stepping, synced pan, flag the winner

GET /b/{name}/compare with the conjunction 404 (containment AND the review
ring), both sides recorded as seen, view state (side, link) mapped from a
closed set onto every link, side-keyed regions, and back=compare in
_mark_redirect. compare.html: two stages sharing one set of rows, the strip
as picker (the side active now), linked and per-side stepping, X/L/Z/A/B/C
keys under the review's guards, synced pan by fraction with an echo guard,
per-side blur reveals, JS-off parity.

The stage machinery moves out of view.html into _stage_js.html
(BoothMode.bind, BoothStage.attach), shared by the review and compare. The
review gains a Compare control and a C key. At phone width a full top bar
wraps.

Tables: r2c's 15 stage rows re-pointed to _stage_js.html; r2b's phone
top-bar row re-anchored (the wrap made it vacuous alone); new r3.toml. The
contract records the wrap, equal stages and C on the compare page.
This commit is contained in:
vh
2026-09-24 13:26:11 -07:00
parent 5d785fe3c4
commit 8c7fe77841
11 changed files with 1841 additions and 111 deletions
+12
View File
@@ -118,6 +118,8 @@ assumptions:
ignored while focus is in something editable, and whenever a modifier
(Ctrl, Meta, Alt) is held: the review's `isEditable` rule, applied to all of
them, not only to Space.
- `C` and `Esc` both return to the review of A. `C` is the view toggle: `C` in
the review opens compare with that item as A, and `C` again goes back to it.
- Without JS, the page renders plain links for "both back", "both forward",
and each side's back and forward, with server-computed targets.
@@ -127,6 +129,15 @@ assumptions:
half the body and labelled `A #09 <name>` / `B #25 <name>`. At 900px and
below they STACK, A above B, each at most 45vh tall. The stack break is the
review's.
- **The two stages are always the same size.** The sides share one set of rows
(subgrid), so a caption under one side takes its height from both stages,
never from that side's alone. Two stages of different sizes would draw the
same picture at two scales in Fit.
- **At phone width (600px and below) a top bar that cannot hold its controls
WRAPS** instead of scrolling the page sideways or crushing a control. This
applies to the review's bar too, which gains the Compare control (only its
glyph below 600px). The review's bar was already full: a fogged booth
overflowed it by 3px at 390px before r3.
- **Each stage is the r2c stage:** Fit fills (up or down, contain, never
cropped), or 1:1 at natural pixels with every pixel reachable. Drag pans a
1:1 picture that overflows. The picture cannot be dragged away. Video and
@@ -295,6 +306,7 @@ Browser (`tests/test_compare_browser.py`):
| tests/mutations/r2c.toml: 21 rows anchor in view.html's script; the 15 on the toggle, the storage listener and drag-pan | `file = view.html`, anchors in the inline script | `file = _stage_js.html`, anchors re-pointed to the parameterised code (e.g. `stage.scrollLeft` becomes the attached stage's name) | the code moved (C4); every re-pointed row must still FALSIFY |
| tests/mutations/r2c.toml: the other 6 of those 21, the arrow placement, including the resize row ("S3 the arrows do not follow a resize") | view.html | unchanged: `place()` and view's ResizeObserver stay in view.html (C4) | — |
| tests/mutations/r2b.toml, the row on Space from a focused button | view.html's keydown | unchanged: the keydown handler stays in view.html | — |
| tests/mutations/r2b.toml, "the top-bar controls squeeze into multi-line stacks at phone width" (declared during the build) | removes the no-wrap rules | removes the no-wrap rules AND the phone-width wrap | a wrapping bar never squeezes, so removing the no-wrap rules alone went vacuous; r3.toml rows the wrap on its own |
| new: tests/mutations/r3.toml | — | rows for: the side-keyed region ids, the conjunction 404, `back=compare`'s ring check, the linked distance, the synced-pan loop guard, the `data-booth` attribute, the `cmp-reveal` Reveal-all rule | the r3 falsifiers |
**The gate for the refactor is the TABLE, not only the suite:**