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:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user