feat(blur): reveal all, and the booth blur control (r2b merge 1: D2 + D2b)

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.
This commit is contained in:
vh
2026-09-23 17:52:33 -07:00
parent 091f4b5f2d
commit 5ded5ffe55
11 changed files with 759 additions and 6 deletions
+11
View File
@@ -680,6 +680,8 @@ def list_booths(data_dir: Path, ttl_seconds: float, now: float | None = None) ->
"has_index": (child / "index.html").is_file(),
"uploaded": (child / UPLOAD_MARKER).exists(),
"kept": is_kept(child),
# r2b D2b: the whole-booth fog, so a blurred strip says why.
"booth_blurred": is_booth_blurred(child),
"marks_total": len(marks),
# `open_marks` and nothing else (INV-2). The count this replaced
# tested `answer is None`, so a half-answered pick read as closed
@@ -746,6 +748,10 @@ def build_gallery(child: Path) -> list[dict]:
suite reaches for it by name in nine places.
"""
out = []
# r2b D2b: the item's OWN blur, apart from the booth's. `blurred` is the
# composed fact the surfaces render; the per-item control changes only
# this, and must not claim an un-blur the booth flag would override.
own_blur = read_blurred(child)
for it in booth_items(child):
body = render_doc_body(child, it)
rendered, rendered_html = body if body is not None else (None, False)
@@ -768,6 +774,7 @@ def build_gallery(child: Path) -> list[dict]:
"rendered": rendered,
"rendered_html": rendered_html,
"blurred": it.blurred,
"blurred_self": it.rel in own_blur,
}
)
return out
@@ -1193,6 +1200,8 @@ def create_app(
# The page could not previously tell keep from release, so it
# offered neither and you had to go back to the index.
"kept": is_kept(booth),
# r2b D2b: the header's blur-booth control says what IS.
"booth_blurred": is_booth_blurred(booth),
# THE GRID RENDERS `shown`; everything else reads `gallery`.
# Filtering is a VIEW: `shown` is `gallery` with non-matching
# items removed and NOTHING re-sorted, so "the third one" means
@@ -1824,6 +1833,8 @@ def create_app(
**base_ctx,
"name": name,
"name_url": quote(name, safe=""),
# r2b D2b: the review's blur-booth control says what IS.
"booth_blurred": is_booth_blurred(booth),
"file": f,
"file_url": quote(f, safe="/"),
# The facts this route never used to carry.