my_sea_visit: give the visitor a top-left deck stack (owner's deck) with a hover-revealed disabled FLIP — TDD
Mirrors the owner's deck stack onto the spectator hex, DRY: - new shared _my_sea_deck_stack.html partial (mono DECK / dubbo DECKS by is_polarized) rendered by BOTH the owner picker (my_sea.html, flip_disabled= hand_complete) AND the visitor cross (flip_disabled=True). Owner markup is byte-identical, so its assertions hold. - the visitor's stack uses the OWNER's deck (everyone at @owner's sea plays the owner's deck — the visitor's own equipped deck is irrelevant), pinned top-left (--visit) across the table from the owner who deals bottom-right. - dubbodeck: the Gravity/Levity name flips above the face + upside-down to signal someone across the table is dealing. - the read-only FLIP (disabled ×) is hidden until its stack is hovered/focused, then eases in (same opacity 0->1 over 0.3s as the shared flip-btn-base reveal; inlined since _gameboard precedes _card-deck in the import order) so a permanent × doesn't clutter the stack. ITs: stack keys on the owner's deck (not the viewer's), dubbo renders 2 named stacks, FLIP is the disabled state, no stack when the owner has no deck. Owner deck-stack IT + FT stay green (identical markup). Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -147,6 +147,51 @@ class MySeaVisitContextTest(TestCase):
|
||||
self.assertIn('data-card-id="1"', content) # owner's drawn card slot
|
||||
self.assertNotIn("my-sea-scroll", content)
|
||||
|
||||
def test_present_renders_owner_deck_stack_top_left_flip_disabled(self):
|
||||
"""The deck stack mirrors the OWNER's deck (everyone plays the owner's
|
||||
deck), pinned top-left (--visit) with FLIP always disabled (read-only
|
||||
spectator can't deal). A polarized owner deck → a dubbodeck."""
|
||||
from apps.epic.models import DeckVariant
|
||||
deck = DeckVariant.objects.filter(is_polarized=True).first()
|
||||
self.assertIsNotNone(deck, "expected a polarized (dubbo) deck seeded")
|
||||
self.owner.equipped_deck = deck # the OWNER's deck, not the viewer's
|
||||
self.owner.save(update_fields=["equipped_deck"])
|
||||
self.invite.token_deposited_at = timezone.now()
|
||||
self.invite.voice_until = timezone.now() + timedelta(hours=24)
|
||||
self.invite.save()
|
||||
html = self.client.get(self.url).content.decode()
|
||||
self.assertIn("my-sea-stacks-wrap--visit", html)
|
||||
# Dubbodeck → two named stacks.
|
||||
self.assertIn("sea-deck-stack--gravity", html)
|
||||
self.assertIn("sea-deck-stack--levity", html)
|
||||
# FLIP is the disabled (×) state; never an enabled FLIP for a visitor.
|
||||
self.assertIn("sea-stack-ok btn-disabled", html)
|
||||
self.assertNotIn(
|
||||
'class="btn btn-reveal sea-stack-ok" type="button">FLIP', html)
|
||||
|
||||
def test_stack_keys_on_owner_deck_not_viewer_deck(self):
|
||||
# Viewer has no deck, owner has one → the stack still renders (it's the
|
||||
# owner's deck in play).
|
||||
from apps.epic.models import DeckVariant
|
||||
self.bud.equipped_deck = None
|
||||
self.bud.save(update_fields=["equipped_deck"])
|
||||
self.owner.equipped_deck = DeckVariant.objects.first()
|
||||
self.owner.save(update_fields=["equipped_deck"])
|
||||
self.invite.token_deposited_at = timezone.now()
|
||||
self.invite.voice_until = timezone.now() + timedelta(hours=24)
|
||||
self.invite.save()
|
||||
html = self.client.get(self.url).content.decode()
|
||||
self.assertIn("my-sea-stacks-wrap--visit", html)
|
||||
|
||||
def test_no_stack_when_owner_has_no_deck(self):
|
||||
self.owner.equipped_deck = None
|
||||
self.owner.save(update_fields=["equipped_deck"])
|
||||
self.invite.token_deposited_at = timezone.now()
|
||||
self.invite.voice_until = timezone.now() + timedelta(hours=24)
|
||||
self.invite.save()
|
||||
html = self.client.get(self.url).content.decode()
|
||||
self.assertNotIn("my-sea-stacks-wrap--visit", html)
|
||||
|
||||
def test_not_present_does_not_render_cross_or_deck_json(self):
|
||||
# Before deposit (gate view) there's no draw surface to seed.
|
||||
content = self.client.get(self.url).content.decode()
|
||||
|
||||
@@ -795,6 +795,40 @@ body.page-gameboard {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
// Spectator (bud-sea) deck stack — the VISITOR's own deck, pinned TOP-LEFT
|
||||
// (across the table from the owner, who deals bottom-right). FLIP renders
|
||||
// disabled server-side (read-only). For a dubbodeck the Gravity/Levity
|
||||
// `.sea-stack-name` flips ABOVE the face + upside-down, signalling that
|
||||
// someone across the table is dealing. (user-spec 2026-05-29)
|
||||
.my-sea-stacks-wrap.my-sea-stacks-wrap--visit {
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
bottom: auto;
|
||||
right: auto;
|
||||
|
||||
// Name above the face (DOM order is face → name) + rotated 180°.
|
||||
.sea-deck-stack { flex-direction: column-reverse; }
|
||||
.sea-stack-name {
|
||||
transform: scaleY(1.2) rotate(180deg);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
// Read-only FLIP (disabled ×) — hidden until its stack is hovered/focused,
|
||||
// then eases in: same reveal shape as the shared `flip-btn-base` mixin
|
||||
// (opacity 0→1 over 0.3s ease), inlined here because _gameboard.scss is
|
||||
// imported before _card-deck.scss (where the mixin lives). Stays non-
|
||||
// interactive — `.btn-disabled` keeps pointer-events:none. The base
|
||||
// `.sea-stack-ok` positioning is untouched (we only add the fade).
|
||||
.sea-stack-ok {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.sea-deck-stack:hover .sea-stack-ok,
|
||||
.sea-deck-stack:focus-within .sea-stack-ok {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Iter 4b: Brief banner + DEL guard portal ─────────────────────────────────
|
||||
// Both reuse shared chrome: the Brief is `.note-banner` from note.js
|
||||
// (portaled atop h2 w. Gaussian glass); the DEL guard is `#id_guard_portal`
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{# Deck stack — mono (single DECK) or dubbo (DECKS: Gravity + Levity), per #}
|
||||
{# `deck.is_polarized`. Shared by the owner picker (my_sea.html) AND the #}
|
||||
{# read-only visitor cross (_my_sea_visit_cross.html). `deck` = the #}
|
||||
{# DeckVariant to render; `flip_disabled` forces the FLIP btn to its #}
|
||||
{# disabled (×) state — owner: hand_complete; visitor: always (a read-only #}
|
||||
{# spectator can't deal). Markup is byte-identical to the owner's prior #}
|
||||
{# inline stack so existing assertions hold. #}
|
||||
{% if deck.is_polarized %}
|
||||
<div class="sea-stacks">
|
||||
<span class="sea-stacks-label">DECKS</span>
|
||||
<div class="sea-deck-stack sea-deck-stack--gravity">
|
||||
<div class="sea-stack-face">
|
||||
<button class="btn btn-reveal sea-stack-ok{% if flip_disabled %} btn-disabled{% endif %}" type="button">{% if flip_disabled %}×{% else %}FLIP{% endif %}</button>
|
||||
</div>
|
||||
<span class="sea-stack-name">Gravity</span>
|
||||
</div>
|
||||
<div class="sea-deck-stack sea-deck-stack--levity">
|
||||
<div class="sea-stack-face">
|
||||
<button class="btn btn-reveal sea-stack-ok{% if flip_disabled %} btn-disabled{% endif %}" type="button">{% if flip_disabled %}×{% else %}FLIP{% endif %}</button>
|
||||
</div>
|
||||
<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 deck.has_card_images %}
|
||||
<img class="sea-stack-face-img" src="{{ deck.back_image_url }}" alt="">
|
||||
{% endif %}
|
||||
<button class="btn btn-reveal sea-stack-ok{% if flip_disabled %} btn-disabled{% endif %}" type="button">{% if flip_disabled %}×{% else %}FLIP{% endif %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -49,5 +49,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{# Deck stack — the OWNER's deck (everyone at @owner's sea plays the #}
|
||||
{# owner's deck), mirrored to the TOP-LEFT for the visitor sitting across #}
|
||||
{# from the owner, who deals bottom-right. FLIP is always disabled #}
|
||||
{# (read-only); a dubbodeck's Gravity/Levity name flips above + upside- #}
|
||||
{# down via `--visit` to signal someone across the table is dealing. #}
|
||||
{% if owner.equipped_deck %}
|
||||
<div class="my-sea-stacks-wrap my-sea-stacks-wrap--visit">
|
||||
{% include "apps/gameboard/_partials/_my_sea_deck_stack.html" with deck=owner.equipped_deck flip_disabled=True %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "apps/gameboard/_partials/_sea_stage.html" %}
|
||||
</div>
|
||||
|
||||
@@ -186,35 +186,7 @@
|
||||
{# collapses to a single unnamed stack (DECKS → DECK). #}
|
||||
{# FLIP btn picks up `.btn-disabled` once hand_complete. #}
|
||||
<div class="my-sea-stacks-wrap">
|
||||
{% 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">
|
||||
<div class="sea-stack-face">
|
||||
<button class="btn btn-reveal sea-stack-ok{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}×{% else %}FLIP{% endif %}</button>
|
||||
</div>
|
||||
<span class="sea-stack-name">Gravity</span>
|
||||
</div>
|
||||
<div class="sea-deck-stack sea-deck-stack--levity">
|
||||
<div class="sea-stack-face">
|
||||
<button class="btn btn-reveal sea-stack-ok{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}×{% else %}FLIP{% endif %}</button>
|
||||
</div>
|
||||
<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{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}×{% else %}FLIP{% endif %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "apps/gameboard/_partials/_my_sea_deck_stack.html" with deck=request.user.equipped_deck flip_disabled=hand_complete %}
|
||||
</div>
|
||||
|
||||
{# Spread modal — opens on #id_sea_btn click (burger fan). #}
|
||||
|
||||
Reference in New Issue
Block a user