A.4 card-deck stack icon + game_kit applet's Card Decks polarization (×2) tooltip decoration — TDD. Sprint A.4 of [[project-image-based-deck-face-rendering]] (folded down from the originally-standalone Sprint D per [[project-card-deck-icon]] 2026-05-25 PM scope-fold). Replaces the <i class="fa-regular fa-id-badge"> placeholder on .token.deck-variant in the gameboard's Game Kit applet w. a new inline SVG card-stack icon: 3 rect children (rx=2.5, 20×32 viewport units inside a 32×48 viewBox to land 5:8 tarot card aspect), stacked tightly at rest w. ±0.4px vertical micro-offsets (suggests stack depth without separating cards visually), whole stack rotated 5° clockwise via .deck-stack-icon__stack group transform. On :hover / :active / :focus of the parent .token.deck-variant, cards 2 + 3 fan out symmetrically — card 2 translates (-5px, -2px) + rotates -12°, card 3 translates (+5px, -2px) + rotates +12° — card 1 stays put on top. Fan-out CSS pseudo-classes match the existing JS-portal tooltip trigger so the splay animation + tooltip-appearance co-activate as user spec'd. Placeholder card-back design: solid --priUser fill + currentColor stroke (= --terUser); detailed Earthman planet-impact illustration deferred to a future art-asset commit (the SVG structure is ready to receive richer fills + pattern elements without re-jigging the stack/fan transforms). Drop-shadow for "lifted off the felt" depth cue: 0.08rem 0.08rem 0.15rem rgba(0, 0, 0, 0.6) — softer than the my-sign-stage card's tray-card-style 1,1 black silhouette since the icon is small + always on a felt background. SVG itself uses overflow: visible so the fan-out exceeds the viewBox bounds; transform-box: fill-box + transform-origin: 50% 50% ensure rotation centers on each card's own geometric center (not the viewBox center). New _deck_stack_icon.html partial in templates/apps/gameboard/_partials/ keeps the SVG markup DRY for the future room.html pile + deck-bag rollouts (per [[project-card-deck-icon]] "other surfaces deferred to later sprints"). New .tt-x2 style in %tt-token-fields placeholder mixin — --terUser color + font-weight 600 — appended inline in .tt-description for is_polarized=True decks (Earthman today): "106-card Tarot deck (×2)" where the (×2) signals "double-polarized = 6 segments = fills 2× as many seats" per [[project-card-deck-icon]]'s decoration rule. Non-polarized decks (Tarot RWS, future Minchiate) render the description without the suffix. 3 new ITs in GameboardViewTest: SVG card-stack renders w. 3 rect children + fa-id-badge gone; polarized Earthman tooltip carries .tt-x2 w. "×2" content; non-polarized RWS tooltip lacks .tt-x2. Out of scope this commit: the dedicated /game-kit/ page's .gk-deck-card rectangles (different template — _game_kit_sections.html) keep their fa-id-badge for now; folding them into the new icon happens in a follow-up "Card Decks rectangle teardown" sprint per user spec ("by the time we finish A.8 the dynamically shaped rectangles around the deck <i> els and their names will be no more"). Tests: 3 new ITs green; 27/27 GameboardViewTest class green; 1293/1293 IT+UT total green (68s; +3 from A.3-polish's 1290). Visual verify pending: browser refresh expected to show the stacked-3-card icon w. 5° rest tilt, fan-out on hover, tooltip + (×2) decoration on Earthman

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-25 00:40:10 -04:00
parent 436a710478
commit b9bb73db69
5 changed files with 123 additions and 3 deletions

View File

@@ -413,6 +413,52 @@ class GameboardViewTest(TestCase):
def test_game_kit_has_dice_set_placeholder(self): def test_game_kit_has_dice_set_placeholder(self):
[_] = self.parsed.cssselect("#id_game_kit #id_kit_dice_set") [_] = self.parsed.cssselect("#id_game_kit #id_kit_dice_set")
def test_deck_token_renders_card_stack_svg_not_fa_id_badge(self):
"""Sprint A.4 — `.token.deck-variant` icon is the inline SVG card-stack
(`.deck-stack-icon`), not the old `<i class="fa-regular fa-id-badge">`
placeholder. Stack contains 3 rect children w. `.deck-stack-icon__card`
classes the CSS keys off for the rest-stack + hover fan-out."""
deck = self.parsed.cssselect("#id_game_kit #id_kit_earthman_deck")[0]
# New SVG icon present
[svg] = deck.cssselect("svg.deck-stack-icon")
cards = svg.cssselect(".deck-stack-icon__card")
self.assertEqual(
len(cards), 3,
"Card-stack icon must render 3 rect cards (top + 2 fan-out)",
)
# Old FA icon removed
self.assertEqual(
len(deck.cssselect("i.fa-id-badge")), 0,
"fa-regular fa-id-badge must be gone from deck-variant token",
)
def test_polarized_deck_tooltip_has_x2_decoration(self):
"""Earthman is the only is_polarized=True deck today (per A.0 migration).
Its tooltip's card-count line should carry a `(×2)` suffix in --terUser
per [[project-card-deck-icon]]'s `is_polarized` tooltip-decoration rule."""
deck = self.parsed.cssselect("#id_game_kit #id_kit_earthman_deck")[0]
tt = deck.cssselect(".tt")[0]
[x2] = tt.cssselect(".tt-x2")
self.assertIn("×2", x2.text_content()) # ×2
def test_nonpolarized_deck_tooltip_lacks_x2_decoration(self):
"""Non-polarized decks (Tarot RWS, future Minchiate) don't get the
`(×2)` decoration — the suffix signals 'double-polarized = 6 segments
= fills 2× as many seats' which only applies to polarized decks."""
from apps.epic.models import DeckVariant
# Use the migration-renamed RWS deck (formerly fiorentine-minchiate).
rws = DeckVariant.objects.get(slug="tarot-rider-waite-smith")
self.user.unlocked_decks.add(rws)
response = self.client.get("/gameboard/")
import lxml.html
parsed = lxml.html.fromstring(response.content)
deck = parsed.cssselect("#id_game_kit #id_kit_tarot_deck")[0]
tt = deck.cssselect(".tt")[0]
self.assertEqual(
len(tt.cssselect(".tt-x2")), 0,
"Non-polarized RWS deck must not show (×2) in tooltip",
)
class GameboardDeckInUseTest(TestCase): class GameboardDeckInUseTest(TestCase):
"""Sprint 2: game kit applet renders in-use state for a deck assigned to an active seat.""" """Sprint 2: game kit applet renders in-use state for a deck assigned to an active seat."""

View File

@@ -79,6 +79,51 @@ body.page-gameboard {
.kit-item { font-size: 1.5rem; } .kit-item { font-size: 1.5rem; }
.kit-item { opacity: 0.6; } .kit-item { opacity: 0.6; }
// Sprint A.4 — card-deck stack icon (.deck-stack-icon) replaces the
// fa-regular fa-id-badge for .token.deck-variant. 3 stacked card-back
// rects, 5° CW rest tilt, fan-out on hover/active/focus of the parent
// .token (animation + tooltip portal trigger lockstep on the same
// pseudo-class set). See [[project-card-deck-icon]].
.deck-stack-icon {
display: inline-block;
width: 1.5rem; // match the prior fa-id-badge visual weight
height: 2.4rem; // 5:8 tarot card aspect
color: rgba(var(--terUser), 1); // stroke color via currentColor
overflow: visible; // fan-out exceeds the viewBox bounds
filter: drop-shadow(0.08rem 0.08rem 0.15rem rgba(0, 0, 0, 0.6));
.deck-stack-icon__stack {
transform: rotate(5deg);
transform-origin: 50% 50%;
transform-box: fill-box;
transition: transform 0.25s ease;
}
.deck-stack-icon__card {
fill: rgba(var(--priUser), 1);
stroke: currentColor;
stroke-width: 1;
transform-origin: 50% 50%;
transform-box: fill-box;
transition: transform 0.25s ease;
}
// Rest: tightly stacked w. tiny vertical offsets (suggests stack
// depth without separating the cards visually).
.deck-stack-icon__card--1 { transform: translateY(-0.4px); }
.deck-stack-icon__card--3 { transform: translateY( 0.4px); }
}
// Hover/active/focus on the parent .token fans cards 2 + 3 out from
// under card 1; card 1 stays put. Tooltip portal is wired to the
// same `.token:hover` trigger via JS so the splay + tooltip-appearance
// co-activate.
.token.deck-variant:hover .deck-stack-icon,
.token.deck-variant:active .deck-stack-icon,
.token.deck-variant:focus .deck-stack-icon {
.deck-stack-icon__card--2 { transform: translate(-5px, -2px) rotate(-12deg); }
.deck-stack-icon__card--3 { transform: translate( 5px, -2px) rotate( 12deg); }
}
} }
} }

View File

@@ -18,6 +18,12 @@
// regardless of whether justify-content cascade reaches this far // regardless of whether justify-content cascade reaches this far
// (belt + suspenders for the space-between we want). // (belt + suspenders for the space-between we want).
.tt-price { font-size: 1rem; color: rgba(var(--priGn), 1); margin-left: auto !important; } .tt-price { font-size: 1rem; color: rgba(var(--priGn), 1); margin-left: auto !important; }
// Sprint A.4 — polarization-multiplier decoration in deck tooltips.
// `(×2)` suffix appended inside .tt-description for is_polarized decks
// (Earthman today); --terUser color signals "double-polarized = 6 segments =
// fills 2× as many seats" per [[project-card-deck-icon]]'s `is_polarized`
// tooltip-decoration rule. Inline w. the card-count text on the same line.
.tt-x2 { color: rgba(var(--terUser), 1); font-weight: 600; }
} }
.token-tooltip, .token-tooltip,

View File

@@ -85,19 +85,22 @@
{% endif %} {% endif %}
{% for deck in deck_variants %} {% for deck in deck_variants %}
<div id="id_kit_{{ deck.short_key }}_deck" class="token deck-variant" data-deck-id="{{ deck.pk }}" data-in-use-room-name="{{ deck.in_use_room_name|default:'' }}"> <div id="id_kit_{{ deck.short_key }}_deck" class="token deck-variant" data-deck-id="{{ deck.pk }}" data-in-use-room-name="{{ deck.in_use_room_name|default:'' }}">
<i class="fa-regular fa-id-badge"></i> {# Sprint A.4 — card-deck stack icon replaces the old fa-id-badge. #}
{# 3 stacked card-back rects (5° rest tilt); the .token:hover/:active #}
{# /:focus pseudo-classes drive the fan-out + tooltip portal in lockstep. #}
{% include "apps/gameboard/_partials/_deck_stack_icon.html" %}
<div class="tt"> <div class="tt">
<div class="tt-equip-btns"> <div class="tt-equip-btns">
{% if deck.in_use_room_name %}<button class="btn btn-equip btn-disabled" data-deck-id="{{ deck.pk }}">×</button><button class="btn btn-unequip btn-disabled" data-deck-id="{{ deck.pk }}">×</button>{% elif deck.pk == equipped_deck_id %}<button class="btn btn-equip btn-disabled" data-deck-id="{{ deck.pk }}">×</button><button class="btn btn-unequip" data-deck-id="{{ deck.pk }}">DOFF</button>{% else %}<button class="btn btn-equip" data-deck-id="{{ deck.pk }}">DON</button><button class="btn btn-unequip btn-disabled" data-deck-id="{{ deck.pk }}">×</button>{% endif %} {% if deck.in_use_room_name %}<button class="btn btn-equip btn-disabled" data-deck-id="{{ deck.pk }}">×</button><button class="btn btn-unequip btn-disabled" data-deck-id="{{ deck.pk }}">×</button>{% elif deck.pk == equipped_deck_id %}<button class="btn btn-equip btn-disabled" data-deck-id="{{ deck.pk }}">×</button><button class="btn btn-unequip" data-deck-id="{{ deck.pk }}">DOFF</button>{% else %}<button class="btn btn-equip" data-deck-id="{{ deck.pk }}">DON</button><button class="btn btn-unequip btn-disabled" data-deck-id="{{ deck.pk }}">×</button>{% endif %}
</div> </div>
<h4 class="tt-title">{{ deck.name }}{% if deck.is_default %} <span class="token-count">(Default)</span>{% endif %}</h4> <h4 class="tt-title">{{ deck.name }}{% if deck.is_default %} <span class="token-count">(Default)</span>{% endif %}</h4>
<p class="tt-description">{{ deck.card_count }}-card Tarot deck</p> <p class="tt-description">{{ deck.card_count }}-card Tarot deck{% if deck.is_polarized %} <span class="tt-x2">(×2)</span>{% endif %}</p>
{% if deck.description %}<p class="tt-shoptalk"><em>{{ deck.description }}</em></p>{% endif %} {% if deck.description %}<p class="tt-shoptalk"><em>{{ deck.description }}</em></p>{% endif %}
<p class="tt-shoptalk">Stock version <span class="tt-subcounter">(0 substitutions)</span></p> <p class="tt-shoptalk">Stock version <span class="tt-subcounter">(0 substitutions)</span></p>
</div> </div>
</div> </div>
{% empty %} {% empty %}
<div id="id_kit_card_deck" class="kit-item"><i class="fa-regular fa-id-badge"></i></div> <div id="id_kit_card_deck" class="kit-item">{% include "apps/gameboard/_partials/_deck_stack_icon.html" %}</div>
{% endfor %} {% endfor %}
<div id="id_kit_dice_set" class="kit-item"><i class="fa-solid fa-dice"></i></div> <div id="id_kit_dice_set" class="kit-item"><i class="fa-solid fa-dice"></i></div>
</div> </div>

View File

@@ -0,0 +1,20 @@
{# Sprint A.4 — card-deck stack SVG icon. Replaces fa-regular fa-id-badge #}
{# wherever a deck is represented by an icon (currently game_kit applet's #}
{# .token.deck-variant; future: room.html pile + deck-bag). #}
{# #}
{# 3 rect-cards stacked tightly at rest; .deck-stack-icon SCSS rotates the #}
{# whole stack 5° CW + drives the fan-out on hover/active/focus of the #}
{# parent .token (or whatever wraps it). The card-back design here is #}
{# placeholder (solid --priUser fill + currentColor stroke). Detailed art #}
{# (Earthman planet-impact illustration, future custom decks) drops in later #}
{# per [[project-card-deck-icon]]. #}
<svg class="deck-stack-icon" viewBox="0 0 32 48" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
<g class="deck-stack-icon__stack">
<rect class="deck-stack-icon__card deck-stack-icon__card--3"
x="6" y="8" width="20" height="32" rx="2.5" />
<rect class="deck-stack-icon__card deck-stack-icon__card--2"
x="6" y="8" width="20" height="32" rx="2.5" />
<rect class="deck-stack-icon__card deck-stack-icon__card--1"
x="6" y="8" width="20" height="32" rx="2.5" />
</g>
</svg>