My Sign picker: hover-preview, click-lock, NVM-unlock + polarity SCSS port + bigger stage — Sprint 4a-cont iteration 2
User-driven polish on iteration 1: separate hover-preview from click-lock semantics (room sig-select pattern), add NVM to unlock, port the room's `.sig-overlay[data-polarity]` polarity-themed CSS to also target `.my-sign-page[data-polarity]`, bump the stage card width so it occupies a bigger slice of the viewport ; **state machine** (inline JS in my_sign.html): three discrete states — (a) idle: stage frame visible but empty (stage card hidden via display:none, stat block hidden via `.sig-stage--frozen` absence, FLIP btn hidden via CSS); (b) hover: hovered .sig-card populates the stage card (preview); mouseleave clears it; mouseover-mouseout sequence guards against transient gaps when moving between adjacent thumbnails (relatedTarget closest('.sig-card') check); (c) locked: click on any grid card freezes the stage — populates content, adds `.sig-stage--frozen` to .sig-stage (which surfaces .sig-stat-block + .my-sign-flip-btn via CSS), enables SAVE SIGN, reveals NVM. Subsequent hovers ignored while locked. NVM click reverts to idle (clears content, hides stat-block + FLIP, disables SAVE, hides NVM) ; **new template** elements: NVM `<button id="id_nvm_sign_btn" class="btn btn-cancel">` next to SAVE SIGN in the form, hidden by default (style="display:none") + revealed on lock. Stage card re-acquires `style="display:none"` (hidden on load, JS-shown on hover/lock). `sig-stage--frozen` class no longer initial — JS-added on click ; **polarity SCSS port** (_card-deck.scss L820-905): extended `.sig-overlay[data-polarity="levity"]` + `[data-polarity="gravity"]` selector lists to include `.my-sign-page[data-polarity="levity"]` + `[gravity"]`. Rules inside (e.g. `.sig-card { background: rgba(--secUser) }`, `.sig-stage-card .fan-card-name { color: --quiUser }`, stat-face-label colour flips, text-shadow polarity variants) automatically apply on the my-sign page since `data-polarity` lives on the page wrapper (descendants .sig-card + .sig-stage-card both inherit). Moved `data-polarity` from `.my-sign-stage` to `.my-sign-page` in the template + JS so descendant scoping works (was a stage-scoped attr in iteration 1, which couldn't reach the sibling .sig-deck-grid) ; **bigger stage** (_card-deck.scss): `.my-sign-page { --sig-card-w: clamp(140px, 36vw, 220px); }` — scales w. viewport, 140px floor for portrait, 220px ceiling for landscape, ~36vw in between. Stage card + stat block both width-driven by this var so they scale together. The clamp() ceiling matches the room sig-select's typical sized card on a mid-laptop ; **FLIP btn visibility** (_card-deck.scss): `.my-sign-flip-btn { display: none }` at rest; `.my-sign-stage.sig-stage--frozen .my-sign-flip-btn { display: inline-flex }` on lock. The btn's position (absolute, bottom-left of card) was already added in iteration 1 ; **on-load lock-restore**: if `User.significator` is set, the picker auto-locks that card via `_lock(savedCardEl)` so the user sees their persisted choice in the locked-state UI (stat block + FLIP visible) instead of an idle empty frame. Polarity initial value (data-polarity on .my-sign-page) reflects `current_significator_reversed` — False=gravity (default), True=levity ; **regression**: 7 FTs in test_bill_my_sign green in 57s. Visual verify deferred to user — picker should now show: idle empty stage + grid below; hover thumbnail → stage card preview; click → preview persists + stat block + FLIP appear; NVM → back to idle; FLIP click → horizontal-perspective Y-axis rotation w. polarity content swap mid-animation. Polarity-themed colour styles (levity inverted palette / gravity stark contrast / per-polarity text-shadows) now apply on my-sign matching the room sig-select look
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:
@@ -615,11 +615,16 @@ html:has(.sig-backdrop) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── My Sign picker — FLIP btn (polarity toggle) ──────────────────────────────
|
||||
// Bottom-left corner of the stage card, mirroring game_kit.html's
|
||||
// `.fan-flip-btn` placement convention (per [[sprint-my-sign-picker-may18h]]).
|
||||
// SPIN (orientation 180° rotation) stays in `.sig-stat-block` — the FLIP btn
|
||||
// here toggles polarity (data-polarity attr + persisted significator_reversed).
|
||||
// ─── 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).
|
||||
.my-sign-page {
|
||||
--sig-card-w: clamp(140px, 36vw, 220px);
|
||||
}
|
||||
|
||||
.my-sign-flip-btn {
|
||||
position: absolute;
|
||||
z-index: 25;
|
||||
@@ -629,6 +634,13 @@ html:has(.sig-backdrop) {
|
||||
// flex-start, anchored to the stage's left padding).
|
||||
left: calc(1.5rem + 0.4rem);
|
||||
margin: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
// FLIP btn appears only when the stage is frozen (post-click). 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;
|
||||
}
|
||||
|
||||
// ─── Mini card grid ───────────────────────────────────────────────────────────
|
||||
@@ -818,7 +830,12 @@ html:has(.sig-backdrop) {
|
||||
// Levity (Leavened): --secUser bg / --priUser text — inverted, lighter feel.
|
||||
// Both mini-cards and the stage preview card follow the same rule.
|
||||
|
||||
.sig-overlay[data-polarity="levity"] {
|
||||
// `.my-sign-page[data-polarity]` parallels `.sig-overlay[data-polarity]` —
|
||||
// same polarity-themed colour rules apply to the standalone Game Sign picker.
|
||||
// data-polarity lives on the page wrapper (not on .my-sign-stage) so descendant
|
||||
// `.sig-card` (in the grid, sibling to the stage) inherits the rules.
|
||||
.sig-overlay[data-polarity="levity"],
|
||||
.my-sign-page[data-polarity="levity"] {
|
||||
// Mini card: inverted palette. game-kit sets explicit colours on .fan-card-name
|
||||
// and .fan-card-corner that out-specifc the parent color, so re-target them here.
|
||||
.sig-card {
|
||||
@@ -871,7 +888,8 @@ html:has(.sig-backdrop) {
|
||||
.sig-info-effect .card-ref { color: rgba(var(--quiUser), 1); }
|
||||
// Cursor colours live in .sig-cursor-float[data-role] rules (portal elements)
|
||||
}
|
||||
.sig-overlay[data-polarity="gravity"] {
|
||||
.sig-overlay[data-polarity="gravity"],
|
||||
.my-sign-page[data-polarity="gravity"] {
|
||||
// Stat block: invert priUser/secUser so gravity gets the same stark contrast as leavened cards
|
||||
.sig-stat-block {
|
||||
background: rgba(var(--secUser), 0.75);
|
||||
|
||||
@@ -14,17 +14,16 @@
|
||||
<div class="my-sign-page"
|
||||
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-current-reversed="{{ current_significator_reversed|yesno:'true,false' }}"
|
||||
data-polarity="{% if current_significator_reversed %}levity{% else %}gravity{% endif %}">
|
||||
|
||||
{# data-polarity drives both the populator's qualifier lookup #}
|
||||
{# (levity_qualifier vs gravity_qualifier) and any polarity-themed #}
|
||||
{# styling that mirrors the room sig-select's .sig-overlay rules. #}
|
||||
{# FLIP toggles between levity + gravity. Initial value reflects #}
|
||||
{# the persisted `significator_reversed` flag — False=levity, #}
|
||||
{# True=gravity (the binary "one or the other" polarity choice). #}
|
||||
<div class="sig-stage my-sign-stage sig-stage--frozen"
|
||||
data-polarity="{% if current_significator_reversed %}levity{% else %}gravity{% endif %}">
|
||||
<div class="sig-stage-card">
|
||||
{# 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>
|
||||
@@ -103,6 +102,7 @@
|
||||
<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>
|
||||
|
||||
{# Picker JS — click .sig-card to pick + populate stage preview via #}
|
||||
@@ -114,11 +114,13 @@
|
||||
(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');
|
||||
@@ -126,9 +128,11 @@
|
||||
var fyiPrev = stage.querySelector('.fyi-prev');
|
||||
var fyiNext = stage.querySelector('.fyi-next');
|
||||
var revInput = document.getElementById('id_save_sign_reversed');
|
||||
var _currentCard = null;
|
||||
var _fyiData = [];
|
||||
var _fyiIdx = 0;
|
||||
var _currentCard = null;
|
||||
var _focusedCardEl = null;
|
||||
var _locked = false; // true after click; cleared by NVM
|
||||
var _fyiData = [];
|
||||
var _fyiIdx = 0;
|
||||
|
||||
// Default polarity = gravity (significator_reversed=False);
|
||||
// FLIP toggles to levity (significator_reversed=True). Mirrors the
|
||||
@@ -138,13 +142,61 @@
|
||||
return revInput.value === '1' ? 'levity' : 'gravity';
|
||||
}
|
||||
|
||||
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 = '';
|
||||
}
|
||||
|
||||
function _clearStage() {
|
||||
stageCard.style.display = 'none';
|
||||
_focusedCardEl = null;
|
||||
_currentCard = null;
|
||||
}
|
||||
|
||||
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 _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';
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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';
|
||||
@@ -158,11 +210,9 @@
|
||||
{ transform: mid, offset: 0.5 },
|
||||
{ transform: rest },
|
||||
], { duration: 500, easing: 'ease' });
|
||||
// Swap polarity at the edge-on midpoint so the new face shows
|
||||
// through the second half of the rotation.
|
||||
setTimeout(function () {
|
||||
revInput.value = revInput.value === '1' ? '0' : '1';
|
||||
stage.setAttribute('data-polarity', _polarity());
|
||||
pageEl.setAttribute('data-polarity', _polarity());
|
||||
if (flipBtn) flipBtn.classList.toggle(
|
||||
'is-reversed', revInput.value === '1');
|
||||
if (_currentCard && window.StageCard) {
|
||||
@@ -182,28 +232,32 @@
|
||||
if (spinBtn) spinBtn.classList.toggle('is-reversed', on);
|
||||
}
|
||||
|
||||
function _selectCard(cardEl) {
|
||||
grid.querySelectorAll('.sig-card.sig-focused').forEach(function (c) {
|
||||
c.classList.remove('sig-focused');
|
||||
});
|
||||
cardEl.classList.add('sig-focused');
|
||||
cardIdInput.value = cardEl.dataset.cardId;
|
||||
saveBtn.removeAttribute('disabled');
|
||||
if (!window.StageCard) return;
|
||||
_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);
|
||||
statBlock.classList.remove('fyi-open');
|
||||
}
|
||||
|
||||
// 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).
|
||||
grid.addEventListener('mouseover', function (e) {
|
||||
if (_locked) return;
|
||||
var cardEl = e.target.closest('.sig-card');
|
||||
if (!cardEl || !window.StageCard) return;
|
||||
_populateStage(cardEl);
|
||||
});
|
||||
grid.addEventListener('mouseout', function (e) {
|
||||
if (_locked) return;
|
||||
var cardEl = e.target.closest('.sig-card');
|
||||
if (!cardEl) return;
|
||||
var nextCard = e.relatedTarget && e.relatedTarget.closest
|
||||
&& e.relatedTarget.closest('.sig-card');
|
||||
if (nextCard) return; // moving between cards — let mouseover update
|
||||
_clearStage();
|
||||
});
|
||||
grid.addEventListener('click', function (e) {
|
||||
var cardEl = e.target.closest('.sig-card');
|
||||
if (cardEl) _selectCard(cardEl);
|
||||
if (!cardEl || !window.StageCard) return;
|
||||
_lock(cardEl);
|
||||
});
|
||||
if (nvmBtn) {
|
||||
nvmBtn.addEventListener('click', function () { _unlock(); });
|
||||
}
|
||||
if (flipBtn) {
|
||||
flipBtn.addEventListener('click', function () {
|
||||
if (!_currentCard) return; // need a selected card to flip
|
||||
@@ -241,14 +295,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
// On-load: if user has a saved sig, pre-select its grid card so
|
||||
// the stage shows the persisted choice rather than an empty frame.
|
||||
// `data-current-card-id` is set on .my-sign-page when significator is set.
|
||||
var pageEl = document.querySelector('.my-sign-page');
|
||||
// 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) _selectCard(savedCardEl);
|
||||
var savedCardEl = grid.querySelector(
|
||||
'.sig-card[data-card-id="' + savedId + '"]');
|
||||
if (savedCardEl) _lock(savedCardEl);
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user