fix(booth): release was painting over the delete ×, which was unclickable

Operator: "release button covers delete button". Measured before touching
anything: release 58x24 at (323,266), × 30x30 at (349,268) — 30x22 px of
overlap on a 30px button, and `elementFromPoint` at the ×'s centre returned the
release form. The × I added yesterday was 100% unclickable from the moment it
shipped.

Cause: both were `position:absolute` on the same corner, each with its own
guessed offset, and `release` is the later sibling so it won. Replaced with one
flex row positioned once — release left, × right at the card corner where the
ephemeral lane's × already lives, so muscle memory transfers and neither can
drift back on top of the other when a label changes width.

Verified by measurement, not inspection: overlap 0 px, and clicks at each
control's centre now land on that control. The ephemeral lane's × and ★ were
re-checked and are unaffected.

ADDS scripts/layout-probe.py, because markup inspection STRUCTURALLY cannot see
this and I have now shipped two dead controls in two days by reading templates
instead of rendering them. It asks a real browser what a click at each
control's centre would hit.

It took four iterations to become trustworthy and the failures are the point:

  1. `top.contains(el)` counted an ANCESTOR overlay as a hit -- the exact case
     it exists to catch. Version one reported OK for a real overlay.
  2. elementFromPoint is viewport-relative, so everything below the fold read
     as occluded and buried the real findings.
  3. getBoundingClientRect() on a WRAPPED INLINE element is the union of its
     line boxes, whose centre lands in the gutter between them -- three zip
     links reported occluded by their own parent. Real geometry, wrong question.

Only the fourth version fires on a genuine overlay while staying silent on the
clean page. Both controls were run; a probe never seen to fail proves nothing.

173 tests pass.
This commit is contained in:
vh
2026-09-21 08:54:56 -07:00
parent 21f4afc033
commit 5f2c9a6a60
4 changed files with 154 additions and 7 deletions
+8 -3
View File
@@ -124,7 +124,13 @@
.badge-kept{background:var(--aus-blue);color:var(--fg-on-accent)}
/* Release sits where the ephemeral card's × sits, but reads as a word rather
than a destructive glyph — it is not the delete, it is what unlocks it. */
.release{position:absolute;top:.4rem;right:.4rem;opacity:0;transition:opacity .12s}
/* One positioned row holds BOTH kept-card controls. They used to pin
themselves to the same corner independently and the later sibling won. */
.kept-actions{position:absolute;top:.4rem;right:.4rem;display:flex;gap:.3rem;
align-items:center;opacity:0;transition:opacity .12s}
.card-kept:hover .kept-actions,.kept-actions:focus-within{opacity:1}
.kept-actions form{position:static;opacity:1;margin:0}
.release{opacity:0;transition:opacity .12s}
.card-kept:hover .release,.release:focus-within{opacity:1}
.release button{font:inherit;font-size:.72rem;line-height:1;padding:.22rem .45rem;
border-radius:.3rem;cursor:pointer;border:1px solid var(--aus-blue);
@@ -255,8 +261,7 @@
.keepit{position:absolute;top:.5rem;left:.5rem;margin:0;opacity:0;transition:opacity .12s}
/* × on a KEPT card. Same shoulder as the ephemeral ×, deliberately tinted so
it does not read as the same weight of action. */
.wipe-kept{position:absolute;top:.5rem;right:.5rem;margin:0;opacity:0;transition:opacity .12s}
.card-kept:hover .wipe-kept,.wipe-kept:focus-within{opacity:1}
/* positioned by .kept-actions, not by itself */
.wipe-kept button{font:inherit;line-height:1;cursor:pointer;border:1px solid var(--line);
border-radius:.3rem;padding:.02rem .3rem;background:var(--bg);color:var(--muted)}
.wipe-kept button:hover{background:var(--aus-red,#ff6b6b);color:var(--fg-on-accent,#fff);
+15 -4
View File
@@ -56,14 +56,25 @@
BUMPS the directory mtime, so the board's age resets and it survives
another full TTL — unkeep-and-wait is a 24h delay, not a delete,
which is exactly why a direct × was worth adding. #}
<form class="wipe wipe-kept" method="post" action="/b/{{ b.name_url }}/delete"
onsubmit="return confirm('WIPE the KEPT booth \u201c{{ b.name }}\u201d?\n\nThis deletes it and its files immediately. Kept booths are the ones nothing else will clean up, so nobody else is going to do this for you — and nothing brings it back.')">
<button title="wipe this KEPT booth now" aria-label="wipe kept booth">×</button>
</form>
{# ⚠ BOTH OF THESE WERE position:absolute ON THE SAME CORNER, and `release`
is the later sibling, so it painted over the × completely: measured
30x22 px of overlap on a 30px button, and elementFromPoint at the ×'s
centre returned the release form. The × was unclickable from the day
it shipped.
One flex row, positioned once, instead of two independently guessed
offsets — so neither control can drift back on top of the other when
a label changes width. #}
<div class="kept-actions">
<form class="release" method="post" action="/b/{{ b.name_url }}/unkeep"
onsubmit="return confirm('Release \u201c{{ b.name }}\u201d?\n\nIt moves to the ephemeral lane so you can wipe it from there. Nothing is deleted by this step.')">
<button title="release this board so it can be wiped">release</button>
</form>
<form class="wipe wipe-kept" method="post" action="/b/{{ b.name_url }}/delete"
onsubmit="return confirm('WIPE the KEPT booth \u201c{{ b.name }}\u201d?\n\nThis deletes it and its files immediately. Kept booths are the ones nothing else will clean up, so nobody else is going to do this for you — and nothing brings it back.')">
<button title="wipe this KEPT booth now" aria-label="wipe kept booth">×</button>
</form>
</div>
</article>
{% endfor %}
</div>