From b9bb73db69ad7b85d081273ef4e568cde2e68cc4 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Mon, 25 May 2026 00:40:10 -0400 Subject: [PATCH] =?UTF-8?q?A.4=20card-deck=20stack=20icon=20+=20game=5Fkit?= =?UTF-8?q?=20applet's=20Card=20Decks=20polarization=20(=C3=972)=20tooltip?= =?UTF-8?q?=20decoration=20=E2=80=94=20TDD.=20Sprint=20A.4=20of=20[[projec?= =?UTF-8?q?t-image-based-deck-face-rendering]]=20(folded=20down=20from=20t?= =?UTF-8?q?he=20originally-standalone=20Sprint=20D=20per=20[[project-card-?= =?UTF-8?q?deck-icon]]=202026-05-25=20PM=20scope-fold).=20Replaces=20the?= =?UTF-8?q?=20``=20placeholder=20o?= =?UTF-8?q?n=20`.token.deck-variant`=20in=20the=20gameboard's=20Game=20Kit?= =?UTF-8?q?=20applet=20w.=20a=20new=20inline=20SVG=20card-stack=20icon:=20?= =?UTF-8?q?3=20rect=20children=20(rx=3D2.5,=2020=C3=9732=20viewport=20unit?= =?UTF-8?q?s=20inside=20a=2032=C3=9748=20viewBox=20to=20land=205:8=20tarot?= =?UTF-8?q?=20card=20aspect),=20stacked=20tightly=20at=20rest=20w.=20?= =?UTF-8?q?=C2=B10.4px=20vertical=20micro-offsets=20(suggests=20stack=20de?= =?UTF-8?q?pth=20without=20separating=20cards=20visually),=20whole=20stack?= =?UTF-8?q?=20rotated=205=C2=B0=20clockwise=20via=20`.deck-stack-icon=5F?= =?UTF-8?q?=5Fstack`=20group=20transform.=20On=20`:hover`=20/=20`:active`?= =?UTF-8?q?=20/=20`:focus`=20of=20the=20parent=20`.token.deck-variant`,=20?= =?UTF-8?q?cards=202=20+=203=20fan=20out=20symmetrically=20=E2=80=94=20car?= =?UTF-8?q?d=202=20translates=20(-5px,=20-2px)=20+=20rotates=20-12=C2=B0,?= =?UTF-8?q?=20card=203=20translates=20(+5px,=20-2px)=20+=20rotates=20+12?= =?UTF-8?q?=C2=B0=20=E2=80=94=20card=201=20stays=20put=20on=20top.=20Fan-o?= =?UTF-8?q?ut=20CSS=20pseudo-classes=20match=20the=20existing=20JS-portal?= =?UTF-8?q?=20tooltip=20trigger=20so=20the=20splay=20animation=20+=20toolt?= =?UTF-8?q?ip-appearance=20co-activate=20as=20user=20spec'd.=20Placeholder?= =?UTF-8?q?=20card-back=20design:=20solid=20`--priUser`=20fill=20+=20`curr?= =?UTF-8?q?entColor`=20stroke=20(=3D=20`--terUser`);=20detailed=20Earthman?= =?UTF-8?q?=20planet-impact=20illustration=20deferred=20to=20a=20future=20?= =?UTF-8?q?art-asset=20commit=20(the=20SVG=20structure=20is=20ready=20to?= =?UTF-8?q?=20receive=20richer=20fills=20+=20pattern=20elements=20without?= =?UTF-8?q?=20re-jigging=20the=20stack/fan=20transforms).=20Drop-shadow=20?= =?UTF-8?q?for=20"lifted=20off=20the=20felt"=20depth=20cue:=20`0.08rem=200?= =?UTF-8?q?.08rem=200.15rem=20rgba(0,=200,=200,=200.6)`=20=E2=80=94=20soft?= =?UTF-8?q?er=20than=20the=20my-sign-stage=20card's=20tray-card-style=201,?= =?UTF-8?q?1=20black=20silhouette=20since=20the=20icon=20is=20small=20+=20?= =?UTF-8?q?always=20on=20a=20felt=20background.=20SVG=20itself=20uses=20`o?= =?UTF-8?q?verflow:=20visible`=20so=20the=20fan-out=20exceeds=20the=20view?= =?UTF-8?q?Box=20bounds;=20`transform-box:=20fill-box`=20+=20`transform-or?= =?UTF-8?q?igin:=2050%=2050%`=20ensure=20rotation=20centers=20on=20each=20?= =?UTF-8?q?card's=20own=20geometric=20center=20(not=20the=20viewBox=20cent?= =?UTF-8?q?er).=20New=20`=5Fdeck=5Fstack=5Ficon.html`=20partial=20in=20`te?= =?UTF-8?q?mplates/apps/gameboard/=5Fpartials/`=20keeps=20the=20SVG=20mark?= =?UTF-8?q?up=20DRY=20for=20the=20future=20room.html=20pile=20+=20deck-bag?= =?UTF-8?q?=20rollouts=20(per=20[[project-card-deck-icon]]=20"other=20surf?= =?UTF-8?q?aces=20deferred=20to=20later=20sprints").=20New=20`.tt-x2`=20st?= =?UTF-8?q?yle=20in=20`%tt-token-fields`=20placeholder=20mixin=20=E2=80=94?= =?UTF-8?q?=20`--terUser`=20color=20+=20font-weight=20600=20=E2=80=94=20ap?= =?UTF-8?q?pended=20inline=20in=20`.tt-description`=20for=20`is=5Fpolarize?= =?UTF-8?q?d=3DTrue`=20decks=20(Earthman=20today):=20"106-card=20Tarot=20d?= =?UTF-8?q?eck=20(=C3=972)"=20where=20the=20(=C3=972)=20signals=20"double-?= =?UTF-8?q?polarized=20=3D=206=20segments=20=3D=20fills=202=C3=97=20as=20m?= =?UTF-8?q?any=20seats"=20per=20[[project-card-deck-icon]]'s=20decoration?= =?UTF-8?q?=20rule.=20Non-polarized=20decks=20(Tarot=20RWS,=20future=20Min?= =?UTF-8?q?chiate)=20render=20the=20description=20without=20the=20suffix.?= =?UTF-8?q?=203=20new=20ITs=20in=20`GameboardViewTest`:=20SVG=20card-stack?= =?UTF-8?q?=20renders=20w.=203=20rect=20children=20+=20fa-id-badge=20gone;?= =?UTF-8?q?=20polarized=20Earthman=20tooltip=20carries=20`.tt-x2`=20w.=20"?= =?UTF-8?q?=C3=972"=20content;=20non-polarized=20RWS=20tooltip=20lacks=20`?= =?UTF-8?q?.tt-x2`.=20Out=20of=20scope=20this=20commit:=20the=20dedicated?= =?UTF-8?q?=20/game-kit/=20page's=20`.gk-deck-card`=20rectangles=20(differ?= =?UTF-8?q?ent=20template=20=E2=80=94=20`=5Fgame=5Fkit=5Fsections.html`)?= =?UTF-8?q?=20keep=20their=20fa-id-badge=20for=20now;=20folding=20them=20i?= =?UTF-8?q?nto=20the=20new=20icon=20happens=20in=20a=20follow-up=20"Card?= =?UTF-8?q?=20Decks=20rectangle=20teardown"=20sprint=20per=20user=20spec?= =?UTF-8?q?=20("by=20the=20time=20we=20finish=20A.8=20the=20dynamically=20?= =?UTF-8?q?shaped=20rectangles=20around=20the=20deck=20=20els=20and=20t?= =?UTF-8?q?heir=20names=20will=20be=20no=20more").=20Tests:=203=20new=20IT?= =?UTF-8?q?s=20green;=2027/27=20GameboardViewTest=20class=20green;=201293/?= =?UTF-8?q?1293=20IT+UT=20total=20green=20(68s;=20+3=20from=20A.3-polish's?= =?UTF-8?q?=201290).=20Visual=20verify=20pending:=20browser=20refresh=20ex?= =?UTF-8?q?pected=20to=20show=20the=20stacked-3-card=20icon=20w.=205=C2=B0?= =?UTF-8?q?=20rest=20tilt,=20fan-out=20on=20hover,=20tooltip=20+=20(=C3=97?= =?UTF-8?q?2)=20decoration=20on=20Earthman?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../gameboard/tests/integrated/test_views.py | 46 +++++++++++++++++++ src/static_src/scss/_gameboard.scss | 45 ++++++++++++++++++ src/static_src/scss/_tooltips.scss | 6 +++ .../gameboard/_partials/_applet-game-kit.html | 9 ++-- .../gameboard/_partials/_deck_stack_icon.html | 20 ++++++++ 5 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 src/templates/apps/gameboard/_partials/_deck_stack_icon.html diff --git a/src/apps/gameboard/tests/integrated/test_views.py b/src/apps/gameboard/tests/integrated/test_views.py index 3eac2f6..7ad443f 100644 --- a/src/apps/gameboard/tests/integrated/test_views.py +++ b/src/apps/gameboard/tests/integrated/test_views.py @@ -413,6 +413,52 @@ class GameboardViewTest(TestCase): def test_game_kit_has_dice_set_placeholder(self): [_] = 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 `` + 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): """Sprint 2: game kit applet renders in-use state for a deck assigned to an active seat.""" diff --git a/src/static_src/scss/_gameboard.scss b/src/static_src/scss/_gameboard.scss index 1465098..dcbfe66 100644 --- a/src/static_src/scss/_gameboard.scss +++ b/src/static_src/scss/_gameboard.scss @@ -79,6 +79,51 @@ body.page-gameboard { .kit-item { font-size: 1.5rem; } .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); } + } } } diff --git a/src/static_src/scss/_tooltips.scss b/src/static_src/scss/_tooltips.scss index 1406c5c..bb7a553 100644 --- a/src/static_src/scss/_tooltips.scss +++ b/src/static_src/scss/_tooltips.scss @@ -18,6 +18,12 @@ // regardless of whether justify-content cascade reaches this far // (belt + suspenders for the space-between we want). .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, diff --git a/src/templates/apps/gameboard/_partials/_applet-game-kit.html b/src/templates/apps/gameboard/_partials/_applet-game-kit.html index 8f4fe48..3f95882 100644 --- a/src/templates/apps/gameboard/_partials/_applet-game-kit.html +++ b/src/templates/apps/gameboard/_partials/_applet-game-kit.html @@ -85,19 +85,22 @@ {% endif %} {% for deck in deck_variants %}
- + {# 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" %}
{% if deck.in_use_room_name %}{% elif deck.pk == equipped_deck_id %}{% else %}{% endif %}

{{ deck.name }}{% if deck.is_default %} (Default){% endif %}

-

{{ deck.card_count }}-card Tarot deck

+

{{ deck.card_count }}-card Tarot deck{% if deck.is_polarized %} (×2){% endif %}

{% if deck.description %}

{{ deck.description }}

{% endif %}

Stock version (0 substitutions)

{% empty %} -
+
{% include "apps/gameboard/_partials/_deck_stack_icon.html" %}
{% endfor %}
diff --git a/src/templates/apps/gameboard/_partials/_deck_stack_icon.html b/src/templates/apps/gameboard/_partials/_deck_stack_icon.html new file mode 100644 index 0000000..fd1eab1 --- /dev/null +++ b/src/templates/apps/gameboard/_partials/_deck_stack_icon.html @@ -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]]. #} +