My Sign picker iteration 3 — SCAN SIGN landing hex + OK/NVM thumbnail two-step + duoUser picker bg — Sprint 4a-cont — TDD

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 <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-19 01:11:41 -04:00
parent ab5b4c95dd
commit 5b06d902a8
3 changed files with 680 additions and 328 deletions

View File

@@ -62,14 +62,12 @@ class MySignPickerTest(FunctionalTest):
# ── Test 1 ─────────────────────────────────────────────────────────────── # ── Test 1 ───────────────────────────────────────────────────────────────
def test_picker_renders_card_grid_from_equipped_deck(self): def test_landing_renders_dry_hex_with_scan_sign_button(self):
"""GET /billboard/my-sign/ → page renders w. a card grid + the page """GET /billboard/my-sign/ → the DRY table hex (1-chair) renders w.
wordmark reads "Game Sign", populated by the user's equipped_deck.""" 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.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/billboard/my-sign/") self.browser.get(self.live_server_url + "/billboard/my-sign/")
# Wordmark. The h2 letter-splitter (base.html) wraps each character
# in its own <span>, so Selenium's `.text` joins them w. newlines —
# strip whitespace before the substring check.
self.wait_for( self.wait_for(
lambda: self.assertIn( lambda: self.assertIn(
"GAMESIGN", "GAMESIGN",
@@ -78,26 +76,70 @@ class MySignPickerTest(FunctionalTest):
), ),
) )
) )
# Target card present in the grid. The data-card-id selector itself # Landing phase markers: hex + 1 chair + SCAN SIGN btn
# 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 "").
self.wait_for( self.wait_for(
lambda: self.browser.find_element( lambda: self.browser.find_element(
By.CSS_SELECTOR, By.CSS_SELECTOR, ".my-sign-page .table-hex"
f'.my-sign-deck-grid .sig-card[data-card-id="{self.target_card.id}"]',
) )
) )
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 ─────────────────────────────────────────────────────────────── # ── Test 2 ───────────────────────────────────────────────────────────────
def test_pick_card_then_save_persists_choice_and_shows_in_applet(self): def test_scan_sign_click_transitions_to_picker_phase(self):
"""Click card → SAVE SIGN btn enables → click → DB updated → applet """Click SCAN SIGN → landing hex hides; picker grid + populated stage
on /billboard/ shows the chosen card.""" frame appear. Target card present in the grid."""
self.create_pre_authenticated_session(self.email) self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/billboard/my-sign/") 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( card_el = self.wait_for(
lambda: self.browser.find_element( lambda: self.browser.find_element(
By.CSS_SELECTOR, By.CSS_SELECTOR,
@@ -105,34 +147,79 @@ class MySignPickerTest(FunctionalTest):
) )
) )
self.browser.execute_script("arguments[0].click()", card_el) self.browser.execute_script("arguments[0].click()", card_el)
# .sig-focused class → OK btn visible
# SAVE SIGN should be enabled now
save_btn = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_save_sign_btn")
)
self.wait_for( self.wait_for(
lambda: self.assertFalse( lambda: self.assertIn(
save_btn.get_attribute("disabled"), "sig-focused",
"SAVE SIGN should be enabled after card click", self.browser.find_element(
By.CSS_SELECTOR,
f'.sig-card[data-card-id="{self.target_card.id}"]',
).get_attribute("class"),
) )
) )
ok_btn = self.browser.find_element(
# Hidden card_id input should match the clicked card By.CSS_SELECTOR,
self.assertEqual( f'.sig-card[data-card-id="{self.target_card.id}"] .sig-ok-btn',
str(self.target_card.id),
self.browser.find_element(By.ID, "id_save_sign_card_id").get_attribute("value"),
) )
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.browser.execute_script("arguments[0].click()", save_btn)
self.wait_for( self.wait_for(
lambda: self.gamer.refresh_from_db() or self.assertEqual( lambda: self.gamer.refresh_from_db() or self.assertEqual(
self.gamer.significator_id, self.target_card.id, self.gamer.significator_id, self.target_card.id,
) )
) )
# Applet on /billboard/ shows the saved card
# Navigate to /billboard/ → applet shows the saved card. Pin by
# data-card-id (same reasoning as test 1 re: CSS-hidden corner rank).
self.browser.get(self.live_server_url + "/billboard/") self.browser.get(self.live_server_url + "/billboard/")
self.wait_for( self.wait_for(
lambda: self.browser.find_element( 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 ─────────────────────────────────────────────────────────────── # ── Test 3 ───────────────────────────────────────────────────────────────
def test_applet_renders_blank_state_when_no_sig_chosen(self): def test_applet_renders_blank_state_when_no_sig_chosen(self):

View File

@@ -616,13 +616,81 @@ html:has(.sig-backdrop) {
} }
// ─── My Sign picker — sizing + state-gated reveal ──────────────────────────── // ─── My Sign picker — sizing + state-gated reveal ────────────────────────────
// Bigger preview card than the room (no shared overlay width budget) — clamp // Two-phase layout: landing (DRY 1-chair hex w. SCAN SIGN center) → picker
// scales w. viewport for portrait/landscape both. FLIP btn + stat block hidden // (sig-card grid below an always-present stage frame). SAVE SIGN rides
// at rest; revealed by `.sig-stage--frozen` (added by JS on click, cleared by // inside .my-sign-stage to its right, sig-select-style. FLIP btn + stat
// NVM). SPIN (orientation 180°) stays in `.sig-stat-block`; FLIP toggles // block hidden at rest; revealed by `.sig-stage--frozen` (added by JS on
// polarity (data-polarity attr on .my-sign-page). // 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 { .my-sign-page {
--sig-card-w: clamp(140px, 36vw, 220px); --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 { .my-sign-flip-btn {
@@ -637,7 +705,7 @@ html:has(.sig-backdrop) {
display: none; 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. // previews don't reveal the polarity toggle — the user hasn't committed yet.
.my-sign-stage.sig-stage--frozen .my-sign-flip-btn { .my-sign-stage.sig-stage--frozen .my-sign-flip-btn {
display: inline-flex; display: inline-flex;
@@ -938,10 +1006,13 @@ html:has(.sig-backdrop) {
margin-left: 4rem; margin-left: 4rem;
margin-right: 3rem; margin-right: 3rem;
} }
.sig-stage { .sig-overlay .sig-stage {
min-width: 0; // allow shrinking in row layout; align-items:flex-end already set 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); grid-template-columns: repeat(6, 2.5rem);
margin: 0; margin: 0;
align-self: flex-end; // sit at the bottom of the modal row align-self: flex-end; // sit at the bottom of the modal row
@@ -954,12 +1025,12 @@ html:has(.sig-backdrop) {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.sig-stage { .sig-overlay .sig-stage {
min-width: auto; min-width: auto;
align-self: stretch; // fill full modal width so JS sizeSigCard() gets correct stageWidth align-self: stretch; // fill full modal width so JS sizeSigCard() gets correct stageWidth
margin-left: 3rem; margin-left: 3rem;
} }
.sig-deck-grid { .sig-overlay .sig-deck-grid {
grid-template-columns: repeat(9, 3rem); grid-template-columns: repeat(9, 3rem);
align-self: center; align-self: center;
} }
@@ -968,7 +1039,7 @@ html:has(.sig-backdrop) {
@media (orientation: landscape) and (min-width: 1400px) { @media (orientation: landscape) and (min-width: 1400px) {
// Wide landscape: 18-card single-row grid. 18×3rem + ~7rem modal margins // 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). // 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); grid-template-columns: repeat(18, 3rem);
} }
} }
@@ -976,11 +1047,11 @@ html:has(.sig-backdrop) {
@media (orientation: landscape) and (min-width: 1800px) { @media (orientation: landscape) and (min-width: 1800px) {
// Sig overlay: clear doubled sidebars (8rem each instead of 4rem/6rem) // Sig overlay: clear doubled sidebars (8rem each instead of 4rem/6rem)
.sig-overlay { padding-left: 8rem; padding-right: 8rem; } .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 align-self: stretch; // fill full modal width so JS sizeSigCard() gets correct stageWidth
margin-left: 3rem; margin-left: 3rem;
} }
.sig-deck-grid { .sig-overlay .sig-deck-grid {
grid-template-columns: repeat(18, 5rem); grid-template-columns: repeat(18, 5rem);
align-self: center; align-self: center;
} }
@@ -990,6 +1061,46 @@ html:has(.sig-backdrop) {
#id_room_menu { right: 2.5rem; } #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 ───────────────────────────────────────────────────────── // ── DRAW SEA overlay ─────────────────────────────────────────────────────────
// Mirrors .sky-* structure but with columns reversed: // Mirrors .sky-* structure but with columns reversed:
// left = transparent (Celtic Cross card positions) // left = transparent (Celtic Cross card positions)

View File

@@ -5,236 +5,291 @@
{% block header_text %}<span>Game</span><span>Sign</span>{% endblock header_text %} {% block header_text %}<span>Game</span><span>Sign</span>{% endblock header_text %}
{% block content %} {% block content %}
{# Solo lift of `_sig_select_overlay.html`. Same card-grid + stage-card #} {# Two-phase picker. Landing renders the DRY table hex (1-chair) w. a #}
{# choreography as the room sig-select, minus countdown / WebSocket / #} {# central SCAN SIGN btn; the stage frame above previews the user's #}
{# polarity / multi-user. FLIP btn (.spin-btn) lets the user choose the #} {# saved sig if any. Clicking SCAN SIGN swaps to picker phase: the hex #}
{# card's orientation; SAVE SIG persists it on the User model. #} {# hides, the card grid appears below the stage. Selection is a two-step #}
{# "Significator" is preserved at the storage layer (User.significator) + #} {# click on the thumbnail itself (matching room sig-select): click thumb #}
{# game-room context; this billboard surface re-brands to "Sign". #} {# → 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". #}
<div class="my-sign-page" <div class="my-sign-page"
data-phase="landing"
data-save-url="{% url 'billboard:save_sign' %}" data-save-url="{% url 'billboard:save_sign' %}"
{% if current_significator %}data-current-card-id="{{ current_significator.id }}"{% endif %} {% if current_significator %}data-current-card-id="{{ current_significator.id }}"{% endif %}
data-current-reversed="{{ current_significator_reversed|yesno:'true,false' }}" data-current-reversed="{{ current_significator_reversed|yesno:'true,false' }}"
data-polarity="{% if current_significator_reversed %}levity{% else %}gravity{% endif %}"> data-polarity="{% if current_significator_reversed %}levity{% else %}gravity{% endif %}">
{# Stage frame always visible; stage card + stat block + FLIP btn #} {# Stage frame always reserved at the top of the page; SAVE SIGN + #}
{# gated by hover (preview) + click (lock). `.sig-stage--frozen` is #} {# NVM ride along to its right (sig-select-style). The stage card #}
{# added on click + cleared by NVM. data-polarity moved to the page #} {# itself starts hidden + appears on hover/preview or saved-sig #}
{# wrapper so descendant .sig-card / .sig-stage-card both get the #} {# preview; .sig-stage--frozen is added on OK confirm + cleared by #}
{# polarity-themed CSS rules. #} {# NVM. data-polarity lives on .my-sign-page so descendant .sig-card #}
<div class="sig-stage my-sign-stage"> {# / .sig-stage-card both pick up polarity-themed CSS rules. #}
<div class="sig-stage-card" style="display:none"> <div class="sig-stage my-sign-stage">
<div class="fan-card-corner fan-card-corner--tl"> <div class="sig-stage-card" style="display:none"
<span class="fan-corner-rank"></span> {% if current_significator %}data-card-id="{{ current_significator.id }}"{% endif %}>
<i class="fa-solid stage-suit-icon" style="display:none"></i> <div class="fan-card-corner fan-card-corner--tl">
<span class="fan-corner-rank"></span>
<i class="fa-solid stage-suit-icon" style="display:none"></i>
</div>
<div class="fan-card-face">
<div class="fan-card-face-upright">
<p class="fan-card-name-group"></p>
<p class="sig-qualifier-above"></p>
<p class="fan-card-name"></p>
<p class="sig-qualifier-below"></p>
</div> </div>
<div class="fan-card-face"> <p class="fan-card-arcana"></p>
<div class="fan-card-face-upright"> <div class="fan-card-face-reversal">
<p class="fan-card-name-group"></p> <p class="fan-card-reversal-name"></p>
<p class="sig-qualifier-above"></p> <p class="fan-card-reversal-qualifier"></p>
<p class="fan-card-name"></p>
<p class="sig-qualifier-below"></p>
</div>
<p class="fan-card-arcana"></p>
<div class="fan-card-face-reversal">
<p class="fan-card-reversal-name"></p>
<p class="fan-card-reversal-qualifier"></p>
</div>
</div>
<div class="fan-card-corner fan-card-corner--br">
<span class="fan-corner-rank"></span>
<i class="fa-solid stage-suit-icon" style="display:none"></i>
</div> </div>
</div> </div>
{# FLIP — bottom-left of the stage card. Mirrors the game-kit fan #} <div class="fan-card-corner fan-card-corner--br">
{# carousel's btn-reveal pattern (game_kit.html#id_fan_flip). Toggles #} <span class="fan-corner-rank"></span>
{# polarity (data-polarity attr + persisted User.significator_reversed). #} <i class="fa-solid stage-suit-icon" style="display:none"></i>
{# SPIN in the stat-block toggles orientation (180° rotation) for #}
{# preview only — not persisted. #}
<button class="btn btn-reveal my-sign-flip-btn" type="button">FLIP</button>
<div class="sig-stat-block">
<button class="btn btn-reverse spin-btn" type="button">SPIN</button>
<button class="btn btn-info fyi-btn" type="button">FYI</button>
<div class="stat-face stat-face--upright">
<p class="stat-face-label">Emanation</p>
<ul class="stat-keywords"></ul>
</div>
<div class="stat-face stat-face--reversed">
<p class="stat-face-label">Reversal</p>
<ul class="stat-keywords"></ul>
</div>
{% include "apps/gameboard/_partials/_sig_fyi_panel.html" with panel_id="id_my_sign_fyi_panel" %}
</div> </div>
</div> </div>
{# FLIP — bottom-left of the stage card. Visible only after lock #}
<div class="sig-deck-grid my-sign-deck-grid"> {# (.sig-stage--frozen). #}
{% for card in cards %} <button class="btn btn-reveal my-sign-flip-btn" type="button">FLIP</button>
<div class="sig-card" <div class="sig-stat-block">
data-card-id="{{ card.id }}" <button class="btn btn-reverse spin-btn" type="button">SPIN</button>
data-suit-icon="{{ card.suit_icon }}" <button class="btn btn-info fyi-btn" type="button">FYI</button>
data-corner-rank="{{ card.corner_rank }}" <div class="stat-face stat-face--upright">
data-name-group="{{ card.name_group }}" <p class="stat-face-label">Emanation</p>
data-name-title="{{ card.name_title }}" <ul class="stat-keywords"></ul>
data-arcana="{{ card.get_arcana_display }}" </div>
data-correspondence="{{ card.correspondence|default:'' }}" <div class="stat-face stat-face--reversed">
data-keywords-upright="{{ card.keywords_upright|join:',' }}" <p class="stat-face-label">Reversal</p>
data-keywords-reversed="{{ card.keywords_reversed|join:',' }}" <ul class="stat-keywords"></ul>
data-energies="{{ card.energies_json }}" </div>
data-operations="{{ card.operations_json }}" {% include "apps/gameboard/_partials/_sig_fyi_panel.html" with panel_id="id_my_sign_fyi_panel" %}
data-levity-qualifier="{{ card.levity_qualifier }}"
data-gravity-qualifier="{{ card.gravity_qualifier }}"
data-reversal-qualifier="{{ card.reversal_qualifier }}"
data-levity-emanation="{{ card.levity_emanation }}"
data-gravity-emanation="{{ card.gravity_emanation }}"
data-levity-reversal="{{ card.levity_reversal }}"
data-gravity-reversal="{{ card.gravity_reversal }}"
data-italic-word="{{ card.italic_word }}">
<div class="fan-card-corner fan-card-corner--tl">
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
</div>
</div>
{% endfor %}
</div> </div>
{# SAVE SIGN + NVM form lives inside the stage so the layout #}
{# matches the room's sig-select: btn sits adjacent to the stat #}
{# block. Disabled until a card is OK-confirmed. #}
<form id="id_save_sign_form" method="POST" action="{% url 'billboard:save_sign' %}"> <form id="id_save_sign_form" method="POST" action="{% url 'billboard:save_sign' %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="card_id" id="id_save_sign_card_id" value="{{ current_significator.id|default:'' }}"> <input type="hidden" name="card_id" id="id_save_sign_card_id" value="{{ current_significator.id|default:'' }}">
<input type="hidden" name="reversed" id="id_save_sign_reversed" value="{{ current_significator_reversed|yesno:'1,0' }}"> <input type="hidden" name="reversed" id="id_save_sign_reversed" value="{{ current_significator_reversed|yesno:'1,0' }}">
<button type="submit" id="id_save_sign_btn" class="btn btn-primary"{% if not current_significator %} disabled{% endif %}>SAVE SIGN</button> <button type="submit" id="id_save_sign_btn" class="btn btn-primary"{% if not current_significator %} disabled{% endif %}>SAVE SIGN</button>
<button type="button" id="id_nvm_sign_btn" class="btn btn-cancel"{% if not current_significator %} style="display:none"{% endif %}>NVM</button>
</form> </form>
</div>
{# Picker JS — click .sig-card to pick + populate stage preview via #} {# Landing phase — DRY table hex w. a single chair + central SCAN #}
{# StageCard.populateCard (shared w. room sig-select + sea-select). #} {# SIGN btn. Reuses the room's hex shell (.room-shell > .room-table #}
{# FLIP toggles `.stage-card--reversed` on the preview AND the #} {# > .room-table-scene > .table-hex-border > .table-hex > #}
{# hidden `reversed` input that gets POSTed on SAVE SIGN. #} {# .table-center) + room.js's scaleTable() for viewport-fluid sizing. #}
<script src="{% static 'apps/epic/stage-card.js' %}"></script> <div class="my-sign-landing">
<script> <div class="room-shell">
(function () { <div id="id_game_table" class="room-table">
var grid = document.querySelector('.my-sign-deck-grid'); <div class="room-table-scene">
if (!grid) return; <div class="table-hex-border">
var pageEl = document.querySelector('.my-sign-page'); <div class="table-hex">
var stage = document.querySelector('.my-sign-stage'); <div class="table-center">
var stageCard = stage.querySelector('.sig-stage-card'); <button id="id_scan_sign_btn" type="button" class="btn btn-primary">SCAN<br>SIGN</button>
var statBlock = stage.querySelector('.sig-stat-block'); </div>
var cardIdInput = document.getElementById('id_save_sign_card_id'); </div>
var saveBtn = document.getElementById('id_save_sign_btn'); </div>
var nvmBtn = document.getElementById('id_nvm_sign_btn'); {# Single founder chair — solo-coded but extensible to the #}
var spinBtn = stage.querySelector('.spin-btn'); {# 6-chair friend-invite plan in [[project-my-sea-roadmap]]. #}
var flipBtn = stage.querySelector('.my-sign-flip-btn'); <div class="table-seat" data-slot="1" data-role="PC">
var fyiBtn = stage.querySelector('.fyi-btn'); <i class="fa-solid fa-chair"></i>
var fyiPanel = stage.querySelector('.sig-info'); </div>
var fyiPrev = stage.querySelector('.fyi-prev'); </div>
var fyiNext = stage.querySelector('.fyi-next'); </div>
var revInput = document.getElementById('id_save_sign_reversed'); </div>
var _currentCard = null; </div>
var _focusedCardEl = null;
var _locked = false; // true after click; cleared by NVM
var _fyiData = [];
var _fyiIdx = 0;
// Default polarity = gravity (significator_reversed=False); {# Picker phase — card grid, hidden until SCAN SIGN click. Each #}
// FLIP toggles to levity (significator_reversed=True). Mirrors the {# .sig-card gets .sig-card-actions w. OK + NVM buttons (CSS gates #}
// user's "gravity-rightside-up by default" design from the Game {# visibility via .sig-focused → OK / .sig-reserved--own → NVM). #}
// Kit fan carousel. <div class="sig-deck-grid my-sign-deck-grid" style="display:none">
function _polarity() { {% for card in cards %}
return revInput.value === '1' ? 'levity' : 'gravity'; <div class="sig-card"
} data-card-id="{{ card.id }}"
data-suit-icon="{{ card.suit_icon }}"
data-corner-rank="{{ card.corner_rank }}"
data-name-group="{{ card.name_group }}"
data-name-title="{{ card.name_title }}"
data-arcana="{{ card.get_arcana_display }}"
data-correspondence="{{ card.correspondence|default:'' }}"
data-keywords-upright="{{ card.keywords_upright|join:',' }}"
data-keywords-reversed="{{ card.keywords_reversed|join:',' }}"
data-energies="{{ card.energies_json }}"
data-operations="{{ card.operations_json }}"
data-levity-qualifier="{{ card.levity_qualifier }}"
data-gravity-qualifier="{{ card.gravity_qualifier }}"
data-reversal-qualifier="{{ card.reversal_qualifier }}"
data-levity-emanation="{{ card.levity_emanation }}"
data-gravity-emanation="{{ card.gravity_emanation }}"
data-levity-reversal="{{ card.levity_reversal }}"
data-gravity-reversal="{{ card.gravity_reversal }}"
data-italic-word="{{ card.italic_word }}">
<div class="fan-card-corner fan-card-corner--tl">
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
</div>
<div class="sig-card-actions">
<button class="sig-ok-btn btn btn-confirm" type="button">OK</button>
<button class="sig-nvm-btn btn btn-cancel" type="button">NVM</button>
</div>
</div>
{% endfor %}
</div>
function _populateStage(cardEl) { {# Picker JS — phase swap (landing → picker), hover preview, two-step #}
_focusedCardEl = cardEl; {# OK/NVM-on-thumbnail selection cycle, FLIP polarity animation lifted #}
_currentCard = StageCard.fromDataset(cardEl); {# from game-kit.js's _flipActive, SPIN orientation toggle. data- #}
StageCard.populateCard(stageCard, _currentCard, _polarity()); {# polarity moved to the page wrapper so descendant .sig-card + #}
StageCard.populateKeywords(statBlock, {# .sig-stage-card both pick up the polarity-themed CSS rules. #}
_currentCard.keywords_upright, _currentCard.keywords_reversed); <script src="{% static 'apps/epic/stage-card.js' %}"></script>
_fyiData = StageCard.buildInfoData(_currentCard); <script src="{% static 'apps/epic/room.js' %}"></script>
_fyiIdx = 0; <script>
if (fyiPanel) StageCard.renderFyi(fyiPanel, _fyiData, _fyiIdx); (function () {
stageCard.style.display = ''; var pageEl = document.querySelector('.my-sign-page');
} if (!pageEl) return;
var landing = pageEl.querySelector('.my-sign-landing');
var grid = pageEl.querySelector('.my-sign-deck-grid');
var stage = pageEl.querySelector('.my-sign-stage');
var stageCard = stage.querySelector('.sig-stage-card');
var statBlock = stage.querySelector('.sig-stat-block');
var scanBtn = document.getElementById('id_scan_sign_btn');
var cardIdInput= document.getElementById('id_save_sign_card_id');
var saveBtn = document.getElementById('id_save_sign_btn');
var spinBtn = stage.querySelector('.spin-btn');
var flipBtn = stage.querySelector('.my-sign-flip-btn');
var fyiBtn = stage.querySelector('.fyi-btn');
var fyiPanel = stage.querySelector('.sig-info');
var fyiPrev = stage.querySelector('.fyi-prev');
var fyiNext = stage.querySelector('.fyi-next');
var revInput = document.getElementById('id_save_sign_reversed');
var _currentCard = null;
var _focusedCardEl = null;
var _locked = false;
var _fyiData = [];
var _fyiIdx = 0;
function _clearStage() { function _polarity() {
stageCard.style.display = 'none'; return revInput.value === '1' ? 'levity' : 'gravity';
_focusedCardEl = null; }
_currentCard = null;
}
function _lock(cardEl) { function _populateStage(cardEl) {
_locked = true; _focusedCardEl = cardEl;
_populateStage(cardEl); _currentCard = StageCard.fromDataset(cardEl);
grid.querySelectorAll('.sig-card.sig-focused').forEach(function (c) { StageCard.populateCard(stageCard, _currentCard, _polarity());
if (c !== cardEl) c.classList.remove('sig-focused'); StageCard.populateKeywords(statBlock,
}); _currentCard.keywords_upright, _currentCard.keywords_reversed);
cardEl.classList.add('sig-focused'); _fyiData = StageCard.buildInfoData(_currentCard);
cardIdInput.value = cardEl.dataset.cardId; _fyiIdx = 0;
saveBtn.removeAttribute('disabled'); if (fyiPanel) StageCard.renderFyi(fyiPanel, _fyiData, _fyiIdx);
if (nvmBtn) nvmBtn.style.display = ''; stageCard.style.display = '';
stage.classList.add('sig-stage--frozen'); stageCard.setAttribute('data-card-id', cardEl.dataset.cardId);
statBlock.classList.remove('fyi-open'); }
}
function _unlock() { function _clearStage() {
_locked = false; stageCard.style.display = 'none';
stage.classList.remove('sig-stage--frozen'); stageCard.removeAttribute('data-card-id');
statBlock.classList.remove('fyi-open'); _focusedCardEl = null;
grid.querySelectorAll('.sig-card.sig-focused').forEach(function (c) { _currentCard = null;
c.classList.remove('sig-focused'); }
});
_clearStage();
cardIdInput.value = '';
saveBtn.setAttribute('disabled', 'disabled');
if (nvmBtn) nvmBtn.style.display = 'none';
}
// Horizontal-perspective FLIP animation lifted from function _focus(cardEl) {
// apps/gameboard/static/apps/gameboard/game-kit.js `_flipActive`. // Click 1 — adds .sig-focused (CSS reveals OK btn) + previews
// 500ms Y-axis rotation; mid-animation (offset 0.5) the populator // the card on the stage. Stage stays unlocked; another click on
// swaps polarity content so the user sees the new face from the // a different thumb just moves the focus.
// start of the second half-rotation. Preserves the SPIN orientation grid.querySelectorAll('.sig-card.sig-focused').forEach(function (c) {
// (.stage-card--reversed) through the flip by including the spin if (c !== cardEl) c.classList.remove('sig-focused');
// rotate(180deg) in both keyframes. data-polarity moved to the page });
// wrapper so descendant .sig-card + .sig-stage-card both pick up cardEl.classList.add('sig-focused');
// the polarity-themed CSS rules ([[feedback-applet-vs-page]] / port _populateStage(cardEl);
// of `.sig-overlay[data-polarity]` to `.my-sign-page[data-polarity]`). }
function _flipPolarityAnimated() {
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 = 'translateX(0px) rotateY(0deg) scale(1)' + spin
+ ' rotateY(90deg)';
stageCard.animate([
{ transform: rest },
{ transform: mid, offset: 0.5 },
{ transform: rest },
], { duration: 500, easing: 'ease' });
setTimeout(function () {
revInput.value = revInput.value === '1' ? '0' : '1';
pageEl.setAttribute('data-polarity', _polarity());
if (flipBtn) flipBtn.classList.toggle(
'is-reversed', revInput.value === '1');
if (_currentCard && window.StageCard) {
StageCard.populateCard(stageCard, _currentCard, _polarity());
}
}, 250);
setTimeout(function () { delete stageCard.dataset.flipping; }, 500);
}
// Orientation toggle (preview-only) — rotates the card 180° + swaps function _lock(cardEl) {
// the stat-block visible face. Not persisted; SAVE SIGN only stores // Click 2 (OK btn) — lock state. Adds .sig-reserved--own to the
// the card identity + polarity (significator_reversed = polarity bit). // thumbnail (CSS swaps OK→NVM), .sig-stage--frozen to the stage
function _toggleOrientation() { // (CSS reveals stat block + FLIP), enables SAVE SIGN.
var on = !stageCard.classList.contains('stage-card--reversed'); _locked = true;
stageCard.classList.toggle('stage-card--reversed', on); _focus(cardEl);
statBlock.classList.toggle('is-reversed', on); cardEl.classList.add('sig-reserved--own');
if (spinBtn) spinBtn.classList.toggle('is-reversed', on); stage.classList.add('sig-stage--frozen');
} statBlock.classList.remove('fyi-open');
cardIdInput.value = cardEl.dataset.cardId;
saveBtn.removeAttribute('disabled');
}
// Hover preview — only when stage isn't locked. mouseenter shows function _unlock() {
// the stage card; mouseleave hides it. Click locks the state + // NVM btn — clears lock + focus + stage. SAVE SIGN disables.
// surfaces the stat block + FLIP btn (via .sig-stage--frozen). _locked = false;
stage.classList.remove('sig-stage--frozen');
statBlock.classList.remove('fyi-open');
grid.querySelectorAll('.sig-card.sig-reserved--own').forEach(function (c) {
c.classList.remove('sig-reserved--own');
});
grid.querySelectorAll('.sig-card.sig-focused').forEach(function (c) {
c.classList.remove('sig-focused');
});
_clearStage();
cardIdInput.value = '';
saveBtn.setAttribute('disabled', 'disabled');
}
function _enterPickerPhase() {
pageEl.setAttribute('data-phase', 'picker');
if (landing) landing.style.display = 'none';
if (grid) grid.style.display = '';
}
// SCAN SIGN — landing → picker phase swap
if (scanBtn) {
scanBtn.addEventListener('click', function () {
_enterPickerPhase();
});
}
// FLIP — horizontal-perspective polarity flip animation lifted
// from game-kit.js's _flipActive (500ms Y-axis rotation; offset
// 0.5 mid-animation swap so the new face renders at the second
// half-rotation). Preserves SPIN orientation (.stage-card--reversed)
// through the flip by including its rotate(180deg) in both keyframes.
function _flipPolarityAnimated() {
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 = 'translateX(0px) rotateY(0deg) scale(1)' + spin
+ ' rotateY(90deg)';
stageCard.animate([
{ transform: rest },
{ transform: mid, offset: 0.5 },
{ transform: rest },
], { duration: 500, easing: 'ease' });
setTimeout(function () {
revInput.value = revInput.value === '1' ? '0' : '1';
pageEl.setAttribute('data-polarity', _polarity());
if (flipBtn) flipBtn.classList.toggle(
'is-reversed', revInput.value === '1');
if (_currentCard && window.StageCard) {
StageCard.populateCard(stageCard, _currentCard, _polarity());
}
}, 250);
setTimeout(function () { delete stageCard.dataset.flipping; }, 500);
}
// SPIN — 180° rotation + stat-block face swap (preview-only)
function _toggleOrientation() {
var on = !stageCard.classList.contains('stage-card--reversed');
stageCard.classList.toggle('stage-card--reversed', on);
statBlock.classList.toggle('is-reversed', on);
if (spinBtn) spinBtn.classList.toggle('is-reversed', on);
}
// Grid hover preview (only when nothing is locked)
if (grid) {
grid.addEventListener('mouseover', function (e) { grid.addEventListener('mouseover', function (e) {
if (_locked) return; if (_locked) return;
var cardEl = e.target.closest('.sig-card'); var cardEl = e.target.closest('.sig-card');
@@ -247,90 +302,108 @@
if (!cardEl) return; if (!cardEl) return;
var nextCard = e.relatedTarget && e.relatedTarget.closest var nextCard = e.relatedTarget && e.relatedTarget.closest
&& e.relatedTarget.closest('.sig-card'); && e.relatedTarget.closest('.sig-card');
if (nextCard) return; // moving between cards — let mouseover update if (nextCard) return;
_clearStage(); _clearStage();
}); });
// Click delegation — OK / NVM / card body
grid.addEventListener('click', function (e) { grid.addEventListener('click', function (e) {
var cardEl = e.target.closest('.sig-card'); if (e.target.closest('.sig-ok-btn')) {
if (!cardEl || !window.StageCard) return; if (_locked) return;
_lock(cardEl); var card = e.target.closest('.sig-card');
if (card) _lock(card);
return;
}
if (e.target.closest('.sig-nvm-btn')) {
_unlock();
return;
}
if (_locked) return; // locked — must NVM first
var card = e.target.closest('.sig-card');
if (!card || !window.StageCard) return;
_focus(card);
}); });
if (nvmBtn) { }
nvmBtn.addEventListener('click', function () { _unlock(); });
}
if (flipBtn) {
flipBtn.addEventListener('click', function () {
if (!_currentCard) return; // need a selected card to flip
_flipPolarityAnimated();
});
}
if (spinBtn) {
spinBtn.addEventListener('click', function () {
_toggleOrientation();
});
}
if (fyiBtn) {
fyiBtn.addEventListener('click', function () {
if (!_currentCard) return; // no card selected yet
statBlock.classList.toggle('fyi-open');
});
}
if (fyiPanel) {
fyiPanel.addEventListener('click', function () {
statBlock.classList.remove('fyi-open');
});
}
if (fyiPrev) {
fyiPrev.addEventListener('click', function () {
if (!_fyiData.length) return;
_fyiIdx = (_fyiIdx - 1 + _fyiData.length) % _fyiData.length;
StageCard.renderFyi(fyiPanel, _fyiData, _fyiIdx);
});
}
if (fyiNext) {
fyiNext.addEventListener('click', function () {
if (!_fyiData.length) return;
_fyiIdx = (_fyiIdx + 1) % _fyiData.length;
StageCard.renderFyi(fyiPanel, _fyiData, _fyiIdx);
});
}
// On-load: if user has a saved sig, lock that card so the stage if (flipBtn) {
// + stat block + FLIP btn appear w. the persisted choice from flipBtn.addEventListener('click', function () {
// the start. Otherwise the stage stays empty until first hover. if (!_currentCard) return;
var savedId = pageEl && pageEl.dataset.currentCardId; _flipPolarityAnimated();
if (savedId) {
var savedCardEl = grid.querySelector(
'.sig-card[data-card-id="' + savedId + '"]');
if (savedCardEl) _lock(savedCardEl);
}
}());
</script>
{# No equipped deck + no saved sig — fire a Brief banner via the #}
{# shared note.js API so positioning + NVM behavior + h2-overlay #}
{# styling matches every other Brief on the site (per #}
{# [[sprint-baltimorean-note-unlock-may18]] portrait h2 measurement). #}
{# FYI links to /gameboard/ (Game Kit equip); NVM dismisses + the #}
{# picker proceeds against the Earthman [Shabby Paperboard] fallback. #}
<script src="{% static 'apps/dashboard/note.js' %}"></script>
{% if show_backup_intro_banner %}
<script>
document.addEventListener('DOMContentLoaded', function () {
if (!window.Brief || !Brief.showBanner) return;
Brief.showBanner({
title: 'Default deck warning',
line_text: 'Look!—no deck is equipped. Navigate to the Game Kit to equip one (FYI) or (NVM) proceed with the Earthman [Shabby Cardstock] deck.',
post_url: '{% url "gameboard" %}',
created_at: '',
kind: 'NUDGE',
});
// Tag the banner so FTs (and any my-sign-specific styling) can
// distinguish this intro nudge from other Briefs on the page.
var banner = document.querySelector('.note-banner');
if (banner) banner.classList.add('my-sign-intro-banner');
}); });
</script> }
{% endif %} if (spinBtn) {
spinBtn.addEventListener('click', function () {
_toggleOrientation();
});
}
if (fyiBtn) {
fyiBtn.addEventListener('click', function () {
if (!_currentCard) return;
statBlock.classList.toggle('fyi-open');
});
}
if (fyiPanel) {
fyiPanel.addEventListener('click', function () {
statBlock.classList.remove('fyi-open');
});
}
if (fyiPrev) {
fyiPrev.addEventListener('click', function () {
if (!_fyiData.length) return;
_fyiIdx = (_fyiIdx - 1 + _fyiData.length) % _fyiData.length;
StageCard.renderFyi(fyiPanel, _fyiData, _fyiIdx);
});
}
if (fyiNext) {
fyiNext.addEventListener('click', function () {
if (!_fyiData.length) return;
_fyiIdx = (_fyiIdx + 1) % _fyiData.length;
StageCard.renderFyi(fyiPanel, _fyiData, _fyiIdx);
});
}
// On-load: if user has a saved sig, populate the stage preview so
// the saved card is visible above the landing hex. The picker grid
// stays hidden until SCAN SIGN is clicked. The saved card is NOT
// auto-locked — that happens on entering picker phase if desired.
var savedId = pageEl.dataset.currentCardId;
if (savedId && grid) {
var savedCardEl = grid.querySelector(
'.sig-card[data-card-id="' + savedId + '"]');
if (savedCardEl) {
_populateStage(savedCardEl);
}
}
// room.js's scaleTable() runs on DOMContentLoaded but at that
// moment the parent .my-sign-page hasn't flushed its flex sizing,
// so #id_game_table.clientWidth/Height read 0 and the hex stays
// unscaled (200×231 inside a 360×320 scene → narrow / elongated).
// Dispatch a resize on the next tick to re-trigger scaleTable
// once layout settles.
window.requestAnimationFrame(function () {
window.dispatchEvent(new Event('resize'));
});
}());
</script>
{# Brief intro banner — Default deck warning. See sprint-4a-follow. #}
<script src="{% static 'apps/dashboard/note.js' %}"></script>
{% if show_backup_intro_banner %}
<script>
document.addEventListener('DOMContentLoaded', function () {
if (!window.Brief || !Brief.showBanner) return;
Brief.showBanner({
title: 'Default deck warning',
line_text: 'Look!—no deck is equipped. Navigate to the Game Kit to equip one (FYI) or (NVM) proceed with the Earthman [Shabby Cardstock] deck.',
post_url: '{% url "gameboard" %}',
created_at: '',
kind: 'NUDGE',
});
var banner = document.querySelector('.note-banner');
if (banner) banner.classList.add('my-sign-intro-banner');
});
</script>
{% endif %}
</div> </div>
{% endblock content %} {% endblock content %}