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:
+28
-18
@@ -1,5 +1,12 @@
|
||||
# R2c — the review stage: every falsifier the contract claims
|
||||
# (docs/contracts/r2c_review_stage.contract.md), and the change each forbids.
|
||||
#
|
||||
# r3 moved the stage machinery (the mode toggle, the storage listener, pannable
|
||||
# and drag-pan) out of view.html into the shared _stage_js.html (r3 C4). The 15
|
||||
# rows that anchor there were RE-POINTED, not rewritten: same test, same
|
||||
# defeating change, in the code's new home, and every one re-proved after the
|
||||
# move. The arrows (place, drawn, view's ResizeObserver) stayed in view.html,
|
||||
# and so did their rows.
|
||||
|
||||
unit = "the review stage: fit / 1:1, the arrows at the picture, drag-pan"
|
||||
|
||||
@@ -23,12 +30,14 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "S2 the toggle stays hidden (the per-picture hide is back)"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_the_toggle_shows_for_every_picture_and_never_without_js"
|
||||
# r3: BoothMode.bind is page level and holds no picture, so the re-pointed hide
|
||||
# reads the page's one picture and stage itself.
|
||||
old = '''
|
||||
toggle.hidden = false;'''
|
||||
new = '''
|
||||
toggle.hidden = img.naturalWidth <= stage.clientWidth;'''
|
||||
toggle.hidden = document.querySelector('.vstage img').naturalWidth <= document.querySelector('.vstage').clientWidth;'''
|
||||
|
||||
[[mutation]]
|
||||
label = "S2 the toggle shows without JS (display beats [hidden])"
|
||||
@@ -59,7 +68,7 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "S2 a storage write that throws swallows the click"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_the_mode_persists_across_prev_next_and_never_flashes"
|
||||
old = '''
|
||||
var setMode = function (one) {
|
||||
@@ -112,7 +121,7 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "S4 the pan runs backwards (the picture flees the pointer)"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_in_one_to_one_a_drag_pans_and_the_picture_cannot_be_dragged_away"
|
||||
old = '''
|
||||
stage.scrollLeft = drag.l - dx;'''
|
||||
@@ -121,7 +130,7 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "S4 no drag threshold (a jittery click pans)"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_in_one_to_one_a_drag_pans_and_the_picture_cannot_be_dragged_away"
|
||||
old = '''
|
||||
if (dx * dx + dy * dy < 16) return; /* under 4px in all: a click */'''
|
||||
@@ -166,7 +175,7 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "a buttonless hover continues a press released outside the stage"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_a_pan_holds_past_the_stage_edge_and_never_starts_on_a_hover"
|
||||
old = '''
|
||||
if (!(e.buttons & 1)) { endDrag(); return; }'''
|
||||
@@ -175,7 +184,7 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "no pointer capture (a pan dies at the stage's edge)"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_a_pan_holds_past_the_stage_edge_and_never_starts_on_a_hover"
|
||||
old = '''
|
||||
try { stage.setPointerCapture(drag.id); } catch (x) {}'''
|
||||
@@ -222,7 +231,7 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "the drag threshold is per axis, not total (a 3,3 diagonal pans nothing)"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_in_one_to_one_a_drag_pans_and_the_picture_cannot_be_dragged_away"
|
||||
old = '''
|
||||
if (dx * dx + dy * dy < 16) return; /* under 4px in all: a click */'''
|
||||
@@ -252,7 +261,7 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "a stage mode chosen in another tab does not reach this one"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_a_stage_mode_chosen_in_one_tab_moves_the_others"
|
||||
old = '''
|
||||
if (e.key !== 'booth.fit' && e.key !== null) return;'''
|
||||
@@ -261,7 +270,7 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "the drag threshold drops to 3px"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_in_one_to_one_a_drag_pans_and_the_picture_cannot_be_dragged_away"
|
||||
old = '''
|
||||
if (dx * dx + dy * dy < 16) return; /* under 4px in all: a click */'''
|
||||
@@ -270,7 +279,7 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "a storage write that throws cuts the click short (buttons never update)"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_the_mode_persists_across_prev_next_and_never_flashes"
|
||||
old = '''
|
||||
try {
|
||||
@@ -281,7 +290,7 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "the Fit button's pressed state is never drawn"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_the_mode_persists_across_prev_next_and_never_flashes"
|
||||
old = '''
|
||||
bFit.setAttribute('aria-pressed', one ? 'false' : 'true');'''
|
||||
@@ -302,16 +311,17 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "S2 the toggle hides, on load, for a picture larger than the stage"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_the_toggle_shows_for_every_picture_and_never_without_js"
|
||||
# r3: `settle` is the stage's own (pannable) then the page's onSettle (place).
|
||||
old = '''
|
||||
function settle() { place(); pannable(); }'''
|
||||
function settle() { pannable(); onSettle(); }'''
|
||||
new = '''
|
||||
function settle() { place(); pannable(); if (img) document.getElementById('vtoggle').hidden = img.naturalWidth > stage.clientWidth; }'''
|
||||
function settle() { pannable(); onSettle(); if (img) document.getElementById('vtoggle').hidden = img.naturalWidth > stage.clientWidth; }'''
|
||||
|
||||
[[mutation]]
|
||||
label = "S2 choosing Fit stores a word instead of forgetting 1:1"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_the_mode_persists_across_prev_next_and_never_flashes"
|
||||
old = '''
|
||||
if (one) localStorage.setItem('booth.fit', 'one'); else localStorage.removeItem('booth.fit');'''
|
||||
@@ -320,7 +330,7 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "S4 pan only when BOTH axes overflow"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_a_picture_that_overflows_one_axis_pans_along_it"
|
||||
old = '''
|
||||
(stage.scrollWidth > stage.clientWidth || stage.scrollHeight > stage.clientHeight);'''
|
||||
@@ -331,7 +341,7 @@ new = '''
|
||||
|
||||
[[mutation]]
|
||||
label = "a press on the stage's scrollbar starts a pan"
|
||||
file = "booth/templates/view.html"
|
||||
file = "booth/templates/_stage_js.html"
|
||||
test = "tests/test_flow_browser.py::test_a_classic_scrollbar_is_neither_under_an_arrow_nor_a_pan"
|
||||
old = '''
|
||||
if (e.clientX - r.left - stage.clientLeft >= stage.clientWidth ||
|
||||
|
||||
Reference in New Issue
Block a user