A.5 my_sea.html central sig card image-rendering + SCSS lift-out fix — TDD. Sprint A.5 of [[project-image-based-deck-face-rendering]]: second visible surface after my_sign A.3. When the user's equipped deck is image-equipped (Minchiate today), the central significator card in the Celtic-Cross-style spread (.sig-stage-card.sea-sig-card inside .sea-pos-core) renders the transparent-PNG <img> w. contour-following arcana-color drop-shadow stroke + tray-card silhouette black shadow — same visual identity as my_sign's saved-sig stage card so the user's "this is my sig" anchor reads the same across both surfaces. Server-side template branch on significator.deck_variant.has_card_images: image branch renders <img class="sig-stage-card-img" src="{{ significator.image_url }}"> + adds .sig-stage-card--image marker class + data-arcana-key="{{ arcana }}" for the stroke-color selector; text branch keeps the existing corner-rank + suit-icon render unchanged (Earthman, RWS). No JS needed — central sig is statically rendered (vs my_sign's stage card which is JS-populated from the picker grid). Critical SCSS lift-out: the A.3 .sig-stage-card--image rule lived nested inside .sig-stage .sig-stage-card, scoped to my_sign.html's stage container only. my_sea's central sig isn't inside .sig-stage (lives in .sea-pos-core), so the rule wasn't applying — image rendered at native pixel dimensions (~620×1024 PNG) instead of being constrained to the card container, showing only a top-left portion (user bug-report 2026-05-25 PM: "It doesn't scale the img down for the sig — just a portion of the full img"). Fix: moved the entire .sig-stage-card.sig-stage-card--image { ... } block OUT of the .sig-stage nest into top-level scope so it applies to ANY .sig-stage-card carrying the --image class regardless of parent (my_sign's .sig-stage, my_sea's .sea-pos-core, future room.html's table center, future deck-bag UI). Same lift-out also expands the display: none list to include .fan-corner-rank + > i.fa-solid — these elements appear in my_sea's text-mode central sig and need hiding when image-mode kicks in (my_sign's text mode uses the wrapped .fan-card-corner + .fan-card-face classes which were already covered). 2 new ITs in MySeaPickerPhaseTemplateTest: image-equipped Minchiate sig renders .sig-stage-card--image class + <img> w. correct v2-convention src; non-image Earthman keeps .fan-corner-rank text + lacks --image class. Earthman Minchiate test fixture needs the super-nomad + super-schizo Note unlocks (granted manually via Note.grant_if_new since the post_save signal only fires on initial user creation, and we promote-to-superuser AFTER create) to let Il Matto (MAJOR 0) through _filter_major_unlocks. Tests: 2 new green; 1300/1300 IT+UT total green (70s; +2 from 750fef8's 1298). Visual verify pending: refresh /gameboard/my-sea/ w. Minchiate equipped + Il Matto as sig → central sig card should now scale the back image to fit the card container instead of showing a top-left crop. Sea Stage modal + drawn-card slot rendering (the bigger A.5 scope) still pending — they go through stage-card.js + the my-sea draw fetch endpoint, which need data-attr + JSON-payload extensions in a follow-up commit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-25 01:20:07 -04:00
parent 750fef890e
commit 82813e9fc1
3 changed files with 135 additions and 59 deletions

View File

@@ -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 <img.sig-stage-card-img>
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 <img> 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-

View File

@@ -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 <img.sig-stage-card-img> 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 <img> 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
// <img.sig-stage-card-img> 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 <img> 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

View File

@@ -130,10 +130,21 @@
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="leave" saved=saved_by_position.leave crossing=False %}
</div>
<div class="sea-crucifix-cell sea-pos-core">
<div class="sig-stage-card sea-sig-card"
data-card-id="{{ significator.id }}">
<span class="fan-corner-rank">{{ significator.corner_rank }}</span>
{% if significator.suit_icon %}<i class="fa-solid {{ significator.suit_icon }}"></i>{% 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 <img> 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). #}
<div class="sig-stage-card sea-sig-card{% if significator.deck_variant.has_card_images %} sig-stage-card--image{% endif %}"
data-card-id="{{ significator.id }}"
data-arcana-key="{{ significator.arcana }}">
{% if significator.deck_variant.has_card_images %}
<img class="sig-stage-card-img" src="{{ significator.image_url }}" alt="{{ significator.name }}">
{% else %}
<span class="fan-corner-rank">{{ significator.corner_rank }}</span>
{% if significator.suit_icon %}<i class="fa-solid {{ significator.suit_icon }}"></i>{% endif %}
{% endif %}
</div>
<div class="sea-pos-cover">
<span class="sea-pos-label" data-position="cover">Action</span>