From 82813e9fc12be60c884390edfc816df618fcbeae Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Mon, 25 May 2026 01:20:07 -0400 Subject: [PATCH] =?UTF-8?q?A.5=20my=5Fsea.html=20central=20sig=20card=20im?= =?UTF-8?q?age-rendering=20+=20SCSS=20lift-out=20fix=20=E2=80=94=20TDD.=20?= =?UTF-8?q?Sprint=20A.5=20of=20[[project-image-based-deck-face-rendering]]?= =?UTF-8?q?:=20second=20visible=20surface=20after=20my=5Fsign=20A.3.=20Whe?= =?UTF-8?q?n=20the=20user's=20equipped=20deck=20is=20image-equipped=20(Min?= =?UTF-8?q?chiate=20today),=20the=20central=20significator=20card=20in=20t?= =?UTF-8?q?he=20Celtic-Cross-style=20spread=20(`.sig-stage-card.sea-sig-ca?= =?UTF-8?q?rd`=20inside=20.sea-pos-core)=20renders=20the=20transparent-PNG?= =?UTF-8?q?=20=20w.=20contour-following=20arcana-color=20drop-shadow?= =?UTF-8?q?=20stroke=20+=20tray-card=20silhouette=20black=20shadow=20?= =?UTF-8?q?=E2=80=94=20same=20visual=20identity=20as=20my=5Fsign's=20saved?= =?UTF-8?q?-sig=20stage=20card=20so=20the=20user's=20"this=20is=20my=20sig?= =?UTF-8?q?"=20anchor=20reads=20the=20same=20across=20both=20surfaces.=20S?= =?UTF-8?q?erver-side=20template=20branch=20on=20`significator.deck=5Fvari?= =?UTF-8?q?ant.has=5Fcard=5Fimages`:=20image=20branch=20renders=20``=20+=20adds=20`.sig-stage-card--image`=20marker?= =?UTF-8?q?=20class=20+=20`data-arcana-key=3D"{{=20arcana=20}}"`=20for=20t?= =?UTF-8?q?he=20stroke-color=20selector;=20text=20branch=20keeps=20the=20e?= =?UTF-8?q?xisting=20corner-rank=20+=20suit-icon=20render=20unchanged=20(E?= =?UTF-8?q?arthman,=20RWS).=20No=20JS=20needed=20=E2=80=94=20central=20sig?= =?UTF-8?q?=20is=20statically=20rendered=20(vs=20my=5Fsign's=20stage=20car?= =?UTF-8?q?d=20which=20is=20JS-populated=20from=20the=20picker=20grid).=20?= =?UTF-8?q?Critical=20SCSS=20lift-out:=20the=20A.3=20`.sig-stage-card--ima?= =?UTF-8?q?ge`=20rule=20lived=20nested=20inside=20`.sig-stage=20.sig-stage?= =?UTF-8?q?-card`,=20scoped=20to=20my=5Fsign.html's=20stage=20container=20?= =?UTF-8?q?only.=20my=5Fsea's=20central=20sig=20isn't=20inside=20`.sig-sta?= =?UTF-8?q?ge`=20(lives=20in=20.sea-pos-core),=20so=20the=20rule=20wasn't?= =?UTF-8?q?=20applying=20=E2=80=94=20image=20rendered=20at=20native=20pixe?= =?UTF-8?q?l=20dimensions=20(~620=C3=971024=20PNG)=20instead=20of=20being?= =?UTF-8?q?=20constrained=20to=20the=20card=20container,=20showing=20only?= =?UTF-8?q?=20a=20top-left=20portion=20(user=20bug-report=202026-05-25=20P?= =?UTF-8?q?M:=20"It=20doesn't=20scale=20the=20img=20down=20for=20the=20sig?= =?UTF-8?q?=20=E2=80=94=20just=20a=20portion=20of=20the=20full=20img").=20?= =?UTF-8?q?Fix:=20moved=20the=20entire=20`.sig-stage-card.sig-stage-card--?= =?UTF-8?q?image=20{=20...=20}`=20block=20OUT=20of=20the=20`.sig-stage`=20?= =?UTF-8?q?nest=20into=20top-level=20scope=20so=20it=20applies=20to=20ANY?= =?UTF-8?q?=20`.sig-stage-card`=20carrying=20the=20`--image`=20class=20reg?= =?UTF-8?q?ardless=20of=20parent=20(my=5Fsign's=20`.sig-stage`,=20my=5Fsea?= =?UTF-8?q?'s=20`.sea-pos-core`,=20future=20room.html's=20table=20center,?= =?UTF-8?q?=20future=20deck-bag=20UI).=20Same=20lift-out=20also=20expands?= =?UTF-8?q?=20the=20`display:=20none`=20list=20to=20include=20`.fan-corner?= =?UTF-8?q?-rank`=20+=20`>=20i.fa-solid`=20=E2=80=94=20these=20elements=20?= =?UTF-8?q?appear=20in=20my=5Fsea's=20text-mode=20central=20sig=20and=20ne?= =?UTF-8?q?ed=20hiding=20when=20image-mode=20kicks=20in=20(my=5Fsign's=20t?= =?UTF-8?q?ext=20mode=20uses=20the=20wrapped=20`.fan-card-corner`=20+=20`.?= =?UTF-8?q?fan-card-face`=20classes=20which=20were=20already=20covered).?= =?UTF-8?q?=202=20new=20ITs=20in=20`MySeaPickerPhaseTemplateTest`:=20image?= =?UTF-8?q?-equipped=20Minchiate=20sig=20renders=20`.sig-stage-card--image?= =?UTF-8?q?`=20class=20+=20=20w.=20correct=20v2-convention=20src;=20n?= =?UTF-8?q?on-image=20Earthman=20keeps=20`.fan-corner-rank`=20text=20+=20l?= =?UTF-8?q?acks=20--image=20class.=20Earthman=20Minchiate=20test=20fixture?= =?UTF-8?q?=20needs=20the=20super-nomad=20+=20super-schizo=20Note=20unlock?= =?UTF-8?q?s=20(granted=20manually=20via=20`Note.grant=5Fif=5Fnew`=20since?= =?UTF-8?q?=20the=20post=5Fsave=20signal=20only=20fires=20on=20initial=20u?= =?UTF-8?q?ser=20creation,=20and=20we=20promote-to-superuser=20AFTER=20cre?= =?UTF-8?q?ate)=20to=20let=20Il=20Matto=20(MAJOR=200)=20through=20`=5Ffilt?= =?UTF-8?q?er=5Fmajor=5Funlocks`.=20Tests:=202=20new=20green;=201300/1300?= =?UTF-8?q?=20IT+UT=20total=20green=20(70s;=20+2=20from=20750fef8's=201298?= =?UTF-8?q?).=20Visual=20verify=20pending:=20refresh=20/gameboard/my-sea/?= =?UTF-8?q?=20w.=20Minchiate=20equipped=20+=20Il=20Matto=20as=20sig=20?= =?UTF-8?q?=E2=86=92=20central=20sig=20card=20should=20now=20scale=20the?= =?UTF-8?q?=20back=20image=20to=20fit=20the=20card=20container=20instead?= =?UTF-8?q?=20of=20showing=20a=20top-left=20crop.=20Sea=20Stage=20modal=20?= =?UTF-8?q?+=20drawn-card=20slot=20rendering=20(the=20bigger=20A.5=20scope?= =?UTF-8?q?)=20still=20pending=20=E2=80=94=20they=20go=20through=20stage-c?= =?UTF-8?q?ard.js=20+=20the=20my-sea=20draw=20fetch=20endpoint,=20which=20?= =?UTF-8?q?need=20data-attr=20+=20JSON-payload=20extensions=20in=20a=20fol?= =?UTF-8?q?low-up=20commit?= 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 | 58 +++++++++ src/static_src/scss/_card-deck.scss | 117 ++++++++++-------- src/templates/apps/gameboard/my_sea.html | 19 ++- 3 files changed, 135 insertions(+), 59 deletions(-) diff --git a/src/apps/gameboard/tests/integrated/test_views.py b/src/apps/gameboard/tests/integrated/test_views.py index ffbd7b3..a10d20c 100644 --- a/src/apps/gameboard/tests/integrated/test_views.py +++ b/src/apps/gameboard/tests/integrated/test_views.py @@ -969,6 +969,64 @@ class MySeaPickerPhaseTemplateTest(TestCase): self.assertContains(response, "sea-pos-leave") self.assertContains(response, "sea-pos-loom") + def test_sea_sig_card_renders_image_when_deck_has_card_images(self): + """Sprint A.5 — central sig card on /gameboard/my-sea/ carries the + `.sig-stage-card--image` marker class + an + child pointing at the deck's image asset when the user's equipped + deck is image-equipped (Minchiate today). Mirrors A.3's my_sign.html + image-mode treatment so the central sig + the Sea Stage modal render + with consistent visual identity.""" + from apps.epic.models import DeckVariant, TarotCard + minchiate = DeckVariant.objects.get(slug="minchiate-fiorentine-1860-1890") + # Override the auto-equipped Earthman w. Minchiate + pick Il Matto as + # the user's sig (it's MAJOR rank 0 → permitted by personal_sig_cards + # IF user has the super-nomad Note unlock; superuser auto-gets it). + self.user.is_superuser = True + self.user.save() + # Re-run the post_save Note grants for the now-superuser by manually + # granting (signal only fires on initial create). + from apps.drama.models import Note + Note.grant_if_new(self.user, "super-nomad") + Note.grant_if_new(self.user, "super-schizo") + self.user.unlocked_decks.add(minchiate) + self.user.equipped_deck = minchiate + il_matto = TarotCard.objects.get(deck_variant=minchiate, slug="il-matto") + self.user.significator = il_matto + self.user.save(update_fields=["equipped_deck", "significator"]) + + import lxml.html + response = self.client.get(reverse("my_sea")) + parsed = lxml.html.fromstring(response.content) + [sig_card] = parsed.cssselect(".sea-sig-card") + self.assertIn( + "sig-stage-card--image", sig_card.get("class", ""), + "Sig card must carry --image marker class for Minchiate-equipped user", + ) + [img] = sig_card.cssselect("img.sig-stage-card-img") + self.assertIn( + "minchiate-fiorentine-1860-1890-trumps-00-il-matto.png", + img.get("src", ""), + "Image src must point at the v2-convention Il Matto asset", + ) + + def test_sea_sig_card_renders_text_when_deck_has_no_images(self): + """Earthman (has_card_images=False) keeps the existing corner-rank + + suit-icon text render — the image branch only applies to image-decks.""" + # Default setUp leaves the user on auto-equipped Earthman. + import lxml.html + response = self.client.get(reverse("my_sea")) + parsed = lxml.html.fromstring(response.content) + [sig_card] = parsed.cssselect(".sea-sig-card") + self.assertNotIn("sig-stage-card--image", sig_card.get("class", "")) + self.assertEqual( + len(sig_card.cssselect("img.sig-stage-card-img")), 0, + "Non-image deck must not render the in the sig card", + ) + self.assertEqual( + len(sig_card.cssselect(".fan-corner-rank")), 1, + "Non-image deck falls through to corner-rank text render", + ) + def test_picker_renders_six_card_only_positions_for_spread_switch(self): # Crown / lay / cross sit in the DOM unconditionally so iter 3's # SPREAD dropdown can reveal them via CSS attribute swap (data- diff --git a/src/static_src/scss/_card-deck.scss b/src/static_src/scss/_card-deck.scss index caacc67..8ed3be7 100644 --- a/src/static_src/scss/_card-deck.scss +++ b/src/static_src/scss/_card-deck.scss @@ -619,61 +619,6 @@ html:has(.sig-backdrop) { .sig-qualifier-below { opacity: 0.25; } } - // Sprint A.3 — image-rendering mode for decks w. DeckVariant.has_card_images=True - // (Minchiate Fiorentine 1860-1890 today; future image-equipped decks - // flip the flag to opt in). When `.sig-stage-card--image` is set by - // stage-card.js _setImageMode, the text scaffold (fan-card-* children) - // hides and an renders inside the same shell. - // Card bg + border go away — the transparent PNG carries its own - // irregular outline; we stack four cardinal-direction drop-shadows on - // the itself to render a stroke-like outline that FOLLOWS the - // alpha contour (per user spec 2026-05-25 PM — NOT a rectangular border - // around the bounding box). Color is arcana-driven: `--quiUser` (cream) - // for minor + middle, `--terUser` (gold) for major per - // [[project-image-based-deck-face-rendering]]'s Q2 lock. - &.sig-stage-card--image { - --img-stroke-color: rgba(var(--quiUser), 1); - background: transparent; - border: 0; - padding: 0; - overflow: visible; - - &[data-arcana-key="MAJOR"] { - --img-stroke-color: rgba(var(--terUser), 1); - } - - .fan-card-corner, - .fan-card-face { - display: none; - } - - .sig-stage-card-img { - display: block; - width: 100%; - height: 100%; - object-fit: contain; - // Filter chain (order matters — each drop-shadow operates on - // the prior result): - // 1-4: 4 cardinal-direction drop-shadows at 0.2rem (~3.2px) - // each → contour-following stroke. Combined apparent width - // ~6.4px. Bump to 8-direction stack if we ever go past - // ~0.5rem so curved edges stay even. - // 5: down-right black 1,1 offset 2px-blur drop-shadow - // matches the silhouette shadow `.tray-cell > img` carries - // (`_tray.scss:272`) — "lifted off the felt" depth cue. - // Comes AFTER the strokes so it traces the stroked - // silhouette, not just the original PNG alpha. - // Mobile-safe: filter on raster images works fine cross-browser - // (the [[feedback-mobile-svg-glow]] dead-end was specifically - // SVG glow, not raster drop-shadow). - filter: - drop-shadow( 0.2rem 0 0 var(--img-stroke-color)) - drop-shadow(-0.2rem 0 0 var(--img-stroke-color)) - drop-shadow( 0 0.2rem 0 var(--img-stroke-color)) - drop-shadow( 0 -0.2rem 0 var(--img-stroke-color)) - drop-shadow( 1px 1px 2px rgba(0, 0, 0, 1)); - } - } } // Stat block — same dimensions as the preview card (width × 5:8 aspect). @@ -702,6 +647,68 @@ html:has(.sig-backdrop) { } } +// Sprint A.3 / A.5 — image-rendering mode for decks w. DeckVariant.has_card_images=True +// (Minchiate Fiorentine 1860-1890 today; future image-equipped decks flip +// the flag to opt in). LIFTED OUT of the `.sig-stage` nest in A.5 polish so +// the same rule applies wherever `.sig-stage-card.sig-stage-card--image` +// renders — my_sign.html's stage card (inside .sig-stage), my_sea.html's +// central sig card (.sea-sig-card inside .sea-pos-core, NOT in .sig-stage), +// future surface drops. When `.sig-stage-card--image` is set (either by +// stage-card.js _setImageMode or server-side template branch), the text +// scaffold (fan-card-* + .fan-corner-rank text children) hides and an +// renders inside the same shell. Card bg + border +// go away — the transparent PNG carries its own irregular outline; four +// cardinal-direction drop-shadows on the render a stroke-like outline +// that FOLLOWS the alpha contour (user spec 2026-05-25 PM — NOT a rectangular +// border around the bounding box). Color is arcana-driven: `--quiUser` (cream) +// for minor + middle, `--terUser` (gold) for major per +// [[project-image-based-deck-face-rendering]]'s Q2 lock. +.sig-stage-card.sig-stage-card--image { + --img-stroke-color: rgba(var(--quiUser), 1); + background: transparent; + border: 0; + padding: 0; + overflow: visible; + + &[data-arcana-key="MAJOR"] { + --img-stroke-color: rgba(var(--terUser), 1); + } + + .fan-card-corner, + .fan-card-face, + .fan-corner-rank, + > i.fa-solid { + display: none; + } + + .sig-stage-card-img { + display: block; + width: 100%; + height: 100%; + object-fit: contain; + // Filter chain (order matters — each drop-shadow operates on + // the prior result): + // 1-4: 4 cardinal-direction drop-shadows at 0.2rem (~3.2px) + // each → contour-following stroke. Combined apparent width + // ~6.4px. Bump to 8-direction stack if we ever go past + // ~0.5rem so curved edges stay even. + // 5: down-right black 1,1 offset 2px-blur drop-shadow + // matches the silhouette shadow `.tray-cell > img` carries + // (`_tray.scss:272`) — "lifted off the felt" depth cue. + // Comes AFTER the strokes so it traces the stroked + // silhouette, not just the original PNG alpha. + // Mobile-safe: filter on raster images works fine cross-browser + // (the [[feedback-mobile-svg-glow]] dead-end was specifically + // SVG glow, not raster drop-shadow). + filter: + drop-shadow( 0.2rem 0 0 var(--img-stroke-color)) + drop-shadow(-0.2rem 0 0 var(--img-stroke-color)) + drop-shadow( 0 0.2rem 0 var(--img-stroke-color)) + drop-shadow( 0 -0.2rem 0 var(--img-stroke-color)) + drop-shadow( 1px 1px 2px rgba(0, 0, 0, 1)); + } +} + // ─── My Sign picker — sizing + state-gated reveal ──────────────────────────── // Two-phase layout: landing (DRY 1-chair hex w. SCAN SIGN center) → picker // (sig-card grid below an always-present stage frame). SAVE SIGN rides diff --git a/src/templates/apps/gameboard/my_sea.html b/src/templates/apps/gameboard/my_sea.html index caac204..d0da289 100644 --- a/src/templates/apps/gameboard/my_sea.html +++ b/src/templates/apps/gameboard/my_sea.html @@ -130,10 +130,21 @@ {% include "apps/gameboard/_partials/_my_sea_slot.html" with position="leave" saved=saved_by_position.leave crossing=False %}
-
- {{ significator.corner_rank }} - {% if significator.suit_icon %}{% endif %} + {# Sprint A.5 — central sig card mirrors my_sign.html's image-mode #} + {# render: when the user's deck has card images (Minchiate today, #} + {# future Earthman), show the transparent-PNG w. contour #} + {# stroke + depth shadow per A.3's `.sig-stage-card--image` rule. #} + {# Otherwise fall through to the existing corner-rank + suit-icon #} + {# text render (Earthman, RWS). #} +
+ {% if significator.deck_variant.has_card_images %} + {{ significator.name }} + {% else %} + {{ significator.corner_rank }} + {% if significator.suit_icon %}{% endif %} + {% endif %}
Action