Game Kit fan stage + FLIP/SPIN; sig/sea/fan refactor — TDD
- fan modal: stage block w. idle-reveal/careen-out; carousel shifts left so focused card sits left-of-center; SPIN rotates whole card via Element.animate(); FLIP toggles polarity (Levity ↔ Gravity) via perspective rotateY w. mid-flip repaint; SPIN state retained across FLIP; FLIP btn hover-revealed only when focused card or btn is hovered (:has) - mobile breakpoints: --fan-card-w / --fan-card-h / --fan-stage-shift / --fan-carousel-step lifted to CSS vars on .tarot-fan-wrap; portrait ≤ 480px @ 150×230, landscape ≤ 500h @ 150×235; corners + face text/padding scale w. card width - shared StageCard JS module (apps/epic/stage-card.js): fromDataset, populateCard, populateKeywords, buildInfoData, renderFyi — sig/sea/fan all delegate; ~150 lines de-duplicated - shared @mixin stat-block-shared (SCSS) lifts duplicated stat-face / stat-keywords / sig-info rules; @mixin stage-card-polarity unifies sea-stage--levity/--gravity + fan[data-polarity] coloring - model rename: TarotCard.reversal → reversal_qualifier (migration 0014); render-time fallback to current polarity's qualifier when blank - class unification: .sig-info-open / .sea-info-open / .fyi-open → .fyi-open (on stat block); .sig-flip-btn / .sea-spin-btn / .fan-spin-btn → .spin-btn; same for .fyi-btn / .fyi-prev / .fyi-next - custom combobox (apps/epic/combobox.js) replaces native <select> for PICK SEA spread picker — keyboard nav, click-outside-close, aria roles; Firefox/Chrome OS-rendered <option> ignored CSS - Jasmine: FanStageSpec.js w. idle-reveal / population / SPIN / FYI / FLIP specs; sig + sea fixtures + IT view assertions updated for renamed classes - 748 ITs + Jasmine green 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:
@@ -0,0 +1,22 @@
|
|||||||
|
"""Rename TarotCard.reversal → TarotCard.reversal_qualifier.
|
||||||
|
|
||||||
|
Symmetric naming with levity_qualifier / gravity_qualifier; disambiguates the
|
||||||
|
qualifier-text field from the reversal *axis* state and the keywords_reversed
|
||||||
|
list. Pure column rename — no data movement.
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("epic", "0013_fix_nomad_icon"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="tarotcard",
|
||||||
|
old_name="reversal",
|
||||||
|
new_name="reversal_qualifier",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -250,7 +250,7 @@ class TarotCard(models.Model):
|
|||||||
slug = models.SlugField(max_length=120)
|
slug = models.SlugField(max_length=120)
|
||||||
correspondence = models.CharField(max_length=200, blank=True) # Tarot / Minchiate equivalent
|
correspondence = models.CharField(max_length=200, blank=True) # Tarot / Minchiate equivalent
|
||||||
group = models.CharField(max_length=100, blank=True) # Earthman major grouping
|
group = models.CharField(max_length=100, blank=True) # Earthman major grouping
|
||||||
reversal = models.CharField(max_length=200, blank=True, default='') # reversed-state title; blank = same as name
|
reversal_qualifier = models.CharField(max_length=200, blank=True, default='') # reversal-axis qualifier (e.g. "Nervous"); polarity-shared; blank = falls back to current polarity's qualifier
|
||||||
levity_qualifier = models.CharField(max_length=100, blank=True, default='')
|
levity_qualifier = models.CharField(max_length=100, blank=True, default='')
|
||||||
gravity_qualifier = models.CharField(max_length=100, blank=True, default='')
|
gravity_qualifier = models.CharField(max_length=100, blank=True, default='')
|
||||||
levity_emanation = models.CharField(max_length=200, blank=True, default='') # polarity-split upright (cards 48-49)
|
levity_emanation = models.CharField(max_length=200, blank=True, default='') # polarity-split upright (cards 48-49)
|
||||||
|
|||||||
123
src/apps/epic/static/apps/epic/combobox.js
Normal file
123
src/apps/epic/static/apps/epic/combobox.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
// Generic CSS-stylable replacement for <select>. Native <option> rendering on
|
||||||
|
// Firefox/Chrome is partly OS-controlled and ignores most CSS; this gives full
|
||||||
|
// styling control via div-based listbox markup.
|
||||||
|
//
|
||||||
|
// Markup contract:
|
||||||
|
// <input type="hidden" id="X" name="..." value="...">
|
||||||
|
// <div data-combobox data-combobox-target="X"
|
||||||
|
// role="combobox" aria-expanded="false" aria-haspopup="listbox"
|
||||||
|
// tabindex="0">
|
||||||
|
// <span class="sea-select-current">…visible label…</span>
|
||||||
|
// <span class="sea-select-arrow" aria-hidden="true">▾</span>
|
||||||
|
// <ul class="sea-select-list" role="listbox">
|
||||||
|
// <li role="option" data-value="..." aria-selected="true|false">…</li>
|
||||||
|
// …
|
||||||
|
// </ul>
|
||||||
|
// </div>
|
||||||
|
//
|
||||||
|
// On selection the hidden input's value is updated and a 'change' event fires
|
||||||
|
// on it, so existing consumers (`document.getElementById('X').value`) keep working.
|
||||||
|
var Combobox = (function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function init(combo) {
|
||||||
|
if (!combo || combo.dataset.comboboxInit) return;
|
||||||
|
combo.dataset.comboboxInit = '1';
|
||||||
|
|
||||||
|
var hiddenId = combo.dataset.comboboxTarget;
|
||||||
|
var hidden = hiddenId && document.getElementById(hiddenId);
|
||||||
|
var current = combo.querySelector('.sea-select-current');
|
||||||
|
var list = combo.querySelector('.sea-select-list');
|
||||||
|
var options = list ? Array.from(list.querySelectorAll('[role="option"]')) : [];
|
||||||
|
var focusIdx = options.findIndex(function (o) { return o.getAttribute('aria-selected') === 'true'; });
|
||||||
|
if (focusIdx < 0) focusIdx = 0;
|
||||||
|
|
||||||
|
function isOpen() { return combo.getAttribute('aria-expanded') === 'true'; }
|
||||||
|
function open() {
|
||||||
|
combo.setAttribute('aria-expanded', 'true');
|
||||||
|
// Re-focus the currently-selected option each time we open
|
||||||
|
focusIdx = options.findIndex(function (o) { return o.getAttribute('aria-selected') === 'true'; });
|
||||||
|
if (focusIdx < 0) focusIdx = 0;
|
||||||
|
highlight();
|
||||||
|
}
|
||||||
|
function close() { combo.setAttribute('aria-expanded', 'false'); }
|
||||||
|
function toggle() { isOpen() ? close() : open(); }
|
||||||
|
|
||||||
|
function highlight() {
|
||||||
|
options.forEach(function (o, i) {
|
||||||
|
o.classList.toggle('sea-select-option--focus', i === focusIdx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function select(i) {
|
||||||
|
if (i < 0 || i >= options.length) return;
|
||||||
|
options.forEach(function (o, idx) {
|
||||||
|
o.setAttribute('aria-selected', idx === i ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
var picked = options[i];
|
||||||
|
if (current) current.textContent = picked.textContent.trim();
|
||||||
|
if (hidden && hidden.value !== picked.dataset.value) {
|
||||||
|
hidden.value = picked.dataset.value;
|
||||||
|
hidden.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
combo.addEventListener('click', function (e) {
|
||||||
|
var opt = e.target.closest('[role="option"]');
|
||||||
|
if (opt) {
|
||||||
|
var i = options.indexOf(opt);
|
||||||
|
if (i >= 0) select(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
combo.addEventListener('keydown', function (e) {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isOpen()) { open(); }
|
||||||
|
else { focusIdx = Math.min(options.length - 1, focusIdx + 1); highlight(); }
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isOpen()) { open(); }
|
||||||
|
else { focusIdx = Math.max(0, focusIdx - 1); highlight(); }
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
case ' ':
|
||||||
|
e.preventDefault();
|
||||||
|
if (isOpen()) select(focusIdx);
|
||||||
|
else open();
|
||||||
|
break;
|
||||||
|
case 'Escape':
|
||||||
|
if (isOpen()) { e.preventDefault(); close(); }
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
if (isOpen()) { e.preventDefault(); focusIdx = 0; highlight(); }
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
if (isOpen()) { e.preventDefault(); focusIdx = options.length - 1; highlight(); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click outside the combo closes the dropdown
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (!combo.contains(e.target)) close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initAll(root) {
|
||||||
|
(root || document).querySelectorAll('[data-combobox]').forEach(init);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
||||||
|
} else {
|
||||||
|
initAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { init: init, initAll: initAll };
|
||||||
|
}());
|
||||||
@@ -13,70 +13,17 @@ var SeaDeal = (function () {
|
|||||||
var _infoOpen = false;
|
var _infoOpen = false;
|
||||||
var _spinOrigLabel, _fyiOrigLabel;
|
var _spinOrigLabel, _fyiOrigLabel;
|
||||||
|
|
||||||
// ── Keyword list ──────────────────────────────────────────────────────────
|
// ── Stage card population (delegates to shared StageCard module) ──────────
|
||||||
|
|
||||||
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) {
|
function _populate(card, isLevity) {
|
||||||
var qualifier = isLevity
|
var polarity = isLevity ? 'levity' : 'gravity';
|
||||||
? (card.levity_qualifier || '')
|
StageCard.populateCard(stageCard, card, polarity);
|
||||||
: (card.gravity_qualifier || '');
|
StageCard.populateKeywords(statBlock, card.keywords_upright, card.keywords_reversed, {
|
||||||
var isMajor = card.arcana === 'MAJOR';
|
uprightSel: '#id_sea_stat_upright',
|
||||||
var title = card.name_title || card.name || '';
|
reversedSel: '#id_sea_stat_reversed',
|
||||||
|
|
||||||
// Corners
|
|
||||||
stageCard.querySelectorAll('.fan-corner-rank').forEach(function (el) {
|
|
||||||
el.textContent = card.corner_rank || '';
|
|
||||||
});
|
});
|
||||||
stageCard.querySelectorAll('.stage-suit-icon').forEach(function (el) {
|
_infoData = StageCard.buildInfoData(card);
|
||||||
if (card.suit_icon) {
|
_infoIdx = 0;
|
||||||
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 = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
// Reset SPIN
|
||||||
stageCard.classList.remove('stage-card--reversed');
|
stageCard.classList.remove('stage-card--reversed');
|
||||||
@@ -87,23 +34,7 @@ var SeaDeal = (function () {
|
|||||||
// ── FYI info panel ────────────────────────────────────────────────────────
|
// ── FYI info panel ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function _renderInfo() {
|
function _renderInfo() {
|
||||||
if (!fyiPanel) return;
|
StageCard.renderFyi(fyiPanel, _infoData, _infoIdx);
|
||||||
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() {
|
function _openInfo() {
|
||||||
@@ -112,7 +43,7 @@ var SeaDeal = (function () {
|
|||||||
if (fyiPanel) fyiPanel.style.display = 'flex';
|
if (fyiPanel) fyiPanel.style.display = 'flex';
|
||||||
if (spinBtn) { spinBtn.classList.add('btn-disabled'); spinBtn.textContent = '×'; }
|
if (spinBtn) { spinBtn.classList.add('btn-disabled'); spinBtn.textContent = '×'; }
|
||||||
if (fyiBtn) { fyiBtn.classList.add('btn-disabled'); fyiBtn.textContent = '×'; }
|
if (fyiBtn) { fyiBtn.classList.add('btn-disabled'); fyiBtn.textContent = '×'; }
|
||||||
stage.classList.add('sea-info-open');
|
statBlock.classList.add('fyi-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
function _closeInfo() {
|
function _closeInfo() {
|
||||||
@@ -120,7 +51,7 @@ var SeaDeal = (function () {
|
|||||||
if (fyiPanel) fyiPanel.style.display = 'none';
|
if (fyiPanel) fyiPanel.style.display = 'none';
|
||||||
if (spinBtn) { spinBtn.classList.remove('btn-disabled'); spinBtn.textContent = _spinOrigLabel || 'SPIN'; }
|
if (spinBtn) { spinBtn.classList.remove('btn-disabled'); spinBtn.textContent = _spinOrigLabel || 'SPIN'; }
|
||||||
if (fyiBtn) { fyiBtn.classList.remove('btn-disabled'); fyiBtn.textContent = _fyiOrigLabel || 'FYI'; }
|
if (fyiBtn) { fyiBtn.classList.remove('btn-disabled'); fyiBtn.textContent = _fyiOrigLabel || 'FYI'; }
|
||||||
if (stage) stage.classList.remove('sea-info-open');
|
if (statBlock) statBlock.classList.remove('fyi-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Slot fill ─────────────────────────────────────────────────────────────
|
// ── Slot fill ─────────────────────────────────────────────────────────────
|
||||||
@@ -187,8 +118,8 @@ var SeaDeal = (function () {
|
|||||||
|
|
||||||
_userPolarity = overlay.dataset.seaUserPolarity || 'levity';
|
_userPolarity = overlay.dataset.seaUserPolarity || 'levity';
|
||||||
|
|
||||||
spinBtn = statBlock.querySelector('.sea-spin-btn');
|
spinBtn = statBlock.querySelector('.spin-btn');
|
||||||
fyiBtn = statBlock.querySelector('.sea-fyi-btn');
|
fyiBtn = statBlock.querySelector('.fyi-btn');
|
||||||
bdrop = stage.querySelector('.sea-stage-backdrop');
|
bdrop = stage.querySelector('.sea-stage-backdrop');
|
||||||
|
|
||||||
fyiPanel = overlay.querySelector('#id_sea_fyi_panel');
|
fyiPanel = overlay.querySelector('#id_sea_fyi_panel');
|
||||||
@@ -196,8 +127,8 @@ var SeaDeal = (function () {
|
|||||||
fyiType = fyiPanel && fyiPanel.querySelector('.sig-info-type');
|
fyiType = fyiPanel && fyiPanel.querySelector('.sig-info-type');
|
||||||
fyiEffect = fyiPanel && fyiPanel.querySelector('.sig-info-effect');
|
fyiEffect = fyiPanel && fyiPanel.querySelector('.sig-info-effect');
|
||||||
fyiIndex = fyiPanel && fyiPanel.querySelector('.sig-info-index');
|
fyiIndex = fyiPanel && fyiPanel.querySelector('.sig-info-index');
|
||||||
fyiPrev = statBlock.querySelector('.sea-fyi-prev');
|
fyiPrev = statBlock.querySelector('.fyi-prev');
|
||||||
fyiNext = statBlock.querySelector('.sea-fyi-next');
|
fyiNext = statBlock.querySelector('.fyi-next');
|
||||||
|
|
||||||
_spinOrigLabel = spinBtn ? spinBtn.textContent : 'SPIN';
|
_spinOrigLabel = spinBtn ? spinBtn.textContent : 'SPIN';
|
||||||
_fyiOrigLabel = fyiBtn ? fyiBtn.textContent : 'FYI';
|
_fyiOrigLabel = fyiBtn ? fyiBtn.textContent : 'FYI';
|
||||||
@@ -225,7 +156,7 @@ var SeaDeal = (function () {
|
|||||||
// Clicking the FYI panel itself dismisses it (same as sig-select caution)
|
// Clicking the FYI panel itself dismisses it (same as sig-select caution)
|
||||||
if (fyiPanel) {
|
if (fyiPanel) {
|
||||||
fyiPanel.addEventListener('click', function (e) {
|
fyiPanel.addEventListener('click', function (e) {
|
||||||
if (!e.target.closest('.sea-fyi-prev') && !e.target.closest('.sea-fyi-next')) {
|
if (!e.target.closest('.fyi-prev') && !e.target.closest('.fyi-next')) {
|
||||||
_closeInfo();
|
_closeInfo();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,63 +36,32 @@ var SigSelect = (function () {
|
|||||||
|
|
||||||
// ── Stage ──────────────────────────────────────────────────────────────
|
// ── Stage ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function _populateKeywordList(listEl, csv) {
|
// _populateKeywordList removed — sig now delegates to StageCard.populateKeywords
|
||||||
var keywords = csv ? csv.split(',').filter(Boolean) : [];
|
|
||||||
listEl.innerHTML = keywords.map(function (k) {
|
|
||||||
return '<li>' + k.trim() + '</li>';
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Caution tooltip ───────────────────────────────────────────────────
|
// ── Caution tooltip ───────────────────────────────────────────────────
|
||||||
|
|
||||||
function _renderCaution() {
|
function _renderCaution() {
|
||||||
if (_cautionData.length === 0) {
|
StageCard.renderFyi(stage.querySelector('.sig-info'), _cautionData, _cautionIdx);
|
||||||
cautionTitle.textContent = 'Energy';
|
// Sig disables PRV/NXT when there's only one entry (sea/fan don't bother).
|
||||||
cautionTitle.className = 'sig-info-title sig-info-title--energies';
|
if (cautionPrev) cautionPrev.disabled = (_cautionData.length <= 1);
|
||||||
if (cautionTypeEl) cautionTypeEl.textContent = '';
|
if (cautionNext) cautionNext.disabled = (_cautionData.length <= 1);
|
||||||
cautionEffect.innerHTML = '<em>No interactions defined.</em>';
|
|
||||||
cautionPrev.disabled = true;
|
|
||||||
cautionNext.disabled = true;
|
|
||||||
cautionIndexEl.textContent = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var entry = _cautionData[_cautionIdx];
|
|
||||||
var isEnergies = entry.category === 'energies';
|
|
||||||
cautionTitle.textContent = isEnergies ? 'Energy' : 'Operation';
|
|
||||||
cautionTitle.className = 'sig-info-title sig-info-title--' + entry.category;
|
|
||||||
if (cautionTypeEl) cautionTypeEl.textContent = entry.type || '';
|
|
||||||
cautionEffect.innerHTML = entry.effect || '';
|
|
||||||
cautionPrev.disabled = (_cautionData.length <= 1);
|
|
||||||
cautionNext.disabled = (_cautionData.length <= 1);
|
|
||||||
cautionIndexEl.textContent = _cautionData.length > 1
|
|
||||||
? (_cautionIdx + 1) + ' / ' + _cautionData.length
|
|
||||||
: '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _openCaution() {
|
function _openCaution() {
|
||||||
if (!_focusedCardEl) return;
|
if (!_focusedCardEl) return;
|
||||||
try {
|
var card = StageCard.fromDataset(_focusedCardEl);
|
||||||
var energies = JSON.parse(_focusedCardEl.dataset.energies || '[]');
|
_cautionData = StageCard.buildInfoData(card);
|
||||||
var operations = JSON.parse(_focusedCardEl.dataset.operations || '[]');
|
|
||||||
_cautionData = energies.map(function (e) {
|
|
||||||
return { type: e.type, effect: e.effect, category: 'energies' };
|
|
||||||
}).concat(operations.map(function (o) {
|
|
||||||
return { type: o.type, effect: o.effect, category: 'operations' };
|
|
||||||
}));
|
|
||||||
} catch (e) {
|
|
||||||
_cautionData = [];
|
|
||||||
}
|
|
||||||
_cautionIdx = 0;
|
_cautionIdx = 0;
|
||||||
_renderCaution();
|
_renderCaution();
|
||||||
_flipBtn.classList.add('btn-disabled');
|
_flipBtn.classList.add('btn-disabled');
|
||||||
_cautionBtn.classList.add('btn-disabled');
|
_cautionBtn.classList.add('btn-disabled');
|
||||||
_flipBtn.textContent = '\u00D7';
|
_flipBtn.textContent = '\u00D7';
|
||||||
_cautionBtn.textContent = '\u00D7';
|
_cautionBtn.textContent = '\u00D7';
|
||||||
stage.classList.add('sig-info-open');
|
statBlock.classList.add('fyi-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
function _closeCaution() {
|
function _closeCaution() {
|
||||||
stage.classList.remove('sig-info-open');
|
if (statBlock) statBlock.classList.remove('fyi-open');
|
||||||
if (_flipBtn) {
|
if (_flipBtn) {
|
||||||
_flipBtn.classList.remove('btn-disabled');
|
_flipBtn.classList.remove('btn-disabled');
|
||||||
_cautionBtn.classList.remove('btn-disabled');
|
_cautionBtn.classList.remove('btn-disabled');
|
||||||
@@ -112,65 +81,19 @@ var SigSelect = (function () {
|
|||||||
}
|
}
|
||||||
_focusedCardEl = cardEl;
|
_focusedCardEl = cardEl;
|
||||||
|
|
||||||
var rank = cardEl.dataset.cornerRank || '';
|
var card = StageCard.fromDataset(cardEl);
|
||||||
var icon = cardEl.dataset.suitIcon || '';
|
StageCard.populateCard(stageCard, card, userPolarity);
|
||||||
var group = cardEl.dataset.nameGroup || '';
|
// Sig hides the correspondence in the stage card (shown in game-kit only)
|
||||||
var title = cardEl.dataset.nameTitle || '';
|
var corrEl = stageCard.querySelector('.fan-card-correspondence');
|
||||||
var arcana= cardEl.dataset.arcana || '';
|
if (corrEl) corrEl.textContent = '';
|
||||||
var corr = cardEl.dataset.correspondence || '';
|
|
||||||
|
|
||||||
stageCard.querySelectorAll('.fan-corner-rank').forEach(function (el) { el.textContent = rank; });
|
// Reset SPIN state, then populate keyword lists
|
||||||
stageCard.querySelectorAll('.stage-suit-icon').forEach(function (el) {
|
|
||||||
if (icon) {
|
|
||||||
el.className = 'fa-solid ' + icon + ' stage-suit-icon';
|
|
||||||
el.style.display = '';
|
|
||||||
} else {
|
|
||||||
el.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stageCard.querySelector('.fan-card-name-group').textContent = group;
|
|
||||||
stageCard.querySelector('.fan-card-arcana').textContent = arcana;
|
|
||||||
stageCard.querySelector('.fan-card-correspondence').textContent = ''; // shown in game-kit only
|
|
||||||
|
|
||||||
var qualifier = userPolarity === 'levity'
|
|
||||||
? (cardEl.dataset.levityQualifier || '')
|
|
||||||
: (cardEl.dataset.gravityQualifier || '');
|
|
||||||
var isMajor = arcana.toLowerCase().indexOf('major') !== -1;
|
|
||||||
// Major arcana: qualifier sits below the title — append comma so it reads as a subtitle.
|
|
||||||
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 : '';
|
|
||||||
|
|
||||||
// Reversed face.
|
|
||||||
// - Major arcana: polarity qualifier + reversal concept name
|
|
||||||
// - Non-major w. reversal: suit qualifier word replaces polarity qualifier;
|
|
||||||
// card name (title) stays the same — two separate lines
|
|
||||||
// - Non-major w/o reversal: fall back to mirroring the polarity qualifier
|
|
||||||
var reversal = cardEl.dataset.reversal || '';
|
|
||||||
if (isMajor) {
|
|
||||||
// Slots are swapped vs. non-major: spin reverses DOM order visually,
|
|
||||||
// so qualifier-slot (DOM-second) appears first and name-slot (DOM-first) appears second.
|
|
||||||
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 = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate stat block keyword faces and reset to upright
|
|
||||||
statBlock.classList.remove('is-reversed');
|
statBlock.classList.remove('is-reversed');
|
||||||
stageCard.classList.remove('stage-card--reversed');
|
stageCard.classList.remove('stage-card--reversed');
|
||||||
_populateKeywordList(
|
StageCard.populateKeywords(statBlock, card.keywords_upright, card.keywords_reversed, {
|
||||||
statBlock.querySelector('#id_stat_keywords_upright'),
|
uprightSel: '#id_stat_keywords_upright',
|
||||||
cardEl.dataset.keywordsUpright
|
reversedSel: '#id_stat_keywords_reversed',
|
||||||
);
|
});
|
||||||
_populateKeywordList(
|
|
||||||
statBlock.querySelector('#id_stat_keywords_reversed'),
|
|
||||||
cardEl.dataset.keywordsReversed
|
|
||||||
);
|
|
||||||
|
|
||||||
stageCard.style.display = '';
|
stageCard.style.display = '';
|
||||||
stage.classList.add('sig-stage--active');
|
stage.classList.add('sig-stage--active');
|
||||||
@@ -639,8 +562,8 @@ var SigSelect = (function () {
|
|||||||
stageCard = stage.querySelector('.sig-stage-card');
|
stageCard = stage.querySelector('.sig-stage-card');
|
||||||
statBlock = stage.querySelector('.sig-stat-block');
|
statBlock = stage.querySelector('.sig-stat-block');
|
||||||
|
|
||||||
_flipBtn = statBlock.querySelector('.sig-flip-btn');
|
_flipBtn = statBlock.querySelector('.spin-btn');
|
||||||
_cautionBtn = statBlock.querySelector('.sig-info-btn');
|
_cautionBtn = statBlock.querySelector('.fyi-btn');
|
||||||
_flipOrigLabel = _flipBtn.textContent;
|
_flipOrigLabel = _flipBtn.textContent;
|
||||||
_cautionOrigLabel = _cautionBtn.textContent;
|
_cautionOrigLabel = _cautionBtn.textContent;
|
||||||
|
|
||||||
@@ -654,8 +577,8 @@ var SigSelect = (function () {
|
|||||||
cautionEffect = cautionEl.querySelector('.sig-info-effect');
|
cautionEffect = cautionEl.querySelector('.sig-info-effect');
|
||||||
cautionTitle = cautionEl.querySelector('.sig-info-title');
|
cautionTitle = cautionEl.querySelector('.sig-info-title');
|
||||||
cautionTypeEl = cautionEl.querySelector('.sig-info-type');
|
cautionTypeEl = cautionEl.querySelector('.sig-info-type');
|
||||||
cautionPrev = statBlock.querySelector('.sig-info-prev');
|
cautionPrev = statBlock.querySelector('.fyi-prev');
|
||||||
cautionNext = statBlock.querySelector('.sig-info-next');
|
cautionNext = statBlock.querySelector('.fyi-next');
|
||||||
cautionIndexEl = cautionEl.querySelector('.sig-info-index');
|
cautionIndexEl = cautionEl.querySelector('.sig-info-index');
|
||||||
|
|
||||||
// Clicking the tooltip (not nav buttons) dismisses it
|
// Clicking the tooltip (not nav buttons) dismisses it
|
||||||
@@ -665,7 +588,7 @@ var SigSelect = (function () {
|
|||||||
|
|
||||||
_cautionBtn.addEventListener('click', function () {
|
_cautionBtn.addEventListener('click', function () {
|
||||||
if (_cautionBtn.classList.contains('btn-disabled')) return;
|
if (_cautionBtn.classList.contains('btn-disabled')) return;
|
||||||
stage.classList.contains('sig-info-open') ? _closeCaution() : _openCaution();
|
statBlock.classList.contains('fyi-open') ? _closeCaution() : _openCaution();
|
||||||
});
|
});
|
||||||
cautionPrev.addEventListener('click', function () {
|
cautionPrev.addEventListener('click', function () {
|
||||||
_cautionIdx = (_cautionIdx - 1 + _cautionData.length) % _cautionData.length;
|
_cautionIdx = (_cautionIdx - 1 + _cautionData.length) % _cautionData.length;
|
||||||
|
|||||||
158
src/apps/epic/static/apps/epic/stage-card.js
Normal file
158
src/apps/epic/static/apps/epic/stage-card.js
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
// Shared stage-card helpers used by Sig Select, Sea Select, and the Game Kit fan.
|
||||||
|
//
|
||||||
|
// Each call site handles its own DOM lookup, click wiring, and polarity decisions;
|
||||||
|
// this module owns the duplicated logic for normalizing a card source, painting
|
||||||
|
// the upright/reversal faces, populating the stat-block keyword lists, and
|
||||||
|
// rendering the FYI panel entries.
|
||||||
|
var StageCard = (function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function _parseCSV(str) {
|
||||||
|
return (str || '').split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
||||||
|
}
|
||||||
|
function _parseJSON(str) {
|
||||||
|
try { return JSON.parse(str || '[]'); } catch (e) { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize a `.sig-card` / `.fan-card` element's data-* attrs into the same
|
||||||
|
// shape that sea.js receives via fetch (so all call sites can share populateCard).
|
||||||
|
function fromDataset(el) {
|
||||||
|
return {
|
||||||
|
corner_rank: el.dataset.cornerRank || '',
|
||||||
|
suit_icon: el.dataset.suitIcon || '',
|
||||||
|
name_group: el.dataset.nameGroup || '',
|
||||||
|
name_title: el.dataset.nameTitle || '',
|
||||||
|
arcana: el.dataset.arcana || '',
|
||||||
|
correspondence: el.dataset.correspondence || '',
|
||||||
|
keywords_upright: _parseCSV(el.dataset.keywordsUpright),
|
||||||
|
keywords_reversed: _parseCSV(el.dataset.keywordsReversed),
|
||||||
|
energies: _parseJSON(el.dataset.energies),
|
||||||
|
operations: _parseJSON(el.dataset.operations),
|
||||||
|
levity_qualifier: el.dataset.levityQualifier || '',
|
||||||
|
gravity_qualifier: el.dataset.gravityQualifier || '',
|
||||||
|
reversal_qualifier: el.dataset.reversalQualifier || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide whether a card object represents Major Arcana — sig sources from
|
||||||
|
// `data-arcana` (Django's `get_arcana_display`, e.g. "Major Arcana"), sea
|
||||||
|
// from card.arcana (model code, e.g. "MAJOR"). Accept both.
|
||||||
|
function _isMajor(card) {
|
||||||
|
var a = (card.arcana || '').toUpperCase();
|
||||||
|
return a === 'MAJOR' || a === 'MAJOR ARCANA';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint the stage-card's upright + reversal faces from a normalized card
|
||||||
|
// object + the active polarity ('levity' | 'gravity'). Reversal-qualifier
|
||||||
|
// falls back to the current polarity's qualifier when blank (6F behavior).
|
||||||
|
function populateCard(stageCard, card, polarity) {
|
||||||
|
if (!stageCard) return;
|
||||||
|
var isLevity = polarity === 'levity';
|
||||||
|
var qualifier = isLevity ? (card.levity_qualifier || '') : (card.gravity_qualifier || '');
|
||||||
|
var isMajor = _isMajor(card);
|
||||||
|
var title = card.name_title || '';
|
||||||
|
var reversalQualifier = card.reversal_qualifier || '';
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
var nameEl = stageCard.querySelector('.fan-card-name');
|
||||||
|
if (nameEl) nameEl.textContent = isMajor ? title + ',' : title;
|
||||||
|
|
||||||
|
var qAbove = stageCard.querySelector('.sig-qualifier-above');
|
||||||
|
if (qAbove) qAbove.textContent = isMajor ? '' : qualifier;
|
||||||
|
var qBelow = stageCard.querySelector('.sig-qualifier-below');
|
||||||
|
if (qBelow) qBelow.textContent = isMajor ? qualifier : '';
|
||||||
|
|
||||||
|
// Reversal face — three cases:
|
||||||
|
// Major: title (with comma) in qualifier slot, qualifier in name slot
|
||||||
|
// Non-major + reversal_qual: reversal_qualifier in qualifier slot, title in name slot
|
||||||
|
// Non-major no reversal_qual: fall back to current polarity's qualifier
|
||||||
|
var rQual = stageCard.querySelector('.fan-card-reversal-qualifier');
|
||||||
|
var rName = stageCard.querySelector('.fan-card-reversal-name');
|
||||||
|
if (rQual && rName) {
|
||||||
|
if (isMajor) {
|
||||||
|
rQual.textContent = title + ',';
|
||||||
|
rName.textContent = qualifier;
|
||||||
|
} else if (reversalQualifier) {
|
||||||
|
rQual.textContent = reversalQualifier;
|
||||||
|
rName.textContent = title;
|
||||||
|
} else {
|
||||||
|
rQual.textContent = qualifier;
|
||||||
|
rName.textContent = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill the upright + reversed keyword <ul>s. Accepts either CSS-selector
|
||||||
|
// overrides or the default `.stat-face--upright .stat-keywords` /
|
||||||
|
// `.stat-face--reversed .stat-keywords`.
|
||||||
|
function populateKeywords(statBlock, uprightItems, reversedItems, opts) {
|
||||||
|
if (!statBlock) return;
|
||||||
|
opts = opts || {};
|
||||||
|
var upSel = opts.uprightSel || '.stat-face--upright .stat-keywords';
|
||||||
|
var rvSel = opts.reversedSel || '.stat-face--reversed .stat-keywords';
|
||||||
|
var ul = statBlock.querySelector(upSel);
|
||||||
|
var rl = statBlock.querySelector(rvSel);
|
||||||
|
if (ul) ul.innerHTML = (uprightItems || []).map(function (k) { return '<li>' + k + '</li>'; }).join('');
|
||||||
|
if (rl) rl.innerHTML = (reversedItems || []).map(function (k) { return '<li>' + k + '</li>'; }).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concatenate energies + operations into a single FYI nav array.
|
||||||
|
function buildInfoData(card) {
|
||||||
|
var data = (card.energies || []).map(function (e) {
|
||||||
|
return { type: e.type, effect: e.effect, category: 'energies' };
|
||||||
|
});
|
||||||
|
return data.concat((card.operations || []).map(function (o) {
|
||||||
|
return { type: o.type, effect: o.effect, category: 'operations' };
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint a single FYI entry into the .sig-info panel. Caller owns idx + data.
|
||||||
|
function renderFyi(infoPanel, data, idx) {
|
||||||
|
if (!infoPanel) return;
|
||||||
|
var title = infoPanel.querySelector('.sig-info-title');
|
||||||
|
var type = infoPanel.querySelector('.sig-info-type');
|
||||||
|
var effect = infoPanel.querySelector('.sig-info-effect');
|
||||||
|
var index = infoPanel.querySelector('.sig-info-index');
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
if (title) { title.textContent = 'Energy';
|
||||||
|
title.className = 'sig-info-title sig-info-title--energies'; }
|
||||||
|
if (type) type.textContent = '';
|
||||||
|
if (effect) effect.innerHTML = '<em>No interactions defined.</em>';
|
||||||
|
if (index) index.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var entry = data[idx];
|
||||||
|
var isEnergies = entry.category === 'energies';
|
||||||
|
if (title) { title.textContent = isEnergies ? 'Energy' : 'Operation';
|
||||||
|
title.className = 'sig-info-title sig-info-title--' + entry.category; }
|
||||||
|
if (type) type.textContent = entry.type || '';
|
||||||
|
if (effect) effect.innerHTML = entry.effect || '';
|
||||||
|
if (index) index.textContent = data.length > 1
|
||||||
|
? (idx + 1) + ' / ' + data.length : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fromDataset: fromDataset,
|
||||||
|
populateCard: populateCard,
|
||||||
|
populateKeywords: populateKeywords,
|
||||||
|
buildInfoData: buildInfoData,
|
||||||
|
renderFyi: renderFyi,
|
||||||
|
};
|
||||||
|
}());
|
||||||
@@ -1112,7 +1112,7 @@ class SigSelectRenderingTest(TestCase):
|
|||||||
def test_sig_stat_block_structure_rendered(self):
|
def test_sig_stat_block_structure_rendered(self):
|
||||||
response = self.client.get(self.url)
|
response = self.client.get(self.url)
|
||||||
self.assertContains(response, "sig-stat-block")
|
self.assertContains(response, "sig-stat-block")
|
||||||
self.assertContains(response, "sig-flip-btn")
|
self.assertContains(response, "spin-btn")
|
||||||
self.assertContains(response, "stat-face--upright")
|
self.assertContains(response, "stat-face--upright")
|
||||||
self.assertContains(response, "stat-face--reversed")
|
self.assertContains(response, "stat-face--reversed")
|
||||||
|
|
||||||
@@ -1124,11 +1124,11 @@ class SigSelectRenderingTest(TestCase):
|
|||||||
def test_sig_info_panel_structure_rendered(self):
|
def test_sig_info_panel_structure_rendered(self):
|
||||||
response = self.client.get(self.url)
|
response = self.client.get(self.url)
|
||||||
self.assertContains(response, "sig-info")
|
self.assertContains(response, "sig-info")
|
||||||
self.assertContains(response, "sig-info-btn")
|
self.assertContains(response, "fyi-btn")
|
||||||
self.assertContains(response, "sig-info-effect")
|
self.assertContains(response, "sig-info-effect")
|
||||||
self.assertContains(response, "sig-info-index")
|
self.assertContains(response, "sig-info-index")
|
||||||
self.assertContains(response, "sig-info-prev")
|
self.assertContains(response, "fyi-prev")
|
||||||
self.assertContains(response, "sig-info-next")
|
self.assertContains(response, "fyi-next")
|
||||||
|
|
||||||
|
|
||||||
class SelectSigCardViewTest(TestCase):
|
class SelectSigCardViewTest(TestCase):
|
||||||
|
|||||||
@@ -1145,7 +1145,7 @@ def sea_deck(request, room_id):
|
|||||||
'name_title': c.name_title,
|
'name_title': c.name_title,
|
||||||
'levity_qualifier': c.levity_qualifier,
|
'levity_qualifier': c.levity_qualifier,
|
||||||
'gravity_qualifier': c.gravity_qualifier,
|
'gravity_qualifier': c.gravity_qualifier,
|
||||||
'reversal': c.reversal,
|
'reversal_qualifier': c.reversal_qualifier,
|
||||||
'keywords_upright': c.keywords_upright,
|
'keywords_upright': c.keywords_upright,
|
||||||
'keywords_reversed': c.keywords_reversed,
|
'keywords_reversed': c.keywords_reversed,
|
||||||
'energies': c.energies,
|
'energies': c.energies,
|
||||||
|
|||||||
@@ -1,93 +1,226 @@
|
|||||||
function initGameKitPage() {
|
var GameKit = (function () {
|
||||||
const dialog = document.getElementById('id_tarot_fan_dialog');
|
'use strict';
|
||||||
if (!dialog) return;
|
|
||||||
|
|
||||||
const fanContent = document.getElementById('id_fan_content');
|
var dialog, fanContent, prevBtn, nextBtn, fanWrap, flipBtn;
|
||||||
const prevBtn = document.getElementById('id_fan_prev');
|
var stageBlock, spinBtn, fyiBtn, fyiPanel;
|
||||||
const nextBtn = document.getElementById('id_fan_next');
|
var fyiTitle, fyiType, fyiEffect, fyiIndex, fyiPrev, fyiNext;
|
||||||
|
var statUpright, statReversed;
|
||||||
|
var _polarity = 'levity'; // FLIP toggles 'levity' ↔ 'gravity'
|
||||||
|
|
||||||
let currentDeckId = null;
|
var currentDeckId = null;
|
||||||
let currentIndex = 0;
|
var currentIndex = 0;
|
||||||
let cards = [];
|
var cards = [];
|
||||||
|
var _revealTimer = null;
|
||||||
|
var REVEAL_DELAY_MS = 500;
|
||||||
|
|
||||||
function storageKey(deckId) {
|
var _infoData = [];
|
||||||
return 'tarot-fan-' + deckId;
|
var _infoIdx = 0;
|
||||||
}
|
var _infoOpen = false;
|
||||||
|
var _spinOrigLabel = 'SPIN';
|
||||||
|
var _fyiOrigLabel = 'FYI';
|
||||||
|
|
||||||
|
// ── Storage ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function storageKey(deckId) { return 'tarot-fan-' + deckId; }
|
||||||
function savePosition() {
|
function savePosition() {
|
||||||
if (currentDeckId !== null) {
|
if (currentDeckId !== null) {
|
||||||
sessionStorage.setItem(storageKey(currentDeckId), currentIndex);
|
sessionStorage.setItem(storageKey(currentDeckId), currentIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function restorePosition(deckId) {
|
function restorePosition(deckId) {
|
||||||
const saved = sessionStorage.getItem(storageKey(deckId));
|
var saved = sessionStorage.getItem(storageKey(deckId));
|
||||||
return saved !== null ? parseInt(saved, 10) : 0;
|
return saved !== null ? parseInt(saved, 10) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Carousel transforms ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function _carouselStep() {
|
||||||
|
// Pull the per-breakpoint step length from CSS so mobile @media rules
|
||||||
|
// can shrink the carousel without code changes.
|
||||||
|
if (!fanWrap) return 200;
|
||||||
|
var raw = getComputedStyle(fanWrap).getPropertyValue('--fan-carousel-step').trim();
|
||||||
|
var n = parseFloat(raw);
|
||||||
|
return isFinite(n) && n > 0 ? n : 200;
|
||||||
|
}
|
||||||
|
|
||||||
function cardTransform(offset) {
|
function cardTransform(offset) {
|
||||||
const abs = Math.abs(offset);
|
var abs = Math.abs(offset);
|
||||||
|
var step = _carouselStep();
|
||||||
return {
|
return {
|
||||||
transform: 'translateX(' + (offset * 200) + 'px) rotateY(' + (offset * 22) + 'deg) scale(' + Math.max(0.3, 1 - abs * 0.15) + ')',
|
transform: 'translateX(' + (offset * step) + 'px) rotateY(' + (offset * 22) + 'deg) scale(' + Math.max(0.3, 1 - abs * 0.15) + ')',
|
||||||
opacity: Math.max(0.15, 1 - abs * 0.25),
|
opacity: Math.max(0.15, 1 - abs * 0.25),
|
||||||
zIndex: 10 - abs,
|
zIndex: 10 - abs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFan() {
|
function updateFan() {
|
||||||
const total = cards.length;
|
var total = cards.length;
|
||||||
if (!total) return;
|
if (!total) return;
|
||||||
cards.forEach(function(card, i) {
|
cards.forEach(function (card, i) {
|
||||||
let offset = i - currentIndex;
|
var offset = i - currentIndex;
|
||||||
if (offset > total / 2) offset -= total;
|
if (offset > total / 2) offset -= total;
|
||||||
if (offset < -total / 2) offset += total;
|
if (offset < -total / 2) offset += total;
|
||||||
|
var abs = Math.abs(offset);
|
||||||
const abs = Math.abs(offset);
|
|
||||||
card.classList.toggle('fan-card--active', offset === 0);
|
card.classList.toggle('fan-card--active', offset === 0);
|
||||||
|
|
||||||
if (abs > 3) {
|
if (abs > 3) {
|
||||||
card.style.display = 'none';
|
card.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
card.style.display = '';
|
card.style.display = '';
|
||||||
const t = cardTransform(offset);
|
var t = cardTransform(offset);
|
||||||
card.style.transform = t.transform;
|
// Active card may also be reversed — append rotate(180deg) to
|
||||||
card.style.opacity = t.opacity;
|
// the carousel transform without disturbing the layout values.
|
||||||
card.style.zIndex = t.zIndex;
|
var spin = (offset === 0 && card.classList.contains('stage-card--reversed'))
|
||||||
|
? ' rotate(180deg)' : '';
|
||||||
|
card.style.transform = t.transform + spin;
|
||||||
|
card.style.opacity = t.opacity;
|
||||||
|
card.style.zIndex = t.zIndex;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
_populateStage();
|
||||||
|
_scheduleReveal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Stage block: idle reveal / careen-out ─────────────────────────────────
|
||||||
|
|
||||||
|
function _scheduleReveal() {
|
||||||
|
if (!stageBlock) return;
|
||||||
|
stageBlock.classList.remove('is-revealed');
|
||||||
|
if (_revealTimer) clearTimeout(_revealTimer);
|
||||||
|
_revealTimer = setTimeout(function () {
|
||||||
|
stageBlock.classList.add('is-revealed');
|
||||||
|
}, REVEAL_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hideStageImmediate() {
|
||||||
|
if (!stageBlock) return;
|
||||||
|
if (_revealTimer) { clearTimeout(_revealTimer); _revealTimer = null; }
|
||||||
|
stageBlock.classList.remove('is-revealed');
|
||||||
|
_closeFyi();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stage population (delegates to shared StageCard module) ──────────────
|
||||||
|
|
||||||
|
function _populateStage() {
|
||||||
|
if (!stageBlock || !cards.length) return;
|
||||||
|
var cardEl = cards[currentIndex];
|
||||||
|
if (!cardEl) return;
|
||||||
|
|
||||||
|
var card = StageCard.fromDataset(cardEl);
|
||||||
|
// Repaint the focused fan-card's faces in the active polarity (so FLIP
|
||||||
|
// and per-polarity qualifier rendering stay consistent with the data).
|
||||||
|
StageCard.populateCard(cardEl, card, _polarity);
|
||||||
|
cardEl.dataset.polarity = _polarity;
|
||||||
|
|
||||||
|
StageCard.populateKeywords(stageBlock, card.keywords_upright, card.keywords_reversed, {
|
||||||
|
uprightSel: '#id_fan_stat_upright',
|
||||||
|
reversedSel: '#id_fan_stat_reversed',
|
||||||
|
});
|
||||||
|
_infoData = StageCard.buildInfoData(card);
|
||||||
|
_infoIdx = 0;
|
||||||
|
|
||||||
|
// Reset SPIN state on every card change — clears rotation on all cards
|
||||||
|
// so a previously-reversed card returns to upright when it leaves focus.
|
||||||
|
stageBlock.classList.remove('is-reversed');
|
||||||
|
cards.forEach(function (c) { c.classList.remove('stage-card--reversed'); });
|
||||||
|
_closeFyi();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FLIP — toggle polarity with perspective horizontal flip animation ────
|
||||||
|
// Retains SPIN state across the flip (the .stage-card--reversed class on
|
||||||
|
// the card is left untouched).
|
||||||
|
|
||||||
|
function _flipActive() {
|
||||||
|
var active = cards[currentIndex];
|
||||||
|
if (!active) return;
|
||||||
|
if (active.dataset.flipping) return; // mid-flip
|
||||||
|
active.dataset.flipping = '1';
|
||||||
|
|
||||||
|
// Build the resting transform (carousel offset 0 + optional SPIN rotate(180))
|
||||||
|
// and the edge-on midpoint by adding a rotateY layer at the end. Keeping
|
||||||
|
// the carousel + spin intact means the card actually rotates in place
|
||||||
|
// around its centre rather than just the inner face.
|
||||||
|
var spin = active.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)';
|
||||||
|
|
||||||
|
active.animate([
|
||||||
|
{ transform: rest },
|
||||||
|
{ transform: mid, offset: 0.5 },
|
||||||
|
{ transform: rest },
|
||||||
|
], { duration: 500, easing: 'ease' });
|
||||||
|
|
||||||
|
_polarity = (_polarity === 'levity') ? 'gravity' : 'levity';
|
||||||
|
// Swap polarity content + colour at the edge-on midpoint, so the user
|
||||||
|
// sees the new face from the start of the second half-rotation.
|
||||||
|
setTimeout(function () {
|
||||||
|
var card = StageCard.fromDataset(active);
|
||||||
|
StageCard.populateCard(active, card, _polarity);
|
||||||
|
active.dataset.polarity = _polarity;
|
||||||
|
}, 250);
|
||||||
|
// Clear the in-flight flag at animation end. Using setTimeout (not
|
||||||
|
// anim.onfinish) so jasmine.clock().tick() can fake-advance it in tests.
|
||||||
|
setTimeout(function () { delete active.dataset.flipping; }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FYI panel ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function _renderFyi() {
|
||||||
|
StageCard.renderFyi(fyiPanel, _infoData, _infoIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _openFyi() {
|
||||||
|
_infoOpen = true;
|
||||||
|
_renderFyi();
|
||||||
|
if (fyiPanel) fyiPanel.style.display = 'flex';
|
||||||
|
if (spinBtn) { spinBtn.classList.add('btn-disabled'); spinBtn.textContent = '×'; }
|
||||||
|
if (fyiBtn) { fyiBtn.classList.add('btn-disabled'); fyiBtn.textContent = '×'; }
|
||||||
|
if (stageBlock) stageBlock.classList.add('fyi-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _closeFyi() {
|
||||||
|
_infoOpen = false;
|
||||||
|
if (fyiPanel) fyiPanel.style.display = 'none';
|
||||||
|
if (spinBtn) { spinBtn.classList.remove('btn-disabled'); spinBtn.textContent = _spinOrigLabel; }
|
||||||
|
if (fyiBtn) { fyiBtn.classList.remove('btn-disabled'); fyiBtn.textContent = _fyiOrigLabel; }
|
||||||
|
if (stageBlock) stageBlock.classList.remove('fyi-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Open / close the dialog ───────────────────────────────────────────────
|
||||||
|
|
||||||
function openFan(deckId) {
|
function openFan(deckId) {
|
||||||
currentDeckId = deckId;
|
currentDeckId = deckId;
|
||||||
currentIndex = restorePosition(deckId);
|
currentIndex = restorePosition(deckId);
|
||||||
|
|
||||||
fetch('/gameboard/game-kit/deck/' + deckId + '/')
|
fetch('/gameboard/game-kit/deck/' + deckId + '/')
|
||||||
.then(function(r) { return r.text(); })
|
.then(function (r) { return r.text(); })
|
||||||
.then(function(html) {
|
.then(function (html) {
|
||||||
fanContent.innerHTML = html;
|
fanContent.innerHTML = html;
|
||||||
cards = Array.from(fanContent.querySelectorAll('.fan-card'));
|
cards = Array.from(fanContent.querySelectorAll('.fan-card'));
|
||||||
if (currentIndex >= cards.length) currentIndex = 0;
|
if (currentIndex >= cards.length) currentIndex = 0;
|
||||||
cards.forEach(function(c) {
|
cards.forEach(function (c) {
|
||||||
c.style.transition = 'transform 0.18s ease-out, opacity 0.18s ease-out';
|
c.style.transition = 'transform 0.18s ease-out, opacity 0.18s ease-out';
|
||||||
});
|
});
|
||||||
updateFan();
|
updateFan();
|
||||||
dialog.showModal();
|
if (dialog && typeof dialog.showModal === 'function') dialog.showModal();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeFan() {
|
function closeFan() {
|
||||||
savePosition();
|
savePosition();
|
||||||
dialog.close();
|
_hideStageImmediate();
|
||||||
|
if (dialog && typeof dialog.close === 'function') dialog.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Navigation ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function navigate(delta) {
|
function navigate(delta) {
|
||||||
if (!cards.length) return;
|
if (!cards.length) return;
|
||||||
currentIndex = (currentIndex + delta + cards.length) % cards.length;
|
currentIndex = (currentIndex + delta + cards.length) % cards.length;
|
||||||
savePosition();
|
savePosition();
|
||||||
|
_hideStageImmediate();
|
||||||
updateFan();
|
updateFan();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step through multiple cards one at a time so intermediate cards are visible
|
|
||||||
var _navTimer = null;
|
var _navTimer = null;
|
||||||
function navigateAnimated(steps) {
|
function navigateAnimated(steps) {
|
||||||
if (!cards.length || steps === 0) return;
|
if (!cards.length || steps === 0) return;
|
||||||
@@ -102,64 +235,182 @@ function initGameKitPage() {
|
|||||||
tick();
|
tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click on the dark backdrop (the dialog or fan-wrap itself, not on any card child) closes
|
// ── Init ──────────────────────────────────────────────────────────────────
|
||||||
var fanWrap = dialog.querySelector('.tarot-fan-wrap');
|
|
||||||
dialog.addEventListener('click', function(e) {
|
|
||||||
if (e.target === dialog || e.target === fanWrap) closeFan();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Arrow key navigation
|
function init() {
|
||||||
dialog.addEventListener('keydown', function(e) {
|
dialog = document.getElementById('id_tarot_fan_dialog');
|
||||||
if (e.key === 'ArrowRight') navigate(1);
|
if (!dialog) return;
|
||||||
if (e.key === 'ArrowLeft') navigate(-1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mousewheel navigation — accumulate delta, cap at 3 cards per event so fast
|
fanContent = document.getElementById('id_fan_content');
|
||||||
// spins don't overshoot; CSS transitions handle the visual smoothness.
|
prevBtn = document.getElementById('id_fan_prev');
|
||||||
var wheelAccum = 0;
|
nextBtn = document.getElementById('id_fan_next');
|
||||||
var wheelDecayTimer = null;
|
flipBtn = document.getElementById('id_fan_flip');
|
||||||
var WHEEL_STEP = 150;
|
fanWrap = dialog.querySelector('.tarot-fan-wrap');
|
||||||
dialog.addEventListener('wheel', function(e) {
|
|
||||||
e.preventDefault();
|
stageBlock = document.getElementById('id_fan_stage_block');
|
||||||
clearTimeout(wheelDecayTimer);
|
if (stageBlock) {
|
||||||
wheelAccum += e.deltaY;
|
spinBtn = stageBlock.querySelector('.spin-btn');
|
||||||
var steps = Math.trunc(wheelAccum / WHEEL_STEP);
|
fyiBtn = stageBlock.querySelector('.fyi-btn');
|
||||||
if (steps !== 0) {
|
fyiPanel = stageBlock.querySelector('#id_fan_fyi_panel');
|
||||||
steps = Math.sign(steps) * Math.min(Math.abs(steps), 3);
|
fyiTitle = fyiPanel && fyiPanel.querySelector('.sig-info-title');
|
||||||
wheelAccum -= steps * WHEEL_STEP;
|
fyiType = fyiPanel && fyiPanel.querySelector('.sig-info-type');
|
||||||
navigate(steps);
|
fyiEffect = fyiPanel && fyiPanel.querySelector('.sig-info-effect');
|
||||||
|
fyiIndex = fyiPanel && fyiPanel.querySelector('.sig-info-index');
|
||||||
|
fyiPrev = stageBlock.querySelector('.fyi-prev');
|
||||||
|
fyiNext = stageBlock.querySelector('.fyi-next');
|
||||||
|
statUpright = stageBlock.querySelector('#id_fan_stat_upright');
|
||||||
|
statReversed = stageBlock.querySelector('#id_fan_stat_reversed');
|
||||||
|
_spinOrigLabel = spinBtn ? spinBtn.textContent : 'SPIN';
|
||||||
|
_fyiOrigLabel = fyiBtn ? fyiBtn.textContent : 'FYI';
|
||||||
}
|
}
|
||||||
wheelDecayTimer = setTimeout(function() { wheelAccum = 0; }, 200);
|
|
||||||
}, { passive: false });
|
|
||||||
|
|
||||||
// Touch/swipe navigation — uses navigateAnimated so intermediate cards are visible
|
// Backdrop click closes
|
||||||
var touchStartX = 0;
|
if (dialog && fanWrap) {
|
||||||
var touchStartY = 0;
|
dialog.addEventListener('click', function (e) {
|
||||||
var touchStartTime = 0;
|
if (e.target === dialog || e.target === fanWrap) closeFan();
|
||||||
dialog.addEventListener('touchstart', function(e) {
|
});
|
||||||
touchStartX = e.touches[0].clientX;
|
}
|
||||||
touchStartY = e.touches[0].clientY;
|
|
||||||
touchStartTime = Date.now();
|
|
||||||
}, { passive: true });
|
|
||||||
dialog.addEventListener('touchend', function(e) {
|
|
||||||
var dx = e.changedTouches[0].clientX - touchStartX;
|
|
||||||
var dy = e.changedTouches[0].clientY - touchStartY;
|
|
||||||
if (Math.abs(dy) > Math.abs(dx)) return; // vertical swipe — ignore
|
|
||||||
if (Math.abs(dx) < 60) return; // dead zone — raise to 40–60 for more deliberate swipe required
|
|
||||||
var elapsed = Math.max(1, Date.now() - touchStartTime);
|
|
||||||
var velocity = Math.abs(dx) / elapsed; // px/ms
|
|
||||||
var steps = velocity > 0.8 // flick threshold — raise (e.g. 0.6) so more swipes use drag formula
|
|
||||||
? Math.max(1, Math.round(velocity * 4)) // flick multiplier — lower (e.g. 4–5) to reduce cards per fast flick
|
|
||||||
: Math.round(Math.abs(dx) / 150); // slow-drag divisor — raise (e.g. 120–150) for fewer cards per short drag
|
|
||||||
navigateAnimated(dx < 0 ? steps : -steps);
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
prevBtn.addEventListener('click', function() { navigate(-1); });
|
// Keyboard nav
|
||||||
nextBtn.addEventListener('click', function() { navigate(1); });
|
if (dialog) {
|
||||||
|
dialog.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'ArrowRight') navigate(1);
|
||||||
|
if (e.key === 'ArrowLeft') navigate(-1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll('.gk-deck-card[data-deck-id]').forEach(function(card) {
|
// Wheel nav
|
||||||
card.addEventListener('click', function() { openFan(card.dataset.deckId); });
|
var wheelAccum = 0;
|
||||||
});
|
var wheelDecayTimer = null;
|
||||||
}
|
var WHEEL_STEP = 150;
|
||||||
|
if (dialog) {
|
||||||
|
dialog.addEventListener('wheel', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
clearTimeout(wheelDecayTimer);
|
||||||
|
wheelAccum += e.deltaY;
|
||||||
|
var steps = Math.trunc(wheelAccum / WHEEL_STEP);
|
||||||
|
if (steps !== 0) {
|
||||||
|
steps = Math.sign(steps) * Math.min(Math.abs(steps), 3);
|
||||||
|
wheelAccum -= steps * WHEEL_STEP;
|
||||||
|
navigate(steps);
|
||||||
|
}
|
||||||
|
wheelDecayTimer = setTimeout(function () { wheelAccum = 0; }, 200);
|
||||||
|
}, { passive: false });
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initGameKitPage);
|
// Touch nav
|
||||||
|
var touchStartX = 0, touchStartY = 0, touchStartTime = 0;
|
||||||
|
if (dialog) {
|
||||||
|
dialog.addEventListener('touchstart', function (e) {
|
||||||
|
touchStartX = e.touches[0].clientX;
|
||||||
|
touchStartY = e.touches[0].clientY;
|
||||||
|
touchStartTime = Date.now();
|
||||||
|
}, { passive: true });
|
||||||
|
dialog.addEventListener('touchend', function (e) {
|
||||||
|
var dx = e.changedTouches[0].clientX - touchStartX;
|
||||||
|
var dy = e.changedTouches[0].clientY - touchStartY;
|
||||||
|
if (Math.abs(dy) > Math.abs(dx)) return;
|
||||||
|
if (Math.abs(dx) < 60) return;
|
||||||
|
var elapsed = Math.max(1, Date.now() - touchStartTime);
|
||||||
|
var velocity = Math.abs(dx) / elapsed;
|
||||||
|
var steps = velocity > 0.8
|
||||||
|
? Math.max(1, Math.round(velocity * 4))
|
||||||
|
: Math.round(Math.abs(dx) / 150);
|
||||||
|
navigateAnimated(dx < 0 ? steps : -steps);
|
||||||
|
}, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevBtn) prevBtn.addEventListener('click', function () { navigate(-1); });
|
||||||
|
if (nextBtn) nextBtn.addEventListener('click', function () { navigate(1); });
|
||||||
|
if (flipBtn) flipBtn.addEventListener('click', function (e) {
|
||||||
|
// Without this the click bubbles to the dialog's backdrop-close
|
||||||
|
// handler and shuts the modal — FLIP is a primary action, not a
|
||||||
|
// dismiss.
|
||||||
|
e.stopPropagation();
|
||||||
|
_flipActive();
|
||||||
|
});
|
||||||
|
|
||||||
|
// SPIN — toggle stat block face AND rotate the active fan-card 180°.
|
||||||
|
// Reapply the active card's transform directly so the rotation animates
|
||||||
|
// off the existing carousel transform without going through updateFan
|
||||||
|
// (which would reset .stage-card--reversed via _populateStage).
|
||||||
|
if (spinBtn) {
|
||||||
|
spinBtn.addEventListener('click', function () {
|
||||||
|
if (spinBtn.classList.contains('btn-disabled')) return;
|
||||||
|
stageBlock.classList.toggle('is-reversed');
|
||||||
|
var active = cards[currentIndex];
|
||||||
|
if (!active) return;
|
||||||
|
active.classList.toggle('stage-card--reversed');
|
||||||
|
var t = cardTransform(0);
|
||||||
|
var spin = active.classList.contains('stage-card--reversed') ? ' rotate(180deg)' : '';
|
||||||
|
active.style.transform = t.transform + spin;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// FYI
|
||||||
|
if (fyiBtn) {
|
||||||
|
fyiBtn.addEventListener('click', function () {
|
||||||
|
if (fyiBtn.classList.contains('btn-disabled')) return;
|
||||||
|
_infoOpen ? _closeFyi() : _openFyi();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fyiPanel) {
|
||||||
|
fyiPanel.addEventListener('click', function (e) {
|
||||||
|
if (!e.target.closest('.fyi-prev') && !e.target.closest('.fyi-next')) {
|
||||||
|
_closeFyi();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (fyiPrev) {
|
||||||
|
fyiPrev.addEventListener('click', function () {
|
||||||
|
if (!_infoData.length) return;
|
||||||
|
_infoIdx = (_infoIdx - 1 + _infoData.length) % _infoData.length;
|
||||||
|
_renderFyi();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (fyiNext) {
|
||||||
|
fyiNext.addEventListener('click', function () {
|
||||||
|
if (!_infoData.length) return;
|
||||||
|
_infoIdx = (_infoIdx + 1) % _infoData.length;
|
||||||
|
_renderFyi();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deck-card click → openFan
|
||||||
|
document.querySelectorAll('.gk-deck-card[data-deck-id]').forEach(function (card) {
|
||||||
|
card.addEventListener('click', function () { openFan(card.dataset.deckId); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
openFan: openFan,
|
||||||
|
closeFan: closeFan,
|
||||||
|
navigate: navigate,
|
||||||
|
reinit: init,
|
||||||
|
_testInit: function () {
|
||||||
|
dialog = null; fanContent = null; prevBtn = null; nextBtn = null; fanWrap = null;
|
||||||
|
flipBtn = null;
|
||||||
|
stageBlock = null; spinBtn = null; fyiBtn = null; fyiPanel = null;
|
||||||
|
fyiTitle = null; fyiType = null; fyiEffect = null; fyiIndex = null;
|
||||||
|
fyiPrev = null; fyiNext = null; statUpright = null; statReversed = null;
|
||||||
|
currentDeckId = null; currentIndex = 0; cards = [];
|
||||||
|
_revealTimer = null; _infoData = []; _infoIdx = 0; _infoOpen = false;
|
||||||
|
_polarity = 'levity';
|
||||||
|
init();
|
||||||
|
},
|
||||||
|
// Test seam: skip fetch by reading already-rendered #id_fan_content
|
||||||
|
_testOpen: function () {
|
||||||
|
cards = Array.from(fanContent.querySelectorAll('.fan-card'));
|
||||||
|
currentIndex = 0;
|
||||||
|
updateFan();
|
||||||
|
},
|
||||||
|
_testNavigate: navigate,
|
||||||
|
};
|
||||||
|
}());
|
||||||
|
|||||||
308
src/static/tests/FanStageSpec.js
Normal file
308
src/static/tests/FanStageSpec.js
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
describe("FanStage", () => {
|
||||||
|
let testDiv, dialog, fanContent, stageBlock, stageCard;
|
||||||
|
|
||||||
|
function makeCardEl({ id, suit_icon = '', corner_rank = 'I', name_group = '',
|
||||||
|
name_title = 'The Magician', arcana = 'Major Arcana',
|
||||||
|
correspondence = '', keywords_upright = 'will,focus,manifestation',
|
||||||
|
keywords_reversed = 'manipulation,illusion',
|
||||||
|
energies = '[]', operations = '[]',
|
||||||
|
levity_qualifier = '', gravity_qualifier = '',
|
||||||
|
reversal_qualifier = '' } = {}) {
|
||||||
|
return `<div class="fan-card"
|
||||||
|
data-index="${id}"
|
||||||
|
data-suit-icon="${suit_icon}"
|
||||||
|
data-corner-rank="${corner_rank}"
|
||||||
|
data-name-group="${name_group}"
|
||||||
|
data-name-title="${name_title}"
|
||||||
|
data-arcana="${arcana}"
|
||||||
|
data-correspondence="${correspondence}"
|
||||||
|
data-keywords-upright="${keywords_upright}"
|
||||||
|
data-keywords-reversed="${keywords_reversed}"
|
||||||
|
data-energies='${energies}'
|
||||||
|
data-operations='${operations}'
|
||||||
|
data-levity-qualifier="${levity_qualifier}"
|
||||||
|
data-gravity-qualifier="${gravity_qualifier}"
|
||||||
|
data-reversal-qualifier="${reversal_qualifier}">
|
||||||
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
|
<span class="fan-corner-rank">${corner_rank}</span>
|
||||||
|
</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>
|
||||||
|
<h3 class="fan-card-name">${name_title}</h3>
|
||||||
|
<p class="sig-qualifier-below"></p>
|
||||||
|
</div>
|
||||||
|
<p class="fan-card-arcana">${arcana}</p>
|
||||||
|
<div class="fan-card-face-reversal">
|
||||||
|
<p class="fan-card-reversal-qualifier"></p>
|
||||||
|
<p class="fan-card-reversal-name"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fan-card-corner fan-card-corner--br">
|
||||||
|
<span class="fan-corner-rank">${corner_rank}</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeFixture() {
|
||||||
|
testDiv = document.createElement("div");
|
||||||
|
testDiv.innerHTML = `
|
||||||
|
<dialog id="id_tarot_fan_dialog">
|
||||||
|
<div class="tarot-fan-wrap">
|
||||||
|
<button id="id_fan_prev" class="fan-nav fan-nav--prev">‹</button>
|
||||||
|
<div id="id_fan_content" class="tarot-fan"></div>
|
||||||
|
<div class="fan-stage-block sig-stat-block" id="id_fan_stage_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" id="id_fan_stat_upright"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="stat-face stat-face--reversed">
|
||||||
|
<p class="stat-face-label">Reversal</p>
|
||||||
|
<ul class="stat-keywords" id="id_fan_stat_reversed"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="sig-info" id="id_fan_fyi_panel" style="display:none">
|
||||||
|
<div class="sig-info-header">
|
||||||
|
<h4 class="sig-info-title"></h4>
|
||||||
|
<p class="sig-info-type"></p>
|
||||||
|
</div>
|
||||||
|
<p class="sig-info-effect"></p>
|
||||||
|
<span class="sig-info-index"></span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-nav-left fyi-prev" type="button">PRV</button>
|
||||||
|
<button class="btn btn-nav-right fyi-next" type="button">NXT</button>
|
||||||
|
</div>
|
||||||
|
<button id="id_fan_flip" class="btn btn-reveal fan-flip-btn" type="button">FLIP</button>
|
||||||
|
<button id="id_fan_next" class="fan-nav fan-nav--next">›</button>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
<button class="gk-deck-card" data-deck-id="1">Earthman</button>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(testDiv);
|
||||||
|
dialog = testDiv.querySelector("#id_tarot_fan_dialog");
|
||||||
|
fanContent = testDiv.querySelector("#id_fan_content");
|
||||||
|
stageBlock = testDiv.querySelector("#id_fan_stage_block");
|
||||||
|
|
||||||
|
// Pre-render two cards into fanContent so openFan's fetch can be skipped via _testOpen
|
||||||
|
fanContent.innerHTML =
|
||||||
|
makeCardEl({ id: 0, name_title: 'The Magician', corner_rank: 'I',
|
||||||
|
levity_qualifier: 'Enlightened',
|
||||||
|
gravity_qualifier: 'Engraven',
|
||||||
|
keywords_upright: 'will,focus',
|
||||||
|
keywords_reversed: 'manipulation',
|
||||||
|
energies: '[{"type":"LIBIDO","effect":"Drive."}]',
|
||||||
|
operations: '[{"type":"COVER","effect":"Shield."}]' }) +
|
||||||
|
makeCardEl({ id: 1, name_title: 'The High Priestess', corner_rank: 'II',
|
||||||
|
levity_qualifier: 'Enlightened',
|
||||||
|
gravity_qualifier: 'Engraven',
|
||||||
|
keywords_upright: 'intuition,subconscious',
|
||||||
|
keywords_reversed: 'secrets,disconnect' });
|
||||||
|
|
||||||
|
jasmine.clock().install();
|
||||||
|
GameKit._testInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jasmine.clock().uninstall();
|
||||||
|
if (testDiv) testDiv.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Stage block reveal / hide ────────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("idle reveal", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("starts with the stage block hidden (no .is-revealed)", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds .is-revealed after 500ms of idle", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
jasmine.clock().tick(500);
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT reveal before 500ms have elapsed", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
jasmine.clock().tick(400);
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-hides immediately on navigation, even after reveal", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
jasmine.clock().tick(500);
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(true);
|
||||||
|
GameKit._testNavigate(1);
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restarts the idle timer after navigation", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
jasmine.clock().tick(500);
|
||||||
|
GameKit._testNavigate(1);
|
||||||
|
jasmine.clock().tick(400);
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(false);
|
||||||
|
jasmine.clock().tick(100);
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Stat block population from focused fan-card ──────────────────────── //
|
||||||
|
|
||||||
|
describe("stat block population", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("populates upright keywords from data-keywords-upright of focused card", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
const list = stageBlock.querySelector("#id_fan_stat_upright");
|
||||||
|
const items = list.querySelectorAll("li");
|
||||||
|
expect(items.length).toBe(2);
|
||||||
|
expect(items[0].textContent).toBe("will");
|
||||||
|
expect(items[1].textContent).toBe("focus");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("populates reversed keywords from data-keywords-reversed of focused card", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
const list = stageBlock.querySelector("#id_fan_stat_reversed");
|
||||||
|
const items = list.querySelectorAll("li");
|
||||||
|
expect(items.length).toBe(1);
|
||||||
|
expect(items[0].textContent).toBe("manipulation");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-populates from the new focused card on navigation", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
GameKit._testNavigate(1);
|
||||||
|
const items = stageBlock.querySelectorAll("#id_fan_stat_upright li");
|
||||||
|
expect(items.length).toBe(2);
|
||||||
|
expect(items[0].textContent).toBe("intuition");
|
||||||
|
expect(items[1].textContent).toBe("subconscious");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── SPIN button (.btn-reverse) ──────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("SPIN", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("toggles .is-reversed on the stat block", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
stageBlock.querySelector(".spin-btn").click();
|
||||||
|
expect(stageBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles back when clicked twice", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
const spin = stageBlock.querySelector(".spin-btn");
|
||||||
|
spin.click(); spin.click();
|
||||||
|
expect(stageBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets .is-reversed when navigating to a new card", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
stageBlock.querySelector(".spin-btn").click();
|
||||||
|
expect(stageBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
|
GameKit._testNavigate(1);
|
||||||
|
expect(stageBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── FYI button (.btn-info) ──────────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("FYI", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("opens the FYI panel and disables SPIN+FYI buttons", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
stageBlock.querySelector(".fyi-btn").click();
|
||||||
|
const panel = stageBlock.querySelector("#id_fan_fyi_panel");
|
||||||
|
expect(panel.style.display).toBe("flex");
|
||||||
|
expect(stageBlock.querySelector(".spin-btn").classList.contains("btn-disabled")).toBe(true);
|
||||||
|
expect(stageBlock.querySelector(".fyi-btn").classList.contains("btn-disabled")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("populates the panel with the first energy entry from data-energies", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
stageBlock.querySelector(".fyi-btn").click();
|
||||||
|
const panel = stageBlock.querySelector("#id_fan_fyi_panel");
|
||||||
|
expect(panel.querySelector(".sig-info-type").textContent).toBe("LIBIDO");
|
||||||
|
expect(panel.querySelector(".sig-info-effect").innerHTML).toBe("Drive.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PRV/NXT cycle through energies + operations", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
stageBlock.querySelector(".fyi-btn").click();
|
||||||
|
stageBlock.querySelector(".fyi-next").click();
|
||||||
|
const panel = stageBlock.querySelector("#id_fan_fyi_panel");
|
||||||
|
expect(panel.querySelector(".sig-info-type").textContent).toBe("COVER");
|
||||||
|
expect(panel.querySelector(".sig-info-effect").innerHTML).toBe("Shield.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── FLIP — polarity toggle (Levity ↔ Gravity) ──────────────────────────── //
|
||||||
|
//
|
||||||
|
// FLIP swaps polarity on the focused card with a perspective rotateY animation.
|
||||||
|
// The repaint fires at the 250ms midpoint via setTimeout (jasmine.clock fakes
|
||||||
|
// it). Element.animate() is called too — but tests assert side effects (the
|
||||||
|
// dataset.polarity attr + the qualifier text content), not the animation.
|
||||||
|
|
||||||
|
describe("FLIP", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
function flipBtn() { return testDiv.querySelector("#id_fan_flip"); }
|
||||||
|
function activeCard() { return testDiv.querySelector(".fan-card--active"); }
|
||||||
|
|
||||||
|
it("starts with polarity = levity (the server-rendered default)", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
jasmine.clock().tick(250);
|
||||||
|
expect(activeCard().dataset.polarity).toBe("levity");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles dataset.polarity to gravity at the midpoint after click", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
flipBtn().click();
|
||||||
|
jasmine.clock().tick(250);
|
||||||
|
expect(activeCard().dataset.polarity).toBe("gravity");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("repaints the upright qualifier slot with the gravity qualifier", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
flipBtn().click();
|
||||||
|
jasmine.clock().tick(250);
|
||||||
|
const card = activeCard();
|
||||||
|
// Major arcana places qualifier in the BELOW slot.
|
||||||
|
expect(card.querySelector(".sig-qualifier-below").textContent).toBe("Engraven");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("FLIPs back to levity on a second click", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
flipBtn().click();
|
||||||
|
jasmine.clock().tick(500);
|
||||||
|
flipBtn().click();
|
||||||
|
jasmine.clock().tick(250);
|
||||||
|
expect(activeCard().dataset.polarity).toBe("levity");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retains SPIN state across a FLIP (.stage-card--reversed survives)", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
stageBlock.querySelector(".spin-btn").click(); // SPIN first
|
||||||
|
expect(activeCard().classList.contains("stage-card--reversed")).toBe(true);
|
||||||
|
flipBtn().click();
|
||||||
|
jasmine.clock().tick(500);
|
||||||
|
expect(activeCard().classList.contains("stage-card--reversed")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores a second click while a flip is in flight", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
flipBtn().click();
|
||||||
|
// Mid-animation second click — should be ignored
|
||||||
|
jasmine.clock().tick(100);
|
||||||
|
flipBtn().click();
|
||||||
|
jasmine.clock().tick(150); // total = 250ms (one repaint window)
|
||||||
|
// Only one polarity swap happened (levity → gravity), not two.
|
||||||
|
expect(activeCard().dataset.polarity).toBe("gravity");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,7 +7,7 @@ describe("SeaDeal", () => {
|
|||||||
corner_rank: "Q", suit_icon: "fa-crown",
|
corner_rank: "Q", suit_icon: "fa-crown",
|
||||||
name_group: "", name_title: "Queen of Crowns",
|
name_group: "", name_title: "Queen of Crowns",
|
||||||
levity_qualifier: "Elevated", gravity_qualifier: "Graven",
|
levity_qualifier: "Elevated", gravity_qualifier: "Graven",
|
||||||
reversal: "Vacant",
|
reversal_qualifier: "Vacant",
|
||||||
keywords_upright: ["nurturing", "practical", "abundance"],
|
keywords_upright: ["nurturing", "practical", "abundance"],
|
||||||
keywords_reversed: ["financial dependence", "smothering"],
|
keywords_reversed: ["financial dependence", "smothering"],
|
||||||
energies: [{ type: "LIBIDO", effect: "Energy entry." }],
|
energies: [{ type: "LIBIDO", effect: "Energy entry." }],
|
||||||
@@ -72,8 +72,8 @@ describe("SeaDeal", () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sig-stat-block sea-stat-block">
|
<div class="sig-stat-block sea-stat-block">
|
||||||
<button class="btn btn-reverse sea-spin-btn" type="button">SPIN</button>
|
<button class="btn btn-reverse spin-btn" type="button">SPIN</button>
|
||||||
<button class="btn btn-info sea-fyi-btn" type="button">FYI</button>
|
<button class="btn btn-info fyi-btn" type="button">FYI</button>
|
||||||
<div class="stat-face stat-face--upright">
|
<div class="stat-face stat-face--upright">
|
||||||
<p class="stat-face-label">Emanation</p>
|
<p class="stat-face-label">Emanation</p>
|
||||||
<ul class="stat-keywords" id="id_sea_stat_upright"></ul>
|
<ul class="stat-keywords" id="id_sea_stat_upright"></ul>
|
||||||
@@ -90,8 +90,8 @@ describe("SeaDeal", () => {
|
|||||||
<p class="sig-info-effect"></p>
|
<p class="sig-info-effect"></p>
|
||||||
<span class="sig-info-index"></span>
|
<span class="sig-info-index"></span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-nav-left sea-fyi-prev" type="button">PRV</button>
|
<button class="btn btn-nav-left fyi-prev" type="button">PRV</button>
|
||||||
<button class="btn btn-nav-right sea-fyi-next" type="button">NXT</button>
|
<button class="btn btn-nav-right fyi-next" type="button">NXT</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,17 +197,17 @@ describe("SeaDeal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("toggles is-reversed on stat block", () => {
|
it("toggles is-reversed on stat block", () => {
|
||||||
testDiv.querySelector(".sea-spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("toggles stage-card--reversed on stage card", () => {
|
it("toggles stage-card--reversed on stage card", () => {
|
||||||
testDiv.querySelector(".sea-spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("second SPIN click restores upright", () => {
|
it("second SPIN click restores upright", () => {
|
||||||
const btn = testDiv.querySelector(".sea-spin-btn");
|
const btn = testDiv.querySelector(".spin-btn");
|
||||||
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
@@ -223,23 +223,23 @@ describe("SeaDeal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("FYI click shows the info panel", () => {
|
it("FYI click shows the info panel", () => {
|
||||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector("#id_sea_fyi_panel").style.display).not.toBe("none");
|
expect(testDiv.querySelector("#id_sea_fyi_panel").style.display).not.toBe("none");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows first energy entry title as 'Energy'", () => {
|
it("shows first energy entry title as 'Energy'", () => {
|
||||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Energy");
|
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Energy");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows first entry type", () => {
|
it("shows first entry type", () => {
|
||||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("LIBIDO");
|
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("LIBIDO");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("NXT advances to operation entry", () => {
|
it("NXT advances to operation entry", () => {
|
||||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
testDiv.querySelector(".sea-fyi-next").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".fyi-next").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Operation");
|
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Operation");
|
||||||
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("COVER");
|
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("COVER");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ describe("SigSelect", () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sig-stat-block">
|
<div class="sig-stat-block">
|
||||||
<button class="btn btn-reverse sig-flip-btn" type="button">SPIN</button>
|
<button class="btn btn-reverse spin-btn" type="button">SPIN</button>
|
||||||
<button class="btn btn-info sig-info-btn" type="button">FYI</button>
|
<button class="btn btn-info fyi-btn" type="button">FYI</button>
|
||||||
<div class="stat-face stat-face--upright">
|
<div class="stat-face stat-face--upright">
|
||||||
<p class="stat-face-label">Emanation</p>
|
<p class="stat-face-label">Emanation</p>
|
||||||
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
|
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
|
||||||
@@ -40,8 +40,8 @@ describe("SigSelect", () => {
|
|||||||
<p class="stat-face-label">Reversal</p>
|
<p class="stat-face-label">Reversal</p>
|
||||||
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-nav-left sig-info-prev" type="button">◀</button>
|
<button class="btn btn-nav-left fyi-prev" type="button">◀</button>
|
||||||
<button class="btn btn-nav-right sig-info-next" type="button">▶</button>
|
<button class="btn btn-nav-right fyi-next" type="button">▶</button>
|
||||||
<div class="sig-info" id="id_sig_info">
|
<div class="sig-info" id="id_sig_info">
|
||||||
<div class="sig-info-header">
|
<div class="sig-info-header">
|
||||||
<h4 class="sig-info-title"></h4>
|
<h4 class="sig-info-title"></h4>
|
||||||
@@ -67,7 +67,7 @@ describe("SigSelect", () => {
|
|||||||
data-operations="[]"
|
data-operations="[]"
|
||||||
data-levity-qualifier="Elevated"
|
data-levity-qualifier="Elevated"
|
||||||
data-gravity-qualifier="Graven"
|
data-gravity-qualifier="Graven"
|
||||||
data-reversal="">
|
data-reversal-qualifier="">
|
||||||
<div class="fan-card-corner fan-card-corner--tl">
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
<span class="fan-corner-rank">K</span>
|
<span class="fan-corner-rank">K</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,24 +211,24 @@ describe("SigSelect", () => {
|
|||||||
infoTitle = testDiv.querySelector(".sig-info-title");
|
infoTitle = testDiv.querySelector(".sig-info-title");
|
||||||
infoType = testDiv.querySelector(".sig-info-type");
|
infoType = testDiv.querySelector(".sig-info-type");
|
||||||
infoIndex = testDiv.querySelector(".sig-info-index");
|
infoIndex = testDiv.querySelector(".sig-info-index");
|
||||||
infoPrev = testDiv.querySelector(".sig-info-prev");
|
infoPrev = testDiv.querySelector(".fyi-prev");
|
||||||
infoNext = testDiv.querySelector(".sig-info-next");
|
infoNext = testDiv.querySelector(".fyi-next");
|
||||||
infoBtn = testDiv.querySelector(".sig-info-btn");
|
infoBtn = testDiv.querySelector(".fyi-btn");
|
||||||
});
|
});
|
||||||
|
|
||||||
function hover() { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); }
|
function hover() { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); }
|
||||||
function openFYI() { hover(); infoBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })); }
|
function openFYI() { hover(); infoBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })); }
|
||||||
|
|
||||||
it("FYI click adds .sig-info-open to the stage", () => {
|
it("FYI click adds .fyi-open to the stat block", () => {
|
||||||
openFYI();
|
openFYI();
|
||||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(true);
|
expect(testDiv.querySelector(".sig-stat-block").classList.contains("fyi-open")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("FYI click when btn-disabled does not toggle", () => {
|
it("FYI click when btn-disabled does not toggle", () => {
|
||||||
openFYI();
|
openFYI();
|
||||||
expect(infoBtn.classList.contains("btn-disabled")).toBe(true);
|
expect(infoBtn.classList.contains("btn-disabled")).toBe(true);
|
||||||
infoBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
infoBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(true);
|
expect(testDiv.querySelector(".sig-stat-block").classList.contains("fyi-open")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows placeholder when both energies and operations are empty", () => {
|
it("shows placeholder when both energies and operations are empty", () => {
|
||||||
@@ -370,9 +370,9 @@ describe("SigSelect", () => {
|
|||||||
|
|
||||||
it("card mouseleave closes the info panel", () => {
|
it("card mouseleave closes the info panel", () => {
|
||||||
openFYI();
|
openFYI();
|
||||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(true);
|
expect(testDiv.querySelector(".sig-stat-block").classList.contains("fyi-open")).toBe(true);
|
||||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(false);
|
expect(testDiv.querySelector(".sig-stat-block").classList.contains("fyi-open")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opening again resets to first entry", () => {
|
it("opening again resets to first entry", () => {
|
||||||
@@ -389,7 +389,7 @@ describe("SigSelect", () => {
|
|||||||
|
|
||||||
it("opening info panel adds .btn-disabled and swaps SPIN/FYI labels to ×", () => {
|
it("opening info panel adds .btn-disabled and swaps SPIN/FYI labels to ×", () => {
|
||||||
openFYI();
|
openFYI();
|
||||||
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
var flipBtn = testDiv.querySelector(".spin-btn");
|
||||||
expect(flipBtn.classList.contains("btn-disabled")).toBe(true);
|
expect(flipBtn.classList.contains("btn-disabled")).toBe(true);
|
||||||
expect(infoBtn.classList.contains("btn-disabled")).toBe(true);
|
expect(infoBtn.classList.contains("btn-disabled")).toBe(true);
|
||||||
expect(flipBtn.textContent).toBe("×");
|
expect(flipBtn.textContent).toBe("×");
|
||||||
@@ -397,7 +397,7 @@ describe("SigSelect", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("closing info panel removes .btn-disabled and restores SPIN/FYI labels", () => {
|
it("closing info panel removes .btn-disabled and restores SPIN/FYI labels", () => {
|
||||||
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
var flipBtn = testDiv.querySelector(".spin-btn");
|
||||||
var origFlip = flipBtn.textContent;
|
var origFlip = flipBtn.textContent;
|
||||||
var origInfo = infoBtn.textContent;
|
var origInfo = infoBtn.textContent;
|
||||||
openFYI();
|
openFYI();
|
||||||
@@ -411,14 +411,14 @@ describe("SigSelect", () => {
|
|||||||
it("clicking the info panel closes it", () => {
|
it("clicking the info panel closes it", () => {
|
||||||
openFYI();
|
openFYI();
|
||||||
infoEffect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
infoEffect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(false);
|
expect(testDiv.querySelector(".sig-stat-block").classList.contains("fyi-open")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("SPIN click when info open (btn-disabled) does nothing", () => {
|
it("SPIN click when info open (btn-disabled) does nothing", () => {
|
||||||
openFYI();
|
openFYI();
|
||||||
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
var flipBtn = testDiv.querySelector(".spin-btn");
|
||||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(true);
|
expect(testDiv.querySelector(".sig-stat-block").classList.contains("fyi-open")).toBe(true);
|
||||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -447,14 +447,14 @@ describe("SigSelect", () => {
|
|||||||
|
|
||||||
it("SPIN click adds .is-reversed to the stat block", () => {
|
it("SPIN click adds .is-reversed to the stat block", () => {
|
||||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
var flipBtn = statBlock.querySelector(".spin-btn");
|
||||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("second SPIN click removes .is-reversed", () => {
|
it("second SPIN click removes .is-reversed", () => {
|
||||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
var flipBtn = statBlock.querySelector(".spin-btn");
|
||||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
@@ -462,7 +462,7 @@ describe("SigSelect", () => {
|
|||||||
|
|
||||||
it("hovering a new card resets .is-reversed", () => {
|
it("hovering a new card resets .is-reversed", () => {
|
||||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
statBlock.querySelector(".sig-flip-btn").dispatchEvent(
|
statBlock.querySelector(".spin-btn").dispatchEvent(
|
||||||
new MouseEvent("click", { bubbles: true })
|
new MouseEvent("click", { bubbles: true })
|
||||||
);
|
);
|
||||||
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
@@ -491,7 +491,7 @@ describe("SigSelect", () => {
|
|||||||
it("SPIN click adds .stage-card--reversed to the stage card", () => {
|
it("SPIN click adds .stage-card--reversed to the stage card", () => {
|
||||||
makeFixture();
|
makeFixture();
|
||||||
hover();
|
hover();
|
||||||
statBlock.querySelector(".sig-flip-btn")
|
statBlock.querySelector(".spin-btn")
|
||||||
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -499,7 +499,7 @@ describe("SigSelect", () => {
|
|||||||
it("second SPIN click removes .stage-card--reversed", () => {
|
it("second SPIN click removes .stage-card--reversed", () => {
|
||||||
makeFixture();
|
makeFixture();
|
||||||
hover();
|
hover();
|
||||||
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
var flipBtn = statBlock.querySelector(".spin-btn");
|
||||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
||||||
@@ -508,7 +508,7 @@ describe("SigSelect", () => {
|
|||||||
it("hovering a new card resets .stage-card--reversed", () => {
|
it("hovering a new card resets .stage-card--reversed", () => {
|
||||||
makeFixture();
|
makeFixture();
|
||||||
hover();
|
hover();
|
||||||
statBlock.querySelector(".sig-flip-btn")
|
statBlock.querySelector(".spin-btn")
|
||||||
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
@@ -516,9 +516,9 @@ describe("SigSelect", () => {
|
|||||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("non-major with data-reversal: reversal-qualifier = suit word, reversal-name = card name", () => {
|
it("non-major with data-reversal-qualifier: reversal-qualifier = suit word, reversal-name = card name", () => {
|
||||||
makeFixture();
|
makeFixture();
|
||||||
card.dataset.reversal = "Nervous";
|
card.dataset.reversalQualifier = "Nervous";
|
||||||
hover();
|
hover();
|
||||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Nervous");
|
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Nervous");
|
||||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
|
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
|
||||||
@@ -539,9 +539,9 @@ describe("SigSelect", () => {
|
|||||||
.toBe("Graven");
|
.toBe("Graven");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("non-major with data-reversal: suit qualifier on own line, upright name repeated below", () => {
|
it("non-major with data-reversal-qualifier: suit qualifier on own line, upright name repeated below", () => {
|
||||||
makeFixture({ polarity: "levity", userRole: "PC" });
|
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||||
card.dataset.reversal = "Vacant";
|
card.dataset.reversalQualifier = "Vacant";
|
||||||
hover();
|
hover();
|
||||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Vacant");
|
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Vacant");
|
||||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
|
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
|
||||||
@@ -558,7 +558,7 @@ describe("SigSelect", () => {
|
|||||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("Elevated");
|
expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("Elevated");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("non-major without data-reversal: qualifier mirrors polarity, name repeats card title", () => {
|
it("non-major without data-reversal-qualifier: qualifier mirrors polarity, name repeats card title", () => {
|
||||||
makeFixture({ polarity: "levity", userRole: "PC" });
|
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||||
// fixture default: Minor Arcana, no reversal word
|
// fixture default: Minor Arcana, no reversal word
|
||||||
hover();
|
hover();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<script src="TraySpec.js"></script>
|
<script src="TraySpec.js"></script>
|
||||||
<script src="SigSelectSpec.js"></script>
|
<script src="SigSelectSpec.js"></script>
|
||||||
<script src="SeaDealSpec.js"></script>
|
<script src="SeaDealSpec.js"></script>
|
||||||
|
<script src="FanStageSpec.js"></script>
|
||||||
<script src="NatusWheelSpec.js"></script>
|
<script src="NatusWheelSpec.js"></script>
|
||||||
<script src="NoteSpec.js"></script>
|
<script src="NoteSpec.js"></script>
|
||||||
<script src="NotePageSpec.js"></script>
|
<script src="NotePageSpec.js"></script>
|
||||||
@@ -30,10 +31,12 @@
|
|||||||
<script src="/static/apps/dashboard/dashboard.js"></script>
|
<script src="/static/apps/dashboard/dashboard.js"></script>
|
||||||
<script src="/static/apps/dashboard/note.js"></script>
|
<script src="/static/apps/dashboard/note.js"></script>
|
||||||
<script src="/static/apps/billboard/note-page.js"></script>
|
<script src="/static/apps/billboard/note-page.js"></script>
|
||||||
|
<script src="/static/apps/epic/stage-card.js"></script>
|
||||||
<script src="/static/apps/epic/role-select.js"></script>
|
<script src="/static/apps/epic/role-select.js"></script>
|
||||||
<script src="/static/apps/epic/tray.js"></script>
|
<script src="/static/apps/epic/tray.js"></script>
|
||||||
<script src="/static/apps/epic/sig-select.js"></script>
|
<script src="/static/apps/epic/sig-select.js"></script>
|
||||||
<script src="/static/apps/epic/sea.js"></script>
|
<script src="/static/apps/epic/sea.js"></script>
|
||||||
|
<script src="/static/apps/gameboard/game-kit.js"></script>
|
||||||
<script src="/static/apps/gameboard/d3.min.js"></script>
|
<script src="/static/apps/gameboard/d3.min.js"></script>
|
||||||
<script src="/static/apps/gameboard/natus-wheel.js"></script>
|
<script src="/static/apps/gameboard/natus-wheel.js"></script>
|
||||||
<!-- Jasmine env config (optional) -->
|
<!-- Jasmine env config (optional) -->
|
||||||
|
|||||||
@@ -3,6 +3,132 @@
|
|||||||
// Shared card display classes (.fan-card, .fan-card-corner, .fan-card-face, .fan-nav)
|
// Shared card display classes (.fan-card, .fan-card-corner, .fan-card-face, .fan-nav)
|
||||||
// extracted from _game-kit.scss; sig-select overlay extracted from _room.scss.
|
// extracted from _game-kit.scss; sig-select overlay extracted from _room.scss.
|
||||||
|
|
||||||
|
// ── Shared stage-card polarity rules ─────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Used by .sea-stage-card (Sea Select polarity is fixed by the deck-stack the
|
||||||
|
// gamer drew from) and .fan-card[data-polarity="..."] (Game Kit fan, FLIP-able).
|
||||||
|
// Sets title/qualifier color uniformly across both upright and reversal slots;
|
||||||
|
// optionally inverts the card frame (bg ⇄ border) for levity polarity, and
|
||||||
|
// applies an optional text-shadow (Sea uses one for the deeper card-art look;
|
||||||
|
// Fan does not).
|
||||||
|
@mixin stage-card-polarity($titles-color, $text-shadow: null, $invert-frame: false) {
|
||||||
|
@if $invert-frame {
|
||||||
|
background: rgba(var(--secUser), 1);
|
||||||
|
border-color: rgba(var(--priUser), 1);
|
||||||
|
}
|
||||||
|
.fan-card-name,
|
||||||
|
.sig-qualifier-above,
|
||||||
|
.sig-qualifier-below,
|
||||||
|
.fan-card-reversal-name,
|
||||||
|
.fan-card-reversal-qualifier {
|
||||||
|
color: $titles-color;
|
||||||
|
@if $text-shadow { text-shadow: $text-shadow; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Shared stat-block contents ───────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Used by .sig-stat-block (Sig Select), .sea-stat-block (Sea Select), and
|
||||||
|
// .fan-stage-block (Game Kit fan). The mixin emits the *inner* rules — stat-face
|
||||||
|
// padding/swap, stat-keywords, sig-info tooltip + header/title/type/effect/index.
|
||||||
|
// Each call site keeps its own outer rule (background, border, animation, button
|
||||||
|
// positioning + class hooks, visibility triggers).
|
||||||
|
@mixin stat-block-shared {
|
||||||
|
// SPIN / FYI buttons — pinned to the top-right edge
|
||||||
|
.spin-btn { position: absolute; top: -1rem; right: -1rem; margin: 0; z-index: 50; }
|
||||||
|
.fyi-btn { position: absolute; top: 1.25rem; right: -1rem; margin: 0; z-index: 50; }
|
||||||
|
|
||||||
|
// PRV / NXT — pinned to bottom corners; hidden by default. Sig + fan reveal
|
||||||
|
// them on .fyi-open (see each site's outer rule). Sea overrides to always
|
||||||
|
// visible (its FYI panel is permanent, not hover-toggled).
|
||||||
|
.fyi-prev, .fyi-next {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1rem;
|
||||||
|
margin: 0;
|
||||||
|
z-index: 70;
|
||||||
|
}
|
||||||
|
.fyi-prev { left: -1rem; }
|
||||||
|
.fyi-next { right: -1rem; }
|
||||||
|
|
||||||
|
.stat-face {
|
||||||
|
display: none;
|
||||||
|
padding: calc(var(--sig-card-w, 120px) * 0.37)
|
||||||
|
calc(var(--sig-card-w, 120px) * 0.1)
|
||||||
|
calc(var(--sig-card-w, 120px) * 0.08);
|
||||||
|
}
|
||||||
|
.stat-face--upright { display: block; }
|
||||||
|
|
||||||
|
&.is-reversed {
|
||||||
|
.stat-face--upright { display: none; }
|
||||||
|
.stat-face--reversed { display: block; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-face-label {
|
||||||
|
font-size: calc(var(--sig-card-w, 120px) * 0.063);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.09em;
|
||||||
|
opacity: 0.7;
|
||||||
|
color: rgba(var(--terUser), 1);
|
||||||
|
margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-keywords {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: calc(var(--sig-card-w, 120px) * 0.083);
|
||||||
|
padding: calc(var(--sig-card-w, 120px) * 0.042) 0;
|
||||||
|
opacity: 1;
|
||||||
|
border-bottom: 0.05rem solid rgba(var(--terUser), 0.18);
|
||||||
|
&:last-child { border-bottom: none; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FYI tooltip — covers the entire stat block when open
|
||||||
|
.sig-info {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 60;
|
||||||
|
background-color: rgba(var(--tooltip-bg), 0.6);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: 0.1rem solid rgba(var(--priYl), 0.35);
|
||||||
|
padding: 0.75rem;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.sig-info-header { display: flex; flex-direction: column; gap: 0.1rem; }
|
||||||
|
.sig-info-title {
|
||||||
|
font-size: calc(var(--sig-card-w, 120px) * 0.093);
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
&--energies, &--operations { color: rgba(var(--quaUser), 1); }
|
||||||
|
}
|
||||||
|
.sig-info-type {
|
||||||
|
font-size: calc(var(--sig-card-w, 120px) * 0.058);
|
||||||
|
opacity: 0.7;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.sig-info-effect {
|
||||||
|
flex: 1;
|
||||||
|
font-size: calc(var(--sig-card-w, 120px) * 0.075);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.55;
|
||||||
|
.card-ref { color: rgba(var(--terUser), 1); font-weight: 600; }
|
||||||
|
}
|
||||||
|
.sig-info-index {
|
||||||
|
font-size: calc(var(--sig-card-w, 120px) * 0.063);
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Tarot fan modal ──────────────────────────────────────────────────────────
|
// ── Tarot fan modal ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#id_tarot_fan_dialog {
|
#id_tarot_fan_dialog {
|
||||||
@@ -22,6 +148,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tarot-fan-wrap {
|
.tarot-fan-wrap {
|
||||||
|
// Fan card dimensions + carousel layout — overrideable per breakpoint via
|
||||||
|
// @media rules. game-kit.js reads --fan-carousel-step at runtime so the JS
|
||||||
|
// transform stack stays in sync with the CSS-driven sizing.
|
||||||
|
--fan-card-w: 220px;
|
||||||
|
--fan-card-h: 340px;
|
||||||
|
--fan-stage-shift: 130px;
|
||||||
|
--fan-carousel-step: 200px;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -41,15 +175,93 @@
|
|||||||
|
|
||||||
.tarot-fan {
|
.tarot-fan {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 220px;
|
width: var(--fan-card-w);
|
||||||
height: 340px;
|
height: var(--fan-card-h);
|
||||||
|
// Shift the whole carousel left so the focused card sits left-of-center,
|
||||||
|
// making symmetric room on the right for .fan-stage-block.
|
||||||
|
transform: translateX(calc(-1 * var(--fan-stage-shift)));
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mobile breakpoints ───────────────────────────────────────────────────────
|
||||||
|
// Override the four geometry vars on .tarot-fan-wrap; everything downstream
|
||||||
|
// (.tarot-fan size, .fan-card size, carousel stride read by JS, .fan-stage-block
|
||||||
|
// width via --sig-card-w, stat-face / stat-keywords typography) scales from them.
|
||||||
|
|
||||||
|
// Portrait mobile — true phone widths (≤ 480px). Narrow desktop windows in
|
||||||
|
// portrait orientation stay on the desktop default.
|
||||||
|
@media (orientation: portrait) and (max-width: 480px) {
|
||||||
|
.tarot-fan-wrap {
|
||||||
|
--fan-card-w: 150px;
|
||||||
|
--fan-card-h: 230px;
|
||||||
|
--fan-stage-shift: 90px;
|
||||||
|
--fan-carousel-step: 130px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Landscape mobile — short viewport, fan needs to stay above the fold
|
||||||
|
@media (orientation: landscape) and (max-height: 500px) {
|
||||||
|
.tarot-fan-wrap {
|
||||||
|
--fan-card-w: 150px;
|
||||||
|
--fan-card-h: 235px;
|
||||||
|
--fan-stage-shift: 90px;
|
||||||
|
--fan-carousel-step: 130px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fan stage block — symmetric right-of-focused-card stat panel ───────────
|
||||||
|
//
|
||||||
|
// Visual styling mirrors .sig-stat-block / .sea-stat-block (see Step-6 DRY note).
|
||||||
|
// Uses --sig-card-w: 220px so the cascaded font-size / padding calc() rules in
|
||||||
|
// the sea-stat-block block (which key off --sig-card-w) produce fan-card-sized
|
||||||
|
// typography. Animation/positioning is fan-specific (carousel symmetric slot,
|
||||||
|
// careen-out on nav, idle-reveal).
|
||||||
|
.fan-stage-block {
|
||||||
|
--sig-card-w: var(--fan-card-w);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: var(--sig-card-w);
|
||||||
|
height: calc(var(--sig-card-w) * 8 / 5);
|
||||||
|
background: rgba(var(--priUser), 1);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: 0.1rem solid rgba(var(--terUser), 0.15);
|
||||||
|
color: rgba(var(--secUser), 1);
|
||||||
|
z-index: 15;
|
||||||
|
|
||||||
|
// Symmetric counterpart to the carousel shift: stat block lives at +stage-shift
|
||||||
|
// from screen-center, mirroring the focused card at -stage-shift.
|
||||||
|
transform: translate(calc(-50% + var(--fan-stage-shift)), -50%) translateX(120vw);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
// Careen-out (default → no .is-revealed): swift exit, fade trails the slide
|
||||||
|
transition: transform 0.2s cubic-bezier(.5,0,.75,0),
|
||||||
|
opacity 0.2s ease 0.1s;
|
||||||
|
|
||||||
|
&.is-revealed {
|
||||||
|
transform: translate(calc(-50% + var(--fan-stage-shift)), -50%) translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
// Reveal: gentler ease-out, opacity leads slightly
|
||||||
|
transition: transform 0.6s ease-out,
|
||||||
|
opacity 0.5s ease-out 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include stat-block-shared;
|
||||||
|
|
||||||
|
&.fyi-open {
|
||||||
|
.sig-info { display: flex; }
|
||||||
|
.fyi-prev,
|
||||||
|
.fyi-next { display: inline-flex; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fan-card {
|
.fan-card {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
width: 220px;
|
width: var(--fan-card-w);
|
||||||
height: 340px;
|
height: var(--fan-card-h);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
background: rgba(var(--priUser), 1);
|
background: rgba(var(--priUser), 1);
|
||||||
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
||||||
@@ -64,6 +276,34 @@
|
|||||||
box-shadow: 0 0 2rem rgba(var(--secUser), 0.3);
|
box-shadow: 0 0 2rem rgba(var(--secUser), 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SPIN — whole card rotates (carousel transform + rotate(180deg) is combined
|
||||||
|
// in JS via updateFan/spinBtn handler, since the inline transform owns the
|
||||||
|
// carousel layout). Inner spans swap opacity so the upright fades and the
|
||||||
|
// reversal pops — matches sig/sea pattern.
|
||||||
|
&.stage-card--reversed {
|
||||||
|
.fan-card-reversal-qualifier,
|
||||||
|
.fan-card-reversal-name { opacity: 1; }
|
||||||
|
.fan-card-name,
|
||||||
|
.sig-qualifier-above,
|
||||||
|
.sig-qualifier-below { opacity: 0.25; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// FLIP — polarity-aware coloring. Default (no data-polarity) is gravity:
|
||||||
|
// priUser bg, secUser border. Levity inverts to secUser bg + priUser border.
|
||||||
|
&[data-polarity="levity"] {
|
||||||
|
@include stage-card-polarity(
|
||||||
|
$titles-color: rgba(var(--quiUser), 1),
|
||||||
|
$invert-frame: true,
|
||||||
|
);
|
||||||
|
.fan-card-corner { color: rgba(var(--priUser), 0.75); }
|
||||||
|
.fan-card-arcana,
|
||||||
|
.fan-card-name-group { color: rgba(var(--priUser), 0.85); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// FLIP — animation runs via Element.animate() in JS (game-kit.js _flipActive)
|
||||||
|
// so the rotateY layer stacks on top of the carousel inline transform
|
||||||
|
// (translateX/rotateY/scale + optional SPIN rotate(180deg)).
|
||||||
|
|
||||||
.fan-card-corner { padding-top: 0.25rem; }
|
.fan-card-corner { padding-top: 0.25rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,27 +320,101 @@
|
|||||||
&--tl { top: 0.4rem; left: 0.4rem; }
|
&--tl { top: 0.4rem; left: 0.4rem; }
|
||||||
&--br { bottom: 0.4rem; right: 0.4rem; transform: rotate(180deg); }
|
&--br { bottom: 0.4rem; right: 0.4rem; transform: rotate(180deg); }
|
||||||
|
|
||||||
|
// Corner rank + suit icon scale with the card so they shrink on mobile
|
||||||
|
// breakpoints alongside .fan-card. 0.109 of card-width ≈ 24px @ 220px (the
|
||||||
|
// original 1.5rem default).
|
||||||
.fan-corner-rank {
|
.fan-corner-rank {
|
||||||
font-size: 1.5rem;
|
font-size: calc(var(--fan-card-w, 220px) * 0.109);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 0.18rem 0;
|
padding: 0.18rem 0;
|
||||||
}
|
}
|
||||||
// Icon always at the outer card edge regardless of rank width
|
// Icon always at the outer card edge regardless of rank width
|
||||||
i { font-size: 1.5rem; align-self: flex-start; }
|
i {
|
||||||
|
font-size: calc(var(--fan-card-w, 220px) * 0.109);
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fan-card-face {
|
.fan-card-face {
|
||||||
padding: 1.25rem;
|
// Padding + gaps scale with card width so they stay proportional on mobile.
|
||||||
|
padding: calc(var(--fan-card-w) * 0.057);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: calc(var(--fan-card-w) * 0.023);
|
||||||
|
// Face flips on SPIN; corners stay put because they live outside .fan-card-face
|
||||||
|
transition: transform 0.4s ease;
|
||||||
|
|
||||||
.fan-card-number { font-size: 0.65rem; }
|
.fan-card-face-upright,
|
||||||
.fan-card-name-group { font-size: 0.65rem; margin: 0; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(var(--secUser), 1); }
|
.fan-card-face-reversal {
|
||||||
.fan-card-name { font-size: 0.95rem; font-weight: bold; margin: 0; color: rgba(var(--terUser), 1); }
|
display: flex;
|
||||||
.fan-card-arcana { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.1em; color: rgba(var(--secUser), 1); }
|
flex-direction: column;
|
||||||
.fan-card-correspondence { font-size: 0.6rem; font-style: italic; color: rgba(var(--secUser), 0.5); }
|
align-items: center;
|
||||||
|
gap: calc(var(--fan-card-w) * 0.007);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qualifier shares the name's typography — same line, different content.
|
||||||
|
// Sizes scale with --fan-card-w so they stay proportional on mobile.
|
||||||
|
.sig-qualifier-above,
|
||||||
|
.sig-qualifier-below,
|
||||||
|
.fan-card-reversal-qualifier,
|
||||||
|
.fan-card-reversal-name,
|
||||||
|
.fan-card-name {
|
||||||
|
font-size: calc(var(--fan-card-w) * 0.1);
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
color: rgba(var(--terUser), 1);
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reversal-face spans pre-rotated so they read forward once the card spins
|
||||||
|
// 180deg via .stage-card--reversed. Matches sig/sea convention (rotation +
|
||||||
|
// base opacity live on the inner spans, NOT the wrapping .fan-card-face-reversal
|
||||||
|
// div — otherwise the outer div's transform stacks with sig/sea's scoped rule
|
||||||
|
// and double-rotates back to upright).
|
||||||
|
.fan-card-reversal-qualifier,
|
||||||
|
.fan-card-reversal-name {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fan-card-number { font-size: calc(var(--fan-card-w) * 0.043); }
|
||||||
|
.fan-card-name-group { font-size: calc(var(--fan-card-w) * 0.043); margin: 0; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(var(--secUser), 1); }
|
||||||
|
.fan-card-arcana { font-size: calc(var(--fan-card-w) * 0.043); text-transform: uppercase; letter-spacing: 0.1em; color: rgba(var(--secUser), 1); }
|
||||||
|
.fan-card-correspondence { font-size: calc(var(--fan-card-w) * 0.04); font-style: italic; color: rgba(var(--secUser), 0.5); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// FLIP button — invisible at rest, fades in when the user hovers/taps the wrap.
|
||||||
|
// Positioned at bottom-left of the focused card slot (carousel-shifted, so its
|
||||||
|
// translateX matches .tarot-fan's leftward shift).
|
||||||
|
.fan-flip-btn {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 25;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(calc(-50% - var(--fan-stage-shift) - var(--fan-card-w) / 2 + 1.5rem),
|
||||||
|
calc(-50% + var(--fan-card-h) / 2 - 1.5rem));
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
// Reveal when the focused card OR the FLIP button itself is hovered. Without
|
||||||
|
// the `.fan-flip-btn:hover` clause the button (z-index 25, sitting on top of
|
||||||
|
// the card) steals :hover from the card the moment the cursor moves onto it,
|
||||||
|
// flipping :has() false, fading the button to opacity:0 + pointer-events:none,
|
||||||
|
// and letting the in-flight click pass through to the dialog backdrop (which
|
||||||
|
// closes the modal). Keeping the button in the trigger list pins it visible
|
||||||
|
// while the cursor is on it.
|
||||||
|
.tarot-fan-wrap:has(.fan-card--active:hover) .fan-flip-btn,
|
||||||
|
.tarot-fan-wrap:has(.fan-flip-btn:hover) .fan-flip-btn,
|
||||||
|
.tarot-fan-wrap.fan-touch-revealed .fan-flip-btn {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.tarot-fan-wrap:has(.fan-card[data-flipping]) .fan-flip-btn {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fan-nav {
|
.fan-nav {
|
||||||
@@ -272,130 +586,15 @@ html:has(.sig-backdrop) {
|
|||||||
display: none;
|
display: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@include stat-block-shared;
|
||||||
.sig-flip-btn {
|
|
||||||
position: absolute;
|
|
||||||
top: -1rem;
|
|
||||||
right: -1rem;
|
|
||||||
margin: 0;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-info-btn {
|
|
||||||
position: absolute;
|
|
||||||
top: 1.25rem;
|
|
||||||
right: -1rem;
|
|
||||||
margin: 0;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caution tooltip — covers the entire stat block (inset: 0), z-index above buttons.
|
|
||||||
.sig-info {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 60;
|
|
||||||
background-color: rgba(var(--tooltip-bg), 0.6);
|
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
border: 0.1rem solid rgba(var(--priYl), 0.35);
|
|
||||||
padding: 0.75rem;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.4rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-info-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-info-title {
|
|
||||||
font-size: calc(var(--sig-card-w, 120px) * 0.093);
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0;
|
|
||||||
&--energies { color: rgba(var(--quaUser), 1); }
|
|
||||||
&--operations { color: rgba(var(--quaUser), 1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-info-type {
|
|
||||||
font-size: calc(var(--sig-card-w, 120px) * 0.058);
|
|
||||||
opacity: 0.7;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-info-effect {
|
|
||||||
flex: 1;
|
|
||||||
font-size: calc(var(--sig-card-w, 120px) * 0.075);
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.55;
|
|
||||||
|
|
||||||
.card-ref {
|
|
||||||
color: rgba(var(--terUser), 1);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-info-index {
|
|
||||||
font-size: calc(var(--sig-card-w, 120px) * 0.063);
|
|
||||||
opacity: 0.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nav arrows portaled out of tooltip — sit at bottom corners above tooltip (z-70)
|
|
||||||
.sig-info-prev,
|
|
||||||
.sig-info-next {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
bottom: -1rem;
|
|
||||||
margin: 0;
|
|
||||||
z-index: 70;
|
|
||||||
}
|
|
||||||
.sig-info-prev { left: -1rem; }
|
|
||||||
.sig-info-next { right: -1rem; }
|
|
||||||
|
|
||||||
.stat-face {
|
|
||||||
display: none;
|
|
||||||
padding: calc(var(--sig-card-w, 120px) * 0.37) calc(var(--sig-card-w, 120px) * 0.1) calc(var(--sig-card-w, 120px) * 0.08);
|
|
||||||
|
|
||||||
&--upright { display: block; }
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-reversed {
|
|
||||||
.stat-face--upright { display: none; }
|
|
||||||
.stat-face--reversed { display: block; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-face-label {
|
|
||||||
font-size: calc(var(--sig-card-w, 120px) * 0.063);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.09em;
|
|
||||||
opacity: 0.4;
|
|
||||||
margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-keywords {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
li {
|
|
||||||
font-size: calc(var(--sig-card-w, 120px) * 0.083);
|
|
||||||
padding: calc(var(--sig-card-w, 120px) * 0.042) 0;
|
|
||||||
opacity: 0.85;
|
|
||||||
border-bottom: 0.05rem solid rgba(var(--terUser), 0.12);
|
|
||||||
|
|
||||||
&:last-child { border-bottom: none; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.sig-stage--frozen .sig-stat-block { display: block; }
|
&.sig-stage--frozen .sig-stat-block { display: block; }
|
||||||
&.sig-info-open .sig-stat-block {
|
|
||||||
.sig-info { display: flex; }
|
// Unified .fyi-open class — opens the FYI panel + reveals PRV/NXT nav.
|
||||||
.sig-info-prev, .sig-info-next { display: inline-flex; }
|
.sig-stat-block.fyi-open {
|
||||||
|
.sig-info { display: flex; }
|
||||||
|
.fyi-prev, .fyi-next { display: inline-flex; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,12 +623,16 @@ html:has(.sig-backdrop) {
|
|||||||
transition: border-color 0.15s, box-shadow 0.15s;
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
// game-kit sets .fan-card-corner { position:absolute; top:0.4rem; left:0.4rem }
|
// game-kit sets .fan-card-corner { position:absolute; top:0.4rem; left:0.4rem;
|
||||||
// Override: center the element within the card instead.
|
// padding-left:0.5rem }. Sig thumbnails reset position to dead-center; we
|
||||||
|
// also zero the inherited padding-left (it's there for game-kit fan corners
|
||||||
|
// that need outer-edge breathing room — at thumb size it nudges the rank +
|
||||||
|
// icon visibly off-center after the translate).
|
||||||
.fan-card-corner--tl {
|
.fan-card-corner--tl {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
|
padding-left: 0;
|
||||||
gap: 0; // game-kit has gap:0.15rem — too large at 0.5rem font-size
|
gap: 0; // game-kit has gap:0.15rem — too large at 0.5rem font-size
|
||||||
|
|
||||||
.fan-corner-rank { font-size: 1rem; font-weight: 700; }
|
.fan-corner-rank { font-size: 1rem; font-weight: 700; }
|
||||||
@@ -962,6 +1165,13 @@ $sea-card-h: 6.5rem;
|
|||||||
background: rgba(var(--priUser), 1);
|
background: rgba(var(--priUser), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mobile: stack crucifix on top, form (select / stacks / LOCK HAND / DEL) below
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.sea-modal-body { flex-direction: column; }
|
||||||
|
.sea-cards-col { flex: 0 0 auto; padding: 1.25rem 1rem; }
|
||||||
|
.sea-form-col { width: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
.sea-form-main {
|
.sea-form-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -976,7 +1186,13 @@ $sea-card-h: 6.5rem;
|
|||||||
label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; }
|
label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom combobox replacement for native <select>. See combobox.js for the
|
||||||
|
// expected markup; SCSS owns all visuals because the OS-native dropdown ignored
|
||||||
|
// option background/color anyway.
|
||||||
.sea-select {
|
.sea-select {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
background: rgba(var(--duoUser), 0.6);
|
background: rgba(var(--duoUser), 0.6);
|
||||||
border: 1px solid rgba(var(--terUser), 0.3);
|
border: 1px solid rgba(var(--terUser), 0.3);
|
||||||
border-radius: 0.3rem;
|
border-radius: 0.3rem;
|
||||||
@@ -984,8 +1200,64 @@ $sea-card-h: 6.5rem;
|
|||||||
padding: 0.4rem 0.5rem;
|
padding: 0.4rem 0.5rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 12.5rem;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
option { background: rgba(var(--priUser), 1); }
|
&:focus-visible { box-shadow: 0 0 0 2px rgba(var(--terUser), 0.5); }
|
||||||
|
|
||||||
|
.sea-select-current {
|
||||||
|
display: block;
|
||||||
|
padding-right: 1.1rem; // room for the arrow
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.sea-select-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sea-select-list {
|
||||||
|
display: none;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0.2rem 0 0;
|
||||||
|
padding: 0.15rem;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(var(--undUser), 1);
|
||||||
|
border: 1px solid rgba(var(--terUser), 0.5);
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.4);
|
||||||
|
|
||||||
|
li[role="option"] {
|
||||||
|
padding: 0.4rem 0.5rem;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
color: rgba(var(--priUser), 1);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s, color 0.1s;
|
||||||
|
}
|
||||||
|
// Hover + keyboard-focus + selected all share the inverted scheme so
|
||||||
|
// the visual feedback stays consistent regardless of input device.
|
||||||
|
li[role="option"]:hover,
|
||||||
|
li[role="option"].sea-select-option--focus,
|
||||||
|
li[role="option"][aria-selected="true"] {
|
||||||
|
background: rgba(var(--priUser), 1);
|
||||||
|
color: rgba(var(--secUser), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-expanded="true"] {
|
||||||
|
.sea-select-list { display: block; }
|
||||||
|
.sea-select-arrow { transform: translateY(-50%) rotate(180deg); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deck stacks — DECKS label + gravity + levity piles
|
// Deck stacks — DECKS label + gravity + levity piles
|
||||||
@@ -1206,25 +1478,20 @@ $_sea-title-shadow: 1px 1px 0 rgba(0,0,0,1), 0 0 0.25rem rgba(var(--ninUser), 0.
|
|||||||
$_sea-title-els: '.fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .fan-card-reversal-name, .fan-card-reversal-qualifier';
|
$_sea-title-els: '.fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .fan-card-reversal-name, .fan-card-reversal-qualifier';
|
||||||
|
|
||||||
.sea-stage--levity .sea-stage-card {
|
.sea-stage--levity .sea-stage-card {
|
||||||
background: rgba(var(--secUser), 1);
|
@include stage-card-polarity(
|
||||||
border-color: rgba(var(--priUser), 1);
|
$titles-color: rgba(var(--quiUser), 1),
|
||||||
|
$text-shadow: $_sea-title-shadow,
|
||||||
|
$invert-frame: true,
|
||||||
|
);
|
||||||
color: rgba(var(--priUser), 1);
|
color: rgba(var(--priUser), 1);
|
||||||
.fan-card-arcana,
|
.fan-card-arcana,
|
||||||
.fan-card-corner {
|
.fan-card-corner { color: rgba(var(--priUser), 1); }
|
||||||
color: rgba(var(--priUser), 1);
|
|
||||||
}
|
|
||||||
.fan-card-name, .sig-qualifier-above, .sig-qualifier-below,
|
|
||||||
.fan-card-reversal-name, .fan-card-reversal-qualifier {
|
|
||||||
color: rgba(var(--quiUser), 1);
|
|
||||||
text-shadow: $_sea-title-shadow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.sea-stage--gravity .sea-stage-card {
|
.sea-stage--gravity .sea-stage-card {
|
||||||
.fan-card-name, .sig-qualifier-above, .sig-qualifier-below,
|
@include stage-card-polarity(
|
||||||
.fan-card-reversal-name, .fan-card-reversal-qualifier {
|
$titles-color: rgba(var(--terUser), 1),
|
||||||
color: rgba(var(--terUser), 1);
|
$text-shadow: $_sea-title-shadow,
|
||||||
text-shadow: $_sea-title-shadow;
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sea stat block — reuses sig-select stat-block sizing, scoped to sea-stage
|
// Sea stat block — reuses sig-select stat-block sizing, scoped to sea-stage
|
||||||
@@ -1238,37 +1505,11 @@ $_sea-title-els: '.fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .f
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.sea-spin-btn { position: absolute; top: -1rem; right: -1rem; margin: 0; z-index: 50; }
|
@include stat-block-shared;
|
||||||
.sea-fyi-btn { position: absolute; top: 1.25rem; right: -1rem; margin: 0; z-index: 50; }
|
|
||||||
|
|
||||||
.stat-face { display: none; padding: calc(var(--sig-card-w, 140px) * 0.37) calc(var(--sig-card-w, 140px) * 0.1) calc(var(--sig-card-w, 140px) * 0.08); }
|
// Sea's FYI panel is permanent (not hover-toggled), so its PRV/NXT nav
|
||||||
.stat-face--upright { display: block; }
|
// buttons are always visible — overrides the mixin's hidden-by-default.
|
||||||
&.is-reversed { .stat-face--upright { display: none; } .stat-face--reversed { display: block; } }
|
.fyi-prev, .fyi-next { display: inline-flex; }
|
||||||
|
|
||||||
.stat-face-label { font-size: calc(var(--sig-card-w, 140px) * 0.063); text-transform: uppercase; letter-spacing: 0.09em; opacity: 0.4; margin: 0 0 calc(var(--sig-card-w, 140px) * 0.07); }
|
|
||||||
.stat-keywords { list-style: none; padding: 0; margin: 0;
|
|
||||||
li { font-size: calc(var(--sig-card-w, 140px) * 0.083); padding: calc(var(--sig-card-w, 140px) * 0.042) 0; opacity: 0.85; border-bottom: 0.05rem solid rgba(var(--terUser), 0.12); &:last-child { border-bottom: none; } }
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-info {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 60;
|
|
||||||
background-color: rgba(var(--tooltip-bg), 0.6);
|
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
border: 0.1rem solid rgba(var(--priYl), 0.35);
|
|
||||||
padding: 0.75rem;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.4rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sea-fyi-prev,
|
|
||||||
.sea-fyi-next { display: inline-flex; position: absolute; bottom: -1rem; margin: 0; z-index: 70; }
|
|
||||||
.sea-fyi-prev { left: -1rem; }
|
|
||||||
.sea-fyi-next { right: -1rem; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
@media (orientation: landscape) {
|
||||||
|
|||||||
308
src/static_src/tests/FanStageSpec.js
Normal file
308
src/static_src/tests/FanStageSpec.js
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
describe("FanStage", () => {
|
||||||
|
let testDiv, dialog, fanContent, stageBlock, stageCard;
|
||||||
|
|
||||||
|
function makeCardEl({ id, suit_icon = '', corner_rank = 'I', name_group = '',
|
||||||
|
name_title = 'The Magician', arcana = 'Major Arcana',
|
||||||
|
correspondence = '', keywords_upright = 'will,focus,manifestation',
|
||||||
|
keywords_reversed = 'manipulation,illusion',
|
||||||
|
energies = '[]', operations = '[]',
|
||||||
|
levity_qualifier = '', gravity_qualifier = '',
|
||||||
|
reversal_qualifier = '' } = {}) {
|
||||||
|
return `<div class="fan-card"
|
||||||
|
data-index="${id}"
|
||||||
|
data-suit-icon="${suit_icon}"
|
||||||
|
data-corner-rank="${corner_rank}"
|
||||||
|
data-name-group="${name_group}"
|
||||||
|
data-name-title="${name_title}"
|
||||||
|
data-arcana="${arcana}"
|
||||||
|
data-correspondence="${correspondence}"
|
||||||
|
data-keywords-upright="${keywords_upright}"
|
||||||
|
data-keywords-reversed="${keywords_reversed}"
|
||||||
|
data-energies='${energies}'
|
||||||
|
data-operations='${operations}'
|
||||||
|
data-levity-qualifier="${levity_qualifier}"
|
||||||
|
data-gravity-qualifier="${gravity_qualifier}"
|
||||||
|
data-reversal-qualifier="${reversal_qualifier}">
|
||||||
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
|
<span class="fan-corner-rank">${corner_rank}</span>
|
||||||
|
</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>
|
||||||
|
<h3 class="fan-card-name">${name_title}</h3>
|
||||||
|
<p class="sig-qualifier-below"></p>
|
||||||
|
</div>
|
||||||
|
<p class="fan-card-arcana">${arcana}</p>
|
||||||
|
<div class="fan-card-face-reversal">
|
||||||
|
<p class="fan-card-reversal-qualifier"></p>
|
||||||
|
<p class="fan-card-reversal-name"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fan-card-corner fan-card-corner--br">
|
||||||
|
<span class="fan-corner-rank">${corner_rank}</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeFixture() {
|
||||||
|
testDiv = document.createElement("div");
|
||||||
|
testDiv.innerHTML = `
|
||||||
|
<dialog id="id_tarot_fan_dialog">
|
||||||
|
<div class="tarot-fan-wrap">
|
||||||
|
<button id="id_fan_prev" class="fan-nav fan-nav--prev">‹</button>
|
||||||
|
<div id="id_fan_content" class="tarot-fan"></div>
|
||||||
|
<div class="fan-stage-block sig-stat-block" id="id_fan_stage_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" id="id_fan_stat_upright"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="stat-face stat-face--reversed">
|
||||||
|
<p class="stat-face-label">Reversal</p>
|
||||||
|
<ul class="stat-keywords" id="id_fan_stat_reversed"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="sig-info" id="id_fan_fyi_panel" style="display:none">
|
||||||
|
<div class="sig-info-header">
|
||||||
|
<h4 class="sig-info-title"></h4>
|
||||||
|
<p class="sig-info-type"></p>
|
||||||
|
</div>
|
||||||
|
<p class="sig-info-effect"></p>
|
||||||
|
<span class="sig-info-index"></span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-nav-left fyi-prev" type="button">PRV</button>
|
||||||
|
<button class="btn btn-nav-right fyi-next" type="button">NXT</button>
|
||||||
|
</div>
|
||||||
|
<button id="id_fan_flip" class="btn btn-reveal fan-flip-btn" type="button">FLIP</button>
|
||||||
|
<button id="id_fan_next" class="fan-nav fan-nav--next">›</button>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
<button class="gk-deck-card" data-deck-id="1">Earthman</button>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(testDiv);
|
||||||
|
dialog = testDiv.querySelector("#id_tarot_fan_dialog");
|
||||||
|
fanContent = testDiv.querySelector("#id_fan_content");
|
||||||
|
stageBlock = testDiv.querySelector("#id_fan_stage_block");
|
||||||
|
|
||||||
|
// Pre-render two cards into fanContent so openFan's fetch can be skipped via _testOpen
|
||||||
|
fanContent.innerHTML =
|
||||||
|
makeCardEl({ id: 0, name_title: 'The Magician', corner_rank: 'I',
|
||||||
|
levity_qualifier: 'Enlightened',
|
||||||
|
gravity_qualifier: 'Engraven',
|
||||||
|
keywords_upright: 'will,focus',
|
||||||
|
keywords_reversed: 'manipulation',
|
||||||
|
energies: '[{"type":"LIBIDO","effect":"Drive."}]',
|
||||||
|
operations: '[{"type":"COVER","effect":"Shield."}]' }) +
|
||||||
|
makeCardEl({ id: 1, name_title: 'The High Priestess', corner_rank: 'II',
|
||||||
|
levity_qualifier: 'Enlightened',
|
||||||
|
gravity_qualifier: 'Engraven',
|
||||||
|
keywords_upright: 'intuition,subconscious',
|
||||||
|
keywords_reversed: 'secrets,disconnect' });
|
||||||
|
|
||||||
|
jasmine.clock().install();
|
||||||
|
GameKit._testInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jasmine.clock().uninstall();
|
||||||
|
if (testDiv) testDiv.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Stage block reveal / hide ────────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("idle reveal", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("starts with the stage block hidden (no .is-revealed)", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds .is-revealed after 500ms of idle", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
jasmine.clock().tick(500);
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT reveal before 500ms have elapsed", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
jasmine.clock().tick(400);
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-hides immediately on navigation, even after reveal", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
jasmine.clock().tick(500);
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(true);
|
||||||
|
GameKit._testNavigate(1);
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restarts the idle timer after navigation", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
jasmine.clock().tick(500);
|
||||||
|
GameKit._testNavigate(1);
|
||||||
|
jasmine.clock().tick(400);
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(false);
|
||||||
|
jasmine.clock().tick(100);
|
||||||
|
expect(stageBlock.classList.contains("is-revealed")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Stat block population from focused fan-card ──────────────────────── //
|
||||||
|
|
||||||
|
describe("stat block population", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("populates upright keywords from data-keywords-upright of focused card", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
const list = stageBlock.querySelector("#id_fan_stat_upright");
|
||||||
|
const items = list.querySelectorAll("li");
|
||||||
|
expect(items.length).toBe(2);
|
||||||
|
expect(items[0].textContent).toBe("will");
|
||||||
|
expect(items[1].textContent).toBe("focus");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("populates reversed keywords from data-keywords-reversed of focused card", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
const list = stageBlock.querySelector("#id_fan_stat_reversed");
|
||||||
|
const items = list.querySelectorAll("li");
|
||||||
|
expect(items.length).toBe(1);
|
||||||
|
expect(items[0].textContent).toBe("manipulation");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-populates from the new focused card on navigation", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
GameKit._testNavigate(1);
|
||||||
|
const items = stageBlock.querySelectorAll("#id_fan_stat_upright li");
|
||||||
|
expect(items.length).toBe(2);
|
||||||
|
expect(items[0].textContent).toBe("intuition");
|
||||||
|
expect(items[1].textContent).toBe("subconscious");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── SPIN button (.btn-reverse) ──────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("SPIN", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("toggles .is-reversed on the stat block", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
stageBlock.querySelector(".spin-btn").click();
|
||||||
|
expect(stageBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles back when clicked twice", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
const spin = stageBlock.querySelector(".spin-btn");
|
||||||
|
spin.click(); spin.click();
|
||||||
|
expect(stageBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets .is-reversed when navigating to a new card", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
stageBlock.querySelector(".spin-btn").click();
|
||||||
|
expect(stageBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
|
GameKit._testNavigate(1);
|
||||||
|
expect(stageBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── FYI button (.btn-info) ──────────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("FYI", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("opens the FYI panel and disables SPIN+FYI buttons", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
stageBlock.querySelector(".fyi-btn").click();
|
||||||
|
const panel = stageBlock.querySelector("#id_fan_fyi_panel");
|
||||||
|
expect(panel.style.display).toBe("flex");
|
||||||
|
expect(stageBlock.querySelector(".spin-btn").classList.contains("btn-disabled")).toBe(true);
|
||||||
|
expect(stageBlock.querySelector(".fyi-btn").classList.contains("btn-disabled")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("populates the panel with the first energy entry from data-energies", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
stageBlock.querySelector(".fyi-btn").click();
|
||||||
|
const panel = stageBlock.querySelector("#id_fan_fyi_panel");
|
||||||
|
expect(panel.querySelector(".sig-info-type").textContent).toBe("LIBIDO");
|
||||||
|
expect(panel.querySelector(".sig-info-effect").innerHTML).toBe("Drive.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PRV/NXT cycle through energies + operations", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
stageBlock.querySelector(".fyi-btn").click();
|
||||||
|
stageBlock.querySelector(".fyi-next").click();
|
||||||
|
const panel = stageBlock.querySelector("#id_fan_fyi_panel");
|
||||||
|
expect(panel.querySelector(".sig-info-type").textContent).toBe("COVER");
|
||||||
|
expect(panel.querySelector(".sig-info-effect").innerHTML).toBe("Shield.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── FLIP — polarity toggle (Levity ↔ Gravity) ──────────────────────────── //
|
||||||
|
//
|
||||||
|
// FLIP swaps polarity on the focused card with a perspective rotateY animation.
|
||||||
|
// The repaint fires at the 250ms midpoint via setTimeout (jasmine.clock fakes
|
||||||
|
// it). Element.animate() is called too — but tests assert side effects (the
|
||||||
|
// dataset.polarity attr + the qualifier text content), not the animation.
|
||||||
|
|
||||||
|
describe("FLIP", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
function flipBtn() { return testDiv.querySelector("#id_fan_flip"); }
|
||||||
|
function activeCard() { return testDiv.querySelector(".fan-card--active"); }
|
||||||
|
|
||||||
|
it("starts with polarity = levity (the server-rendered default)", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
jasmine.clock().tick(250);
|
||||||
|
expect(activeCard().dataset.polarity).toBe("levity");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles dataset.polarity to gravity at the midpoint after click", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
flipBtn().click();
|
||||||
|
jasmine.clock().tick(250);
|
||||||
|
expect(activeCard().dataset.polarity).toBe("gravity");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("repaints the upright qualifier slot with the gravity qualifier", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
flipBtn().click();
|
||||||
|
jasmine.clock().tick(250);
|
||||||
|
const card = activeCard();
|
||||||
|
// Major arcana places qualifier in the BELOW slot.
|
||||||
|
expect(card.querySelector(".sig-qualifier-below").textContent).toBe("Engraven");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("FLIPs back to levity on a second click", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
flipBtn().click();
|
||||||
|
jasmine.clock().tick(500);
|
||||||
|
flipBtn().click();
|
||||||
|
jasmine.clock().tick(250);
|
||||||
|
expect(activeCard().dataset.polarity).toBe("levity");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retains SPIN state across a FLIP (.stage-card--reversed survives)", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
stageBlock.querySelector(".spin-btn").click(); // SPIN first
|
||||||
|
expect(activeCard().classList.contains("stage-card--reversed")).toBe(true);
|
||||||
|
flipBtn().click();
|
||||||
|
jasmine.clock().tick(500);
|
||||||
|
expect(activeCard().classList.contains("stage-card--reversed")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores a second click while a flip is in flight", () => {
|
||||||
|
GameKit._testOpen();
|
||||||
|
flipBtn().click();
|
||||||
|
// Mid-animation second click — should be ignored
|
||||||
|
jasmine.clock().tick(100);
|
||||||
|
flipBtn().click();
|
||||||
|
jasmine.clock().tick(150); // total = 250ms (one repaint window)
|
||||||
|
// Only one polarity swap happened (levity → gravity), not two.
|
||||||
|
expect(activeCard().dataset.polarity).toBe("gravity");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,7 +7,7 @@ describe("SeaDeal", () => {
|
|||||||
corner_rank: "Q", suit_icon: "fa-crown",
|
corner_rank: "Q", suit_icon: "fa-crown",
|
||||||
name_group: "", name_title: "Queen of Crowns",
|
name_group: "", name_title: "Queen of Crowns",
|
||||||
levity_qualifier: "Elevated", gravity_qualifier: "Graven",
|
levity_qualifier: "Elevated", gravity_qualifier: "Graven",
|
||||||
reversal: "Vacant",
|
reversal_qualifier: "Vacant",
|
||||||
keywords_upright: ["nurturing", "practical", "abundance"],
|
keywords_upright: ["nurturing", "practical", "abundance"],
|
||||||
keywords_reversed: ["financial dependence", "smothering"],
|
keywords_reversed: ["financial dependence", "smothering"],
|
||||||
energies: [{ type: "LIBIDO", effect: "Energy entry." }],
|
energies: [{ type: "LIBIDO", effect: "Energy entry." }],
|
||||||
@@ -72,8 +72,8 @@ describe("SeaDeal", () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sig-stat-block sea-stat-block">
|
<div class="sig-stat-block sea-stat-block">
|
||||||
<button class="btn btn-reverse sea-spin-btn" type="button">SPIN</button>
|
<button class="btn btn-reverse spin-btn" type="button">SPIN</button>
|
||||||
<button class="btn btn-info sea-fyi-btn" type="button">FYI</button>
|
<button class="btn btn-info fyi-btn" type="button">FYI</button>
|
||||||
<div class="stat-face stat-face--upright">
|
<div class="stat-face stat-face--upright">
|
||||||
<p class="stat-face-label">Emanation</p>
|
<p class="stat-face-label">Emanation</p>
|
||||||
<ul class="stat-keywords" id="id_sea_stat_upright"></ul>
|
<ul class="stat-keywords" id="id_sea_stat_upright"></ul>
|
||||||
@@ -90,8 +90,8 @@ describe("SeaDeal", () => {
|
|||||||
<p class="sig-info-effect"></p>
|
<p class="sig-info-effect"></p>
|
||||||
<span class="sig-info-index"></span>
|
<span class="sig-info-index"></span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-nav-left sea-fyi-prev" type="button">PRV</button>
|
<button class="btn btn-nav-left fyi-prev" type="button">PRV</button>
|
||||||
<button class="btn btn-nav-right sea-fyi-next" type="button">NXT</button>
|
<button class="btn btn-nav-right fyi-next" type="button">NXT</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,17 +197,17 @@ describe("SeaDeal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("toggles is-reversed on stat block", () => {
|
it("toggles is-reversed on stat block", () => {
|
||||||
testDiv.querySelector(".sea-spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("toggles stage-card--reversed on stage card", () => {
|
it("toggles stage-card--reversed on stage card", () => {
|
||||||
testDiv.querySelector(".sea-spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".spin-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("second SPIN click restores upright", () => {
|
it("second SPIN click restores upright", () => {
|
||||||
const btn = testDiv.querySelector(".sea-spin-btn");
|
const btn = testDiv.querySelector(".spin-btn");
|
||||||
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
@@ -223,23 +223,23 @@ describe("SeaDeal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("FYI click shows the info panel", () => {
|
it("FYI click shows the info panel", () => {
|
||||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector("#id_sea_fyi_panel").style.display).not.toBe("none");
|
expect(testDiv.querySelector("#id_sea_fyi_panel").style.display).not.toBe("none");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows first energy entry title as 'Energy'", () => {
|
it("shows first energy entry title as 'Energy'", () => {
|
||||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Energy");
|
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Energy");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows first entry type", () => {
|
it("shows first entry type", () => {
|
||||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("LIBIDO");
|
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("LIBIDO");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("NXT advances to operation entry", () => {
|
it("NXT advances to operation entry", () => {
|
||||||
testDiv.querySelector(".sea-fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".fyi-btn").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
testDiv.querySelector(".sea-fyi-next").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
testDiv.querySelector(".fyi-next").dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Operation");
|
expect(testDiv.querySelector(".sig-info-title").textContent).toBe("Operation");
|
||||||
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("COVER");
|
expect(testDiv.querySelector(".sig-info-type").textContent).toBe("COVER");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ describe("SigSelect", () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sig-stat-block">
|
<div class="sig-stat-block">
|
||||||
<button class="btn btn-reverse sig-flip-btn" type="button">SPIN</button>
|
<button class="btn btn-reverse spin-btn" type="button">SPIN</button>
|
||||||
<button class="btn btn-info sig-info-btn" type="button">FYI</button>
|
<button class="btn btn-info fyi-btn" type="button">FYI</button>
|
||||||
<div class="stat-face stat-face--upright">
|
<div class="stat-face stat-face--upright">
|
||||||
<p class="stat-face-label">Emanation</p>
|
<p class="stat-face-label">Emanation</p>
|
||||||
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
|
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
|
||||||
@@ -40,8 +40,8 @@ describe("SigSelect", () => {
|
|||||||
<p class="stat-face-label">Reversal</p>
|
<p class="stat-face-label">Reversal</p>
|
||||||
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-nav-left sig-info-prev" type="button">◀</button>
|
<button class="btn btn-nav-left fyi-prev" type="button">◀</button>
|
||||||
<button class="btn btn-nav-right sig-info-next" type="button">▶</button>
|
<button class="btn btn-nav-right fyi-next" type="button">▶</button>
|
||||||
<div class="sig-info" id="id_sig_info">
|
<div class="sig-info" id="id_sig_info">
|
||||||
<div class="sig-info-header">
|
<div class="sig-info-header">
|
||||||
<h4 class="sig-info-title"></h4>
|
<h4 class="sig-info-title"></h4>
|
||||||
@@ -67,7 +67,7 @@ describe("SigSelect", () => {
|
|||||||
data-operations="[]"
|
data-operations="[]"
|
||||||
data-levity-qualifier="Elevated"
|
data-levity-qualifier="Elevated"
|
||||||
data-gravity-qualifier="Graven"
|
data-gravity-qualifier="Graven"
|
||||||
data-reversal="">
|
data-reversal-qualifier="">
|
||||||
<div class="fan-card-corner fan-card-corner--tl">
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
<span class="fan-corner-rank">K</span>
|
<span class="fan-corner-rank">K</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,24 +211,24 @@ describe("SigSelect", () => {
|
|||||||
infoTitle = testDiv.querySelector(".sig-info-title");
|
infoTitle = testDiv.querySelector(".sig-info-title");
|
||||||
infoType = testDiv.querySelector(".sig-info-type");
|
infoType = testDiv.querySelector(".sig-info-type");
|
||||||
infoIndex = testDiv.querySelector(".sig-info-index");
|
infoIndex = testDiv.querySelector(".sig-info-index");
|
||||||
infoPrev = testDiv.querySelector(".sig-info-prev");
|
infoPrev = testDiv.querySelector(".fyi-prev");
|
||||||
infoNext = testDiv.querySelector(".sig-info-next");
|
infoNext = testDiv.querySelector(".fyi-next");
|
||||||
infoBtn = testDiv.querySelector(".sig-info-btn");
|
infoBtn = testDiv.querySelector(".fyi-btn");
|
||||||
});
|
});
|
||||||
|
|
||||||
function hover() { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); }
|
function hover() { card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true })); }
|
||||||
function openFYI() { hover(); infoBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })); }
|
function openFYI() { hover(); infoBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })); }
|
||||||
|
|
||||||
it("FYI click adds .sig-info-open to the stage", () => {
|
it("FYI click adds .fyi-open to the stat block", () => {
|
||||||
openFYI();
|
openFYI();
|
||||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(true);
|
expect(testDiv.querySelector(".sig-stat-block").classList.contains("fyi-open")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("FYI click when btn-disabled does not toggle", () => {
|
it("FYI click when btn-disabled does not toggle", () => {
|
||||||
openFYI();
|
openFYI();
|
||||||
expect(infoBtn.classList.contains("btn-disabled")).toBe(true);
|
expect(infoBtn.classList.contains("btn-disabled")).toBe(true);
|
||||||
infoBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
infoBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(true);
|
expect(testDiv.querySelector(".sig-stat-block").classList.contains("fyi-open")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows placeholder when both energies and operations are empty", () => {
|
it("shows placeholder when both energies and operations are empty", () => {
|
||||||
@@ -370,9 +370,9 @@ describe("SigSelect", () => {
|
|||||||
|
|
||||||
it("card mouseleave closes the info panel", () => {
|
it("card mouseleave closes the info panel", () => {
|
||||||
openFYI();
|
openFYI();
|
||||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(true);
|
expect(testDiv.querySelector(".sig-stat-block").classList.contains("fyi-open")).toBe(true);
|
||||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(false);
|
expect(testDiv.querySelector(".sig-stat-block").classList.contains("fyi-open")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opening again resets to first entry", () => {
|
it("opening again resets to first entry", () => {
|
||||||
@@ -389,7 +389,7 @@ describe("SigSelect", () => {
|
|||||||
|
|
||||||
it("opening info panel adds .btn-disabled and swaps SPIN/FYI labels to ×", () => {
|
it("opening info panel adds .btn-disabled and swaps SPIN/FYI labels to ×", () => {
|
||||||
openFYI();
|
openFYI();
|
||||||
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
var flipBtn = testDiv.querySelector(".spin-btn");
|
||||||
expect(flipBtn.classList.contains("btn-disabled")).toBe(true);
|
expect(flipBtn.classList.contains("btn-disabled")).toBe(true);
|
||||||
expect(infoBtn.classList.contains("btn-disabled")).toBe(true);
|
expect(infoBtn.classList.contains("btn-disabled")).toBe(true);
|
||||||
expect(flipBtn.textContent).toBe("×");
|
expect(flipBtn.textContent).toBe("×");
|
||||||
@@ -397,7 +397,7 @@ describe("SigSelect", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("closing info panel removes .btn-disabled and restores SPIN/FYI labels", () => {
|
it("closing info panel removes .btn-disabled and restores SPIN/FYI labels", () => {
|
||||||
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
var flipBtn = testDiv.querySelector(".spin-btn");
|
||||||
var origFlip = flipBtn.textContent;
|
var origFlip = flipBtn.textContent;
|
||||||
var origInfo = infoBtn.textContent;
|
var origInfo = infoBtn.textContent;
|
||||||
openFYI();
|
openFYI();
|
||||||
@@ -411,14 +411,14 @@ describe("SigSelect", () => {
|
|||||||
it("clicking the info panel closes it", () => {
|
it("clicking the info panel closes it", () => {
|
||||||
openFYI();
|
openFYI();
|
||||||
infoEffect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
infoEffect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(false);
|
expect(testDiv.querySelector(".sig-stat-block").classList.contains("fyi-open")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("SPIN click when info open (btn-disabled) does nothing", () => {
|
it("SPIN click when info open (btn-disabled) does nothing", () => {
|
||||||
openFYI();
|
openFYI();
|
||||||
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
var flipBtn = testDiv.querySelector(".spin-btn");
|
||||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-info-open")).toBe(true);
|
expect(testDiv.querySelector(".sig-stat-block").classList.contains("fyi-open")).toBe(true);
|
||||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -447,14 +447,14 @@ describe("SigSelect", () => {
|
|||||||
|
|
||||||
it("SPIN click adds .is-reversed to the stat block", () => {
|
it("SPIN click adds .is-reversed to the stat block", () => {
|
||||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
var flipBtn = statBlock.querySelector(".spin-btn");
|
||||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("second SPIN click removes .is-reversed", () => {
|
it("second SPIN click removes .is-reversed", () => {
|
||||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
var flipBtn = statBlock.querySelector(".spin-btn");
|
||||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
@@ -462,7 +462,7 @@ describe("SigSelect", () => {
|
|||||||
|
|
||||||
it("hovering a new card resets .is-reversed", () => {
|
it("hovering a new card resets .is-reversed", () => {
|
||||||
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
statBlock.querySelector(".sig-flip-btn").dispatchEvent(
|
statBlock.querySelector(".spin-btn").dispatchEvent(
|
||||||
new MouseEvent("click", { bubbles: true })
|
new MouseEvent("click", { bubbles: true })
|
||||||
);
|
);
|
||||||
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
@@ -491,7 +491,7 @@ describe("SigSelect", () => {
|
|||||||
it("SPIN click adds .stage-card--reversed to the stage card", () => {
|
it("SPIN click adds .stage-card--reversed to the stage card", () => {
|
||||||
makeFixture();
|
makeFixture();
|
||||||
hover();
|
hover();
|
||||||
statBlock.querySelector(".sig-flip-btn")
|
statBlock.querySelector(".spin-btn")
|
||||||
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -499,7 +499,7 @@ describe("SigSelect", () => {
|
|||||||
it("second SPIN click removes .stage-card--reversed", () => {
|
it("second SPIN click removes .stage-card--reversed", () => {
|
||||||
makeFixture();
|
makeFixture();
|
||||||
hover();
|
hover();
|
||||||
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
var flipBtn = statBlock.querySelector(".spin-btn");
|
||||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
||||||
@@ -508,7 +508,7 @@ describe("SigSelect", () => {
|
|||||||
it("hovering a new card resets .stage-card--reversed", () => {
|
it("hovering a new card resets .stage-card--reversed", () => {
|
||||||
makeFixture();
|
makeFixture();
|
||||||
hover();
|
hover();
|
||||||
statBlock.querySelector(".sig-flip-btn")
|
statBlock.querySelector(".spin-btn")
|
||||||
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
expect(stageCard.classList.contains("stage-card--reversed")).toBe(true);
|
||||||
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
@@ -516,9 +516,9 @@ describe("SigSelect", () => {
|
|||||||
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
expect(stageCard.classList.contains("stage-card--reversed")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("non-major with data-reversal: reversal-qualifier = suit word, reversal-name = card name", () => {
|
it("non-major with data-reversal-qualifier: reversal-qualifier = suit word, reversal-name = card name", () => {
|
||||||
makeFixture();
|
makeFixture();
|
||||||
card.dataset.reversal = "Nervous";
|
card.dataset.reversalQualifier = "Nervous";
|
||||||
hover();
|
hover();
|
||||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Nervous");
|
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Nervous");
|
||||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
|
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
|
||||||
@@ -539,9 +539,9 @@ describe("SigSelect", () => {
|
|||||||
.toBe("Graven");
|
.toBe("Graven");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("non-major with data-reversal: suit qualifier on own line, upright name repeated below", () => {
|
it("non-major with data-reversal-qualifier: suit qualifier on own line, upright name repeated below", () => {
|
||||||
makeFixture({ polarity: "levity", userRole: "PC" });
|
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||||
card.dataset.reversal = "Vacant";
|
card.dataset.reversalQualifier = "Vacant";
|
||||||
hover();
|
hover();
|
||||||
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Vacant");
|
expect(stageCard.querySelector(".fan-card-reversal-qualifier").textContent).toBe("Vacant");
|
||||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
|
expect(stageCard.querySelector(".fan-card-reversal-name").textContent)
|
||||||
@@ -558,7 +558,7 @@ describe("SigSelect", () => {
|
|||||||
expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("Elevated");
|
expect(stageCard.querySelector(".fan-card-reversal-name").textContent).toBe("Elevated");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("non-major without data-reversal: qualifier mirrors polarity, name repeats card title", () => {
|
it("non-major without data-reversal-qualifier: qualifier mirrors polarity, name repeats card title", () => {
|
||||||
makeFixture({ polarity: "levity", userRole: "PC" });
|
makeFixture({ polarity: "levity", userRole: "PC" });
|
||||||
// fixture default: Minor Arcana, no reversal word
|
// fixture default: Minor Arcana, no reversal word
|
||||||
hover();
|
hover();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<script src="TraySpec.js"></script>
|
<script src="TraySpec.js"></script>
|
||||||
<script src="SigSelectSpec.js"></script>
|
<script src="SigSelectSpec.js"></script>
|
||||||
<script src="SeaDealSpec.js"></script>
|
<script src="SeaDealSpec.js"></script>
|
||||||
|
<script src="FanStageSpec.js"></script>
|
||||||
<script src="NatusWheelSpec.js"></script>
|
<script src="NatusWheelSpec.js"></script>
|
||||||
<script src="NoteSpec.js"></script>
|
<script src="NoteSpec.js"></script>
|
||||||
<script src="NotePageSpec.js"></script>
|
<script src="NotePageSpec.js"></script>
|
||||||
@@ -30,10 +31,12 @@
|
|||||||
<script src="/static/apps/dashboard/dashboard.js"></script>
|
<script src="/static/apps/dashboard/dashboard.js"></script>
|
||||||
<script src="/static/apps/dashboard/note.js"></script>
|
<script src="/static/apps/dashboard/note.js"></script>
|
||||||
<script src="/static/apps/billboard/note-page.js"></script>
|
<script src="/static/apps/billboard/note-page.js"></script>
|
||||||
|
<script src="/static/apps/epic/stage-card.js"></script>
|
||||||
<script src="/static/apps/epic/role-select.js"></script>
|
<script src="/static/apps/epic/role-select.js"></script>
|
||||||
<script src="/static/apps/epic/tray.js"></script>
|
<script src="/static/apps/epic/tray.js"></script>
|
||||||
<script src="/static/apps/epic/sig-select.js"></script>
|
<script src="/static/apps/epic/sig-select.js"></script>
|
||||||
<script src="/static/apps/epic/sea.js"></script>
|
<script src="/static/apps/epic/sea.js"></script>
|
||||||
|
<script src="/static/apps/gameboard/game-kit.js"></script>
|
||||||
<script src="/static/apps/gameboard/d3.min.js"></script>
|
<script src="/static/apps/gameboard/d3.min.js"></script>
|
||||||
<script src="/static/apps/gameboard/natus-wheel.js"></script>
|
<script src="/static/apps/gameboard/natus-wheel.js"></script>
|
||||||
<!-- Jasmine env config (optional) -->
|
<!-- Jasmine env config (optional) -->
|
||||||
|
|||||||
@@ -62,16 +62,36 @@
|
|||||||
|
|
||||||
<div class="sea-form-main">
|
<div class="sea-form-main">
|
||||||
<div class="sea-field">
|
<div class="sea-field">
|
||||||
<label for="id_sea_spread">Spread</label>
|
<label for="id_sea_spread" id="id_sea_spread_label">Spread</label>
|
||||||
<select id="id_sea_spread" name="spread" class="sea-select">
|
{% comment %}
|
||||||
{% if user_polarity == "levity" %}
|
Custom combobox — native <select> dropdowns ignore most CSS on
|
||||||
<option value="waite-smith" selected>Celtic Cross, Waite-Smith</option>
|
Firefox/Chrome (OS-rendered list); this gives full styling control.
|
||||||
<option value="escape-velocity">Celtic Cross, Escape Velocity</option>
|
combobox.js wires up the keyboard nav, click-outside-to-close, and
|
||||||
{% else %}
|
writes the chosen value to the hidden <input id="id_sea_spread"> so
|
||||||
<option value="escape-velocity" selected>Celtic Cross, Escape Velocity</option>
|
sea.js's existing `spreadSel.value` read still works.
|
||||||
<option value="waite-smith">Celtic Cross, Waite-Smith</option>
|
{% endcomment %}
|
||||||
{% endif %}
|
<input type="hidden" id="id_sea_spread" name="spread"
|
||||||
</select>
|
value="{% if user_polarity == 'levity' %}waite-smith{% else %}escape-velocity{% endif %}">
|
||||||
|
<div class="sea-select"
|
||||||
|
data-combobox
|
||||||
|
data-combobox-target="id_sea_spread"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-labelledby="id_sea_spread_label"
|
||||||
|
tabindex="0">
|
||||||
|
<span class="sea-select-current">{% if user_polarity == "levity" %}Celtic Cross, Waite-Smith{% else %}Celtic Cross, Escape Velocity{% endif %}</span>
|
||||||
|
<span class="sea-select-arrow" aria-hidden="true">▾</span>
|
||||||
|
<ul class="sea-select-list" role="listbox">
|
||||||
|
{% if user_polarity == "levity" %}
|
||||||
|
<li role="option" data-value="waite-smith" aria-selected="true">Celtic Cross, Waite-Smith</li>
|
||||||
|
<li role="option" data-value="escape-velocity" aria-selected="false">Celtic Cross, Escape Velocity</li>
|
||||||
|
{% else %}
|
||||||
|
<li role="option" data-value="escape-velocity" aria-selected="true">Celtic Cross, Escape Velocity</li>
|
||||||
|
<li role="option" data-value="waite-smith" aria-selected="false">Celtic Cross, Waite-Smith</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Two face-down deck piles — tap to proffer OK #}
|
{# Two face-down deck piles — tap to proffer OK #}
|
||||||
@@ -114,8 +134,8 @@
|
|||||||
{# ── Sea stage — big card viewer ─────────────────────────────────────────── #}
|
{# ── Sea stage — big card viewer ─────────────────────────────────────────── #}
|
||||||
<div class="sea-stage" id="id_sea_stage" style="display:none">
|
<div class="sea-stage" id="id_sea_stage" style="display:none">
|
||||||
<div class="sea-stage-backdrop"></div>
|
<div class="sea-stage-backdrop"></div>
|
||||||
<div class="sea-stage-content">
|
<div class="sea-stage-content" style="--sig-card-w:180px">
|
||||||
<div class="sig-stage-card sea-stage-card" style="--sig-card-w:140px">
|
<div class="sig-stage-card sea-stage-card">
|
||||||
<div class="fan-card-corner fan-card-corner--tl">
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
<span class="fan-corner-rank"></span>
|
<span class="fan-corner-rank"></span>
|
||||||
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
||||||
@@ -139,8 +159,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sig-stat-block sea-stat-block">
|
<div class="sig-stat-block sea-stat-block">
|
||||||
<button class="btn btn-reverse sea-spin-btn" type="button">SPIN</button>
|
<button class="btn btn-reverse spin-btn" type="button">SPIN</button>
|
||||||
<button class="btn btn-info sea-fyi-btn" type="button">FYI</button>
|
<button class="btn btn-info fyi-btn" type="button">FYI</button>
|
||||||
<div class="stat-face stat-face--upright">
|
<div class="stat-face stat-face--upright">
|
||||||
<p class="stat-face-label">Emanation</p>
|
<p class="stat-face-label">Emanation</p>
|
||||||
<ul class="stat-keywords" id="id_sea_stat_upright"></ul>
|
<ul class="stat-keywords" id="id_sea_stat_upright"></ul>
|
||||||
@@ -157,8 +177,8 @@
|
|||||||
<p class="sig-info-effect"></p>
|
<p class="sig-info-effect"></p>
|
||||||
<span class="sig-info-index"></span>
|
<span class="sig-info-index"></span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-nav-left sea-fyi-prev" type="button">PRV</button>
|
<button class="btn btn-nav-left fyi-prev" type="button">PRV</button>
|
||||||
<button class="btn btn-nav-right sea-fyi-next" type="button">NXT</button>
|
<button class="btn btn-nav-right fyi-next" type="button">NXT</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sig-stat-block">
|
<div class="sig-stat-block">
|
||||||
<button class="btn btn-reverse sig-flip-btn" type="button">SPIN</button>
|
<button class="btn btn-reverse spin-btn" type="button">SPIN</button>
|
||||||
<button class="btn btn-info sig-info-btn" type="button">FYI</button>
|
<button class="btn btn-info fyi-btn" type="button">FYI</button>
|
||||||
<div class="stat-face stat-face--upright">
|
<div class="stat-face stat-face--upright">
|
||||||
<p class="stat-face-label">Emanation</p>
|
<p class="stat-face-label">Emanation</p>
|
||||||
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
|
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
|
||||||
@@ -60,8 +60,8 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
|
|||||||
<p class="sig-info-effect"></p>
|
<p class="sig-info-effect"></p>
|
||||||
<span class="sig-info-index"></span>
|
<span class="sig-info-index"></span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-nav-left sig-info-prev" type="button">PRV</button>
|
<button class="btn btn-nav-left fyi-prev" type="button">PRV</button>
|
||||||
<button class="btn btn-nav-right sig-info-next" type="button">NXT</button>
|
<button class="btn btn-nav-right fyi-next" type="button">NXT</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
|
|||||||
data-operations="{{ card.operations_json }}"
|
data-operations="{{ card.operations_json }}"
|
||||||
data-levity-qualifier="{{ card.levity_qualifier }}"
|
data-levity-qualifier="{{ card.levity_qualifier }}"
|
||||||
data-gravity-qualifier="{{ card.gravity_qualifier }}"
|
data-gravity-qualifier="{{ card.gravity_qualifier }}"
|
||||||
data-reversal="{{ card.reversal }}">
|
data-reversal-qualifier="{{ card.reversal_qualifier }}">
|
||||||
<div class="fan-card-corner fan-card-corner--tl">
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||||
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
||||||
|
|||||||
@@ -1,16 +1,44 @@
|
|||||||
{% for card in cards %}
|
{% for card in cards %}
|
||||||
<div class="fan-card" data-index="{{ forloop.counter0 }}">
|
<div class="fan-card"
|
||||||
|
data-index="{{ forloop.counter0 }}"
|
||||||
|
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 }}">
|
||||||
<div class="fan-card-corner fan-card-corner--tl">
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||||
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="fan-card-face">
|
<div class="fan-card-face">
|
||||||
{% if card.name_group %}<p class="fan-card-name-group">{{ card.name_group }}</p>{% endif %}
|
<div class="fan-card-face-upright">
|
||||||
<h3 class="fan-card-name">{{ card.name_title }}</h3>
|
{% if card.name_group %}<p class="fan-card-name-group">{{ card.name_group }}</p>{% endif %}
|
||||||
|
{% if card.arcana != "MAJOR" and card.levity_qualifier %}
|
||||||
|
<p class="sig-qualifier-above">{{ card.levity_qualifier }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<h3 class="fan-card-name">{{ card.name_title }}{% if card.arcana == "MAJOR" and card.levity_qualifier %},{% endif %}</h3>
|
||||||
|
{% if card.arcana == "MAJOR" and card.levity_qualifier %}
|
||||||
|
<p class="sig-qualifier-below">{{ card.levity_qualifier }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<p class="fan-card-arcana">{{ card.get_arcana_display }}</p>
|
<p class="fan-card-arcana">{{ card.get_arcana_display }}</p>
|
||||||
{% if card.correspondence %}
|
<div class="fan-card-face-reversal">
|
||||||
<p class="fan-card-correspondence">{{ card.correspondence }}</p>
|
{% if card.arcana == "MAJOR" %}
|
||||||
{% endif %}
|
<p class="fan-card-reversal-name">{{ card.levity_qualifier|default:card.gravity_qualifier }}</p>
|
||||||
|
<p class="fan-card-reversal-qualifier">{{ card.name_title }}{% if card.levity_qualifier %},{% endif %}</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="fan-card-reversal-name">{{ card.name_title }}</p>
|
||||||
|
<p class="fan-card-reversal-qualifier">{{ card.reversal_qualifier|default:card.gravity_qualifier }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fan-card-corner fan-card-corner--br">
|
<div class="fan-card-corner fan-card-corner--br">
|
||||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||||
|
|||||||
@@ -17,11 +17,35 @@
|
|||||||
<div class="tarot-fan-wrap">
|
<div class="tarot-fan-wrap">
|
||||||
<button id="id_fan_prev" class="fan-nav fan-nav--prev" aria-label="Previous card">‹</button>
|
<button id="id_fan_prev" class="fan-nav fan-nav--prev" aria-label="Previous card">‹</button>
|
||||||
<div id="id_fan_content" class="tarot-fan"></div>
|
<div id="id_fan_content" class="tarot-fan"></div>
|
||||||
|
<button id="id_fan_flip" class="btn btn-reveal fan-flip-btn" type="button">FLIP</button>
|
||||||
|
<div class="fan-stage-block sig-stat-block" id="id_fan_stage_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" id="id_fan_stat_upright"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="stat-face stat-face--reversed">
|
||||||
|
<p class="stat-face-label">Reversal</p>
|
||||||
|
<ul class="stat-keywords" id="id_fan_stat_reversed"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="sig-info" id="id_fan_fyi_panel" style="display:none">
|
||||||
|
<div class="sig-info-header">
|
||||||
|
<h4 class="sig-info-title"></h4>
|
||||||
|
<p class="sig-info-type"></p>
|
||||||
|
</div>
|
||||||
|
<p class="sig-info-effect"></p>
|
||||||
|
<span class="sig-info-index"></span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-nav-left fyi-prev" type="button">PRV</button>
|
||||||
|
<button class="btn btn-nav-right fyi-next" type="button">NXT</button>
|
||||||
|
</div>
|
||||||
<button id="id_fan_next" class="fan-nav fan-nav--next" aria-label="Next card">›</button>
|
<button id="id_fan_next" class="fan-nav fan-nav--next" aria-label="Next card">›</button>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
<script src="{% static 'apps/epic/stage-card.js' %}"></script>
|
||||||
<script src="{% static 'apps/gameboard/game-kit.js' %}"></script>
|
<script src="{% static 'apps/gameboard/game-kit.js' %}"></script>
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
|
|||||||
@@ -112,6 +112,8 @@
|
|||||||
<script src="{% static 'apps/epic/room.js' %}"></script>
|
<script src="{% static 'apps/epic/room.js' %}"></script>
|
||||||
<script src="{% static 'apps/epic/gatekeeper.js' %}"></script>
|
<script src="{% static 'apps/epic/gatekeeper.js' %}"></script>
|
||||||
<script src="{% static 'apps/epic/role-select.js' %}"></script>
|
<script src="{% static 'apps/epic/role-select.js' %}"></script>
|
||||||
|
<script src="{% static 'apps/epic/stage-card.js' %}"></script>
|
||||||
|
<script src="{% static 'apps/epic/combobox.js' %}"></script>
|
||||||
<script src="{% static 'apps/epic/sig-select.js' %}"></script>
|
<script src="{% static 'apps/epic/sig-select.js' %}"></script>
|
||||||
<script src="{% static 'apps/epic/sea.js' %}"></script>
|
<script src="{% static 'apps/epic/sea.js' %}"></script>
|
||||||
<script src="{% static 'apps/epic/tray.js' %}"></script>
|
<script src="{% static 'apps/epic/tray.js' %}"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user