burger Sea sub-btn: first-draw --priYl glow handoff (phase 3/3) — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed

Final slice of the Sea sub-btn rollout (phase 1 = .active wiring 3ae85b9; phase 2 = modal extraction + CONT DRAW 6fbeed7). Adds a --priYl + --ninUser glow that rides the affordance chain to teach the user where to click pre-first-draw.

## The handoff chain

  burger  →  click  →  sea_btn  →  click  →  .sea-select  →  click  →  end

- Modal close (Esc / backdrop / DEL guard-OK) restarts the cycle on the burger.
- Burger fan close w/o sea_btn click ALSO restarts on the burger.
- AUTO DRAW guard-OK ends the cycle permanently (user found the path).
- `#id_sea_action_btn` data-state → 'gate-view' (last card landed via ANY path — AUTO DRAW or manual FLIP) ALSO ends permanently.

## SCSS

`static_src/scss/_burger.scss` — `.glow-handoff` on burger / sea_btn = --priYl color + border + --ninUser glow.
`static_src/scss/_gameboard.scss` — `.glow-handoff` on .sea-select = --terUser border + --ninUser glow (no font-color change per spec).

## Server side

`apps/gameboard/views.py` — new `sea_first_draw_pending = show_picker and not hand_non_empty`. True when picker is active w. an empty hand (paid-draw entry, or page reload of a freshly-entered picker). The FREE-DRAW → picker transition fires client-side w. show_picker=False on the rendered template, so the FREE DRAW JS handler seeds the burger glow itself in that path.

`templates/apps/gameboard/_partials/_burger.html` — `#id_burger_btn` conditionally renders `class="glow-handoff"` when `sea_first_draw_pending`.

`templates/apps/gameboard/my_sea.html` — FREE DRAW transition handler adds `.glow-handoff` to burger at the same SEAT_ANIM_MS moment data-phase swaps to 'picker' (covers the client-side path).

## JS state machine

`templates/apps/gameboard/my_sea.html` — new inline IIFE owns the .glow-handoff transitions:
- `burger.click` → if .glow-handoff on burger, transfer to sea_btn.
- `sea_btn.click` → if .glow-handoff on sea_btn, transfer to .sea-select.
- `.sea-select.click` → end this cycle (just clear the glow; cycle restarts on next modal open).
- AUTO DRAW guard-OK (via doc-level click listener) → sets `autoDrawConfirmed`.
- Modal `hidden`-attr observer: AUTO DRAW path → endPermanently; any other close (Esc / backdrop / DEL) → startOnBurger (skip if glow already permanently ended).
- Burger `class`-attr observer: fan closes (`.active` removed) while glow on sea_btn → restart on burger.
- `#id_sea_action_btn` `data-state`-attr observer: flips to 'gate-view' (last card landed via ANY path — AUTO DRAW finishing OR manual FLIP filling the final slot) → endPermanently.

The data-state observer makes the "stop glowing when all slots filled" guarantee async + decoupled from how the cards arrived.

## CONT DRAW polish (drag-in from prior commit's spec gap)

`apps/gameboard/views.py` — `show_cont_draw` now additionally requires `bool(active_draw.hand)` (at least one card drawn). Pre-draw NVM-to-landing falls through to the existing 3-way state machine (PAID DRAW / GATE VIEW / FREE DRAW) instead of misleading w. CONT DRAW that lands back on an empty picker.

## Tests (4 new ITs)

`apps/gameboard/tests/integrated/test_views.py::MySeaViewTest`:
- `test_burger_renders_glow_handoff_class_when_sea_first_draw_pending` — paid-draw entry to picker w. empty hand → burger has .glow-handoff.
- `test_burger_omits_glow_handoff_when_hand_non_empty` — mid-draw → no .glow-handoff.
- `test_burger_omits_glow_handoff_on_landing` — landing → no .glow-handoff (FREE DRAW handler seeds client-side instead).
- `test_force_landing_hides_cont_draw_when_hand_empty` — pre-first-draw NVM → no CONT DRAW.

(JS state-machine behaviour is verified visually; not Jasmine-tested since the IIFE lives inline on my_sea.html, not as a separate module.)

## Verification

All 1374 IT+UT green (+4 from Phase 3). Visual verification of glow handoff + hand-complete auto-end confirmed.

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-27 00:39:46 -04:00
parent a39053d3f6
commit c30b63cd5d
6 changed files with 240 additions and 3 deletions

View File

@@ -8,7 +8,7 @@
{# sub-btns are inactive; clicking an inactive sub-btn flashes a brief #}
{# --priRd glow (twice, fast cadence) — burger-btn.js owns the delegated #}
{# click + flash. #}
<button id="id_burger_btn" type="button" aria-label="Open burger menu" aria-expanded="false">
<button id="id_burger_btn" type="button" aria-label="Open burger menu" aria-expanded="false"{% if sea_first_draw_pending %} class="glow-handoff"{% endif %}>
<i class="fa-solid fa-burger"></i>
</button>
<div id="id_burger_fan" aria-hidden="true">

View File

@@ -1009,6 +1009,12 @@
// + GATE VIEW stay reachable inside the modal.
var seaBtn = document.getElementById('id_sea_btn');
if (seaBtn) seaBtn.classList.add('active');
// Phase 3 — the FREE DRAW path enters the picker
// w. an empty hand → first-draw pending. Start
// the glow handoff on the burger so the user
// knows where to click next.
var burgerBtn = document.getElementById('id_burger_btn');
if (burgerBtn) burgerBtn.classList.add('glow-handoff');
}, SEAT_ANIM_MS);
});
}
@@ -1149,5 +1155,120 @@
});
}());
</script>
{# Phase 3 — first-draw glow handoff state machine. Owner of the #}
{# .glow-handoff transitions across burger → sea_btn → sea-select → #}
{# end. Modal close w/o AUTO DRAW restarts at burger. AUTO DRAW #}
{# guard-OK ends the cycle permanently. The initial glow on burger #}
{# is set either server-side (see sea_first_draw_pending below) OR #}
{# client-side in the FREE DRAW handler that transitions data-phase #}
{# to 'picker'. #}
<script>
(function () {
var burgerBtn = document.getElementById('id_burger_btn');
var seaBtn = document.getElementById('id_sea_btn');
var modal = document.getElementById('id_sea_spread_modal');
if (!burgerBtn || !seaBtn || !modal) return;
var seaSelect = modal.querySelector('.sea-select');
var actionBtn = document.getElementById('id_sea_action_btn');
if (!seaSelect || !actionBtn) return;
var glowDone = false;
var autoDrawConfirmed = false;
// Set by ANY guard-YES (AUTO DRAW or DEL). Distinguishes "user
// confirmed a destructive action" from "user dismissed the
// modal w/o action". Latter restarts the glow on burger; former
// doesn't — a page redirect is en route (DEL) or AUTO DRAW
// anim is taking over (endPermanently). Without this flag, DEL
// briefly flashed burger w. glow before the post-DEL redirect.
var guardYesClosing = false;
function startOnBurger() {
if (glowDone) return;
burgerBtn.classList.add('glow-handoff');
seaBtn.classList.remove('glow-handoff');
seaSelect.classList.remove('glow-handoff');
}
function endPermanently() {
glowDone = true;
burgerBtn.classList.remove('glow-handoff');
seaBtn.classList.remove('glow-handoff');
seaSelect.classList.remove('glow-handoff');
}
// burger click → glow hands off to sea_btn (only if burger had it)
burgerBtn.addEventListener('click', function () {
if (glowDone) return;
if (!burgerBtn.classList.contains('glow-handoff')) return;
burgerBtn.classList.remove('glow-handoff');
seaBtn.classList.add('glow-handoff');
});
// sea_btn click → glow hands off to .sea-select (modal opens too,
// owned by the Phase 2 IIFE above — both run in target phase, no
// ordering coupling).
seaBtn.addEventListener('click', function () {
if (glowDone) return;
if (!seaBtn.classList.contains('glow-handoff')) return;
seaBtn.classList.remove('glow-handoff');
seaSelect.classList.add('glow-handoff');
});
// .sea-select click → cycle ends (this round). Glow doesn't move
// anywhere; it just clears. Cycle restarts if modal closes w/o
// AUTO DRAW (see MutationObserver below).
seaSelect.addEventListener('click', function () {
if (glowDone) return;
seaSelect.classList.remove('glow-handoff');
});
// AUTO DRAW guard-OK → mark the next modal-close as the permanent
// end (the draw is about to happen — user found the path).
document.addEventListener('click', function (e) {
if (!e.target.closest('#id_guard_portal .guard-yes')) return;
if (actionBtn.dataset.state === 'auto-draw') {
autoDrawConfirmed = true;
}
});
// Modal close handler — observes the `hidden` attribute. If the
// close was triggered by AUTO DRAW guard-OK, end the cycle for
// good. Otherwise restart on burger so the user gets another
// nudge cycle the next time they open the modal.
var observer = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var m = mutations[i];
if (m.attributeName !== 'hidden') continue;
if (!modal.hasAttribute('hidden')) continue;
if (autoDrawConfirmed) {
endPermanently();
autoDrawConfirmed = false;
} else if (!glowDone) {
startOnBurger();
}
}
});
observer.observe(modal, {attributes: true, attributeFilter: ['hidden']});
// Hand-complete observer — end the glow permanently when ALL
// slots are filled, regardless of how (AUTO DRAW or manual
// FLIP). #id_sea_action_btn's data-state flips to 'gate-view'
// at that exact moment (see the picker IIFE _setComplete).
// Async + decoupled from guard-YES tracking — works for both
// the AUTO DRAW path AND the all-manual-FLIP path.
var actionObserver = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var m = mutations[i];
if (m.attributeName !== 'data-state') continue;
if (actionBtn.dataset.state === 'gate-view') {
endPermanently();
}
}
});
actionObserver.observe(actionBtn, {attributes: true, attributeFilter: ['data-state']});
}());
</script>
</div>
{% endblock content %}