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 " 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:
@@ -94,10 +94,27 @@ var SeaDeal = (function () {
|
|||||||
// partial also stamps `data-pos-key="{{ position }}"` raw.
|
// partial also stamps `data-pos-key="{{ position }}"` raw.
|
||||||
// Standardize so both code paths agree (2026-05-21 fix).
|
// Standardize so both code paths agree (2026-05-21 fix).
|
||||||
slot.dataset.posKey = _posName(posSelector);
|
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, '"') + '">';
|
||||||
|
} else {
|
||||||
|
slot.classList.remove('sea-card-slot--image');
|
||||||
|
slot.removeAttribute('data-arcana-key');
|
||||||
slot.innerHTML =
|
slot.innerHTML =
|
||||||
'<span class="fan-corner-rank">' + card.corner_rank + '</span>' +
|
'<span class="fan-corner-rank">' + card.corner_rank + '</span>' +
|
||||||
(card.suit_icon ? '<i class="fa-solid ' + card.suit_icon + '"></i>' : '');
|
(card.suit_icon ? '<i class="fa-solid ' + card.suit_icon + '"></i>' : '');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Show / hide stage ─────────────────────────────────────────────────────
|
// ── Show / hide stage ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,15 @@ def card_dict(card, reversal_prob=STACK_REVERSAL_PROBABILITY):
|
|||||||
'energies': card.energies,
|
'energies': card.energies,
|
||||||
'operations': card.operations,
|
'operations': card.operations,
|
||||||
'reversed': _random.random() < reversal_prob,
|
'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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -281,6 +281,15 @@ def my_sea(request):
|
|||||||
"polarity": entry.get("polarity", "gravity"),
|
"polarity": entry.get("polarity", "gravity"),
|
||||||
"corner_rank": c.corner_rank if c else "",
|
"corner_rank": c.corner_rank if c else "",
|
||||||
"suit_icon": c.suit_icon 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", {
|
return render(request, "apps/gameboard/my_sea.html", {
|
||||||
|
|||||||
@@ -665,7 +665,8 @@ html:has(.sig-backdrop) {
|
|||||||
// [[project-image-based-deck-face-rendering]]'s Q2 lock.
|
// [[project-image-based-deck-face-rendering]]'s Q2 lock.
|
||||||
.sig-stage-card.sig-stage-card--image,
|
.sig-stage-card.sig-stage-card--image,
|
||||||
.my-sign-applet-card.my-sign-applet-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);
|
--img-stroke-color: rgba(var(--quiUser), 1);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
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);
|
border-color: rgba(var(--quaUser), 0.65);
|
||||||
box-shadow: $_sea-shadow;
|
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)
|
// Glow on hover, :active, and while OK is showing (--active class set by JS)
|
||||||
.sea-deck-stack--levity:hover .sea-stack-face,
|
.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:hover .sea-stack-face,
|
||||||
.sea-deck-stack--gravity:active .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; }
|
.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
|
// Form action row — LOCK HAND + DEL side by side at the bottom
|
||||||
.sea-form-actions {
|
.sea-form-actions {
|
||||||
|
|||||||
@@ -8,11 +8,19 @@
|
|||||||
{# crossing — bool; pass True for the cross slot (gets the #}
|
{# crossing — bool; pass True for the cross slot (gets the #}
|
||||||
{# `.sea-card-slot--crossing` modifier in iter-4a HTML) #}
|
{# `.sea-card-slot--crossing` modifier in iter-4a HTML) #}
|
||||||
{% if saved %}
|
{% 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-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>
|
<span class="fan-corner-rank">{{ saved.corner_rank }}</span>
|
||||||
{% if saved.suit_icon %}<i class="fa-solid {{ saved.suit_icon }}"></i>{% endif %}
|
{% if saved.suit_icon %}<i class="fa-solid {{ saved.suit_icon }}"></i>{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="sea-card-slot sea-card-slot--empty{% if crossing %} sea-card-slot--crossing{% endif %}"></div>
|
<div class="sea-card-slot sea-card-slot--empty{% if crossing %} sea-card-slot--crossing{% endif %}"></div>
|
||||||
|
|||||||
@@ -210,6 +210,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="sea-stacks">
|
||||||
<span class="sea-stacks-label">DECKS</span>
|
<span class="sea-stacks-label">DECKS</span>
|
||||||
<div class="sea-deck-stack sea-deck-stack--gravity">
|
<div class="sea-deck-stack sea-deck-stack--gravity">
|
||||||
@@ -225,6 +237,19 @@
|
|||||||
<span class="sea-stack-name">Levity</span>
|
<span class="sea-stack-name">Levity</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="sea-form-actions">
|
<div class="sea-form-actions">
|
||||||
|
|||||||
Reference in New Issue
Block a user