A.7-polish my_sea slot image-rendering (server-saved + mid-draw JS) + non-polarized single-deck-stack collapse — TDD. End-of-session wrap-up 2026-05-25 PM. Three changes covering my_sea.html surfaces that weren't in A.5/A.7's central-sig-only scope: (1) saved-hand slot rendering (_my_sea_slot.html server-rendered partial fired when a draw is resumed via refresh); (2) mid-draw slot fill (sea.js's _fillSlot writes slot.innerHTML on each card-deposit click; previously rendered corner-rank + suit-icon only, NOW renders <img> when card.image_url is non-empty); (3) deck-stack collapse for non-polarized decks (Minchiate today) — the bottom-right of my_sea.html showed two side-by-side GRAVITY + LEVITY stacks regardless of equipped-deck polarization; for non-polarized decks polarity has no meaning so the dual layout misleads. **Critical lock**: collapse is my_sea-ONLY. room.html keeps the dual stacks since multiple gamers contribute (each might bring a different polarization). Server-side template branches {% if request.user.equipped_deck.is_polarized %} to pick dual vs. single rendering; the --single stack carries the actual deck back-image via <img class="sea-stack-face-img"> (object-fit: cover) when has_card_images=True. Sub-changes: card_dict() in apps/epic/utils.py now includes image_url + arcana_key fields so the picker grid's JSON payload carries the data the JS fill-handler needs (single source of truth shared w. the gameroom sea_deck endpoint — apps/gameboard/views.py's saved_by_position dict gets the parallel additions for server-rendered saved hand). SCSS: extended the shared image-mode rule's comma-list selector in _card-deck.scss to include .sea-card-slot.sea-card-slot--image so the contour stroke + depth shadow apply to both saved + mid-draw slots from a single rule definition. Also added .sea-deck-stack--single .sea-stack-face block w. neutral --priUser/--terUser palette (vs. gravity's --quiUser/--quaUser + levity's --terUser/--ninUser) + the corresponding hover/active glow rule positioned AFTER the $_sea-shadow SCSS variable definition at line 1808 (initial draft hit a compile error: Undefined variable: "$_sea-shadow" because the hover rule was placed before the variable was defined; SCSS variables are scope/order-dependent). JS: _fillSlot in sea.js branches on card.image_url — when non-empty, write <img class="sig-stage-card-img"> + add .sea-card-slot--image marker + data-arcana-key attr; otherwise legacy corner-rank + suit-icon. innerHTML alt-attribute properly escapes " to &quot; so card names w. quotes (none today, but defensive) don't break HTML. Existing JS that activates a clicked stack (_activeStack flow + _showOk / _hideOk) works unchanged w. the single-stack variant since the selector .sea-deck-stack matches all variants regardless of polarity suffix; the isLevity = stack.classList.contains('sea-deck-stack--levity') check at the deposit moment returns false for --single → defaults to gravity polarity assignment, which is fine for non-polarized decks (polarity field has no card-content effect). Memory updated: project_image_based_deck_face_rendering.md now lists A.0-A.7 done + this polish + room.html (A.8) as the sole remaining surface for tomorrow. The 6-surface scope sheet shows A.8 as the last red box; everything else green. Tests: 1306/1306 IT+UT total green (73s). No new ITs in this commit — the saved-slot render touch was an extension of existing saved_by_position view context shape (covered by existing slot-render tests' implicit invariance); the JS change is hard to test via Django ITs (would need Jasmine spec or FT, deferred); the deck-stack collapse is a template branch (visual; user verified live in browser this session). Tomorrow: A.8 room.html image-rendering (multi-user surface via Channels WebSocket payload + same template branch pattern; keep dual gravity/levity stacks per user spec)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-25 02:16:53 -04:00
parent dd99364b78
commit 15025b4188
6 changed files with 105 additions and 8 deletions

View File

@@ -94,10 +94,27 @@ var SeaDeal = (function () {
// partial also stamps `data-pos-key="{{ position }}"` raw.
// Standardize so both code paths agree (2026-05-21 fix).
slot.dataset.posKey = _posName(posSelector);
// Sprint A.7-polish — image-mode branch. When the drawn card carries
// an `image_url` (deck has_card_images=True, Minchiate today), render
// an <img> + add the `.sea-card-slot--image` marker class so the
// shared `_card-deck.scss` comma-list rule applies the contour
// stroke + depth shadow. Mirrors the server-rendered branch in
// `_my_sea_slot.html` for saved-hand parity. Otherwise fall through
// to the legacy corner-rank + suit-icon render.
if (card.image_url) {
slot.classList.add('sea-card-slot--image');
if (card.arcana_key) slot.dataset.arcanaKey = card.arcana_key;
slot.innerHTML =
'<img class="sig-stage-card-img" src="' + card.image_url +
'" alt="' + (card.name || '').replace(/"/g, '&quot;') + '">';
} else {
slot.classList.remove('sea-card-slot--image');
slot.removeAttribute('data-arcana-key');
slot.innerHTML =
'<span class="fan-corner-rank">' + card.corner_rank + '</span>' +
(card.suit_icon ? '<i class="fa-solid ' + card.suit_icon + '"></i>' : '');
}
}
// ── Show / hide stage ─────────────────────────────────────────────────────

View File

@@ -67,6 +67,15 @@ def card_dict(card, reversal_prob=STACK_REVERSAL_PROBABILITY):
'energies': card.energies,
'operations': card.operations,
'reversed': _random.random() < reversal_prob,
# Sprint A.7-polish — image-mode payload for the my-sea picker's
# JS-driven `_fillSlot` (which writes slot.innerHTML on draw). Empty
# strings for legacy text-only decks (Earthman, RWS); non-empty when
# `deck.has_card_images=True` (Minchiate today). JS branches on
# `image_url` to render an <img> instead of corner-rank + suit-icon
# so the mid-draw slot fill matches the server-rendered saved-hand
# `_my_sea_slot.html` partial branch.
'image_url': card.image_url,
'arcana_key': card.arcana,
}

View File

@@ -281,6 +281,15 @@ def my_sea(request):
"polarity": entry.get("polarity", "gravity"),
"corner_rank": c.corner_rank if c else "",
"suit_icon": c.suit_icon if c else "",
# Sprint A.7-polish: extra fields for image-mode slot render
# in `_my_sea_slot.html`. Empty strings when the card's deck
# has no images (legacy text-only); template branches on
# `has_card_images` to pick render mode.
"has_card_images": (c.deck_variant.has_card_images
if c and c.deck_variant else False),
"image_url": c.image_url if c else "",
"arcana": c.arcana if c else "",
"name": c.name if c else "",
}
return render(request, "apps/gameboard/my_sea.html", {

View File

@@ -665,7 +665,8 @@ html:has(.sig-backdrop) {
// [[project-image-based-deck-face-rendering]]'s Q2 lock.
.sig-stage-card.sig-stage-card--image,
.my-sign-applet-card.my-sign-applet-card--image,
.my-sea-slot.my-sea-slot--image {
.my-sea-slot.my-sea-slot--image,
.sea-card-slot.sea-card-slot--image {
--img-stroke-color: rgba(var(--quiUser), 1);
background: transparent;
border: 0;
@@ -1819,6 +1820,27 @@ $_glow-gravity: 0 0 0.8rem 0.15rem rgba(var(--quaUser), 0.6);
border-color: rgba(var(--quaUser), 0.65);
box-shadow: $_sea-shadow;
}
// Sprint A.7-polish — single (non-polarized) deck stack for my_sea.html
// when the equipped deck has no polarity (Minchiate today). Neutral palette
// vs. gravity/levity polarity colors. When the deck also has card images,
// the actual back-image overlays the face via .sea-stack-face-img (object-
// fit: cover so the PNG fills the rect cleanly; positioned absolute so the
// FLIP btn on top still sits center).
.sea-deck-stack--single .sea-stack-face {
background: rgba(var(--priUser), 0.88);
border-color: rgba(var(--terUser), 0.65);
box-shadow: $_sea-shadow;
overflow: hidden; // clip the back-img to the rounded-rect face
}
.sea-stack-face-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0.15rem; // inner radius matches the face's outer
z-index: 0;
}
// Glow on hover, :active, and while OK is showing (--active class set by JS)
.sea-deck-stack--levity:hover .sea-stack-face,
@@ -1827,6 +1849,13 @@ $_glow-gravity: 0 0 0.8rem 0.15rem rgba(var(--quaUser), 0.6);
.sea-deck-stack--gravity:hover .sea-stack-face,
.sea-deck-stack--gravity:active .sea-stack-face,
.sea-deck-stack--gravity.sea-deck-stack--active .sea-stack-face { box-shadow: $_sea-shadow, $_glow-gravity; }
// Single-stack hover/active glow — neutral --terUser tone vs. gravity's
// --quaUser + levity's --ninUser, matching the face border color.
.sea-deck-stack--single:hover .sea-stack-face,
.sea-deck-stack--single:active .sea-stack-face,
.sea-deck-stack--single.sea-deck-stack--active .sea-stack-face {
box-shadow: $_sea-shadow, 0 0 0.8rem 0.15rem rgba(var(--terUser), 0.6);
}
// Form action row — LOCK HAND + DEL side by side at the bottom
.sea-form-actions {

View File

@@ -8,11 +8,19 @@
{# crossing — bool; pass True for the cross slot (gets the #}
{# `.sea-card-slot--crossing` modifier in iter-4a HTML) #}
{% if saved %}
<div class="sea-card-slot sea-card-slot--filled sea-card-slot--visible sea-card-slot--{{ saved.polarity }}{% if saved.reversed %} sea-card-slot--reversed{% endif %}{% if crossing %} sea-card-slot--crossing{% endif %}"
<div class="sea-card-slot sea-card-slot--filled sea-card-slot--visible sea-card-slot--{{ saved.polarity }}{% if saved.reversed %} sea-card-slot--reversed{% endif %}{% if crossing %} sea-card-slot--crossing{% endif %}{% if saved.has_card_images %} sea-card-slot--image{% endif %}"
data-card-id="{{ saved.card_id }}"
data-pos-key="{{ position }}">
data-pos-key="{{ position }}"
{% if saved.has_card_images %}data-arcana-key="{{ saved.arcana }}"{% endif %}>
{% if saved.has_card_images %}
{# Sprint A.7-polish — image-mode slot for saved-hand bypass. #}
{# Shares `.sig-stage-card-img` filter chain via the comma-list #}
{# selector extension in `_card-deck.scss` (.sea-card-slot.--image). #}
<img class="sig-stage-card-img" src="{{ saved.image_url }}" alt="{{ saved.name }}">
{% else %}
<span class="fan-corner-rank">{{ saved.corner_rank }}</span>
{% if saved.suit_icon %}<i class="fa-solid {{ saved.suit_icon }}"></i>{% endif %}
{% endif %}
</div>
{% else %}
<div class="sea-card-slot sea-card-slot--empty{% if crossing %} sea-card-slot--crossing{% endif %}"></div>

View File

@@ -210,6 +210,18 @@
</div>
</div>
{# Sprint A.7-polish — deck-stack rendering depends on #}
{# the equipped deck's polarization. Polarized decks #}
{# (Earthman) keep the dual gravity + levity stacks + #}
{# named labels — those are the two halves of a 6- #}
{# segment deck. Non-polarized decks (Minchiate, RWS) #}
{# collapse to a single unnamed stack (DECKS → DECK): #}
{# polarity has no meaning at the deck level, so the #}
{# split labels would mislead. user spec 2026-05-25 PM. #}
{# Critical: this collapse is my_sea-ONLY — room.html #}
{# keeps the dual layout since multiple gamers contribute #}
{# (each might bring a different polarization). #}
{% if request.user.equipped_deck.is_polarized %}
<div class="sea-stacks">
<span class="sea-stacks-label">DECKS</span>
<div class="sea-deck-stack sea-deck-stack--gravity">
@@ -225,6 +237,19 @@
<span class="sea-stack-name">Levity</span>
</div>
</div>
{% else %}
<div class="sea-stacks sea-stacks--single">
<span class="sea-stacks-label">DECK</span>
<div class="sea-deck-stack sea-deck-stack--single">
<div class="sea-stack-face">
{% if request.user.equipped_deck.has_card_images %}
<img class="sea-stack-face-img" src="{{ request.user.equipped_deck.back_image_url }}" alt="">
{% endif %}
<button class="btn btn-reveal sea-stack-ok" type="button">FLIP</button>
</div>
</div>
</div>
{% endif %}
</div>
<div class="sea-form-actions">