feat(review): the review stage fills, its arrows sit at the picture, 1:1 pans (r2c)
The operator: "fit and 1:1 modes as well as moving the forward and back arrows closer to the edge of the image ... mouse click and pan for 1:1 mode if it exceeds page width (defeat drag drop of image)". Ruled: "Fit may enlarge." - Fit: the picture's box is the stage's inner box, and object-fit: contain draws it whole at the largest size that fits, up or down, never cropped. It works with or without JS. 1:1 is natural pixels. - The Fit | 1:1 toggle shows for every picture; the per-picture hide is gone. It stays hidden without JS. - The mode persists as `stage-one` on <html>, set by the head script before the stage exists, so a 1:1 reel never paints a stage in Fit. Anything stored but "one" reads as Fit. Storage never raises. - The arrows sit wholly outside the DRAWN picture (near edge 8px), clamped 8px inside the stage. They sit over the picture only when it spans the stage, and never over the rail. They are re-placed on load, resize, mode switch and 1:1 scroll, and keep their CSS spot until the drawn box is known. - 1:1 drag-to-pan when the picture overflows either axis: the picture follows the pointer, a 4px threshold, pointer capture, grab/grabbing. The picture is draggable=false. The stage's reveal button moves out of the scrolled content to sit over the stage (a pan carried it off), so no control is a pan source. Contract docs/contracts/r2c_review_stage.contract.md (heid contract panel 4/4 folded; it changed the no-flash mechanism). Declared test changes: the Nyx stage-edge arrow test is replaced; the stage class and the toggle's `hidden` are updated. tests/mutations/r2c.toml 16/16. 803 passed.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
---
|
||||
contract_version: "0.1"
|
||||
status: "PROPOSED 2026-09-23 by design-dev; heid contract panel (4/4) folded, from the operator's ask relayed by booth-dev (thread 01M38FPYAY5RSMSB9BGQ23CFM7) and his ruling 'Fit may enlarge' (thread 01M38EESKR8T7A8XMCQPHRNP0E). Sequenced after r2b and before r3 (compare), so compare reuses this machinery rather than growing a second copy."
|
||||
module: "templates/view.html + base.html CSS (the review stage: fit / 1:1, the prev/next arrows, drag-pan)"
|
||||
purpose: "The operator, verbatim: 'fit and 1:1 modes as well as moving the forward and back arrows closer to the edge of the image instead of out at the edges unless the image spans the entire width. mouse click and pan for 1:1 mode if it exceeds page width (defeat drag drop of image)'. Fit/1:1 exists but hides whenever a picture fits at natural size, and Fit never enlarges, so the two modes often look identical and the toggle comes and goes from picture to picture. The arrows sit at the stage's edges, hundreds of pixels from a portrait picture. 1:1 pans only by scrollbars, and a drag picks the picture up."
|
||||
depends_on:
|
||||
- "view.html (R2 C6): the stage `#vstage` (server-rendered `vstage fit`), `#vimg`, `#vtoggle` with `#btn-fit`/`#btn-one`, the `.vnav.vprev`/`.vnav.vnext` anchors inside `.review-body`, the review keys and their `isEditable` guard."
|
||||
- "base.html: `.vstage`, `.vstage.fit`, `.vstage.one`, `.review-body` (grid: stage + 360px rail; stacked at <=900px)."
|
||||
- "r2b D2: Reveal all and the stage's own reveal (`#vreveal`) — untouched; they read the blur classes, not the fit classes."
|
||||
language: "jinja + css + a little javascript"
|
||||
complexity: "medium"
|
||||
estimated_loc: 220
|
||||
confidence: 0.7
|
||||
touches:
|
||||
- "booth/templates/view.html (the toggle markup; the stage-mode script; the arrow placement; drag-pan)"
|
||||
- "booth/templates/base.html (Fit-fills CSS; 1:1 cursor; the stage reveal's position; `stage-one` in the head script)"
|
||||
- "tests/test_flow.py, tests/test_flow_browser.py; tests/mutations/r2c.toml (new)"
|
||||
assumptions:
|
||||
- "ONE VIEWER, per r2: the stage mode is a per-browser preference."
|
||||
- "No server change: every part of this is markup, CSS and page script."
|
||||
---
|
||||
|
||||
# R2c — the review stage
|
||||
|
||||
## S1 — Fit fills; 1:1 is the pixel truth
|
||||
|
||||
- **Fit scales the picture to the largest size at which it is WHOLE inside the
|
||||
stage, UP or down, undistorted** (ruling "Fit may enlarge"): contain, never
|
||||
cover — nothing is ever cropped in Fit. It is CSS: the image box fills the
|
||||
stage and `object-fit: contain` places the picture in it. So the no-JS render
|
||||
is also Fit-fills — a declared change to R2's INV-3 note ("with scripts off
|
||||
the image shows at fit size"): the size changes, the promise (one picture at a
|
||||
readable size, no judgment behind a script) holds.
|
||||
- The drop shadow follows the picture's own pixels (`drop-shadow`), not the
|
||||
letterboxed box.
|
||||
- **1:1 shows natural pixels**, centred when smaller than the stage and
|
||||
scrollable when larger. The upscale softness in Fit is exactly why 1:1 exists
|
||||
and is always one click away.
|
||||
|
||||
## S2 — the toggle is always there for a picture
|
||||
|
||||
- **Fit | 1:1 shows for EVERY picture**, never hidden because a picture happens
|
||||
to fit — that per-picture hide is why the operator could not find the
|
||||
feature. Video and audio still get no toggle.
|
||||
- It is in the markup with `hidden` for pictures only; the script removes
|
||||
`hidden`. Without JS it never shows (Fit-fills needs no toggle).
|
||||
- **The mode persists across prev/next, per browser**: every click writes
|
||||
`localStorage["booth.fit"]` = `one` or removes it (Fit); arrowing through a
|
||||
set in 1:1 is how detail gets compared.
|
||||
- **The mode is ONE class on `<html>`, `stage-one`** (absent = Fit), set by
|
||||
the early `<head>` script — the one r2b uses for the theme and Reveal all —
|
||||
BEFORE THE STAGE EXISTS in the document. So no paint can ever show a 1:1
|
||||
reel's stage in Fit: the class is there before the stage is parsed. The
|
||||
stage's CSS keys off it (`.stage-one .vstage`); the server renders the
|
||||
stage as plain `vstage` (Fit is the default, no class needed).
|
||||
- A stored value other than `one` reads as Fit. A read that throws reads as
|
||||
Fit; a write that throws still applies the click. Never raises.
|
||||
- The buttons' pressed state is drawn from the `<html>` class, the one
|
||||
record of the mode on the page (storage is its memory, off the page).
|
||||
|
||||
## S3 — the arrows sit at the picture
|
||||
|
||||
- **Each arrow sits wholly outside the picture's DRAWN edge, its near edge 8px
|
||||
from the picture**, vertically centred on the stage. The measure is always
|
||||
the DRAWN picture, never the file's natural size: in Fit, the `object-fit:
|
||||
contain` content box (from the natural size and the box); in 1:1, the part of
|
||||
the picture visible in the stage (its box intersected with the stage's). A
|
||||
video's own box counts the same way; audio keeps the stage-edge arrows.
|
||||
- **Clamped to the stage**: an arrow never goes past the stage's edge (8px
|
||||
inset), never over the rail, never off the stage. When there is no room for
|
||||
it outside the drawn picture — the drawn picture spans, or nearly spans, the
|
||||
stage's width — it sits at the stage edge, over the picture. That is the
|
||||
ONLY case the arrows sit at the stage edge (the operator: "closer to the edge
|
||||
of the image instead of out at the edges unless the image spans the entire
|
||||
width").
|
||||
- Re-placed on picture load (or at once when it is already loaded), stage
|
||||
resize (a `ResizeObserver`, which covers window resizes and the rail
|
||||
stacking), mode switch and 1:1 scroll.
|
||||
- **Before the picture's size is known** (JS on, picture still loading), and if
|
||||
it fails to load, the arrows stay at today's stage-edge CSS; they move once
|
||||
the drawn box is known. Without JS they stay there.
|
||||
- The anchors, their classes and their hrefs are unchanged (test_booth,
|
||||
test_navigation, test_flow pin them).
|
||||
|
||||
## S4 — drag to pan in 1:1
|
||||
|
||||
- **In 1:1, when the picture overflows the stage on EITHER axis,
|
||||
press-and-drag pans it**, along whichever axes overflow. `grab` cursor at rest,
|
||||
`grabbing` while dragging, pointer capture.
|
||||
- **The picture follows the pointer** (the grab convention): a drag of +dx,
|
||||
+dy changes the stage's scroll by −dx, −dy.
|
||||
- A press that moves less than 4px is not a drag: nothing pans.
|
||||
- **Pan listens on the stage only**, and no control is in the stage's
|
||||
scrolled content: the arrows never were, and **the stage's own reveal
|
||||
button moves OUT of the stage to sit over it** (found building this: in
|
||||
1:1 a panned picture carried the button out of view with it). So a control
|
||||
is never a pan source and keeps its own click, at any scroll.
|
||||
- **The picture cannot be dragged away**: `draggable="false"` on `#vimg` and a
|
||||
`dragstart` `preventDefault` on the stage.
|
||||
- Fit, or a 1:1 picture that fits: no pan, no grab cursor.
|
||||
- Keys, the stage reveal, Reveal all, the rail and the filmstrip are
|
||||
unchanged.
|
||||
|
||||
## Invariants
|
||||
|
||||
- **INV-1 — no server change.** Markup, CSS, page script.
|
||||
- **INV-2 — JS-off parity.** Without JS: Fit-fills, stage-edge arrows, no
|
||||
toggle, no pan, and every judgment (the rail's flag, note and pick forms) and
|
||||
navigation (the arrows and the filmstrip) intact.
|
||||
- **INV-3 — one record of the mode** on the page: `stage-one` on `<html>`.
|
||||
- **INV-4 — the arrows never cover the rail and never leave the stage.**
|
||||
- **INV-5 — storage never raises**, read or write.
|
||||
|
||||
## TESTS
|
||||
|
||||
- `fit_fills_the_stage_up_or_down` [tracer]: a picture smaller than the stage
|
||||
and one larger both draw at the scale `min(W/w, H/h)` in Fit — the contain
|
||||
content box, never cropped — and at natural size in 1:1.
|
||||
- `the_toggle_shows_for_every_picture_and_never_without_js`: a picture that
|
||||
fits at natural size still gets the toggle; video and audio do not; with JS
|
||||
off it never shows.
|
||||
- `the_mode_persists_across_prev_next_and_never_flashes`: choose 1:1, press
|
||||
→ ; an observer installed before any page script records `<html>`'s class at
|
||||
the moment the stage ELEMENT is inserted by the parser — it is already
|
||||
`stage-one` (so no paint can show that stage in Fit); storage throwing still
|
||||
applies the click; a stray stored value reads as Fit.
|
||||
- `the_arrows_sit_just_outside_the_picture_and_clamp_to_the_stage`: a portrait
|
||||
picture whose natural width exceeds the stage but which is DRAWN narrower
|
||||
(height-bound in Fit) — each arrow wholly outside the drawn picture, its near
|
||||
edge 8px (±2) from it; a landscape drawn as wide as the stage — arrows inside
|
||||
the stage at its edges, over the picture, never over the rail; after a window
|
||||
resize they follow the new drawn box.
|
||||
- `in_one_to_one_a_drag_pans_and_the_picture_cannot_be_dragged_away`: a picture
|
||||
overflowing both axes in 1:1 — a drag of (+80, +60) changes the scroll by
|
||||
(−80, −60); a 2px press pans nothing; a press on the stage's reveal button
|
||||
reveals and does not pan; `#vimg` is `draggable=false`; in Fit a drag does not
|
||||
scroll.
|
||||
|
||||
## Assertions that change (declared before the code)
|
||||
|
||||
| test | today | after | why |
|
||||
|---|---|---|---|
|
||||
| test_flow_browser `test_the_next_arrow_clears_the_rail_only_beside_it` | the next arrow's computed `right` is 360px wide / 0px narrow | replaced by `the_arrows_sit_just_outside_the_picture_and_clamp_to_the_stage` (never over the rail; at the picture's edge) | the arrows now track the picture, not the stage edge (operator) |
|
||||
| test_flow `test_only_a_picture_gets_the_fit_toggle_and_blur_stays_honest` | `id="vtoggle"` present for a picture (hidden by inline style); the stage is `class="vstage fit is-blurred"` | the same presence, now with the `hidden` attribute; the stage is `class="vstage is-blurred"` | the toggle is `hidden` until the script shows it; the mode moved to `<html>` (never flash) |
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Synced pan / the same crop across items, and two panes: r3 (compare).
|
||||
- A zoom level between Fit and 1:1, wheel zoom, pinch.
|
||||
- A key for the mode toggle.
|
||||
Reference in New Issue
Block a user