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:
@@ -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."""
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
20
src/templates/apps/gameboard/_partials/_deck_stack_icon.html
Normal file
20
src/templates/apps/gameboard/_partials/_deck_stack_icon.html
Normal 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>
|
||||||
Reference in New Issue
Block a user