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:
@@ -969,6 +969,64 @@ class MySeaPickerPhaseTemplateTest(TestCase):
|
|||||||
self.assertContains(response, "sea-pos-leave")
|
self.assertContains(response, "sea-pos-leave")
|
||||||
self.assertContains(response, "sea-pos-loom")
|
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):
|
def test_picker_renders_six_card_only_positions_for_spread_switch(self):
|
||||||
# Crown / lay / cross sit in the DOM unconditionally so iter 3's
|
# Crown / lay / cross sit in the DOM unconditionally so iter 3's
|
||||||
# SPREAD dropdown can reveal them via CSS attribute swap (data-
|
# SPREAD dropdown can reveal them via CSS attribute swap (data-
|
||||||
|
|||||||
@@ -619,61 +619,6 @@ html:has(.sig-backdrop) {
|
|||||||
.sig-qualifier-below { opacity: 0.25; }
|
.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).
|
// 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 ────────────────────────────
|
// ─── My Sign picker — sizing + state-gated reveal ────────────────────────────
|
||||||
// Two-phase layout: landing (DRY 1-chair hex w. SCAN SIGN center) → picker
|
// 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
|
// (sig-card grid below an always-present stage frame). SAVE SIGN rides
|
||||||
|
|||||||
@@ -130,10 +130,21 @@
|
|||||||
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="leave" saved=saved_by_position.leave crossing=False %}
|
{% include "apps/gameboard/_partials/_my_sea_slot.html" with position="leave" saved=saved_by_position.leave crossing=False %}
|
||||||
</div>
|
</div>
|
||||||
<div class="sea-crucifix-cell sea-pos-core">
|
<div class="sea-crucifix-cell sea-pos-core">
|
||||||
<div class="sig-stage-card sea-sig-card"
|
{# Sprint A.5 — central sig card mirrors my_sign.html's image-mode #}
|
||||||
data-card-id="{{ significator.id }}">
|
{# render: when the user's deck has card images (Minchiate today, #}
|
||||||
<span class="fan-corner-rank">{{ significator.corner_rank }}</span>
|
{# future Earthman), show the transparent-PNG <img> w. contour #}
|
||||||
{% if significator.suit_icon %}<i class="fa-solid {{ significator.suit_icon }}"></i>{% endif %}
|
{# 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>
|
||||||
<div class="sea-pos-cover">
|
<div class="sea-pos-cover">
|
||||||
<span class="sea-pos-label" data-position="cover">Action</span>
|
<span class="sea-pos-label" data-position="cover">Action</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user