PICK SEA Sprint C: sea stage card viewer — FLIP in, SPIN/FYI, deposit/re-expand — TDD

- sea.js: SeaDeal module — openStage() shows big card viewer w. flip-in animation;
  SPIN toggles stage-card--reversed; FYI shows energies/operations (Energy/Operation
  titles, PRV/NXT nav); backdrop click deposits card to slot; click deposited slot
  re-opens stage; resetHand() clears hand on DEL
- sea_deck view: adds name_group/name_title/reversal/keywords_upright/keywords_reversed/
  energies/operations to each card dict (full sig-select stage data set)
- _sea_overlay.html: data-sea-user-polarity attr; sea stage HTML (sig-stage-card shell
  + fan-card-face-upright/reversal structure + sea-stat-block w. SPIN/FYI/PRV/NXT);
  FLIP click calls SeaDeal.openStage(); _fillPos removed (sea.js handles slot fill);
  _reset calls SeaDeal.resetHand()
- room.html: sea.js included alongside sig-select.js
- _card-deck.scss: sea-stage layout (fixed overlay, backdrop, content row); sea-stage-card
  w. @keyframes sea-flip-in (3D rotateY perspective); sea-stat-block scoped styles
  incl. SPIN/FYI btns, stat faces, sig-info FYI panel
- SeaDealSpec.js: 20 Jasmine specs — openStage, SPIN, FYI, backdrop dismiss, slot re-expand

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-04-29 01:12:06 -04:00
parent 2af59b3a7f
commit 08aa4dc819
11 changed files with 1140 additions and 64 deletions

View File

@@ -0,0 +1,270 @@
var SeaDeal = (function () {
'use strict';
var overlay, stage, stageCard, statBlock;
var spinBtn, fyiBtn, bdrop;
var fyiPanel, fyiTitle, fyiType, fyiEffect, fyiIndex, fyiPrev, fyiNext;
var _userPolarity = 'levity';
var _seaHand = {}; // posSelector → {card, isLevity}
var _viewingPos = null;
var _infoData = [];
var _infoIdx = 0;
var _infoOpen = false;
var _spinOrigLabel, _fyiOrigLabel;
// ── Keyword list ──────────────────────────────────────────────────────────
function _populateList(listEl, items) {
if (!listEl) return;
listEl.innerHTML = (items || []).map(function (k) {
return '<li>' + k + '</li>';
}).join('');
}
// ── Stage card population ─────────────────────────────────────────────────
function _populate(card, isLevity) {
var qualifier = isLevity
? (card.levity_qualifier || '')
: (card.gravity_qualifier || '');
var isMajor = card.arcana === 'MAJOR';
var title = card.name_title || card.name || '';
// Corners
stageCard.querySelectorAll('.fan-corner-rank').forEach(function (el) {
el.textContent = card.corner_rank || '';
});
stageCard.querySelectorAll('.stage-suit-icon').forEach(function (el) {
if (card.suit_icon) {
el.className = 'fa-solid ' + card.suit_icon + ' stage-suit-icon';
el.style.display = '';
} else {
el.style.display = 'none';
}
});
// Upright face
var nameGroupEl = stageCard.querySelector('.fan-card-name-group');
if (nameGroupEl) nameGroupEl.textContent = card.name_group || '';
var arcanaEl = stageCard.querySelector('.fan-card-arcana');
if (arcanaEl) arcanaEl.textContent = isMajor ? 'Major Arcana' : 'Middle Arcana';
stageCard.querySelector('.fan-card-name').textContent = isMajor ? title + ',' : title;
stageCard.querySelector('.sig-qualifier-above').textContent = isMajor ? '' : qualifier;
stageCard.querySelector('.sig-qualifier-below').textContent = isMajor ? qualifier : '';
// Reversal face (same slot-swap logic as sig-select)
var reversal = card.reversal || '';
if (isMajor) {
stageCard.querySelector('.fan-card-reversal-qualifier').textContent = title + ',';
stageCard.querySelector('.fan-card-reversal-name').textContent = qualifier;
} else if (reversal) {
stageCard.querySelector('.fan-card-reversal-qualifier').textContent = reversal;
stageCard.querySelector('.fan-card-reversal-name').textContent = title;
} else {
stageCard.querySelector('.fan-card-reversal-qualifier').textContent = qualifier;
stageCard.querySelector('.fan-card-reversal-name').textContent = '';
}
// Keywords
_populateList(overlay.querySelector('#id_sea_stat_upright'), card.keywords_upright);
_populateList(overlay.querySelector('#id_sea_stat_reversed'), card.keywords_reversed);
// FYI data (energies + operations)
_infoData = (card.energies || []).map(function (e) {
return { type: e.type, effect: e.effect, category: 'energies' };
}).concat((card.operations || []).map(function (o) {
return { type: o.type, effect: o.effect, category: 'operations' };
}));
_infoIdx = 0;
// Reset SPIN
stageCard.classList.remove('stage-card--reversed');
statBlock.classList.remove('is-reversed');
_closeInfo();
}
// ── FYI info panel ────────────────────────────────────────────────────────
function _renderInfo() {
if (!fyiPanel) return;
if (_infoData.length === 0) {
fyiTitle.textContent = 'Energy';
fyiTitle.className = 'sig-info-title sig-info-title--energies';
if (fyiType) fyiType.textContent = '';
if (fyiEffect) fyiEffect.innerHTML = '<em>No interactions defined.</em>';
fyiIndex.textContent = '';
return;
}
var entry = _infoData[_infoIdx];
var isEnergies = entry.category === 'energies';
fyiTitle.textContent = isEnergies ? 'Energy' : 'Operation';
fyiTitle.className = 'sig-info-title sig-info-title--' + entry.category;
if (fyiType) fyiType.textContent = entry.type || '';
if (fyiEffect) fyiEffect.innerHTML = entry.effect || '';
fyiIndex.textContent = _infoData.length > 1
? (_infoIdx + 1) + ' / ' + _infoData.length : '';
}
function _openInfo() {
_infoOpen = true;
_renderInfo();
if (fyiPanel) fyiPanel.style.display = '';
if (spinBtn) { spinBtn.classList.add('btn-disabled'); spinBtn.textContent = '×'; }
if (fyiBtn) { fyiBtn.classList.add('btn-disabled'); fyiBtn.textContent = '×'; }
stage.classList.add('sea-info-open');
}
function _closeInfo() {
_infoOpen = false;
if (fyiPanel) fyiPanel.style.display = 'none';
if (spinBtn) { spinBtn.classList.remove('btn-disabled'); spinBtn.textContent = _spinOrigLabel || 'SPIN'; }
if (fyiBtn) { fyiBtn.classList.remove('btn-disabled'); fyiBtn.textContent = _fyiOrigLabel || 'FYI'; }
if (stage) stage.classList.remove('sea-info-open');
}
// ── Slot fill ─────────────────────────────────────────────────────────────
function _fillSlot(posSelector, card, isLevity) {
var cell = overlay.querySelector(posSelector);
if (!cell) return;
var slot = cell.querySelector('.sea-card-slot');
if (!slot) return;
slot.classList.remove('sea-card-slot--empty');
slot.classList.add('sea-card-slot--filled');
slot.classList.add(isLevity ? 'sea-card-slot--levity' : 'sea-card-slot--gravity');
slot.dataset.cardId = String(card.id);
slot.dataset.posKey = posSelector;
slot.innerHTML =
'<span class="fan-corner-rank">' + card.corner_rank + '</span>' +
(card.suit_icon ? '<i class="fa-solid ' + card.suit_icon + '"></i>' : '');
}
// ── Show / hide stage ─────────────────────────────────────────────────────
function _showStage() {
stage.style.display = '';
stageCard.classList.add('sea-stage-card--shown');
}
function _hideStage() {
stage.style.display = 'none';
stageCard.classList.remove('sea-stage-card--shown');
_viewingPos = null;
_closeInfo();
}
// ── Public API ─────────────────────────────────────────────────────────────
function openStage(card, posSelector, isLevity) {
_viewingPos = posSelector;
_seaHand[posSelector] = { card: card, isLevity: isLevity };
_populate(card, isLevity);
_fillSlot(posSelector, card, isLevity);
_showStage();
}
// ── Init ──────────────────────────────────────────────────────────────────
function init() {
overlay = document.getElementById('id_sea_overlay');
if (!overlay) return;
stage = overlay.querySelector('#id_sea_stage');
stageCard = stage && stage.querySelector('.sea-stage-card');
statBlock = stage && stage.querySelector('.sea-stat-block');
if (!stage || !stageCard || !statBlock) return;
_userPolarity = overlay.dataset.seaUserPolarity || 'levity';
spinBtn = statBlock.querySelector('.sea-spin-btn');
fyiBtn = statBlock.querySelector('.sea-fyi-btn');
bdrop = stage.querySelector('.sea-stage-backdrop');
fyiPanel = overlay.querySelector('#id_sea_fyi_panel');
fyiTitle = fyiPanel && fyiPanel.querySelector('.sig-info-title');
fyiType = fyiPanel && fyiPanel.querySelector('.sig-info-type');
fyiEffect = fyiPanel && fyiPanel.querySelector('.sig-info-effect');
fyiIndex = fyiPanel && fyiPanel.querySelector('.sig-info-index');
fyiPrev = statBlock.querySelector('.sea-fyi-prev');
fyiNext = statBlock.querySelector('.sea-fyi-next');
_spinOrigLabel = spinBtn ? spinBtn.textContent : 'SPIN';
_fyiOrigLabel = fyiBtn ? fyiBtn.textContent : 'FYI';
// SPIN
if (spinBtn) {
spinBtn.addEventListener('click', function () {
if (spinBtn.classList.contains('btn-disabled')) return;
statBlock.classList.toggle('is-reversed');
stageCard.classList.toggle('stage-card--reversed');
});
}
// FYI
if (fyiBtn) {
fyiBtn.addEventListener('click', function () {
if (fyiBtn.classList.contains('btn-disabled')) return;
_infoOpen ? _closeInfo() : _openInfo();
});
}
// FYI nav
if (fyiPrev) {
fyiPrev.addEventListener('click', function () {
_infoIdx = (_infoIdx - 1 + _infoData.length) % _infoData.length;
_renderInfo();
});
}
if (fyiNext) {
fyiNext.addEventListener('click', function () {
_infoIdx = (_infoIdx + 1) % _infoData.length;
_renderInfo();
});
}
// Backdrop dismiss
if (bdrop) {
bdrop.addEventListener('click', _hideStage);
}
// Click on deposited slot → re-open
overlay.addEventListener('click', function (e) {
var slot = e.target.closest('.sea-card-slot--filled');
if (!slot) return;
var pos = slot.dataset.posKey;
if (!pos || !_seaHand[pos]) return;
var h = _seaHand[pos];
_viewingPos = pos;
_populate(h.card, h.isLevity);
_showStage();
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
function resetHand() {
_seaHand = {};
_viewingPos = null;
_hideStage();
}
return {
openStage: openStage,
resetHand: resetHand,
_testInit: function () {
overlay = null; stage = null; stageCard = null; statBlock = null;
spinBtn = null; fyiBtn = null; bdrop = null;
fyiPanel = null; fyiTitle = null; fyiType = null;
fyiEffect = null; fyiIndex = null; fyiPrev = null; fyiNext = null;
_userPolarity = 'levity';
_seaHand = {}; _viewingPos = null;
_infoData = []; _infoIdx = 0; _infoOpen = false;
init();
},
};
}());

View File

@@ -1141,8 +1141,15 @@ def sea_deck(request, room_id):
'number': c.number,
'corner_rank': c.corner_rank,
'suit_icon': c.suit_icon,
'name_group': c.name_group,
'name_title': c.name_title,
'levity_qualifier': c.levity_qualifier,
'gravity_qualifier': c.gravity_qualifier,
'reversal': c.reversal,
'keywords_upright': c.keywords_upright,
'keywords_reversed': c.keywords_reversed,
'energies': c.energies,
'operations': c.operations,
}
available = list(