From 5b06d902a8785974ce556713dff7bca768d2da21 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Tue, 19 May 2026 01:11:41 -0400 Subject: [PATCH] =?UTF-8?q?My=20Sign=20picker=20iteration=203=20=E2=80=94?= =?UTF-8?q?=20SCAN=20SIGN=20landing=20hex=20+=20OK/NVM=20thumbnail=20two-s?= =?UTF-8?q?tep=20+=20duoUser=20picker=20bg=20=E2=80=94=20Sprint=204a-cont?= =?UTF-8?q?=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two-phase picker. Landing phase renders the DRY 1-chair table hex w. a central SCAN SIGN .btn-primary; clicking it swaps the page to picker phase (hex hides, sig-card grid + always-present stage frame + SAVE SIGN visible). Stage frame previews the saved sig on landing if User.significator is set ; sig-card selection lifts the room's two-step OK/NVM-on-thumbnail pattern via `.sig-card-actions` w. `.sig-ok-btn`/`.sig-nvm-btn`: click thumb → `.sig-focused` (CSS reveals OK badge, stage previews card, no lock); click OK → `.sig-reserved--own` (CSS swaps OK→NVM badge, `.sig-stage--frozen` reveals stat block + FLIP, SAVE SIGN enables); click NVM → unlock + clear focus + disable SAVE SIGN ; SAVE SIGN form pinned `position:absolute; bottom:0.75rem; right:1rem` to .my-sign-stage so it stops shifting across the stage row when the stat block reveals on lock (was getting shoved left as a flex item alongside the stat-block reveal) ; .my-sign-page mirrors .room-page's `flex:1; min-height:0; display:flex; flex-direction:column` so the DRY hex container chain propagates real height down into #id_game_table for room.js's scaleTable() to compute against (was reading 0 + leaving the hex unscaled at 200×231 in a 360×320 scene) ; stage min-height gated to picker phase (`.my-sign-page[data-phase="picker"] .my-sign-stage`) — landing-phase stage is natural-sized so the hex centers in the bigger available area instead of being bottom-anchored by a 376px stage reservation ; picker-phase bg uses `rgba(var(--duoUser), 1)` so the transition from "hex face" → "card pile on felt" reads as a continuous surface rather than a context swap ; room sig-select media queries re-scoped to `.sig-overlay .sig-deck-grid` so they don't bleed into my-sign — my-sign gets its own breakpoint cascade: 6×3rem (portrait) → 9×3rem (≥900px landscape) → 18×3rem (≥1600px) → 18×5rem (≥2200px); thresholds bumped from sig-select's 1400/1800px so 18×col + sidebar/footer margins clear the viewport at fluid-rem ceiling (rem=22 → 18×3rem=1188px + 220px margins=1408, safe with 1600px floor) ; default `repeat(6, 1fr)` collapsed to 0-width when paired w. `align-self:center` (no parent width for `fr` to resolve against, hence the dotted-line miniscule cards in portrait); fixed `repeat(6, 3rem)` at portrait default fixes it ; SCAN SIGN font-size 0.75rem (vs .btn-primary's default 0.875rem) so the 2-line "SCAN/SIGN" label fits inside the 4rem circle without crowding the border — treated as a smaller variant via `#id_scan_sign_btn` rule scoped under .my-sign-landing ; room.js's scaleTable() runs on DOMContentLoaded before flex layout flushes (#id_game_table.clientWidth/Height read 0 at that moment) — added `requestAnimationFrame → dispatchEvent('resize')` tick at the end of the inline IIFE so scaleTable re-fires once layout settles ; tests — 6 FTs in test_bill_my_sign.py rewritten for the new flow: test_landing_renders_dry_hex_with_scan_sign_button pins the 1-chair hex + central SCAN SIGN + hidden picker grid; test_scan_sign_click_transitions_to_picker_phase pins the phase swap (hex hides, grid shows); test_click_thumbnail_shows_OK_btn_without_locking pins step 1 (focus + OK appears, no lock yet); test_OK_click_locks_thumbnail_and_enables_save_sign pins step 2 (lock + NVM appears + SAVE SIGN enables + persists to /billboard/ applet); test_NVM_click_deselects_and_disables_save_sign pins NVM unlock cycle; test_landing_previews_saved_sig_on_stage pins the on-load saved-sig preview behavior — all green visually verified across portrait + landscape Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Sonnet 4.6 --- src/functional_tests/test_bill_my_sign.py | 234 ++++++-- src/static_src/scss/_card-deck.scss | 137 ++++- src/templates/apps/billboard/my_sign.html | 637 ++++++++++++---------- 3 files changed, 680 insertions(+), 328 deletions(-) diff --git a/src/functional_tests/test_bill_my_sign.py b/src/functional_tests/test_bill_my_sign.py index c91ed33..d7bdb64 100644 --- a/src/functional_tests/test_bill_my_sign.py +++ b/src/functional_tests/test_bill_my_sign.py @@ -62,14 +62,12 @@ class MySignPickerTest(FunctionalTest): # ── Test 1 ─────────────────────────────────────────────────────────────── - def test_picker_renders_card_grid_from_equipped_deck(self): - """GET /billboard/my-sign/ → page renders w. a card grid + the page - wordmark reads "Game Sign", populated by the user's equipped_deck.""" + def test_landing_renders_dry_hex_with_scan_sign_button(self): + """GET /billboard/my-sign/ → the DRY table hex (1-chair) renders w. + a central .btn-primary reading "SCAN SIGN"; the card grid is hidden + until SCAN SIGN is clicked.""" self.create_pre_authenticated_session(self.email) self.browser.get(self.live_server_url + "/billboard/my-sign/") - # Wordmark. The h2 letter-splitter (base.html) wraps each character - # in its own , so Selenium's `.text` joins them w. newlines — - # strip whitespace before the substring check. self.wait_for( lambda: self.assertIn( "GAMESIGN", @@ -78,26 +76,70 @@ class MySignPickerTest(FunctionalTest): ), ) ) - # Target card present in the grid. The data-card-id selector itself - # is the assertion — find_element raises if absent. Avoid asserting - # against `.fan-corner-rank .text` (CSS hides it via font-size or - # similar, so Selenium returns ""). + # Landing phase markers: hex + 1 chair + SCAN SIGN btn self.wait_for( lambda: self.browser.find_element( - By.CSS_SELECTOR, - f'.my-sign-deck-grid .sig-card[data-card-id="{self.target_card.id}"]', + By.CSS_SELECTOR, ".my-sign-page .table-hex" ) ) + chairs = self.browser.find_elements( + By.CSS_SELECTOR, ".my-sign-page .table-seat" + ) + self.assertEqual(len(chairs), 1, "Landing hex should render exactly 1 chair") + scan_btn = self.browser.find_element(By.ID, "id_scan_sign_btn") + self.assertIn("SCAN", scan_btn.text.upper()) + self.assertIn("SIGN", scan_btn.text.upper()) + # Picker grid not yet visible — landing phase only + grid = self.browser.find_element(By.CSS_SELECTOR, ".my-sign-deck-grid") + self.assertFalse( + grid.is_displayed(), + "Picker grid should be hidden on landing phase", + ) # ── Test 2 ─────────────────────────────────────────────────────────────── - def test_pick_card_then_save_persists_choice_and_shows_in_applet(self): - """Click card → SAVE SIGN btn enables → click → DB updated → applet - on /billboard/ shows the chosen card.""" + def test_scan_sign_click_transitions_to_picker_phase(self): + """Click SCAN SIGN → landing hex hides; picker grid + populated stage + frame appear. Target card present in the grid.""" self.create_pre_authenticated_session(self.email) self.browser.get(self.live_server_url + "/billboard/my-sign/") + scan_btn = self.wait_for( + lambda: self.browser.find_element(By.ID, "id_scan_sign_btn") + ) + self.browser.execute_script("arguments[0].click()", scan_btn) + # Phase swap — picker now visible, hex now hidden + self.wait_for( + lambda: self.assertTrue( + self.browser.find_element( + By.CSS_SELECTOR, ".my-sign-deck-grid" + ).is_displayed(), + "Picker grid should be visible after SCAN SIGN click", + ) + ) + # Hex collapses (display:none) after transition + self.assertFalse( + self.browser.find_element( + By.CSS_SELECTOR, ".my-sign-page .table-hex" + ).is_displayed(), + "Landing hex should hide once picker phase is active", + ) + # Target card present in the grid + self.browser.find_element( + By.CSS_SELECTOR, + f'.my-sign-deck-grid .sig-card[data-card-id="{self.target_card.id}"]', + ) - # Click target card + # ── Test 3 ─────────────────────────────────────────────────────────────── + + def test_click_thumbnail_shows_OK_btn_without_locking(self): + """In picker phase: click a thumbnail → .sig-focused class + OK btn + visible on it; stat block + FLIP still hidden (no lock yet); SAVE + SIGN still disabled.""" + self.create_pre_authenticated_session(self.email) + self.browser.get(self.live_server_url + "/billboard/my-sign/") + self.browser.execute_script( + "document.getElementById('id_scan_sign_btn').click()" + ) card_el = self.wait_for( lambda: self.browser.find_element( By.CSS_SELECTOR, @@ -105,34 +147,79 @@ class MySignPickerTest(FunctionalTest): ) ) self.browser.execute_script("arguments[0].click()", card_el) - - # SAVE SIGN should be enabled now - save_btn = self.wait_for( - lambda: self.browser.find_element(By.ID, "id_save_sign_btn") - ) + # .sig-focused class → OK btn visible self.wait_for( - lambda: self.assertFalse( - save_btn.get_attribute("disabled"), - "SAVE SIGN should be enabled after card click", + lambda: self.assertIn( + "sig-focused", + self.browser.find_element( + By.CSS_SELECTOR, + f'.sig-card[data-card-id="{self.target_card.id}"]', + ).get_attribute("class"), ) ) - - # Hidden card_id input should match the clicked card - self.assertEqual( - str(self.target_card.id), - self.browser.find_element(By.ID, "id_save_sign_card_id").get_attribute("value"), + ok_btn = self.browser.find_element( + By.CSS_SELECTOR, + f'.sig-card[data-card-id="{self.target_card.id}"] .sig-ok-btn', ) + self.assertTrue(ok_btn.is_displayed(), "OK btn should be visible on focused thumb") + # Stat block still hidden (no lock yet) + stage = self.browser.find_element(By.CSS_SELECTOR, ".my-sign-stage") + self.assertNotIn("sig-stage--frozen", stage.get_attribute("class")) + # SAVE SIGN still disabled + save_btn = self.browser.find_element(By.ID, "id_save_sign_btn") + self.assertTrue(save_btn.get_attribute("disabled")) - # Save → DB updated + # ── Test 4 ─────────────────────────────────────────────────────────────── + + def test_OK_click_locks_thumbnail_and_enables_save_sign(self): + """In picker phase: click thumbnail → click its OK btn → stage locks + (.sig-stage--frozen), NVM btn visible on the thumbnail, SAVE SIGN + enables. Then save persists + applet shows the card.""" + self.create_pre_authenticated_session(self.email) + self.browser.get(self.live_server_url + "/billboard/my-sign/") + self.browser.execute_script( + "document.getElementById('id_scan_sign_btn').click()" + ) + card_el = self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, + f'.my-sign-deck-grid .sig-card[data-card-id="{self.target_card.id}"]', + ) + ) + self.browser.execute_script("arguments[0].click()", card_el) + ok_btn = self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, + f'.sig-card[data-card-id="{self.target_card.id}"] .sig-ok-btn', + ) + ) + self.browser.execute_script("arguments[0].click()", ok_btn) + # Lock applied + self.wait_for( + lambda: self.assertIn( + "sig-stage--frozen", + self.browser.find_element( + By.CSS_SELECTOR, ".my-sign-stage" + ).get_attribute("class"), + ) + ) + # NVM btn on the same thumbnail now visible + nvm_btn = self.browser.find_element( + By.CSS_SELECTOR, + f'.sig-card[data-card-id="{self.target_card.id}"] .sig-nvm-btn', + ) + self.assertTrue(nvm_btn.is_displayed(), "NVM btn should be visible after OK confirm") + # SAVE SIGN enabled + save_btn = self.browser.find_element(By.ID, "id_save_sign_btn") + self.assertFalse(save_btn.get_attribute("disabled")) + # Save persists self.browser.execute_script("arguments[0].click()", save_btn) self.wait_for( lambda: self.gamer.refresh_from_db() or self.assertEqual( self.gamer.significator_id, self.target_card.id, ) ) - - # Navigate to /billboard/ → applet shows the saved card. Pin by - # data-card-id (same reasoning as test 1 re: CSS-hidden corner rank). + # Applet on /billboard/ shows the saved card self.browser.get(self.live_server_url + "/billboard/") self.wait_for( lambda: self.browser.find_element( @@ -141,6 +228,87 @@ class MySignPickerTest(FunctionalTest): ) ) + # ── Test 5 ─────────────────────────────────────────────────────────────── + + def test_NVM_click_deselects_and_disables_save_sign(self): + """After OK-lock, click NVM on the thumbnail → lock clears (no + .sig-stage--frozen), .sig-focused removed, SAVE SIGN disables.""" + self.create_pre_authenticated_session(self.email) + self.browser.get(self.live_server_url + "/billboard/my-sign/") + self.browser.execute_script( + "document.getElementById('id_scan_sign_btn').click()" + ) + card_el = self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, + f'.my-sign-deck-grid .sig-card[data-card-id="{self.target_card.id}"]', + ) + ) + self.browser.execute_script("arguments[0].click()", card_el) + ok_btn = self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, + f'.sig-card[data-card-id="{self.target_card.id}"] .sig-ok-btn', + ) + ) + self.browser.execute_script("arguments[0].click()", ok_btn) + # Now NVM + nvm_btn = self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, + f'.sig-card[data-card-id="{self.target_card.id}"] .sig-nvm-btn', + ) + ) + self.browser.execute_script("arguments[0].click()", nvm_btn) + # Lock cleared + self.wait_for( + lambda: self.assertNotIn( + "sig-stage--frozen", + self.browser.find_element( + By.CSS_SELECTOR, ".my-sign-stage" + ).get_attribute("class"), + ) + ) + # SAVE SIGN disables again + save_btn = self.browser.find_element(By.ID, "id_save_sign_btn") + self.assertTrue(save_btn.get_attribute("disabled")) + # .sig-focused removed from thumb + self.assertNotIn( + "sig-focused", + self.browser.find_element( + By.CSS_SELECTOR, + f'.sig-card[data-card-id="{self.target_card.id}"]', + ).get_attribute("class"), + ) + + # ── Test 6 ─────────────────────────────────────────────────────────────── + + def test_landing_previews_saved_sig_on_stage(self): + """If user already has a saved significator, the landing-phase stage + frame should preview the saved card (above/behind the hex).""" + # Pre-save a sig in the DB + self.gamer.significator = self.target_card + self.gamer.save(update_fields=["significator"]) + self.create_pre_authenticated_session(self.email) + self.browser.get(self.live_server_url + "/billboard/my-sign/") + # Landing phase still shows hex w. SCAN SIGN + self.wait_for( + lambda: self.browser.find_element(By.ID, "id_scan_sign_btn") + ) + # Stage card is visible w. the saved card populated (corner-rank + + # name pinned by data-card-id on the stage card wrapper) + stage_card = self.browser.find_element( + By.CSS_SELECTOR, ".my-sign-stage .sig-stage-card" + ) + self.assertTrue( + stage_card.is_displayed(), + "Stage card should preview the saved sig on landing", + ) + self.assertEqual( + stage_card.get_attribute("data-card-id"), + str(self.target_card.id), + ) + # ── Test 3 ─────────────────────────────────────────────────────────────── def test_applet_renders_blank_state_when_no_sig_chosen(self): diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index 6c2a1f3..e9ab5f4 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -616,13 +616,81 @@ html:has(.sig-backdrop) { } // ─── My Sign picker — sizing + state-gated reveal ──────────────────────────── -// Bigger preview card than the room (no shared overlay width budget) — clamp -// scales w. viewport for portrait/landscape both. FLIP btn + stat block hidden -// at rest; revealed by `.sig-stage--frozen` (added by JS on click, cleared by -// NVM). SPIN (orientation 180°) stays in `.sig-stat-block`; FLIP toggles -// polarity (data-polarity attr on .my-sign-page). +// Two-phase layout: landing (DRY 1-chair hex w. SCAN SIGN center) → picker +// (sig-card grid below an always-present stage frame). SAVE SIGN rides +// inside .my-sign-stage to its right, sig-select-style. FLIP btn + stat +// block hidden at rest; revealed by `.sig-stage--frozen` (added by JS on +// OK confirm, cleared by NVM). SPIN (orientation 180°) stays in +// `.sig-stat-block`; FLIP toggles polarity (data-polarity on .my-sign-page). +// .my-sign-page mirrors .room-page's flex-column-fill-aperture pattern so +// the DRY hex inside .my-sign-landing gets a non-zero #id_game_table size +// for room.js's scaleTable() to compute against. Without flex:1 + min-height:0 +// the container chain collapses + the hex renders unscaled (200×231 inside +// a 360×320 scene, looking elongated/portrait). .my-sign-page { --sig-card-w: clamp(140px, 36vw, 220px); + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + position: relative; +} + +// Stage frame — fixed slice in picker phase, natural-sized on landing. +// The picker min-height reserves real estate so hover-preview cards don't +// shift adjacent layout; on landing the stage shrinks to its actual content +// (empty or saved-sig preview) so the DRY hex below gets fair vertical +// space. SAVE SIGN form is absolutely positioned (see below) so it stays +// pinned when the stat block reveals on OK confirm. +.my-sign-stage { + flex: 0 0 auto; + position: relative; +} + +.my-sign-page[data-phase="picker"] .my-sign-stage { + min-height: calc(var(--sig-card-w, 140px) * 8 / 5 + 1.5rem); +} + +// SAVE SIGN form — pinned to the bottom-right of the stage so it stays in +// place across hover/lock states (the stat block reveal would otherwise +// shove a flex-positioned btn around the stage row). +#id_save_sign_form { + position: absolute; + bottom: 0.75rem; + right: 1rem; + margin: 0; + z-index: 6; +} + +// Landing phase — DRY hex container. flex:1 + min-height:0 propagates the +// available vertical space into .room-shell → #id_game_table → scaleTable(). +.my-sign-landing { + flex: 1; + min-height: 0; + display: flex; + + // SCAN SIGN btn — centered in the hex. Default .btn-primary text + // (0.875rem) scales tighter than the room's PICK SIGS btn font; this + // bumps it down a notch so the 2-line "SCAN/SIGN" label sits cleanly + // inside the 4rem circle without crowding the border. + #id_scan_sign_btn { + font-size: 0.75rem; + line-height: 1.1; + white-space: normal; + } +} + +// Hide SAVE SIGN on landing — the form only makes sense once the user +// has entered the picker. Saved-sig preview on landing is read-only. +.my-sign-page[data-phase="landing"] #id_save_sign_form { + display: none; +} + +// Picker phase — bg matches the table hex's interior (--duoUser) so the +// transition from "hex face" → "card pile on felt" reads as a continuous +// surface rather than a context swap. Landing phase keeps the body bg. +.my-sign-page[data-phase="picker"] { + background: rgba(var(--duoUser), 1); } .my-sign-flip-btn { @@ -637,7 +705,7 @@ html:has(.sig-backdrop) { display: none; } -// FLIP btn appears only when the stage is frozen (post-click). Hover-only +// FLIP btn appears only when the stage is frozen (post-OK confirm). Hover-only // previews don't reveal the polarity toggle — the user hasn't committed yet. .my-sign-stage.sig-stage--frozen .my-sign-flip-btn { display: inline-flex; @@ -938,10 +1006,13 @@ html:has(.sig-backdrop) { margin-left: 4rem; margin-right: 3rem; } - .sig-stage { + .sig-overlay .sig-stage { min-width: 0; // allow shrinking in row layout; align-items:flex-end already set } - .sig-deck-grid { + // Scoped to .sig-overlay — the room sig-select modal has its own width + // budget. .my-sign-page gets its own breakpoints below (different col + // counts + thresholds tuned for the full content area). + .sig-overlay .sig-deck-grid { grid-template-columns: repeat(6, 2.5rem); margin: 0; align-self: flex-end; // sit at the bottom of the modal row @@ -954,12 +1025,12 @@ html:has(.sig-backdrop) { flex-direction: column; align-items: stretch; } - .sig-stage { + .sig-overlay .sig-stage { min-width: auto; align-self: stretch; // fill full modal width so JS sizeSigCard() gets correct stageWidth margin-left: 3rem; } - .sig-deck-grid { + .sig-overlay .sig-deck-grid { grid-template-columns: repeat(9, 3rem); align-self: center; } @@ -968,7 +1039,7 @@ html:has(.sig-backdrop) { @media (orientation: landscape) and (min-width: 1400px) { // Wide landscape: 18-card single-row grid. 18×3rem + ~7rem modal margins // clears the viewport here even at the fluid-rem ceiling (rem=22 → ~1376px). - .sig-deck-grid { + .sig-overlay .sig-deck-grid { grid-template-columns: repeat(18, 3rem); } } @@ -976,11 +1047,11 @@ html:has(.sig-backdrop) { @media (orientation: landscape) and (min-width: 1800px) { // Sig overlay: clear doubled sidebars (8rem each instead of 4rem/6rem) .sig-overlay { padding-left: 8rem; padding-right: 8rem; } - .sig-stage { + .sig-overlay .sig-stage { align-self: stretch; // fill full modal width so JS sizeSigCard() gets correct stageWidth margin-left: 3rem; } - .sig-deck-grid { + .sig-overlay .sig-deck-grid { grid-template-columns: repeat(18, 5rem); align-self: center; } @@ -990,6 +1061,46 @@ html:has(.sig-backdrop) { #id_room_menu { right: 2.5rem; } } +// ─── My Sign picker grid ──────────────────────────────────────────────────── +// align-self:center horizontally centers the shrink-to-content grid in +// .my-sign-page's flex column; overflow:visible avoids the modal's hidden +// clip. Col counts ramp up at wider viewports — sig-select's breakpoints +// are tuned for the modal's width budget so we use our own thresholds that +// account for the navbar/footer sidebars (~5rem each) eating viewport width. +// Portrait: fixed rem cols (default repeat(6, 1fr) collapses to 0 width +// w. align-self:center because 1fr has no defined parent to fr against). +.my-sign-deck-grid { + align-self: center; + margin: 1rem auto; + overflow: visible; + grid-template-columns: repeat(6, 3rem); +} + +@media (orientation: landscape) and (min-width: 900px) { + // Middling landscape: 9-card row × 2 (mirrors sig-select's middling step). + .my-sign-deck-grid { + grid-template-columns: repeat(9, 3rem); + } +} + +@media (orientation: landscape) and (min-width: 1600px) { + // Wide landscape: 18-card single row. Bumped from sig-select's 1400px so + // 18×3rem + the doubled-sidebar margins (~10rem) still clears the viewport + // at the fluid-rem ceiling (rem=22 → 18×3rem=1188px + 220px margins = 1408, + // safe with 1600px floor). + .my-sign-deck-grid { + grid-template-columns: repeat(18, 3rem); + } +} + +@media (orientation: landscape) and (min-width: 2200px) { + // XL landscape: 18×5rem. Bumped from sig-select's 1800px — 18×5rem=1980px + // at rem=22 needs ~2200px viewport after sidebar/footer clearance. + .my-sign-deck-grid { + grid-template-columns: repeat(18, 5rem); + } +} + // ── DRAW SEA overlay ───────────────────────────────────────────────────────── // Mirrors .sky-* structure but with columns reversed: // left = transparent (Celtic Cross card positions) diff --git a/src/templates/apps/billboard/my_sign.html b/src/templates/apps/billboard/my_sign.html index f866e05..a85b83b 100644 --- a/src/templates/apps/billboard/my_sign.html +++ b/src/templates/apps/billboard/my_sign.html @@ -5,236 +5,291 @@ {% block header_text %}GameSign{% endblock header_text %} {% block content %} -{# Solo lift of `_sig_select_overlay.html`. Same card-grid + stage-card #} -{# choreography as the room sig-select, minus countdown / WebSocket / #} -{# polarity / multi-user. FLIP btn (.spin-btn) lets the user choose the #} -{# card's orientation; SAVE SIG persists it on the User model. #} -{# "Significator" is preserved at the storage layer (User.significator) + #} -{# game-room context; this billboard surface re-brands to "Sign". #} +{# Two-phase picker. Landing renders the DRY table hex (1-chair) w. a #} +{# central SCAN SIGN btn; the stage frame above previews the user's #} +{# saved sig if any. Clicking SCAN SIGN swaps to picker phase: the hex #} +{# hides, the card grid appears below the stage. Selection is a two-step #} +{# click on the thumbnail itself (matching room sig-select): click thumb #} +{# → OK btn appears; click OK → lock (stat block + FLIP + SAVE SIGN #} +{# enable + NVM appears for deselect). #} +{# "Significator" is preserved at the storage layer (User.significator); #} +{# this billboard surface re-brands to "Sign". #}
- {# Stage frame always visible; stage card + stat block + FLIP btn #} - {# gated by hover (preview) + click (lock). `.sig-stage--frozen` is #} - {# added on click + cleared by NVM. data-polarity moved to the page #} - {# wrapper so descendant .sig-card / .sig-stage-card both get the #} - {# polarity-themed CSS rules. #} -
-