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:
@@ -5,236 +5,291 @@
|
||||
{% block header_text %}<span>Game</span><span>Sign</span>{% 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". #}
|
||||
<div class="my-sign-page"
|
||||
data-phase="landing"
|
||||
data-save-url="{% url 'billboard:save_sign' %}"
|
||||
{% if current_significator %}data-current-card-id="{{ current_significator.id }}"{% endif %}
|
||||
data-current-reversed="{{ current_significator_reversed|yesno:'true,false' }}"
|
||||
data-polarity="{% if current_significator_reversed %}levity{% else %}gravity{% endif %}">
|
||||
|
||||
{# 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. #}
|
||||
<div class="sig-stage my-sign-stage">
|
||||
<div class="sig-stage-card" style="display:none">
|
||||
<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>
|
||||
{# Stage frame — always reserved at the top of the page; SAVE SIGN + #}
|
||||
{# NVM ride along to its right (sig-select-style). The stage card #}
|
||||
{# itself starts hidden + appears on hover/preview or saved-sig #}
|
||||
{# preview; .sig-stage--frozen is added on OK confirm + cleared by #}
|
||||
{# NVM. data-polarity lives on .my-sign-page so descendant .sig-card #}
|
||||
{# / .sig-stage-card both pick up polarity-themed CSS rules. #}
|
||||
<div class="sig-stage my-sign-stage">
|
||||
<div class="sig-stage-card" style="display:none"
|
||||
{% if current_significator %}data-card-id="{{ current_significator.id }}"{% endif %}>
|
||||
<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 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>
|
||||
<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>
|
||||
<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>
|
||||
{# FLIP — bottom-left of the stage card. Mirrors the game-kit fan #}
|
||||
{# carousel's btn-reveal pattern (game_kit.html#id_fan_flip). Toggles #}
|
||||
{# polarity (data-polarity attr + persisted User.significator_reversed). #}
|
||||
{# 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 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 class="sig-deck-grid my-sign-deck-grid">
|
||||
{% for card in cards %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
{# FLIP — bottom-left of the stage card. Visible only after lock #}
|
||||
{# (.sig-stage--frozen). #}
|
||||
<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>
|
||||
|
||||
{# 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' %}">
|
||||
{% csrf_token %}
|
||||
<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' }}">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{# Picker JS — click .sig-card to pick + populate stage preview via #}
|
||||
{# StageCard.populateCard (shared w. room sig-select + sea-select). #}
|
||||
{# FLIP toggles `.stage-card--reversed` on the preview AND the #}
|
||||
{# hidden `reversed` input that gets POSTed on SAVE SIGN. #}
|
||||
<script src="{% static 'apps/epic/stage-card.js' %}"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var grid = document.querySelector('.my-sign-deck-grid');
|
||||
if (!grid) return;
|
||||
var pageEl = document.querySelector('.my-sign-page');
|
||||
var stage = document.querySelector('.my-sign-stage');
|
||||
var stageCard = stage.querySelector('.sig-stage-card');
|
||||
var statBlock = stage.querySelector('.sig-stat-block');
|
||||
var cardIdInput = document.getElementById('id_save_sign_card_id');
|
||||
var saveBtn = document.getElementById('id_save_sign_btn');
|
||||
var nvmBtn = document.getElementById('id_nvm_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; // true after click; cleared by NVM
|
||||
var _fyiData = [];
|
||||
var _fyiIdx = 0;
|
||||
{# Landing phase — DRY table hex w. a single chair + central SCAN #}
|
||||
{# SIGN btn. Reuses the room's hex shell (.room-shell > .room-table #}
|
||||
{# > .room-table-scene > .table-hex-border > .table-hex > #}
|
||||
{# .table-center) + room.js's scaleTable() for viewport-fluid sizing. #}
|
||||
<div class="my-sign-landing">
|
||||
<div class="room-shell">
|
||||
<div id="id_game_table" class="room-table">
|
||||
<div class="room-table-scene">
|
||||
<div class="table-hex-border">
|
||||
<div class="table-hex">
|
||||
<div class="table-center">
|
||||
<button id="id_scan_sign_btn" type="button" class="btn btn-primary">SCAN<br>SIGN</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{# Single founder chair — solo-coded but extensible to the #}
|
||||
{# 6-chair friend-invite plan in [[project-my-sea-roadmap]]. #}
|
||||
<div class="table-seat" data-slot="1" data-role="PC">
|
||||
<i class="fa-solid fa-chair"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Default polarity = gravity (significator_reversed=False);
|
||||
// FLIP toggles to levity (significator_reversed=True). Mirrors the
|
||||
// user's "gravity-rightside-up by default" design from the Game
|
||||
// Kit fan carousel.
|
||||
function _polarity() {
|
||||
return revInput.value === '1' ? 'levity' : 'gravity';
|
||||
}
|
||||
{# Picker phase — card grid, hidden until SCAN SIGN click. Each #}
|
||||
{# .sig-card gets .sig-card-actions w. OK + NVM buttons (CSS gates #}
|
||||
{# visibility via .sig-focused → OK / .sig-reserved--own → NVM). #}
|
||||
<div class="sig-deck-grid my-sign-deck-grid" style="display:none">
|
||||
{% for card in cards %}
|
||||
<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) {
|
||||
_focusedCardEl = cardEl;
|
||||
_currentCard = StageCard.fromDataset(cardEl);
|
||||
StageCard.populateCard(stageCard, _currentCard, _polarity());
|
||||
StageCard.populateKeywords(statBlock,
|
||||
_currentCard.keywords_upright, _currentCard.keywords_reversed);
|
||||
_fyiData = StageCard.buildInfoData(_currentCard);
|
||||
_fyiIdx = 0;
|
||||
if (fyiPanel) StageCard.renderFyi(fyiPanel, _fyiData, _fyiIdx);
|
||||
stageCard.style.display = '';
|
||||
}
|
||||
{# Picker JS — phase swap (landing → picker), hover preview, two-step #}
|
||||
{# OK/NVM-on-thumbnail selection cycle, FLIP polarity animation lifted #}
|
||||
{# from game-kit.js's _flipActive, SPIN orientation toggle. data- #}
|
||||
{# polarity moved to the page wrapper so descendant .sig-card + #}
|
||||
{# .sig-stage-card both pick up the polarity-themed CSS rules. #}
|
||||
<script src="{% static 'apps/epic/stage-card.js' %}"></script>
|
||||
<script src="{% static 'apps/epic/room.js' %}"></script>
|
||||
<script>
|
||||
(function () {
|
||||
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() {
|
||||
stageCard.style.display = 'none';
|
||||
_focusedCardEl = null;
|
||||
_currentCard = null;
|
||||
}
|
||||
function _polarity() {
|
||||
return revInput.value === '1' ? 'levity' : 'gravity';
|
||||
}
|
||||
|
||||
function _lock(cardEl) {
|
||||
_locked = true;
|
||||
_populateStage(cardEl);
|
||||
grid.querySelectorAll('.sig-card.sig-focused').forEach(function (c) {
|
||||
if (c !== cardEl) c.classList.remove('sig-focused');
|
||||
});
|
||||
cardEl.classList.add('sig-focused');
|
||||
cardIdInput.value = cardEl.dataset.cardId;
|
||||
saveBtn.removeAttribute('disabled');
|
||||
if (nvmBtn) nvmBtn.style.display = '';
|
||||
stage.classList.add('sig-stage--frozen');
|
||||
statBlock.classList.remove('fyi-open');
|
||||
}
|
||||
function _populateStage(cardEl) {
|
||||
_focusedCardEl = cardEl;
|
||||
_currentCard = StageCard.fromDataset(cardEl);
|
||||
StageCard.populateCard(stageCard, _currentCard, _polarity());
|
||||
StageCard.populateKeywords(statBlock,
|
||||
_currentCard.keywords_upright, _currentCard.keywords_reversed);
|
||||
_fyiData = StageCard.buildInfoData(_currentCard);
|
||||
_fyiIdx = 0;
|
||||
if (fyiPanel) StageCard.renderFyi(fyiPanel, _fyiData, _fyiIdx);
|
||||
stageCard.style.display = '';
|
||||
stageCard.setAttribute('data-card-id', cardEl.dataset.cardId);
|
||||
}
|
||||
|
||||
function _unlock() {
|
||||
_locked = false;
|
||||
stage.classList.remove('sig-stage--frozen');
|
||||
statBlock.classList.remove('fyi-open');
|
||||
grid.querySelectorAll('.sig-card.sig-focused').forEach(function (c) {
|
||||
c.classList.remove('sig-focused');
|
||||
});
|
||||
_clearStage();
|
||||
cardIdInput.value = '';
|
||||
saveBtn.setAttribute('disabled', 'disabled');
|
||||
if (nvmBtn) nvmBtn.style.display = 'none';
|
||||
}
|
||||
function _clearStage() {
|
||||
stageCard.style.display = 'none';
|
||||
stageCard.removeAttribute('data-card-id');
|
||||
_focusedCardEl = null;
|
||||
_currentCard = null;
|
||||
}
|
||||
|
||||
// Horizontal-perspective FLIP animation lifted from
|
||||
// apps/gameboard/static/apps/gameboard/game-kit.js `_flipActive`.
|
||||
// 500ms Y-axis rotation; mid-animation (offset 0.5) the populator
|
||||
// swaps polarity content so the user sees the new face from the
|
||||
// start of the second half-rotation. Preserves the SPIN orientation
|
||||
// (.stage-card--reversed) through the flip by including the spin
|
||||
// rotate(180deg) in both keyframes. data-polarity moved to the page
|
||||
// wrapper so descendant .sig-card + .sig-stage-card both pick up
|
||||
// the polarity-themed CSS rules ([[feedback-applet-vs-page]] / port
|
||||
// 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);
|
||||
}
|
||||
function _focus(cardEl) {
|
||||
// Click 1 — adds .sig-focused (CSS reveals OK btn) + previews
|
||||
// the card on the stage. Stage stays unlocked; another click on
|
||||
// a different thumb just moves the focus.
|
||||
grid.querySelectorAll('.sig-card.sig-focused').forEach(function (c) {
|
||||
if (c !== cardEl) c.classList.remove('sig-focused');
|
||||
});
|
||||
cardEl.classList.add('sig-focused');
|
||||
_populateStage(cardEl);
|
||||
}
|
||||
|
||||
// Orientation toggle (preview-only) — rotates the card 180° + swaps
|
||||
// the stat-block visible face. Not persisted; SAVE SIGN only stores
|
||||
// the card identity + polarity (significator_reversed = polarity bit).
|
||||
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);
|
||||
}
|
||||
function _lock(cardEl) {
|
||||
// Click 2 (OK btn) — lock state. Adds .sig-reserved--own to the
|
||||
// thumbnail (CSS swaps OK→NVM), .sig-stage--frozen to the stage
|
||||
// (CSS reveals stat block + FLIP), enables SAVE SIGN.
|
||||
_locked = true;
|
||||
_focus(cardEl);
|
||||
cardEl.classList.add('sig-reserved--own');
|
||||
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
|
||||
// the stage card; mouseleave hides it. Click locks the state +
|
||||
// surfaces the stat block + FLIP btn (via .sig-stage--frozen).
|
||||
function _unlock() {
|
||||
// NVM btn — clears lock + focus + stage. SAVE SIGN disables.
|
||||
_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) {
|
||||
if (_locked) return;
|
||||
var cardEl = e.target.closest('.sig-card');
|
||||
@@ -247,90 +302,108 @@
|
||||
if (!cardEl) return;
|
||||
var nextCard = e.relatedTarget && e.relatedTarget.closest
|
||||
&& e.relatedTarget.closest('.sig-card');
|
||||
if (nextCard) return; // moving between cards — let mouseover update
|
||||
if (nextCard) return;
|
||||
_clearStage();
|
||||
});
|
||||
|
||||
// Click delegation — OK / NVM / card body
|
||||
grid.addEventListener('click', function (e) {
|
||||
var cardEl = e.target.closest('.sig-card');
|
||||
if (!cardEl || !window.StageCard) return;
|
||||
_lock(cardEl);
|
||||
if (e.target.closest('.sig-ok-btn')) {
|
||||
if (_locked) return;
|
||||
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
|
||||
// + stat block + FLIP btn appear w. the persisted choice from
|
||||
// the start. Otherwise the stage stays empty until first hover.
|
||||
var savedId = pageEl && pageEl.dataset.currentCardId;
|
||||
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');
|
||||
if (flipBtn) {
|
||||
flipBtn.addEventListener('click', function () {
|
||||
if (!_currentCard) return;
|
||||
_flipPolarityAnimated();
|
||||
});
|
||||
</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>
|
||||
{% endblock content %}
|
||||
|
||||
Reference in New Issue
Block a user