A.7.5 Game Kit carousel image-mode + universal stat-block top-left chip + EMANATION/REVERSAL --secUser convention — TDD. Mid-session 2026-05-25 PM (Sprint A.7.5 of [[project-image-based-deck-face-rendering]] — slotted between A.7 polish + tomorrow's A.8 room.html). Three threads bundled: (1) Game Kit _tarot_fan.html carousel modal gets the image-mode branch + per-card FLIP-to-back for non-polarized image-equipped decks (Minchiate today; brings the carousel into parity w. the other 5 image-mode surfaces shipped in A.3-A.7); (2) the A.3 Q3-spec top-left rank+suit chip lands across all 4 stat-block surfaces (my_sign main / _applet-my-sign / _sea_stage modal / new game_kit fan stage), retrofitting work that A.3 explicitly deferred per the "Lower-priority follow-ups" list in the project memory; (3) chip + EMANATION/REVERSAL label adopt --secUser as the new universal color convention so the title (--quaUser/--terUser per arcana) stays the focal text + the chip-and-label header recedes visually.
(1) _tarot_fan.html image-mode branch — server-side `{% if card.deck_variant.has_card_images %}` gate: image-mode renders `<img class="sig-stage-card-img">` + (for non-polarized decks) a sibling `<img class="sig-stage-card-back-img">` for the FLIP-to-back affordance; text-mode keeps the existing `.fan-card-corner --tl/--br` + `.fan-card-face` scaffold unchanged (Earthman + RWS today; will be removed once both decks get artwork — user's plan: scrape RWS art tonight + Earthman public-domain paintings to follow; "shabby cardstock" non-equippable Earthman variant retains text rendering as legacy preservation). New `.fan-card.fan-card--image` marker class added to the shared image-mode comma-list selector (`_card-deck.scss:705-765`) so the carousel cards pick up the contour-stroke + depth-shadow filter chain + `.is-flipped-to-back` toggle for free — single SCSS source of truth across all 5 image-mode surfaces. Also added `data-arcana-key="{{ card.arcana }}"` + `data-image-url="{{ card.image_url|default:'' }}"` data-attrs to every fan-card so `StageCard.fromDataset` + `_setImageMode` flow w. no extra plumbing.
(2) Game Kit carousel JS rewiring (`game-kit.js`): `_populateStage` now also calls `StageCard.populateStatExtras(stageBlock, card)` so the carousel stat block gets title + arcana + chip populated on every card focus (previously the stage block had only the keyword list; the call site simply wasn't wired). SPIN handler gates the 180° card rotation behind `!active.classList.contains('fan-card--image')` — for image-mode cards SPIN now just toggles `.is-reversed` on the stat block to swap EMANATION ↔ REVERSAL content w/o rotating the artwork (user-spec 2026-05-25 PM: "monodecks shouldn't have gravity and levity polarity"; image artwork is symmetric + shouldn't be inverted by a UI cycle). New `_flipToBack` helper mirrors the my_sign.html A.5-polish-2 FLIP-to-back animation (rotateY 0→90→0 over 500ms, `.is-flipped-to-back` toggle at 250ms midpoint, `data-flipping` cleared at 500ms); the existing `_flipActive` dispatches to it via `active.querySelector('.sig-stage-card-back-img')` presence check (the back-img element is only server-rendered for non-polarized image-equipped decks, so its presence is the gate). Polarized text-mode (Earthman) keeps the existing polarity-cycle FLIP. Per-card-change cleanup also clears `.is-flipped-to-back` on every card so a back-flipped card returns to front when it leaves focus (mirrors the SPIN reset semantics).
(3) Top-left rank+suit chip retrofit (4 stat-block surfaces): the A.3 Q3 spec called for a chip but explicitly deferred to "Lower-priority follow-ups" in the project memory; user pulled it in this sprint as part of the carousel rewrite. New `.stat-face-header` flex wrapper holds the chip + EMANATION/REVERSAL label inline (chip is 2 rows tall, label is 1 — flex `align-items: flex-start` keeps them "vaguely inline" per spec). Chip mirrors the existing `.fan-card-corner` pattern: vertically stacked rank + suit-icon, no chrome (initial draft had a bordered pill — corrected per user clarification 2026-05-25 PM "vertically stacked, --secUser, in the top-left corner"). All 4 stat-block templates (my_sign.html / _applet-my-sign.html / _sea_stage.html / game_kit.html's `#id_fan_stage_block`) get the new header wrapper around their existing `.stat-face-label`. Applet renders the chip server-side from `card.corner_rank` + `card.suit_icon`; the other 3 surfaces leave the chip elements empty + populated by `StageCard.populateStatExtras` on each card focus (the helper now also walks `.stat-chip-rank` + `.stat-chip-icon` w. the same find-all + textContent / className pattern it already uses for title + arcana). Chip color is --secUser by default; polarity-aware overrides for surfaces whose gravity bg flips to --secUser (sig-stat-block / sea-stat-block / fan-stage-block) flip the chip to --priUser for visibility — same logical inversion the keyword list rules already use.
(4) Trump fa-hand-dots fallback in `TarotCard.suit_icon` — was reading the per-card `icon` field then returning `''` for any major arcana w/o an explicit override. Earthman's seed migration 0007 set `icon="fa-hand-dots"` on trumps 2+ as the universal trump symbol, but trumps 0/1 + every Minchiate trump fell through to empty + rendered the chip as just a number/numeral w. no icon below. Promoted the fallback into the model property (per-card override still wins via the `self.icon` branch), so every trump everywhere — chip, text-mode corner, future surfaces — gets a hand-with-dots glyph for free. Updated `TarotCardSuitIconTest.test_major_without_icon_returns_empty` → `test_major_without_icon_defaults_to_hand_dots`.
(5) EMANATION/REVERSAL → --secUser (user-spec 2026-05-25 PM, mid-sprint): label color was --terUser (gold) across all 4 surfaces; flipped to --secUser everywhere so the label recedes against the title (gold/--quaUser per arcana stays the focal text). Default in the shared `stat-block-shared` mixin + applet bespoke `.stat-face-label` rule both updated. Per-polarity overrides: levity (bg --priUser) → label --secUser everywhere; gravity overrides preserved at --quiUser on the 3 surfaces whose gravity bg flips to --secUser (sig-stat-block / sea-stat-block / fan-stage-block — --secUser label would be invisible against --secUser bg, so --quiUser stays for contrast); applet gravity bg is --priUser (just full alpha vs. the default 0.8 — different from the other surfaces) so its gravity override removed entirely, label uses the shared --secUser default in both polarities. User-confirmed visually 2026-05-25 PM: applet EMANATION now in --secUser (`rgb(162, 170, 173)`) matching the chip color — chip + label read as a coordinated header pair rather than competing w. the title.
Tests: 1314/1314 IT+UT total green (76s; +8 new in this sprint — 4 chip-presence ITs across the 4 stat-block surfaces, 3 _tarot_fan image-mode-branch ITs covering image-equipped + text-mode + polarized-image-equipped permutations, 1 UT-rename for the trump fa-hand-dots default). Surfaces NOT covered by ITs: SCSS layout (visual-only — verified live via Claudezilla on /gameboard/game-kit/ Minchiate carousel, /billboard/my-sign/ stage card, /billboard/ applet preview); JS-side chip-fill via populateStatExtras (covered transitively by the populateStatExtras existing call sites — no new test for the chip-specific code path since the test surface for stage-card.js is currently Jasmine-only via FanStageSpec.js, deferred). No new FT runs per [[feedback-ft-run-discipline]] — all changes are template / SCSS / JS / model property; IT coverage is comprehensive for the server-rendered surfaces + the visual verify covered the JS-populated surfaces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,14 +45,30 @@
|
||||
<div class="sig-stat-block sea-stat-block">
|
||||
<button class="btn btn-reverse spin-btn" type="button">SPIN</button>
|
||||
<button class="btn btn-info fyi-btn" type="button">FYI</button>
|
||||
{# Sprint A.7.5 — `.stat-face-header` wraps the rank+suit chip #}
|
||||
{# inline w. EMANATION/REVERSAL per [[project-image-based-deck- #}
|
||||
{# face-rendering]]'s A.3 Q3 spec. Chip empty by default; stage- #}
|
||||
{# card.js populateStatExtras fills both faces' chips identically.#}
|
||||
<div class="stat-face stat-face--upright">
|
||||
<p class="stat-face-label">Emanation</p>
|
||||
<div class="stat-face-header">
|
||||
<span class="stat-face-chip">
|
||||
<span class="stat-chip-rank"></span>
|
||||
<i class="fa-solid stat-chip-icon" style="display:none"></i>
|
||||
</span>
|
||||
<p class="stat-face-label">Emanation</p>
|
||||
</div>
|
||||
<p class="stat-face-title"></p>
|
||||
<p class="stat-face-arcana"></p>
|
||||
<ul class="stat-keywords" id="id_sea_stat_upright"></ul>
|
||||
</div>
|
||||
<div class="stat-face stat-face--reversed">
|
||||
<p class="stat-face-label">Reversal</p>
|
||||
<div class="stat-face-header">
|
||||
<span class="stat-face-chip">
|
||||
<span class="stat-chip-rank"></span>
|
||||
<i class="fa-solid stat-chip-icon" style="display:none"></i>
|
||||
</span>
|
||||
<p class="stat-face-label">Reversal</p>
|
||||
</div>
|
||||
<p class="stat-face-title"></p>
|
||||
<p class="stat-face-arcana"></p>
|
||||
<ul class="stat-keywords" id="id_sea_stat_reversed"></ul>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{% load tarot_filters %}
|
||||
{% for card in cards %}
|
||||
<div class="fan-card"
|
||||
<div class="fan-card{% if card.deck_variant.has_card_images %} fan-card--image{% endif %}"
|
||||
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-arcana-key="{{ card.arcana }}"
|
||||
data-correspondence="{{ card.correspondence|default:'' }}"
|
||||
data-keywords-upright="{{ card.keywords_upright|join:',' }}"
|
||||
data-keywords-reversed="{{ card.keywords_reversed|join:',' }}"
|
||||
@@ -20,53 +21,74 @@
|
||||
data-gravity-emanation="{{ card.gravity_emanation }}"
|
||||
data-levity-reversal="{{ card.levity_reversal }}"
|
||||
data-gravity-reversal="{{ card.gravity_reversal }}"
|
||||
data-italic-word="{{ card.italic_word }}">
|
||||
<div class="fan-card-corner fan-card-corner--tl">
|
||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
||||
</div>
|
||||
<div class="fan-card-face">
|
||||
<div class="fan-card-face-upright">
|
||||
{% if card.gravity_emanation %}
|
||||
{# Polarity-split title (cards 48-49 + trumps 19-21); no qualifier slots — qualifier is baked into the title between "The" and the proper noun #}
|
||||
<p class="fan-card-name {{ card.title_squeeze_class }}">{{ card.gravity_emanation|italicize:card.italic_word }}</p>
|
||||
{% else %}
|
||||
{% if card.name_group %}<p class="fan-card-name-group">{{ card.name_group }}</p>{% endif %}
|
||||
{% if card.arcana != "MAJOR" and card.gravity_qualifier %}
|
||||
<p class="sig-qualifier-above">{{ card.gravity_qualifier }}</p>
|
||||
{% endif %}
|
||||
<p class="fan-card-name {{ card.title_squeeze_class }}">{{ card.name_title|italicize:card.italic_word }}{% if card.arcana == "MAJOR" and card.gravity_qualifier %},{% endif %}</p>
|
||||
{% if card.arcana == "MAJOR" and card.gravity_qualifier %}
|
||||
<p class="sig-qualifier-below">{{ card.gravity_qualifier }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
data-italic-word="{{ card.italic_word }}"
|
||||
data-image-url="{{ card.image_url|default:'' }}">
|
||||
{% if card.deck_variant.has_card_images %}
|
||||
{# Sprint A.7.5 — image-mode card face. The image IS the card; the #}
|
||||
{# adjacent stat block (in #id_fan_stage_block) is the sole home for #}
|
||||
{# textual metadata (chip, EMANATION/REVERSAL header, title, arcana, #}
|
||||
{# keywords). For non-polarized image-equipped decks the FLIP btn #}
|
||||
{# flips this card to its back-image (mirrors my_sign.html's A.5- #}
|
||||
{# polish-2 pattern). The back-img defaults to display:none via CSS; #}
|
||||
{# `.fan-card.is-flipped-to-back` toggles visibility. #}
|
||||
<img class="sig-stage-card-img" src="{{ card.image_url }}" alt="{{ card.name_title }}">
|
||||
{% if not card.deck_variant.is_polarized %}
|
||||
<img class="sig-stage-card-back-img" alt=""
|
||||
src="{{ card.deck_variant.back_image_url }}">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{# Text-mode (Earthman + RWS today): existing corner + face scaffold #}
|
||||
{# unchanged from pre-A.7.5. Will be removed once both decks have #}
|
||||
{# images (user's plan: scrape RWS art today; Earthman public-domain #}
|
||||
{# paintings to follow). "Shabby cardstock" non-equippable Earthman #}
|
||||
{# variant will retain this text rendering as a legacy preservation. #}
|
||||
<div class="fan-card-corner fan-card-corner--tl">
|
||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
||||
</div>
|
||||
<p class="fan-card-arcana">{{ card.get_arcana_display }}</p>
|
||||
<div class="fan-card-face-reversal">
|
||||
{% comment %}
|
||||
Class names always match semantic content: qualifier text in
|
||||
.fan-card-reversal-qualifier, title text in .fan-card-reversal-name.
|
||||
DOM order is per-arcana, controlling visual layout after the 180°
|
||||
SPIN rotation (DOM-second appears visually on top):
|
||||
Major / polarity-split — title on top → name class is DOM-second
|
||||
Non-major — qualifier on top → qualifier class is DOM-second
|
||||
{% endcomment %}
|
||||
{% if card.gravity_reversal %}
|
||||
{# Polarity-split: single-line title in the name slot, qualifier slot empty. #}
|
||||
<p class="fan-card-reversal-qualifier"></p>
|
||||
<p class="fan-card-reversal-name {{ card.title_squeeze_class }}">{{ card.gravity_reversal|italicize:card.italic_word }}</p>
|
||||
{% elif card.arcana == "MAJOR" %}
|
||||
<p class="fan-card-reversal-qualifier">{{ card.gravity_qualifier|default:card.levity_qualifier }}</p>
|
||||
<p class="fan-card-reversal-name {{ card.title_squeeze_class }}">{{ card.name_title|italicize:card.italic_word }}{% if card.gravity_qualifier %},{% endif %}</p>
|
||||
{% else %}
|
||||
<p class="fan-card-reversal-name {{ card.title_squeeze_class }}">{{ card.name_title|italicize:card.italic_word }}</p>
|
||||
<p class="fan-card-reversal-qualifier">{{ card.reversal_qualifier|default:card.gravity_qualifier }}</p>
|
||||
{% endif %}
|
||||
<div class="fan-card-face">
|
||||
<div class="fan-card-face-upright">
|
||||
{% if card.gravity_emanation %}
|
||||
{# Polarity-split title (cards 48-49 + trumps 19-21); no qualifier slots — qualifier is baked into the title between "The" and the proper noun #}
|
||||
<p class="fan-card-name {{ card.title_squeeze_class }}">{{ card.gravity_emanation|italicize:card.italic_word }}</p>
|
||||
{% else %}
|
||||
{% if card.name_group %}<p class="fan-card-name-group">{{ card.name_group }}</p>{% endif %}
|
||||
{% if card.arcana != "MAJOR" and card.gravity_qualifier %}
|
||||
<p class="sig-qualifier-above">{{ card.gravity_qualifier }}</p>
|
||||
{% endif %}
|
||||
<p class="fan-card-name {{ card.title_squeeze_class }}">{{ card.name_title|italicize:card.italic_word }}{% if card.arcana == "MAJOR" and card.gravity_qualifier %},{% endif %}</p>
|
||||
{% if card.arcana == "MAJOR" and card.gravity_qualifier %}
|
||||
<p class="sig-qualifier-below">{{ card.gravity_qualifier }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="fan-card-arcana">{{ card.get_arcana_display }}</p>
|
||||
<div class="fan-card-face-reversal">
|
||||
{% comment %}
|
||||
Class names always match semantic content: qualifier text in
|
||||
.fan-card-reversal-qualifier, title text in .fan-card-reversal-name.
|
||||
DOM order is per-arcana, controlling visual layout after the 180°
|
||||
SPIN rotation (DOM-second appears visually on top):
|
||||
Major / polarity-split — title on top → name class is DOM-second
|
||||
Non-major — qualifier on top → qualifier class is DOM-second
|
||||
{% endcomment %}
|
||||
{% if card.gravity_reversal %}
|
||||
{# Polarity-split: single-line title in the name slot, qualifier slot empty. #}
|
||||
<p class="fan-card-reversal-qualifier"></p>
|
||||
<p class="fan-card-reversal-name {{ card.title_squeeze_class }}">{{ card.gravity_reversal|italicize:card.italic_word }}</p>
|
||||
{% elif card.arcana == "MAJOR" %}
|
||||
<p class="fan-card-reversal-qualifier">{{ card.gravity_qualifier|default:card.levity_qualifier }}</p>
|
||||
<p class="fan-card-reversal-name {{ card.title_squeeze_class }}">{{ card.name_title|italicize:card.italic_word }}{% if card.gravity_qualifier %},{% endif %}</p>
|
||||
{% else %}
|
||||
<p class="fan-card-reversal-name {{ card.title_squeeze_class }}">{{ card.name_title|italicize:card.italic_word }}</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">
|
||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
||||
</div>
|
||||
<div class="fan-card-corner fan-card-corner--br">
|
||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user