The operator ruled blur A, and made it urgent: "per booth blurring is now
important since we are showing up to 4 images."
- Reveal all: one control per booth, in the booth header and the review's
top bar, outside every data-region. It is in the markup only when
something is blurred, always `hidden` until the script shows it.
- The state is sessionStorage per booth, per tab, and nothing reaches
the server. It is carried as one `reveal-all` class on <html>, applied
before first paint from the page's own data-booth, so booth A's reveal
cannot follow you into booth B and the index is never revealed.
- Per-item reveal buttons stand down by stylesheet, and an item's own
reveal is never touched, so "blur again" restores each item as it was.
- A storage write that throws still applies the click.
- The booth blur control: a plain form to booth-dev's POST /blurbooth, so
it works with scripts off. Its label follows is_booth_blurred; from the
review it carries `back` and lands on the same item. A fogged booth's
Desk row says "◉ blurred".
- Found by rendering it: under a fogged booth every item reported
`blurred`, so an item blurred only by the booth offered an un-blur that
visibly did nothing. The gallery now carries `blurred_self`, and such an
item shows "◉ booth", a label rather than a control.
Contract docs/contracts/r2b_desk_reveal_theme.contract.md (heid contract
panel 4/4, folded). tests/mutations/r2b.toml: 14/14 proved. 765 passed.
19 KiB
contract_version, status, module, purpose, depends_on, language, complexity, estimated_loc, confidence, touches, assumptions, resolved_questions
| contract_version | status | module | purpose | depends_on | language | complexity | estimated_loc | confidence | touches | assumptions | resolved_questions | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0.1 | PROPOSED 2026-09-23 by design-dev, from operator rulings relayed by booth-dev the same day (thread 01M38BJ30WVQT870MS6WGM49EK): blur=A; three Desk-row rulings; a theme toggle. Contract panel folded. Both open points answered by the operator (thread 01M38CT9DH2N3Z4FSJ0MNE4DR1): × hides (A), and the theme reaches inside verbatim pages. DELIVERED IN TWO MERGES, blur first (operator: 'per booth blurring is now important since we are showing up to 4 images'): merge 1 = D2 + D2b, merge 2 = D1 + D3. | templates + base.html CSS/JS + the vendored token sheet (the Desk row, Reveal all, the theme toggle) | Three operator rulings, one contract. THE DESK ROW: kept vs ephemeral reads at a glance; download/keep/release appear only on hover, at no space cost; the zip link leaves the middle. REVEAL ALL: one control reveals every blurred item in a booth for the life of the tab. THE THEME TOGGLE: System / Light / Dark at the top of every page. |
|
jinja + css + a little javascript | medium | 350 | 0.7 |
|
|
|
R2b — the Desk row, Reveal all, the theme toggle
D1 — the Desk row
The operator, verbatim: "let's make it obvious which are kept and which are ephemeral"; "the zip download button is in between keep/release and wipe, and looks awkward"; "let's have the download, keep and release buttons only appear on mouseover".
- The lifetime is a pill in the row's right column, always visible. It is
state, not a control, so it stays when the controls hide, and the right column
scans down the Desk as one column of state.
life-keptwhenb.kept: sage (SVOS: a judgment made), prefixed★.life-heldwhen not kept andb.holdisopenorunreadable: amber.life-countotherwise: neutral outline, prefixed◷.- The pill wraps
lifetime(...), whose output is unchanged; the class is chosen fromkept/holdalone. - The badges (open count, new, pickup, marks unreadable) stay in the same column, above the pill. The column always renders now, because every row has a lifetime.
- The facts line is facts only: item count and flag count. The lifetime and every control leave it.
- The controls are one cluster,
.desk-acts, in this order:⬇ zip, then★ keeporrelease, then× wipe, set apart from the other two. Zip leaves the middle; release stays next to × (the operator's earlier "x next to release"). - Where a real hover exists, the cluster takes no room. "Real hover" is
(hover: hover) and (pointer: fine)with NO coarse pointer present (any-pointer: coarsedoes not match). A touch laptop reports a mouse, but a finger on it cannot hover, so it gets the touch treatment below.- At rest the cluster is absolutely positioned over the top-right corner of
the row's preview strip, at opacity 0 and
pointer-events: none. On row:hoveror:focus-within, BOTH are restored: opacity 1 andpointer-events: auto. A visible control that cannot be clicked is a defect. - It covers pictures, never information: its box never intersects
.desk-mainor.desk-sideat any width. - Keyboard: the controls stay in the tab order while hidden (opacity, never
visibility/display), and focusing one reveals the cluster. - At ≤700px the strip is the row's first line, full width, and the text column, pill and badges wrap below it. The cluster sits at the strip's top-right, which is still picture.
- At rest the cluster is absolutely positioned over the top-right corner of
the row's preview strip, at opacity 0 and
- Everywhere else (no hover, a coarse primary pointer, or any coarse pointer present), the cluster is visible and in flow, on its own line at the bottom of the row. Hover-only would mean no controls at all on touch. Every control there is at least 28px square (r2's Slate T2 floor).
- × hides with the others (OPEN-1, answered yes). A visible × on every row would be a standing invitation to the one irreversible action. A visible × on every row is a standing invitation to the one irreversible action, and hiding the safe controls while the destructive one stays inverts the priority. It appears with the cluster, last, set apart.
- Unchanged: the forms, their POST targets,
data-confirm,data-booth, the confirm script and itsshown(), the ≥28px coarse-pointer floor, and "no page scrolls sideways at any width".
D1b — dates (operator, added 2026-09-23: "I think I want creation and update dates on the booths now too")
Merge 2, with the row. booth-dev has put both on the record (thread 01M38D39ANKF2TW2F15TEYJ1GT):
created_atis the directory's birth time viastatx, a float epoch, or None when the filesystem cannot say. None renders as NOTHING, never a guess.- "Updated" is
landed_at, the content clock the "new" section already reads.
They render on the Desk row and in the booth header as dated FACTS, a different kind of thing from the lifetime pill (state) and the controls (actions). Their exact form is settled in merge 2 against the built row.
D2 — Reveal all (blur ruling A)
- One STATE per booth, shown by a control in two places: "👁 reveal all —
blur is cosmetic" / "🙈 blur again". One instance is in the booth header,
one in the review's top bar, and both sit OUTSIDE every
data-region, so no in-place swap replaces them. The server puts the control in the markup only when at least one item of the booth is blurred (Item.blurred), and always with thehiddenattribute. The script removeshiddenand binds it. Without JS it is in the markup but never shown. - State:
sessionStorage["booth.reveal:" + <booth name>] = "1". Per booth, per tab, gone when the tab closes, so a blurred booth is blurred again next time. Nothing reaches the server.- A READ that throws (a private window, blocked site data) reads as "not revealed".
- A WRITE that throws still applies the click to the page in front of you: it is only not remembered for the next page. A control that does nothing when clicked is a defect. Neither case ever raises.
- The mechanism is one class on
<html>,reveal-all. CSS lifts the blur under it on every booth surface: tiles, the flag tray, the filmstrip and the review stage.<html>is outside everydata-region, so an in-place swap can never drop it.<html data-booth="<name>">is on EVERY page rendered for one booth: the booth page, the review, the doc view and the marks page. It is an autoescaped attribute, read bygetAttributeand never templated into script. An early<head>script addsreveal-allbefore first paint when that booth's key is set, so a revealed booth does not flash blurred on the next page of the reel.- Because every change of page is a full load, the class is re-decided per
page, from that page's
data-booth. Booth A's reveal cannot follow you into booth B. - The index carries no
data-booth, so nothing on the index is revealed by D2, the Desk strip included.
- The per-item reveal defers to it, BY STYLESHEET. Under
.reveal-allthe per-tile and stage reveal buttons aredisplay: none. That is a CSS consequence of the class, so markup swapped in after a save obeys it with no script. Reveal all never touches an item's ownrevealedclass: "blur again" returns every item to exactly the per-item state it had, an item revealed on its own staying revealed. - INV-8 of r2 holds: the server renders every blurred item blurred; the reveal stays per-browser and client-side; the copy keeps saying it is cosmetic.
D2b — the booth blur toggle (added after the contract panel was dispatched)
booth-dev landed the whole-booth marker and its route while the panel was reading; this is the control the operator uses, which the blur ruling assumed.
- "◌ blur booth" / "◉ booth blurred" in the booth header and the review's
tape bar: a plain
<form method=post action=/b/<name>/blurbooth>withon=1|0, so it works with scripts off (INV-2). From the review it carriesback=<rel>and lands back on the same item. - Its state comes from the server, never from the client:
booth_blurred = is_booth_blurred(booth)in both contexts. The label says what IS, and pressing it flips it. - The Desk row carries a
blurredbadge whenbooth_blurred, so a fogged strip says why. It is oneis_booth_blurredcall per booth in the passlist_boothsalready makes. The badge is information: it is not the control, and it does not hide on hover. - It is independent of Reveal all. Fogging a booth sets server state for every viewer; Reveal all lifts the fog for one tab.
- Each item's own blur control tells the truth under a fogged booth.
Item.blurredis the COMPOSED fact (own OR booth). The per-item form changes only the item's own entry in.blurred, so the gallery also carriesblurred_self, read from the same singleread_blurred.- An item blurred only because the booth is shows "◉ booth", a label with no form, pointing at the header. A per-item un-blur there would be overridden by the booth flag and visibly do nothing.
- An item blurred on its own keeps its "◉ blurred" un-blur.
- Found by rendering the built page, not by any review.
D3 — the theme toggle
-
System · Light · Dark, in the top bar of every page. A segmented control of three buttons with
aria-pressed. The top bar is outside everydata-region, so no swap replaces it. It is in the markup withhidden, and the script removes that and binds it. -
State:
localStorage["booth.theme"]∈ {light,dark}; absent = System. The opposite lifetime to Reveal all, deliberately: a theme should outlive the tab, a reveal must not. A READ that throws reads as System. A WRITE that throws still applies the choice to this page, and it is only not remembered. -
Mechanism:
data-themeon<html>. Absent = the OS preference, exactly today's sheet. An early<head>script sets it before first paint, so a forced theme never flashes the other one. -
System is live-following BY CONSTRUCTION. Choosing System removes
data-theme, and theprefers-color-schememedia query takes over. A media query tracks the OS live, so nomatchMedialistener is needed: JS never computes the theme. -
The token sheet is re-vendored at the same SVOS SHA (ed2f8d8) with a new scoping transform, and no value changes. The complete selector list:
block selector primitives, dark, art layer :root(unconditional: dark is the default, and what forced dark leaves standing)light + art-light @media (prefers-color-scheme: light)→:root:not([data-theme="dark"])light + art-light :root[data-theme="light"]dark-hc @media (prefers-contrast: more)→:rootlight-hc @media (prefers-contrast: more) and (prefers-color-scheme: light)→:root:not([data-theme="dark"])light-hc @media (prefers-contrast: more)→:root[data-theme="light"]- Forced dark excludes both light rows, so the unconditional dark block stands, with dark-hc under more contrast.
- Forced light matches the bare light row at specificity (0,2,0), which
beats dark-hc's
:root(0,1,0). The light-hc row then applies under more contrast. - The preservation check runs in BOTH directions at vendoring time: every declaration of the old sheet is in the new, and the new has none the old lacked. The committed test checks each re-scoped copy against the UNMOVED dark block. SVOS's light and dark declare the same 42 properties, and its dark-hc and light-hc the same 41, so a declaration the transform drops fails it.
-
No JS: no toggle, and the page follows the OS, as today.
-
The toggle reaches inside verbatim pages (operator: "theme toggle reaches inside").
embed.jsreads the samelocalStorage["booth.theme"](the same origin) and marks each.bk-askit injects withdata-bk-theme. Its colours follow that attribute exactly as the Booth's own sheet followsdata-theme: forced when set, OS when absent. It follows a change made in another tab through thestorageevent. It sets nothing on the host page's own<html>: the author's page is not ours to theme, only our guest chrome inside it. If a forced theme makes the chrome look actively broken against a host page, that goes back to the operator rather than being absorbed.
Invariants
- INV-1 — nothing new on the server beyond READS. No route and no file are
added:
booth_blurredin two contexts and on the Desk row (is_booth_blurred), andblurred_selfper gallery item (read_blurred, once per page). D2 and D3 are per-browser state; D1 is markup and CSS. - INV-2 — JS-off parity (r2 INV-3). Every control on the row works with scripts off. Reveal all and the toggle do not render without JS. The page follows the OS.
- INV-3 — no reserved room for a hidden control where a real hover exists. The box of every element in the row other than the cluster — the strip, each preview image, the text column, the side column, the pill — is identical with the cluster present or removed.
- INV-4 — blur honesty (r2 INV-8). Nothing on the index is revealed by D2.
- INV-5 — autoescape. The booth name reaches the page only as an escaped attribute value.
- INV-6 — no flash. A forced theme and a set reveal are applied before first paint.
TESTS
the_row_controls_take_no_room_where_a_hover_exists[tracer]: at rest the cluster is at opacity 0 and cannot be clicked. On row hover it is at opacity 1 and a click on keep reaches the server. The box of every other element in the row is identical with the cluster removed. The cluster's box never intersects the text or side column, at 390 / 720 / 1000 / 1400px.on_touch_the_row_controls_are_visible_in_flow_and_at_least_28px: a touch context (no hover, coarse pointer).the_lifetime_pill_class_is_kept_held_or_counting: the class is chosen by state, the lifetime words are unchanged, and the pill is visible with no hover.the_row_controls_run_zip_keep_or_release_then_wipe.reveal_all_reveals_every_blurred_surface_and_survives_the_next_page: tiles and tray on the booth page, stage and filmstrip on the review, across a navigation in the same tab; a fresh tab (new context) is blurred again.reveal_all_never_reaches_the_desk.the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review: header and review forms POST/blurbooth; the label followsis_booth_blurred; the review form carriesback; the Desk row showsblurred.reveal_all_survives_an_in_place_save: after a save, the blur is still lifted, the control still reads "blur again" and still works, and the per-tile buttons are still hidden.reveal_all_on_booth_a_does_not_reveal_booth_b.blur_again_restores_each_items_own_reveal.reveal_all_is_absent_without_blurred_items_and_hidden_without_js: no markup when nothing is blurred; with blurred items, the markup carrieshiddenand a JS-disabled context never shows it.a_storage_failure_still_applies_the_click: sessionStorage and localStorage throwing on write; the reveal and the theme still apply to the page.the_theme_toggle_forces_light_and_dark_and_system_follows_the_os_live: pressing Light/Dark changes--surface-baseand survives a reload (new page, same context); System plus an emulated OS scheme flip changes it WITHOUT a reload.a_forced_theme_follows_high_contrast: forced dark +prefers-contrast: moreresolves dark-hc's surface token.a_forced_theme_and_the_os_theme_are_the_same_declarations: each light copy equals the other and declares exactly the dark block's property set; each light-hc copy equals the other and declares exactly the dark-hc block's.
Assertions that change (declared before the code)
| test | today | after | why |
|---|---|---|---|
test_flow_browser test_a_rows_keep_release_and_wipe_take_no_room_of_their_own |
controls visible at rest; a row with no badge has no side column (gap ≤14px) | replaced by the_row_controls_take_no_room_where_a_hover_exists |
the operator ruled hover-reveal; the side column now always holds the lifetime pill |
test_flow_browser test_on_a_touch_screen_the_row_controls_keep_their_tap_floor |
measures .desk-facts form button |
the same floor, measured on .desk-acts controls |
the controls moved; the floor did not |
Out of scope
- The booth page header's keep / release / wipe (
.keep-lg,.wipe-lg) — the rulings named the Desk row. - A site-wide blur switch (ruling A, not B).
- The tagline copy.