fix(booth): put the blur toggle on every item kind, and make it look like a control

The operator asked "no UI option to blur/unblur?" and was right twice over.

MISSING ENTIRELY ON TWO OF THREE BRANCHES. booth.html renders docs, media and
everything-else through separate <figure> blocks. The toggle went into the
media branch only, so inline docs -- the branch that puts readable text straight
on the page, the one that needs blur most -- had no control at all, and `other`
files only got a caption row if they happened to carry a caption.

This is the SECOND time this feature shipped having patched some branches and
not others; the blurred class itself had the same gap one commit ago. So the
toggle is now a single Jinja macro called from all three sites, which makes
"patched two of three" impossible rather than merely unlikely, and
test_every_item_kind_gets_exactly_one_blur_toggle counts toggles against figures
across mixed kinds so a fourth branch cannot quietly skip it.

INVISIBLE WHERE IT DID RENDER. v1 was a bare `◌` at 0.78rem in --muted with no
border, no label and no hover affordance. It now reads `◌ blur` / `◉ blurred`
with a border, matching the other per-item controls. A control nobody can find
is a control that is not there.

Docs get it in the doc bar beside ⤢ ⬇ ✕, with stopPropagation so submitting it
does not collapse the <details> it lives inside.

Verified live on all three kinds: 3 figures, 3 toggles, and the POST round trip
blurs and un-blurs. 167 tests pass.
This commit is contained in:
vh
2026-09-19 23:50:32 -07:00
parent 2e7fd7157e
commit 271cb11b70
4 changed files with 88 additions and 12 deletions
+11 -1
View File
@@ -52,7 +52,17 @@ booth blur <name> <file>... # hide from a glance
booth unblur <name> <file>... booth unblur <name> <file>...
``` ```
Or the ◌ / ◉ toggle in each item's caption row on the booth page. Or the **`◌ blur` / `◉ blurred`** button on every item in the booth page —
in the caption row for images, video, audio and plain files, and in the doc bar
beside ⤢ ⬇ ✕ for inline docs.
⚠ The toggle is emitted by ONE Jinja macro (`blurtoggle`) called from all three
item branches. booth.html renders docs, media and everything-else through
separate `<figure>` blocks, and this feature was twice shipped having patched
only some of them — first the blur class, then the toggle itself. Add a fourth
branch and you must call the macro from it;
`test_every_item_kind_gets_exactly_one_blur_toggle` counts toggles against
figures across mixed kinds and will fail if you don't.
- **State** is `.blurred` in the booth dir — one booth-relative item path per - **State** is `.blurred` in the booth dir — one booth-relative item path per
line, the same filesystem-is-the-state idiom as `.pins` and `.forever`. An line, the same filesystem-is-the-state idiom as `.pins` and `.forever`. An
+11 -4
View File
@@ -270,10 +270,17 @@
font:inherit;font-size:.72rem;line-height:1;padding:.24rem .5rem;cursor:pointer; font:inherit;font-size:.72rem;line-height:1;padding:.24rem .5rem;cursor:pointer;
border:1px solid var(--line);border-radius:.3rem;background:var(--bg);color:var(--fg)} border:1px solid var(--line);border-radius:.3rem;background:var(--bg);color:var(--fg)}
.item.blurred .reveal:hover{background:var(--aus-blue);color:var(--fg-on-accent)} .item.blurred .reveal:hover{background:var(--aus-blue);color:var(--fg-on-accent)}
.blurtoggle{display:inline;margin:0} /* Legible on purpose. v1 was a bare muted glyph with no border and no label,
.blurtoggle button{font:inherit;font-size:.78rem;line-height:1;padding:0 .2rem; and the operator's reaction to it was "no UI option to blur/unblur?" — a
cursor:pointer;border:0;background:none;color:var(--muted)} control nobody can find is a control that is not there. */
.blurtoggle button:hover{color:var(--fg)} .blurtoggle{display:inline-block;margin:0}
.blurtoggle button{font:inherit;font-size:.7rem;line-height:1;padding:.2rem .4rem;
cursor:pointer;border:1px solid var(--line);border-radius:.3rem;
background:var(--bg);color:var(--fg);white-space:nowrap}
.blurtoggle button:hover{background:var(--aus-blue);color:var(--fg-on-accent)}
/* In the doc bar it sits beside ⤢ ⬇ ✕ and should not out-shout them. */
.doc-bar .blurtoggle{margin-left:.35rem}
.doc-bar .blurtoggle button{font-size:.66rem;padding:.14rem .34rem}
/* Cover thumbs on the index inherit the blur so the front page cannot undo it. */ /* Cover thumbs on the index inherit the blur so the front page cannot undo it. */
.blurred-thumb{filter:blur(16px)} .blurred-thumb{filter:blur(16px)}
/* opaque dark control-scrim + always-light glyph — legible over any thumbnail /* opaque dark control-scrim + always-light glyph — legible over any thumbnail
+26 -7
View File
@@ -1,4 +1,18 @@
{% extends "base.html" %} {% extends "base.html" %}
{# The blur toggle, defined ONCE. There are three item branches in this file
(doc / media / other) and the first cut of this feature patched only one of
them, so docs rendered with no control at all. A macro makes "patched two of
three" impossible rather than merely unlikely. #}
{% macro blurtoggle(name_url, it, cls='') -%}
<form class="blurtoggle {{ cls }}" method="post" action="/b/{{ name_url }}/blur">
<input type="hidden" name="f" value="{{ it.name }}">
<input type="hidden" name="on" value="{{ '0' if it.blurred else '1' }}">
<button title="{{ 'un-blur this item' if it.blurred else 'blur this item — cosmetic only, the file is still served' }}"
aria-label="{{ 'un-blur' if it.blurred else 'blur' }} {{ it.name }}"
>{{ '◉ blurred' if it.blurred else '◌ blur' }}</button>
</form>
{%- endmacro %}
{% block title %}{{ name }} · The Booth{% endblock %} {% block title %}{{ name }} · The Booth{% endblock %}
{% block content %} {% block content %}
<div class="boothhead"> <div class="boothhead">
@@ -109,6 +123,7 @@
<span class="doc-spacer"></span> <span class="doc-spacer"></span>
<a class="doc-act" href="view?f={{ it.url }}" title="open full page">⤢</a> <a class="doc-act" href="view?f={{ it.url }}" title="open full page">⤢</a>
<a class="doc-act" href="{{ it.url }}" download title="download {{ it.name }}">⬇</a> <a class="doc-act" href="{{ it.url }}" download title="download {{ it.name }}">⬇</a>
{{ blurtoggle(name_url, it, 'doc-act') }}
<button type="button" class="doc-act doc-close" title="close (hide for now)" aria-label="close">✕</button> <button type="button" class="doc-act doc-close" title="close (hide for now)" aria-label="close">✕</button>
</summary> </summary>
{% if it.rendered_html %} {% if it.rendered_html %}
@@ -142,17 +157,17 @@
<a class="dl" href="{{ it.url }}" download>⬇ {{ it.name }}</a> <a class="dl" href="{{ it.url }}" download>⬇ {{ it.name }}</a>
{% endif %} {% endif %}
{% if it.kind == 'other' %} {% if it.kind == 'other' %}
{% if it.caption %}<figcaption><span class="cap-text">{{ it.caption }}</span></figcaption>{% endif %} {# Always render the caption row now: it carries the blur toggle, and
"no caption" is not a reason to deny an item its controls. #}
<figcaption>
{% if it.caption %}<span class="cap-text">{{ it.caption }}</span>{% endif %}
{{ blurtoggle(name_url, it) }}
</figcaption>
{% else %} {% else %}
<figcaption> <figcaption>
<a class="dl-link" href="{{ it.url }}" download title="download {{ it.name }}">⬇</a> <a class="dl-link" href="{{ it.url }}" download title="download {{ it.name }}">⬇</a>
<span class="cap-text">{{ it.caption or it.name }}</span> <span class="cap-text">{{ it.caption or it.name }}</span>
<form class="blurtoggle" method="post" action="/b/{{ name_url }}/blur"> {{ blurtoggle(name_url, it) }}
<input type="hidden" name="f" value="{{ it.name }}">
<input type="hidden" name="on" value="{{ '0' if it.blurred else '1' }}">
<button title="{{ 'un-blur this item' if it.blurred else 'blur this item (cosmetic only — the file is still served)' }}"
aria-label="{{ 'un-blur' if it.blurred else 'blur' }} {{ it.name }}">{{ '◉' if it.blurred else '◌' }}</button>
</form>
</figcaption> </figcaption>
{% endif %} {% endif %}
</figure> </figure>
@@ -200,6 +215,10 @@
"collapse". Collapse stays available via the rest of the summary bar. With "collapse". Collapse stays available via the rest of the summary bar. With
JS off the button is inert and collapse via <details> still works. */ JS off the button is inert and collapse via <details> still works. */
(function () { (function () {
/* A form inside <summary> would otherwise collapse the doc on submit. */
document.querySelectorAll('.doc-bar .blurtoggle').forEach(function (f) {
f.addEventListener('click', function (ev) { ev.stopPropagation(); });
});
document.querySelectorAll('.doc-close').forEach(function (btn) { document.querySelectorAll('.doc-close').forEach(function (btn) {
btn.addEventListener('click', function (ev) { btn.addEventListener('click', function (ev) {
ev.preventDefault(); ev.preventDefault();
+40
View File
@@ -1447,3 +1447,43 @@ def test_blur_applies_to_inline_docs_not_just_images(client):
assert body.count('<figure class="item item-doc blurred"') == 1, ( assert body.count('<figure class="item item-doc blurred"') == 1, (
"exactly the blurred doc, not every doc" "exactly the blurred doc, not every doc"
) )
def test_every_item_kind_gets_exactly_one_blur_toggle(client):
"""The regression guard for this whole feature's recurring bug.
booth.html has THREE item branches — doc, media, other — and each pass at
this feature patched some and missed others: first the blurred class landed
on media only, then the toggle landed on media only and the operator asked
"no UI option to blur/unblur?". A count assertion across mixed kinds is the
check that actually catches it; a spot check on one item never will.
"""
c, root = client
d = root / "bo"
d.mkdir()
(d / "note.txt").write_text("doc branch") # -> item-doc
_png(d / "pic.png") # -> item-image
(d / "blob.bin").write_bytes(b"\x00\x01binary") # -> item-other
body = c.get("/b/bo/").text
figures = body.count('<figure class="item item-')
toggles = body.count('class="blurtoggle')
assert figures == 3, f"expected all three kinds to render, got {figures}"
assert toggles == figures, (
f"{figures} items but {toggles} blur toggles — a branch was missed again"
)
def test_blur_toggle_posts_the_opposite_state(client):
"""The button must flip, not just set. A toggle hard-coded to on=1 looks
identical in the markup and silently cannot un-blur."""
c, root = client
d = root / "bo"
d.mkdir()
_png(d / "pic.png")
assert 'name="on" value="1"' in c.get("/b/bo/").text
set_blurred(d, "pic.png", True)
body = c.get("/b/bo/").text
assert 'name="on" value="0"' in body, "a blurred item must offer un-blur"
assert "◉ blurred" in body