Compare commits

...

3 Commits

Author SHA1 Message Date
Disco DeDisco
d50645b216 sea deck stack: rename the stale .sea-stack-ok class to .sea-stack-flip (the btn renders FLIP now) — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful
Pure rename, no behavior change — the deck-stack reveal btn kept the name `.sea-stack-ok` from when it said OK; it has rendered FLIP for a while. Renamed across all 8 references: _card-deck.scss (reveal + pointer-events rules) + a _gameboard.scss comment; the 3 templates that emit or query it (_sea_overlay.html, _my_sea_deck_stack.html, my_sea.html inline JS); and the 3 tests that select it (test_game_room_select_sea FT, test_game_my_sea FT, test_sea_visit IT rendered-HTML asserts). Verified 47 green across all three surfaces (visit IT + gameroom PickSeaDeal stack FT + my_sea CardDraw FT).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:04:51 -04:00
Disco DeDisco
c9fc5a2fd4 sea-select deal FT: scroll the revealed FLIP btn into view before the is_displayed assert — TDD
test_clicking_stack_shows_ok_btn is the only PickSeaDealTest that pairs _choose_spread w. an is_displayed() assert (the six-draws test checks the class instead). Once a spread is OK'd the felt scroll-snaps onto two pages + the deck stacks sit on the page the post-OK rAF scroll-to-cross targets — but headless Firefox DROPS that delayed scroll, leaving the clicked stack's revealed FLIP btn off the visible page → is_displayed False. scrollIntoView the btn first so the assert reflects the `--active` reveal, not the scroll position. Surfaced in CI build 374's channels stage (a flake that 373 survived on retry; my bd9155c preview edits only shifted the timing).

[[feedback-headless-delayed-scroll-dropped]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:00:51 -04:00
Disco DeDisco
1a83c5f01c sea stage: gate the FLIP-back 0.3 polarity tint to the cloned dubbodeck (Sea Select); my_sea/visit monodeck backs render un-tinted
- the `.sea-stage--gravity/levity .sea-stage-card.is-flipped-to-back::after` 0.3 fill differentiates the two halves of a monodeck CLONED into a two-toned dubbodeck — only meaningful in Sea Select. my_sea / my_sea_visit draw a single monodeck that is never cloned, so the tint was just noise on their FLIP'd card-back
- the shared `_sea_stage.html` now takes a `sea_stage_dubbo` flag; only `_sea_overlay.html` (the gameroom Sea Select felt) passes it → `.sea-stage--dubbo` on #id_sea_stage. The two tint rules gate on `.sea-stage--dubbo.sea-stage--gravity/levity`; non-dubbo stages fall through to the empty, fill-less base `::after` (no visible overlay)

[[project-deck-segment-model]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:29:04 -04:00
9 changed files with 47 additions and 30 deletions

View File

@@ -165,9 +165,9 @@ class MySeaVisitContextTest(TestCase):
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.assertIn("sea-stack-flip btn-disabled", html)
self.assertNotIn(
'class="btn btn-reveal sea-stack-ok" type="button">FLIP', html)
'class="btn btn-reveal sea-stack-flip" 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

View File

@@ -756,7 +756,7 @@ class MySeaCardDrawTest(FunctionalTest):
)
stack.click()
flip = self.wait_for(
lambda: stack.find_element(By.CSS_SELECTOR, ".sea-stack-ok")
lambda: stack.find_element(By.CSS_SELECTOR, ".sea-stack-flip")
)
self.wait_for(lambda: self.assertTrue(flip.is_displayed()))
flip.click()

View File

@@ -299,9 +299,18 @@ class PickSeaDealTest(ChannelsFunctionalTest):
))
self.browser.execute_script("arguments[0].click()", stack)
ok_btn = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok"
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-flip"
))
self.assertTrue(ok_btn.is_displayed())
# Choosing the spread scroll-snaps the felt onto two pages; the deck
# stacks sit on a different page than the post-OK scroll lands on in
# headless (the rAF scroll-to-cross is dropped, see
# [[feedback-headless-delayed-scroll-dropped]]), leaving the revealed OK
# btn off the visible page. Pull it into view so is_displayed reflects
# the `--active` reveal, not the scroll position.
self.browser.execute_script(
"arguments[0].scrollIntoView({block: 'center'})", ok_btn
)
self.wait_for(lambda: self.assertTrue(ok_btn.is_displayed()))
def test_clicking_elsewhere_hides_ok_btn(self):
"""Clicking outside a focused stack dismisses the OK btn."""
@@ -311,14 +320,14 @@ class PickSeaDealTest(ChannelsFunctionalTest):
))
self.browser.execute_script("arguments[0].click()", stack)
self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok"
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-flip"
).is_displayed())
# Click the sea cards column (not a stack)
col = self.browser.find_element(By.CSS_SELECTOR, ".sea-cards-col")
self.browser.execute_script("arguments[0].click()", col)
self.wait_for(lambda: not any(
el.is_displayed()
for el in self.browser.find_elements(By.CSS_SELECTOR, ".sea-stack-ok")
for el in self.browser.find_elements(By.CSS_SELECTOR, ".sea-stack-flip")
))
# ── Card draw ─────────────────────────────────────────────────────── #
@@ -331,7 +340,7 @@ class PickSeaDealTest(ChannelsFunctionalTest):
))
self.browser.execute_script("arguments[0].click()", stack)
ok_btn = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok"
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-flip"
))
self.browser.execute_script("arguments[0].click()", ok_btn)
self.wait_for(lambda: self.browser.find_element(
@@ -352,7 +361,7 @@ class PickSeaDealTest(ChannelsFunctionalTest):
))
self.browser.execute_script("arguments[0].click()", stack)
ok_btn = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok"
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-flip"
))
self.browser.execute_script("arguments[0].click()", ok_btn)
@@ -361,7 +370,7 @@ class PickSeaDealTest(ChannelsFunctionalTest):
self.wait_for(lambda: self.assertIn(
"btn-disabled",
self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok"
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-flip"
).get_attribute("class"),
))
@@ -374,7 +383,7 @@ class PickSeaDealTest(ChannelsFunctionalTest):
))
self.browser.execute_script("arguments[0].click()", stack)
ok_btn = self.wait_for(lambda: self.browser.find_element(
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-ok"
By.CSS_SELECTOR, ".sea-deck-stack--levity .sea-stack-flip"
))
self.browser.execute_script("arguments[0].click()", ok_btn)
self.wait_for(lambda: self.browser.find_element(

View File

@@ -2046,7 +2046,7 @@ $_sea-card-glow: 0 0 0.5rem 0.5rem rgba(var(--ninUser), 0.3), 0 0 0.4rem rgba(0,
z-index: 1; // sits above the name label
}
.sea-stack-ok {
.sea-stack-flip {
position: absolute;
top: 50%;
left: 0;
@@ -2064,9 +2064,9 @@ $_sea-card-glow: 0 0 0.5rem 0.5rem rgba(var(--ninUser), 0.3), 0 0 0.4rem rgba(0,
pointer-events: none;
transition: opacity 0.3s ease;
}
.sea-deck-stack:hover .sea-stack-ok,
.sea-deck-stack:focus-within .sea-stack-ok,
.sea-deck-stack--active .sea-stack-ok { opacity: 1; }
.sea-deck-stack:hover .sea-stack-flip,
.sea-deck-stack:focus-within .sea-stack-flip,
.sea-deck-stack--active .sea-stack-flip { opacity: 1; }
// Interactivity is gated on the PERSIST state (`--active`), NOT hover. Hover is
// a purely VISUAL preview — same as the spectator's disabled FLIP, whose "hover
// effect" is just the reveal. Keeping the FLIP click-through on hover preserves
@@ -2074,7 +2074,7 @@ $_sea-card-glow: 0 0 0.5rem 0.5rem rgba(var(--ninUser), 0.3), 0 0 0.4rem rgba(0,
// Were it clickable on hover, a single stack-click would land on the centred
// FLIP itself + deal early. The spectator's FLIP stays `.btn-disabled` (read-
// only) so it never becomes interactive even while persisted.
.sea-deck-stack--active .sea-stack-ok:not(.btn-disabled) { pointer-events: auto; }
.sea-deck-stack--active .sea-stack-flip:not(.btn-disabled) { pointer-events: auto; }
.sea-deck-stack { gap: 0; } // remove gap so name slides under the face
@@ -2354,10 +2354,15 @@ $_sea-title-els: '.fan-card-name, .sig-qualifier-above, .sig-qualifier-below, .f
border-radius: 0.4rem;
}
}
.sea-stage--gravity .sea-stage-card.is-flipped-to-back {
// The 0.3 fill only earns its keep where a monodeck is CLONED into a two-toned
// gravity/levity dubbodeck (Sea Select — `.sea-stage--dubbo`, flagged by
// _sea_overlay.html) and the two stacks must read apart. my_sea / my_sea_visit
// draw from a single monodeck that is never cloned, so their flipped back stays
// un-tinted (no `--dubbo` flag → only the empty, fill-less base `::after` above).
.sea-stage--dubbo.sea-stage--gravity .sea-stage-card.is-flipped-to-back {
&::after { background: rgba(var(--quiUser), 0.3); }
}
.sea-stage--levity .sea-stage-card.is-flipped-to-back {
.sea-stage--dubbo.sea-stage--levity .sea-stage-card.is-flipped-to-back {
&::after { background: rgba(var(--terUser), 0.3); }
}

View File

@@ -954,7 +954,7 @@ body.page-gameboard {
transform-origin: center;
}
// FLIP reveal (hover-fade + click-persist) is now the SHARED `.sea-stack-ok`
// FLIP reveal (hover-fade + click-persist) is now the SHARED `.sea-stack-flip`
// behaviour in _card-deck.scss — unified with the owner picker per user-spec
// 2026-05-30, so the visitor's prior hover-only override is gone. The
// spectator's click-persist (`.sea-deck-stack--active`) is wired in

View File

@@ -10,13 +10,13 @@
<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 %}&times;{% else %}FLIP{% endif %}</button>
<button class="btn btn-reveal sea-stack-flip{% if flip_disabled %} btn-disabled{% endif %}" type="button">{% if flip_disabled %}&times;{% 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 %}&times;{% else %}FLIP{% endif %}</button>
<button class="btn btn-reveal sea-stack-flip{% if flip_disabled %} btn-disabled{% endif %}" type="button">{% if flip_disabled %}&times;{% else %}FLIP{% endif %}</button>
</div>
<span class="sea-stack-name">Levity</span>
</div>
@@ -29,7 +29,7 @@
{% 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 %}&times;{% else %}FLIP{% endif %}</button>
<button class="btn btn-reveal sea-stack-flip{% if flip_disabled %} btn-disabled{% endif %}" type="button">{% if flip_disabled %}&times;{% else %}FLIP{% endif %}</button>
</div>
</div>
</div>

View File

@@ -131,13 +131,13 @@ via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
<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 %}&times;{% else %}FLIP{% endif %}</button>
<button class="btn btn-reveal sea-stack-flip{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}&times;{% 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 %}&times;{% else %}FLIP{% endif %}</button>
<button class="btn btn-reveal sea-stack-flip{% if hand_complete %} btn-disabled{% endif %}" type="button">{% if hand_complete %}&times;{% else %}FLIP{% endif %}</button>
</div>
<span class="sea-stack-name">Levity</span>
</div>
@@ -146,7 +146,10 @@ via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
</div>
{# Sea stage — portaled big-card viewer (position:fixed, escapes the snap). #}
{% include "apps/gameboard/_partials/_sea_stage.html" %}
{# `sea_stage_dubbo` flags the gameroom's cloned two-toned dubbodeck so the #}
{# FLIP-back polarity tint applies HERE only (my_sea/visit draw a monodeck #}
{# that is never cloned — no tint there). See _card-deck.scss. #}
{% include "apps/gameboard/_partials/_sea_stage.html" with sea_stage_dubbo=True %}
</div>
</div>
@@ -258,7 +261,7 @@ via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
}
function _setComplete(on, live) {
_locked = on;
overlay.querySelectorAll('.sea-deck-stack .sea-stack-ok').forEach(function (btn) {
overlay.querySelectorAll('.sea-deck-stack .sea-stack-flip').forEach(function (btn) {
btn.classList.toggle('btn-disabled', on);
btn.innerHTML = on ? '×' : 'FLIP';
});
@@ -337,7 +340,7 @@ via epic:sea_save. `?seat` threads the CARTE-selected seat onto the action URLs.
e.stopPropagation();
if (_activeStack === stack) _hideOk(); else _showOk(stack);
});
var ok = stack.querySelector('.sea-stack-ok');
var ok = stack.querySelector('.sea-stack-flip');
if (ok) {
ok.addEventListener('click', function (e) {
e.stopPropagation();

View File

@@ -6,7 +6,7 @@
{# #}
{# Shared by the gameroom SEA SELECT phase and the my-sea picker — same #}
{# HTML, same SeaDeal module bindings; only the parent overlay differs. #}
<div class="sea-stage" id="id_sea_stage" style="display:none">
<div class="sea-stage{% if sea_stage_dubbo %} sea-stage--dubbo{% endif %}" id="id_sea_stage" style="display:none">
<div class="sea-stage-backdrop"></div>
<div class="sea-stage-content">
<div class="sig-stage-card sea-stage-card">

View File

@@ -439,7 +439,7 @@
// draw, NOT at completion).
_locked = on;
picker.classList.toggle('my-sea-picker--locked', on);
picker.querySelectorAll('.sea-deck-stack .sea-stack-ok').forEach(function (btn) {
picker.querySelectorAll('.sea-deck-stack .sea-stack-flip').forEach(function (btn) {
btn.classList.toggle('btn-disabled', on);
// Mirrors DEL btn convention: disabled label is × (the
// template's initial render does the same), active is
@@ -477,7 +477,7 @@
if (_activeStack === stack) _hideOk();
else _showOk(stack);
});
var ok = stack.querySelector('.sea-stack-ok');
var ok = stack.querySelector('.sea-stack-flip');
if (ok) {
ok.addEventListener('click', function (e) {
e.stopPropagation();