diff --git a/src/apps/epic/static/apps/epic/sig-select.js b/src/apps/epic/static/apps/epic/sig-select.js index df81ca1..5a68a49 100644 --- a/src/apps/epic/static/apps/epic/sig-select.js +++ b/src/apps/epic/static/apps/epic/sig-select.js @@ -98,6 +98,11 @@ var SigSelect = (function () { uprightSel: '#id_stat_keywords_upright', reversedSel: '#id_stat_keywords_reversed', }); + // Fill the rank-chip + title + arcana of the DRY _stat_face.html block + // (unified w. my_sign 2026-06-03) — previously the sig stat-block was a + // reduced label-only copy, so this call was absent and those fields + // stayed blank. + StageCard.populateStatExtras(statBlock, card); stageCard.style.display = ''; stage.classList.add('sig-stage--active'); @@ -113,6 +118,32 @@ var SigSelect = (function () { updateStage(cardEl); } + // ── FLIP — reveal the deck card-back (non-polarized image decks) ────── + // + // my_sign parity (its `_flipToBackAnimated`): a 500ms Y-axis half-rotation + // that swaps the front face for the deck's card-back at the midpoint. The + // back-img + .sig-flip-btn render only for non-polarized image decks + // (template gate), so this no-ops on Earthman/text decks. SPIN orientation + // (.stage-card--reversed) is preserved through the flip. `data-flipping` + // hides the in-card FLIP btn mid-rotate (shared SCSS rule). + function _flipToBack() { + if (!stageCard || stageCard.dataset.flipping) return; + stageCard.dataset.flipping = '1'; + var spin = stageCard.classList.contains('stage-card--reversed') + ? ' rotate(180deg)' : ''; + var rest = 'translateX(0px) rotateY(0deg) scale(1)' + spin; + var mid = rest + ' rotateY(90deg)'; + stageCard.animate([ + { transform: rest }, + { transform: mid, offset: 0.5 }, + { transform: rest }, + ], { duration: 500, easing: 'ease' }); + setTimeout(function () { + stageCard.classList.toggle('is-flipped-to-back'); + }, 250); + setTimeout(function () { delete stageCard.dataset.flipping; }, 500); + } + // ── Hover events ────────────────────────────────────────────────────── function onCardEnter(e) { @@ -482,8 +513,9 @@ var SigSelect = (function () { function _dismissSigOverlay() { _hideCountdown(); _hideTakeSigBtn(); - var backdrop = document.querySelector('.sig-backdrop'); - if (backdrop) backdrop.remove(); + // Removing the felt overlay reveals the hex + waiting message that sit + // behind it in the pane (no separate dark backdrop element anymore — + // the felt lives on .sig-overlay itself since the 2026-06-03 unify). if (overlay) { overlay.remove(); overlay = null; } // Remove all floating cursors (hover + thumbs-up) from the portal Object.keys(_reservedFloats).forEach(function (role) { @@ -594,9 +626,38 @@ var SigSelect = (function () { _flipBtn.addEventListener('click', function () { if (_flipBtn.classList.contains('btn-disabled')) return; statBlock.classList.toggle('is-reversed'); + // data-spinning hides the in-card FLIP btn for the 0.4s CSS rotate + // (shared `.sig-stage-card[data-spinning] .my-sign-flip-btn` rule) + // so it doesn't ride the spin between bottom-left + top-right. + stageCard.dataset.spinning = '1'; stageCard.classList.toggle('stage-card--reversed'); + setTimeout(function () { delete stageCard.dataset.spinning; }, 400); }); + // FLIP-to-back (non-polarized image decks only — btn renders just then). + var sigFlipBtn = stageCard.querySelector('.sig-flip-btn'); + if (sigFlipBtn) { + sigFlipBtn.addEventListener('click', _flipToBack); + } + + // Reserved thumbs-up + hover cursors portal to a body-root fixed + // container, so they'd hang over the reelhouse when the aperture scroll- + // snaps down to it. Drive their visibility straight off the aperture's + // scrollTop: 0 == snapped on the hex (show); any non-zero == the user + // has begun scrolling toward the reelhouse (hide). This vanishes them + // the INSTANT the scroll starts and restores them only once it lands + // fully back on the hex — user-spec timing 2026-06-03. + var _aperture = document.getElementById('id_room_aperture'); + if (_aperture) { + _aperture.addEventListener('scroll', function () { + var portal = document.getElementById('id_sig_cursor_portal'); + // .cursors-hidden hides INSTANTLY (transition:none) at scroll + // start; removing it on the return eases opacity 0→1 over 0.5s + // via the portal's base transition. + if (portal) portal.classList.toggle('cursors-hidden', _aperture.scrollTop > 0); + }, { passive: true }); + } + cautionEl = stage.querySelector('.sig-info'); cautionEffect = cautionEl.querySelector('.sig-info-effect'); cautionTitle = cautionEl.querySelector('.sig-info-title'); diff --git a/src/apps/epic/tests/integrated/test_views.py b/src/apps/epic/tests/integrated/test_views.py index 212029a..eb9b142 100644 --- a/src/apps/epic/tests/integrated/test_views.py +++ b/src/apps/epic/tests/integrated/test_views.py @@ -1494,6 +1494,69 @@ class SigSelectRenderingTest(TestCase): self.assertContains(response, "fyi-next") +class SigSelectUnifiedStageTest(TestCase): + """Sig Select stage unified with the my_sign card-stage apparatus: + the shared DRY _stat_face.html stat-block (rank-chip + title + arcana, + not just keywords), per-card face-image plumbing, and the green + --duoUser felt that replaces the hex-pane content (my_sea-style) instead + of the old fixed dark-Gaussian modal. Founder is PC (levity), mid-pick.""" + + def setUp(self): + self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self) + self.url = reverse("epic:room", kwargs={"room_id": self.room.id}) + + # ── Workstream A — DRY stat-block ─────────────────────────────────────── + def test_stat_block_uses_dry_stat_face_partial(self): + # The overlay must render the SHARED _stat_face.html (rank-chip + + # title + arcana), not the old reduced label-only stat-face. + content = self.client.get(self.url).content.decode() + self.assertIn("stat-face-title", content) + self.assertIn("stat-chip-rank", content) + self.assertIn("stat-face-arcana", content) + # Keyword