Sea Select: keep the sky btn lit on a revisited felt (+ clean felt-swap); slow the glow-handoff ease-out — TDD

Symmetry (user-spec): Sky Select leaves the burger sea btn lit while the sky
felt is up; Sea Select now mirrors it — openSea disables ONLY id_text_btn, no
longer id_sky_btn, so a revisited Sea Select keeps the completed sky one click
away. An adversarial pass flagged that the two felts are equal-z (z-index:5)
siblings and neither open path drops the other's open-class, so leaving the sky
btn lit and clicking it from inside the sea felt would DOUBLE-OPEN (sea paints
over sky → confusing no-op) — the same latent stack already reachable in the
sky->sea direction on a both-complete reload. Fixed in BOTH directions: openSea
now closes the sky felt first + openSky closes the sea felt first (each exposes
its close on window: closeSeaFelt / closeSkyFelt), so clicking the other phase's
lit btn performs a clean SWAP. The text-btn disable/restore chain stays correct
across the swap (closeSky restores text, openSea re-disables + recaches it).

Glow: the glow-handoff pulse ease-OUT now runs ~2.8s (was ~1.4s) — moved the
cycle to 3.2s with the bright peak at 12.5%, keeping the ease-IN swell at ~0.4s
(user asked for a longer linger).

[[feedback-felt-aperture-fill-covers-felt]] [[feedback-inline-partial-script-defer-for-later-partial]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-08 00:57:24 -04:00
parent 82f4af9bcc
commit 945d110171
4 changed files with 53 additions and 10 deletions

View File

@@ -4270,6 +4270,25 @@ class PickSeaUnifiedFeltTest(TestCase):
[sea] = parsed.cssselect("#id_pick_sea_btn")
self.assertIn("hex-phase-btn--out", sea.get("class", ""))
def test_sea_felt_leaves_sky_btn_lit_and_swaps_cleanly(self):
"""Symmetric with Sky Select (which leaves the sea btn lit while the sky
felt is up): opening the Sea Select felt must NOT grey the burger Sky
sub-btn — it stays .active so a revisited Sea Select keeps the completed
sky one click away (user-spec 2026-06-08). openSea therefore disables
ONLY id_text_btn (not id_sky_btn). And because both felts are z-5 siblings
that otherwise STACK (sea paints over sky; neither open path drops the
other's open-class), clicking the lit sky btn must SWAP felts: openSea
closes the sky felt + openSky closes the sea felt (both expose their close
on window). Regression guard for the symmetry + clean-swap fix."""
content = self.client.get(self.url).content.decode()
# openSea disables only the Text btn now — the old 2-btn list is gone.
self.assertIn("['id_text_btn']", content)
self.assertNotIn("'id_text_btn', 'id_sky_btn'", content)
# Clean felt-swap wiring both ways (sea exposes + sky calls it; sky
# exposes + sea calls it).
self.assertIn("window.closeSeaFelt", content)
self.assertIn("window.closeSkyFelt", content)
def test_sea_btn_and_glow_bindings_defer_past_parse_time(self):
"""room.html includes _sea_overlay.html (~line 77) BEFORE _burger.html
(~line 167, which holds #id_burger_btn + #id_sea_btn). So the overlay's

View File

@@ -191,13 +191,14 @@
#id_sea_btn.glow-handoff {
color: rgba(var(--priId), 1);
border-color: rgba(var(--priId), 1);
animation: glow-handoff-pulse 1.8s infinite;
animation: glow-handoff-pulse 3.2s infinite;
}
// Quick ease-IN to the bright peak over the first ~22% of the cycle, then a slow
// ease-OUT fade back to the dim trough across the remaining ~78%. The per-segment
// timing fns set the in/out asymmetry; the lopsided keystop split makes the swell
// feel snappy and the decay linger.
// Quick ease-IN to the bright peak over the first ~12.5% of the cycle (~0.4s),
// then a slow ease-OUT fade back to the dim trough across the remaining ~87.5%
// (~2.8s — roughly twice the earlier decay; user asked for a longer linger). The
// per-segment timing fns set the in/out asymmetry; the lopsided keystop split
// keeps the swell snappy while the fade lingers.
@keyframes glow-handoff-pulse {
0% {
box-shadow:
@@ -205,7 +206,7 @@
0 0 0.7rem 0.1rem rgba(var(--ninUser), 0.12);
animation-timing-function: ease-in;
}
22% {
12% {
box-shadow:
0 0 0.6rem 0.15rem rgba(var(--ninUser), 0.95),
0 0 1.4rem 0.4rem rgba(var(--ninUser), 0.55);

View File

@@ -449,13 +449,18 @@ via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
}
hidden.addEventListener('change', sync);
// ── Open / close the felt (DRAW SEA ⇄ gear NVM nav). On open: disable the
// burger Text + Sky sub-btns. The sea btn is NOT activated here — it's the
// post-completion reopen affordance (see the cascade + the reopen binding).
// ── Open / close the felt (DRAW SEA ⇄ gear NVM nav). On open: disable ONLY the
// burger Text sub-btn (its swipe machine would drive into a scroll-locked
// reelhouse). The SKY sub-btn is left .active (lit) — symmetric with Sky Select,
// which leaves the sea btn lit while the sky felt is up (user-spec 2026-06-08):
// both completed phases stay one click apart. The sea btn itself is NOT
// activated here — it's the post-completion reopen affordance (see the cascade
// + the reopen binding). Clicking the lit sky btn from here SWAPS felts cleanly
// (openSea/openSky close each other — see openSea below).
var _disabledBtns = [];
function _disablePhaseBtns() {
_disabledBtns = [];
['id_text_btn', 'id_sky_btn'].forEach(function (id) {
['id_text_btn'].forEach(function (id) {
var b = document.getElementById(id);
if (b && b.classList.contains('active')) { b.classList.remove('active'); _disabledBtns.push(b); }
});
@@ -465,6 +470,13 @@ via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
_disabledBtns = [];
}
function openSea() {
// Clean SWAP, not a double-open: if the sky felt is up (its lit sea btn was
// just clicked) close it first — both felts are z-5 siblings that otherwise
// stack (sea paints over sky), and neither open path drops the other's
// open-class on its own. Mirrored by openSky closing the sea felt.
if (document.documentElement.classList.contains('sky-open') && window.closeSkyFelt) {
window.closeSkyFelt();
}
document.documentElement.classList.add('sea-open');
document.documentElement.classList.add('sea-entered');
_disablePhaseBtns();
@@ -476,6 +488,7 @@ via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
if (window.RoomViews && window.RoomViews.syncGear) window.RoomViews.syncGear();
}
window.openSeaFelt = openSea;
window.closeSeaFelt = closeSea;
var pickSeaBtn = document.getElementById('id_pick_sea_btn');
if (pickSeaBtn) pickSeaBtn.addEventListener('click', openSea);

View File

@@ -184,6 +184,13 @@
}
function openSky() {
// Clean SWAP, not a double-open: if the sea felt is up (its lit sky btn was
// just clicked) close it first. Both felts are z-5 siblings that otherwise
// stack; neither open path drops the other's open-class on its own. Mirrored
// by openSea closing the sky felt (user-spec 2026-06-08).
if (document.documentElement.classList.contains('sea-open') && window.closeSeaFelt) {
window.closeSeaFelt();
}
document.documentElement.classList.add('sky-open');
_disableTextBtn();
// Re-sync the room gear to the sky NVM pane (room-views.js owns the gear
@@ -624,6 +631,9 @@
// Exposed so the burger fan's #id_sky_btn (active once the sky is saved) can
// reopen the saved wheel. burger-btn.js calls window.openSkyFelt().
window.openSkyFelt = openSky;
// Exposed so openSea can close the sky felt for a clean felt-swap when a user
// clicks the lit sky btn from inside the sea felt (user-spec 2026-06-08).
window.closeSkyFelt = closeSky;
// ── Restore persisted form data + prime a saved (confirmed) wheel ───────────
// Pre-save the wheel draw is deferred to openSky (form shows alone). Once the