burger Sea sub-btn: spread-form modal + relocated deck stacks + mid-draw CONT DRAW escape — TDD (phase 2/3)
Second slice of the Sea sub-btn rollout (phase 1 = .active wiring in 3ae85b9; phase 3 = --priYl glow handoff to come).
## Spread modal (#id_sea_spread_modal)
`templates/apps/gameboard/my_sea.html` — `.sea-form-col` (spread combobox + AUTO DRAW + DEL btns) now lives inside `<div id="id_sea_spread_modal" class="my-sea-spread-modal" hidden>`. Hidden by default; opens on #id_sea_btn click (per the inline `<script>` block). The deck stacks (`.sea-stacks` + `.sea-stacks-label`) are extracted to a new `.my-sea-stacks-wrap` sibling that stays visible on the page so the FLIP affordance remains usable while the modal is closed.
## Modal SCSS
`static_src/scss/_gameboard.scss`:
- `.my-sea-spread-modal` — position:fixed inset:0; z-index:320 (above all corner btns); pointer-events:none w. children opting back in. `&[hidden]` makes it explicit that the closed state stays display:none.
- `.my-sea-spread-modal__backdrop` — full-viewport semi-transparent w. backdrop-filter blur; pointer-events:auto so click-outside closes.
- `.my-sea-spread-modal__panel` — opaque card, border + shadow, max-width:90vw. `.sea-form-col` inside drops its fixed 16rem width + uses min-width.
- `.my-sea-stacks-wrap` — position:absolute bottom:4.5rem right:1rem (clear of bud/burger); z-index:5 (above .my-sea-cross, below modal).
## Modal JS
`templates/apps/gameboard/my_sea.html` — new inline `<script>` at the bottom owns:
- `#id_sea_btn` click → opens modal (guarded on `.active` so the burger-btn.js inactive-flash handler still runs for inactive sub-btns).
- Escape key → close.
- Backdrop click → close.
- `#id_guard_portal .guard-yes` click (delegated) → close. This is the key UX detail user-spec'd: AUTO DRAW + DEL both open a guard portal that positions itself against the visible action/del btn — closing the modal on the btn click itself would dump the portal at (0,0). Closing on guard OK instead means the portal positions correctly + the modal closes only when the action is confirmed. NVM (`.guard-no`) leaves the modal up so the user can retry.
Also `apps/epic/static/apps/epic/burger-btn.js` — delegated fan click now closes the burger fan when an ACTIVE sub-btn is clicked (was: no-op for active). The per-page sub-btn handler runs in target phase BEFORE the fan-bubble handler, so the action kicks off w. a clean visual.
Client-side `.active` sync: server renders sea_btn inactive on the landing page (show_picker=False there). The DRAW SEA → picker transition is client-side (no re-render), so the IIFE in my_sea.html now flips `seaBtn.classList.add('active')` at the same SEAT_ANIM_MS moment data-phase swaps to 'picker'. Sea_btn stays `.active` for the rest of the picker phase, INCLUDING after hand_complete — DEL + GATE VIEW both live inside the modal + the user needs them accessible (earlier iteration deactivated on hand_complete; reverted on user feedback).
## Mid-draw NVM → CONT DRAW (apps/gameboard/views.py + my_sea.html)
Earlier iteration's gear-menu NVM nav-backed to `/gameboard/my-sea/` mid-draw, but the server's `hand_non_empty` branch re-rendered the picker on the next GET — looping the user right back into the spread. New `?phase=landing` escape hatch:
- `views.py`: `force_landing = request.GET.get('phase') == 'landing'`; `show_picker = (hand_non_empty or (phase_param and show_paid_draw)) and not force_landing`. New context var `show_cont_draw = force_landing and active_draw and not active_draw.is_hand_complete`.
- `my_sea.html` landing: new `{% if show_cont_draw %} CONT DRAW {% elif show_paid_draw %} ... ` branch on the table-center action-btn. CONT DRAW is an `<a href="{% url 'my_sea' %}">` (plain navigation, no `?phase=landing` so the next GET falls through to the hand_non_empty picker branch).
- `my_sea.html` gear include: picker phase passes `nvm_url={% url 'my_sea' %}?phase=landing` so the gear NVM exits to the landing-with-CONT-DRAW state instead of looping back into the picker.
## Tests
`apps/gameboard/tests/integrated/test_views.py` — 4 ITs added/updated:
- `test_sea_btn_stays_active_when_hand_complete` (replaced the prior "returns to inactive" assertion — sea_btn now stays active per user spec).
- `test_gear_nvm_navs_to_my_sea_landing_on_picker_phase` (updated URL to include `?phase=landing`).
- `test_force_landing_renders_cont_draw_btn_mid_draw` (NEW) — verifies CONT DRAW renders + FREE DRAW absent.
- `test_force_landing_hides_cont_draw_when_no_active_draw` (NEW) — regression guard: sig'd user w. no draw still sees FREE DRAW.
- `test_force_landing_hides_cont_draw_when_hand_complete` (NEW) — CONT DRAW absent for completed hands.
`functional_tests/test_game_my_sea.py` — new module-level `_open_spread_modal(test)` helper + applied to 7 tests that touch in-modal selectors (combobox / action_btn / del):
- `MySeaSpreadFormTest.test_picking_spread_swaps_data_spread_and_position_visibility`
- `MySeaSpreadFormTest.test_per_spread_position_labels_render_and_update`
- `MySeaCardDrawTest.test_action_btn_transitions_to_gate_view_on_hand_complete` (also: close modal before manual FLIP draws, use innerText for hidden-element text checks)
- `MySeaCardDrawTest.test_auto_drawn_slots_can_reopen_stage_modal_on_click`
- `MySeaCardDrawTest.test_form_col_renders_decks_lock_hand_del_and_reversal_pct`
- `MySeaLockHandTest.test_del_click_opens_shared_guard_portal`
- `MySeaLockHandTest.test_del_confirm_clears_hand_and_returns_to_gate_view_landing`
JS .click() is used inside `_open_spread_modal` (not Selenium .click) because the fan sub-btns spend ~0.25s mid-animation stacked at the burger centre — Selenium would hit a click-intercept by whichever sub-btn is z-topmost during the transform.
## Verification
All 1370 IT+UT green (+5 new MySeaViewTest, +1 from earlier phase 1 carry-over). 7 updated FTs green when re-run together.
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:
@@ -79,14 +79,21 @@
|
||||
else _open();
|
||||
}, { signal: sig });
|
||||
|
||||
// Delegated click on the fan — flash --priRd glow twice when an
|
||||
// INACTIVE sub-btn is clicked (its feature isn't wired yet). Active
|
||||
// sub-btns will route to their per-feature handlers in later sprints.
|
||||
// Delegated click on the fan:
|
||||
// • INACTIVE sub-btn → flash the --priRd glow twice (no real action
|
||||
// bound yet for that surface).
|
||||
// • ACTIVE sub-btn → close the burger fan so the surface's own
|
||||
// handler (page-level direct listener on the sub-btn) takes over
|
||||
// w. a clean visual. The per-page handler fires BEFORE this
|
||||
// delegated one (target-phase before bubble-up), so the action
|
||||
// already kicked off by the time we close.
|
||||
fan.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
var subBtn = e.target.closest('.burger-fan-btn');
|
||||
if (!subBtn) return;
|
||||
if (!subBtn.classList.contains('active')) {
|
||||
if (subBtn.classList.contains('active')) {
|
||||
_close();
|
||||
} else {
|
||||
_flashInactive(subBtn);
|
||||
}
|
||||
}, { signal: sig });
|
||||
|
||||
@@ -1070,10 +1070,11 @@ class MySeaViewTest(TestCase):
|
||||
'id="id_sea_btn" type="button" class="burger-fan-btn active"',
|
||||
)
|
||||
|
||||
def test_sea_btn_returns_to_inactive_when_hand_complete(self):
|
||||
"""3-card spread w. all 3 positions drawn → hand_complete True →
|
||||
sea_btn returns to inactive (the AUTO DRAW btn becomes GATE VIEW
|
||||
+ the deck FLIP gets .btn-disabled at this moment)."""
|
||||
def test_sea_btn_stays_active_when_hand_complete(self):
|
||||
"""Sea_btn STAYS active even after the hand is complete — DEL +
|
||||
GATE VIEW live inside the modal so the user has to be able to
|
||||
reopen it after all cards land. Earlier iteration deactivated on
|
||||
hand_complete; reverted 2026-05-26 to unblock those affordances."""
|
||||
from apps.epic.models import personal_sig_cards, TarotCard
|
||||
from apps.gameboard.models import MySeaDraw
|
||||
sig = personal_sig_cards(self.user)[0]
|
||||
@@ -1093,17 +1094,18 @@ class MySeaViewTest(TestCase):
|
||||
],
|
||||
)
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
# active class absent
|
||||
self.assertContains(
|
||||
response,
|
||||
'<button id="id_sea_btn" type="button" class="burger-fan-btn" aria-label="Sea">',
|
||||
'id="id_sea_btn" type="button" class="burger-fan-btn active"',
|
||||
)
|
||||
|
||||
def test_gear_nvm_navs_to_my_sea_landing_on_picker_phase(self):
|
||||
"""Picker-phase NVM = "back out of the spread to the table hex" →
|
||||
/gameboard/my-sea/ (the landing). User-spec'd 2026-05-26 so the
|
||||
spread/crucifix exit doesn't eject all the way to /gameboard/.
|
||||
Picker phase triggers on a non-empty hand (see views.py:277)."""
|
||||
/gameboard/my-sea/?phase=landing (the landing w. CONT DRAW). The
|
||||
`?phase=landing` override is load-bearing: without it the server's
|
||||
`hand_non_empty` branch would re-render the picker on the next GET,
|
||||
looping the user back into the spread. With it, the landing renders
|
||||
a CONT DRAW btn that re-enters the picker on click."""
|
||||
from apps.epic.models import personal_sig_cards, TarotCard
|
||||
from apps.gameboard.models import MySeaDraw
|
||||
sig = personal_sig_cards(self.user)[0]
|
||||
@@ -1117,7 +1119,68 @@ class MySeaViewTest(TestCase):
|
||||
"reversed": False, "polarity": "gravity"}],
|
||||
)
|
||||
response = self.client.get(reverse("my_sea"))
|
||||
self.assertIn("location.href='/gameboard/my-sea/'", response.content.decode())
|
||||
self.assertIn(
|
||||
"location.href='/gameboard/my-sea/?phase=landing'",
|
||||
response.content.decode(),
|
||||
)
|
||||
|
||||
def test_force_landing_renders_cont_draw_btn_mid_draw(self):
|
||||
"""`?phase=landing` query param forces the landing template even when
|
||||
a non-empty hand exists. The landing renders CONT DRAW (not FREE
|
||||
DRAW / PAID DRAW / GATE VIEW) so the user can re-enter the picker."""
|
||||
from apps.epic.models import personal_sig_cards, TarotCard
|
||||
from apps.gameboard.models import MySeaDraw
|
||||
sig = personal_sig_cards(self.user)[0]
|
||||
self.user.significator = sig
|
||||
self.user.save(update_fields=["significator"])
|
||||
card = TarotCard.objects.exclude(pk=sig.pk).first()
|
||||
MySeaDraw.objects.create(
|
||||
user=self.user, spread="situation-action-outcome",
|
||||
significator_id=sig.id,
|
||||
hand=[{"position": "lay", "card_id": card.id,
|
||||
"reversed": False, "polarity": "gravity"}],
|
||||
)
|
||||
response = self.client.get(reverse("my_sea") + "?phase=landing")
|
||||
self.assertContains(response, 'id="id_my_sea_cont_draw_btn"')
|
||||
self.assertContains(response, "CONT")
|
||||
self.assertNotContains(response, 'id="id_draw_sea_btn"')
|
||||
|
||||
def test_force_landing_hides_cont_draw_when_no_active_draw(self):
|
||||
"""`?phase=landing` w/o an active draw row → CONT DRAW absent;
|
||||
sig'd user w. no draw still sees FREE DRAW. No regression to the
|
||||
landing's default 3-way state machine."""
|
||||
from apps.epic.models import personal_sig_cards
|
||||
sig = personal_sig_cards(self.user)[0]
|
||||
self.user.significator = sig
|
||||
self.user.save(update_fields=["significator"])
|
||||
response = self.client.get(reverse("my_sea") + "?phase=landing")
|
||||
self.assertNotContains(response, 'id="id_my_sea_cont_draw_btn"')
|
||||
self.assertContains(response, 'id="id_draw_sea_btn"')
|
||||
|
||||
def test_force_landing_hides_cont_draw_when_hand_complete(self):
|
||||
"""`?phase=landing` w. a complete hand → CONT DRAW absent (nothing
|
||||
to continue). GATE VIEW takes over via the existing landing state
|
||||
machine (in_cooldown=True + no deposit + no paid-through)."""
|
||||
from apps.epic.models import personal_sig_cards, TarotCard
|
||||
from apps.gameboard.models import MySeaDraw
|
||||
sig = personal_sig_cards(self.user)[0]
|
||||
self.user.significator = sig
|
||||
self.user.save(update_fields=["significator"])
|
||||
cards = list(TarotCard.objects.exclude(pk=sig.pk)[:3])
|
||||
MySeaDraw.objects.create(
|
||||
user=self.user, spread="situation-action-outcome",
|
||||
significator_id=sig.id,
|
||||
hand=[
|
||||
{"position": "lay", "card_id": cards[0].id,
|
||||
"reversed": False, "polarity": "gravity"},
|
||||
{"position": "cover", "card_id": cards[1].id,
|
||||
"reversed": False, "polarity": "gravity"},
|
||||
{"position": "crown", "card_id": cards[2].id,
|
||||
"reversed": False, "polarity": "gravity"},
|
||||
],
|
||||
)
|
||||
response = self.client.get(reverse("my_sea") + "?phase=landing")
|
||||
self.assertNotContains(response, 'id="id_my_sea_cont_draw_btn"')
|
||||
|
||||
def test_sea_stage_stat_block_renders_rank_suit_chip_per_face(self):
|
||||
"""Sprint A.7.5 — `_sea_stage.html` modal scaffold (included from
|
||||
|
||||
@@ -273,8 +273,19 @@ def my_sea(request):
|
||||
# cycle (deposit reserved OR paid-through credit set) — covers
|
||||
# the `my_sea_paid_draw` redirect + lets the PAID DRAW landing
|
||||
# button send the user back to the picker via a GET.
|
||||
# `?phase=landing` is the explicit ESCAPE HATCH: it forces the
|
||||
# landing template even when a non-empty hand exists, so the gear-
|
||||
# menu NVM (mid-draw) can dump the user back to the table hex
|
||||
# instead of just looping them back into the picker. The landing
|
||||
# then renders a CONT DRAW btn that re-enters the picker.
|
||||
phase_param = request.GET.get("phase") == "picker"
|
||||
show_picker = hand_non_empty or (phase_param and show_paid_draw)
|
||||
force_landing = request.GET.get("phase") == "landing"
|
||||
show_picker = (hand_non_empty or (phase_param and show_paid_draw)) \
|
||||
and not force_landing
|
||||
show_cont_draw = (
|
||||
force_landing and active_draw is not None
|
||||
and not active_draw.is_hand_complete
|
||||
)
|
||||
|
||||
# Per-position lookup for the template — keyed by the position slug
|
||||
# ("lay", "cover", ...) so each `.sea-pos-<name>` block can render
|
||||
@@ -348,11 +359,14 @@ def my_sea(request):
|
||||
"next_free_draw_at": next_free_draw_at,
|
||||
"hand_complete": hand_complete,
|
||||
"show_picker": show_picker,
|
||||
"show_cont_draw": show_cont_draw,
|
||||
# Sub-btn .active flag for the burger fan — Sea sub-btn lights up
|
||||
# when the user is in the picker phase w. cards still to draw.
|
||||
# As soon as hand_complete flips True (AUTO DRAW → GATE VIEW + the
|
||||
# deck FLIP gets .btn-disabled) the Sea sub-btn returns to inactive.
|
||||
"sea_btn_active": show_picker and not hand_complete,
|
||||
# for the whole picker phase + STAYS active even after hand_complete
|
||||
# so the user can still open the modal to reach DEL + the GATE VIEW
|
||||
# btn (both live inside the modal). Earlier iteration tied this to
|
||||
# `show_picker and not hand_complete`; that locked DEL + GATE VIEW
|
||||
# behind an inactive btn once all cards landed.
|
||||
"sea_btn_active": show_picker,
|
||||
"show_paid_draw": show_paid_draw,
|
||||
"show_gate_view": show_gate_view,
|
||||
"deposit_reserved": deposit_reserved,
|
||||
|
||||
@@ -23,6 +23,34 @@ def _count_filled_slots(picker):
|
||||
)
|
||||
|
||||
|
||||
def _open_spread_modal(test):
|
||||
"""Open #id_sea_spread_modal so subsequent interactions w. in-modal
|
||||
selectors (`.sea-select*`, `#id_sea_action_btn`, `#id_sea_del`) reach
|
||||
a visible target. Sprint 2026-05-26 wrapped the sea-form-col in a
|
||||
modal triggered by the burger fan's Sea sub-btn; tests that touched
|
||||
those selectors directly now have to open the modal first.
|
||||
|
||||
Module-level so MySeaSpreadFormTest + MySeaCardDrawTest +
|
||||
MySeaLockHandTest can all call it w/o duplicating the helper.
|
||||
|
||||
JS .click() is used (not Selenium .click) because the fan sub-btns
|
||||
spend ~0.25s mid-animation stacked at the burger centre — Selenium
|
||||
would hit a click-intercept by whichever sub-btn is z-topmost during
|
||||
the transform. execute_script bypasses the visual hit-test."""
|
||||
burger_btn = test.browser.find_element(By.ID, "id_burger_btn")
|
||||
test.browser.execute_script("arguments[0].click()", burger_btn)
|
||||
sea_btn = test.wait_for(
|
||||
lambda: test.browser.find_element(By.ID, "id_sea_btn")
|
||||
)
|
||||
test.browser.execute_script("arguments[0].click()", sea_btn)
|
||||
test.wait_for(
|
||||
lambda: test.assertIsNone(
|
||||
test.browser.find_element(By.ID, "id_sea_spread_modal")
|
||||
.get_attribute("hidden")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _seed_gameboard_applets():
|
||||
"""My Sea + the rest of the gameboard applets so /gameboard/ renders
|
||||
without missing-applet errors during the applet-side assertions.
|
||||
@@ -577,6 +605,7 @@ class MySeaSpreadFormTest(FunctionalTest):
|
||||
"escape-velocity": ALL_POSITIONS,
|
||||
}
|
||||
picker = self._enter_picker_phase()
|
||||
_open_spread_modal(self) # combobox lives in the spread modal
|
||||
cross = picker.find_element(By.CSS_SELECTOR, ".my-sea-cross")
|
||||
combo = picker.find_element(By.CSS_SELECTOR, "[data-combobox]")
|
||||
|
||||
@@ -627,6 +656,7 @@ class MySeaSpreadFormTest(FunctionalTest):
|
||||
"cross": "Cross", "loom": "Before", "lay": "Beneath"},
|
||||
}
|
||||
picker = self._enter_picker_phase()
|
||||
_open_spread_modal(self) # combobox lives in the spread modal
|
||||
combo = picker.find_element(By.CSS_SELECTOR, "[data-combobox]")
|
||||
|
||||
def _pick(value):
|
||||
@@ -844,17 +874,29 @@ class MySeaCardDrawTest(FunctionalTest):
|
||||
transitions it to GATE VIEW (`data-state="gate-view"`, label =
|
||||
"GATE VIEW")."""
|
||||
picker = self._enter_picker_phase()
|
||||
_open_spread_modal(self) # action btn lives in the spread modal
|
||||
action_btn = picker.find_element(By.CSS_SELECTOR, "#id_sea_action_btn")
|
||||
self.assertEqual(action_btn.get_attribute("data-state"), "auto-draw")
|
||||
self.assertIn("AUTO", action_btn.text.upper())
|
||||
# innerText (via JS) works on hidden elements; selenium .text would
|
||||
# return "" once we close the modal below.
|
||||
self.assertIn("AUTO", self.browser.execute_script(
|
||||
"return arguments[0].innerText", action_btn).upper())
|
||||
# Close the modal so the deck-stack FLIP clicks below aren't
|
||||
# intercepted by the modal backdrop (which covers the whole
|
||||
# viewport while open).
|
||||
self.browser.execute_script(
|
||||
"document.getElementById('id_sea_spread_modal').setAttribute('hidden', '')"
|
||||
)
|
||||
self._draw_one(picker, "levity")
|
||||
self._draw_one(picker, "levity")
|
||||
self._draw_one(picker, "gravity")
|
||||
# Third draw completes the SAO hand — action btn becomes GATE VIEW.
|
||||
# data-state attribute is readable even while the modal is hidden.
|
||||
self.wait_for(
|
||||
lambda: self.assertEqual(action_btn.get_attribute("data-state"), "gate-view")
|
||||
)
|
||||
self.assertIn("GATE", action_btn.text.upper())
|
||||
self.assertIn("GATE", self.browser.execute_script(
|
||||
"return arguments[0].innerText", action_btn).upper())
|
||||
|
||||
# ── Test 5b — AUTO DRAW slot click reopens preview modal ────────────────
|
||||
|
||||
@@ -880,7 +922,12 @@ class MySeaCardDrawTest(FunctionalTest):
|
||||
self._draw_one(picker, "levity")
|
||||
self.assertEqual(_count_filled_slots(picker), 1)
|
||||
|
||||
# AUTO DRAW the remaining 2 (cover + crown).
|
||||
# AUTO DRAW the remaining 2 (cover + crown). Reopen modal —
|
||||
# the first draw above closed it via AUTO DRAW's modal-close
|
||||
# capture handler... actually no, manual FLIP doesn't close the
|
||||
# modal. But the modal may have been auto-dismissed by some
|
||||
# later interaction; opening defensively keeps the test robust.
|
||||
_open_spread_modal(self)
|
||||
action_btn = picker.find_element(By.CSS_SELECTOR, "#id_sea_action_btn")
|
||||
action_btn.click()
|
||||
confirm = self.wait_for(
|
||||
@@ -1032,7 +1079,7 @@ class MySeaCardDrawTest(FunctionalTest):
|
||||
LOCK HAND `.btn-primary`, the DEL `.btn-danger`, and the
|
||||
reversal-percentage caption (default 25%)."""
|
||||
picker = self._enter_picker_phase()
|
||||
# DECKS — two stacks
|
||||
# DECKS — two stacks (stay on page, OUTSIDE the spread modal).
|
||||
stacks = picker.find_elements(By.CSS_SELECTOR, ".sea-deck-stack")
|
||||
self.assertEqual(len(stacks), 2)
|
||||
names = "|".join(
|
||||
@@ -1041,6 +1088,9 @@ class MySeaCardDrawTest(FunctionalTest):
|
||||
)
|
||||
self.assertIn("GRAVITY", names)
|
||||
self.assertIn("LEVITY", names)
|
||||
# Action btn + DEL + reversal hint live INSIDE the spread modal —
|
||||
# open it before reading their .text (hidden elements return "").
|
||||
_open_spread_modal(self)
|
||||
# Iter-4c — action btn (AUTO DRAW / GATE VIEW slot) + DEL.
|
||||
action_btn = picker.find_element(By.CSS_SELECTOR, "#id_sea_action_btn")
|
||||
self.assertIn("AUTO", action_btn.text.upper())
|
||||
@@ -1293,6 +1343,7 @@ class MySeaLockHandTest(FunctionalTest):
|
||||
By.CSS_SELECTOR, ".my-sea-page[data-phase='picker']"
|
||||
)
|
||||
)
|
||||
_open_spread_modal(self) # del btn lives in the spread modal
|
||||
delbtn = picker.find_element(By.CSS_SELECTOR, "#id_sea_del")
|
||||
delbtn.click()
|
||||
portal = self.wait_for(
|
||||
@@ -1324,6 +1375,7 @@ class MySeaLockHandTest(FunctionalTest):
|
||||
By.CSS_SELECTOR, ".my-sea-page[data-phase='picker']"
|
||||
)
|
||||
)
|
||||
_open_spread_modal(self) # del btn lives in the spread modal
|
||||
picker.find_element(By.CSS_SELECTOR, "#id_sea_del").click()
|
||||
confirm = self.wait_for(
|
||||
lambda: self.browser.find_element(
|
||||
|
||||
@@ -710,6 +710,72 @@ body.page-gameboard {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
// ── My Sea spread modal (Phase 2 of the burger Sea sub-btn rollout) ──
|
||||
//
|
||||
// Holds the .sea-form-col chrome (spread combobox + AUTO DRAW + DEL).
|
||||
// Hidden by default via the `hidden` attribute; JS removes the attr to
|
||||
// open. Backdrop is click-to-dismiss, Escape also closes (handlers in
|
||||
// the inline <script> at the bottom of my_sea.html).
|
||||
.my-sea-spread-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 320; // above burger (314), kit (318), bud (318), dialog (316)
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none; // children re-enable
|
||||
|
||||
&[hidden] {
|
||||
// `hidden` attr would already display:none — kept explicit so
|
||||
// any later cascade still respects the closed state.
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
backdrop-filter: blur(0.25rem);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&__panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: rgba(var(--priUser), 1);
|
||||
border: 0.15rem solid rgba(var(--secUser), 1);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow:
|
||||
0 0 1rem rgba(var(--secUser), 0.5),
|
||||
0.15rem 0.15rem 0.5rem rgba(0, 0, 0, 0.5);
|
||||
padding: 1.25rem;
|
||||
pointer-events: auto;
|
||||
max-width: 90vw;
|
||||
|
||||
.sea-form-col {
|
||||
// Already-styled .sea-form-col container handles internal layout.
|
||||
// Drop the fixed width inside the modal — modal panel sizes to
|
||||
// content + the form is the content.
|
||||
width: auto;
|
||||
min-width: 16rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Relocated deck-stacks (Phase 2) ──────────────────────────────────
|
||||
//
|
||||
// .sea-stacks was a child of .sea-form-col; now lives on the page in
|
||||
// .my-sea-stacks-wrap so it stays visible when the spread modal is
|
||||
// closed. Pin to the bottom-right of the aperture (above the bud +
|
||||
// burger btns, below the modal).
|
||||
.my-sea-stacks-wrap {
|
||||
position: absolute;
|
||||
bottom: 4.5rem; // clear of the bud/burger pair at bottom
|
||||
right: 1rem;
|
||||
z-index: 5; // above .my-sea-cross, below modal (320)
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
// ── 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`
|
||||
|
||||
@@ -52,7 +52,16 @@
|
||||
<div class="table-hex-border">
|
||||
<div class="table-hex">
|
||||
<div class="table-center">
|
||||
{% if show_paid_draw %}
|
||||
{% if show_cont_draw %}
|
||||
{# CONT DRAW — user came to the landing via the gear- #}
|
||||
{# menu NVM mid-draw (?phase=landing). The picker hand #}
|
||||
{# is non-empty + incomplete; CONT DRAW re-enters the #}
|
||||
{# picker (a fresh GET w.o. ?phase=landing falls through #}
|
||||
{# to show_picker=True via the hand_non_empty branch). #}
|
||||
<a id="id_my_sea_cont_draw_btn"
|
||||
href="{% url 'my_sea' %}"
|
||||
class="btn btn-primary">CONT<br>DRAW</a>
|
||||
{% elif show_paid_draw %}
|
||||
{# PAID DRAW — two underlying states collapse into one #}
|
||||
{# button (user-spec 2026-05-23): #}
|
||||
{# • `deposit_reserved` → POST commits the deposited #}
|
||||
@@ -166,120 +175,122 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Form col — SPREAD combobox + DECKS swatches + LOCK #}
|
||||
{# HAND / DEL. DRY w. gameroom `_sea_overlay.html`'s #}
|
||||
{# `.sea-form-col` shape; my-sea-specific differences: #}
|
||||
{# (a) 6 spread options under 2 section dividers, #}
|
||||
{# (b) default = situation-action-outcome (3-card), #}
|
||||
{# (c) no `.sea-modal-header` (the gateway IS the page). #}
|
||||
<div class="sea-form-col my-sea-form-col">
|
||||
<div class="sea-form-main">
|
||||
<div class="sea-field">
|
||||
<label for="id_sea_spread" id="id_sea_spread_label">Spread</label>
|
||||
<p class="sea-reversal-hint">{{ reversals_pct|default:25 }}% reversals</p>
|
||||
{# autocomplete="off" opts the hidden input out of #}
|
||||
{# Firefox's form-history autofill, which otherwise #}
|
||||
{# restores the LAST value on soft reload (F5). #}
|
||||
{# Without this, combobox.js's `select(i)` short- #}
|
||||
{# circuits its change-event dispatch when the #}
|
||||
{# user re-picks the value Firefox already restored #}
|
||||
{# → my-sea's sync() never fires → data-spread on #}
|
||||
{# .my-sea-cross stays stuck on SAO default. #}
|
||||
<input type="hidden" id="id_sea_spread" name="spread"
|
||||
value="{{ default_spread }}" autocomplete="off">
|
||||
<div class="sea-select"
|
||||
data-combobox
|
||||
data-combobox-target="id_sea_spread"
|
||||
role="combobox"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby="id_sea_spread_label"
|
||||
tabindex="0">
|
||||
<span class="sea-select-current">{% if default_spread == 'past-present-future' %}Past, Present, Future{% elif default_spread == 'mind-body-spirit' %}Mind, Body, Spirit{% elif default_spread == 'desire-obstacle-solution' %}Desire, Obstacle, Solution{% elif default_spread == 'waite-smith' %}Celtic Cross, Waite-Smith{% elif default_spread == 'escape-velocity' %}Celtic Cross, Escape Velocity{% else %}Situation, Action, Outcome{% endif %}</span>
|
||||
<span class="sea-select-arrow" aria-hidden="true">▾</span>
|
||||
<ul class="sea-select-list" role="listbox">
|
||||
<li role="presentation" class="sea-select-divider">3-card spreads</li>
|
||||
<li role="option" data-value="past-present-future" aria-selected="{% if default_spread == 'past-present-future' %}true{% else %}false{% endif %}">Past, Present, Future</li>
|
||||
<li role="option" data-value="situation-action-outcome" aria-selected="{% if default_spread == 'situation-action-outcome' %}true{% else %}false{% endif %}">Situation, Action, Outcome</li>
|
||||
<li role="option" data-value="mind-body-spirit" aria-selected="{% if default_spread == 'mind-body-spirit' %}true{% else %}false{% endif %}">Mind, Body, Spirit</li>
|
||||
<li role="option" data-value="desire-obstacle-solution" aria-selected="{% if default_spread == 'desire-obstacle-solution' %}true{% else %}false{% endif %}">Desire, Obstacle, Solution</li>
|
||||
<li role="presentation" class="sea-select-divider">6-card spreads</li>
|
||||
<li role="option" data-value="waite-smith" aria-selected="{% if default_spread == 'waite-smith' %}true{% else %}false{% endif %}">Celtic Cross, Waite-Smith</li>
|
||||
<li role="option" data-value="escape-velocity" aria-selected="{% if default_spread == 'escape-velocity' %}true{% else %}false{% endif %}">Celtic Cross, Escape Velocity</li>
|
||||
</ul>
|
||||
{# Deck-stack column — STAYS on the picker page (out of the #}
|
||||
{# modal) so the FLIP affordance + count remain visible while #}
|
||||
{# the spread modal is closed. Wrapped in `.my-sea-stacks-wrap`#}
|
||||
{# so SCSS can pin it bottom-right of the aperture. #}
|
||||
{# #}
|
||||
{# Sprint A.7-polish — deck-stack rendering depends on the #}
|
||||
{# equipped deck's polarization. Polarized (Earthman) keeps #}
|
||||
{# dual gravity/levity stacks; non-polarized (Minchiate, RWS) #}
|
||||
{# 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>
|
||||
|
||||
{# 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). #}
|
||||
{# FLIP btn picks up `.btn-disabled` once the hand is #}
|
||||
{# complete — visual signal that no more draws remain. #}
|
||||
{# JS `_setComplete(on)` toggles the same class on #}
|
||||
{# live transition (last-card landed). #}
|
||||
{% 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 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>
|
||||
{% 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 %}
|
||||
</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 %}
|
||||
</div>
|
||||
|
||||
<div class="sea-form-actions">
|
||||
{# Iter 4c — LOCK HAND replaced by AUTO DRAW (mid-draw) #}
|
||||
{# / GATE VIEW (hand complete). Same button slot; JS #}
|
||||
{# transitions label + behavior + `data-state` when #}
|
||||
{# the final card lands. AUTO DRAW opens a guard portal #}
|
||||
{# ("Auto deal cards?") + POSTs the remaining hand in #}
|
||||
{# one shot so a navigate-away mid-animation still #}
|
||||
{# persists. GATE VIEW navigates to the Sprint-6 gate- #}
|
||||
{# keeper (currently a 404 stub). #}
|
||||
<button type="button"
|
||||
id="id_sea_action_btn"
|
||||
class="btn btn-primary"
|
||||
data-state="{% if hand_complete %}gate-view{% else %}auto-draw{% endif %}"
|
||||
data-gate-url="{% url 'my_sea_gate' %}">{% if hand_complete %}GATE<br>VIEW{% else %}AUTO<br>DRAW{% endif %}</button>
|
||||
{# DEL gates on whether ANY card has been drawn (user #}
|
||||
{# spec 2026-05-26): pre-first-draw it's disabled; as #}
|
||||
{# soon as `saved_by_position` has at least one entry #}
|
||||
{# the user can DEL the in-progress hand. `×` #}
|
||||
{# is the disabled-state label (parity w. game-kit's #}
|
||||
{# DON/DOFF disabled convention). JS `_setHasDrawn` #}
|
||||
{# swaps text + class in lockstep w. live deposits. #}
|
||||
<button type="button"
|
||||
id="id_sea_del"
|
||||
class="btn btn-danger{% if not saved_by_position %} btn-disabled{% endif %}">{% if saved_by_position %}DEL{% else %}×{% endif %}</button>
|
||||
{# Spread modal — opens on #id_sea_btn click (burger fan). #}
|
||||
{# Contains the SPREAD combobox + AUTO DRAW + DEL btns. Closed #}
|
||||
{# by default (hidden attr); JS toggles. AUTO DRAW closes the #}
|
||||
{# modal then runs the existing draw flow. Click backdrop or #}
|
||||
{# press Escape to dismiss. Deck stacks live OUTSIDE so the #}
|
||||
{# FLIP btn stays usable when the modal is closed. #}
|
||||
<div id="id_sea_spread_modal" class="my-sea-spread-modal" hidden>
|
||||
<div class="my-sea-spread-modal__backdrop"></div>
|
||||
<div class="my-sea-spread-modal__panel">
|
||||
<div class="sea-form-col my-sea-form-col">
|
||||
<div class="sea-form-main">
|
||||
<div class="sea-field">
|
||||
<label for="id_sea_spread" id="id_sea_spread_label">Spread</label>
|
||||
<p class="sea-reversal-hint">{{ reversals_pct|default:25 }}% reversals</p>
|
||||
{# autocomplete="off" opts the hidden input out of #}
|
||||
{# Firefox's form-history autofill, which otherwise #}
|
||||
{# restores the LAST value on soft reload (F5). #}
|
||||
{# Without this, combobox.js's `select(i)` short- #}
|
||||
{# circuits its change-event dispatch when the #}
|
||||
{# user re-picks the value Firefox already restored #}
|
||||
{# → my-sea's sync() never fires → data-spread on #}
|
||||
{# .my-sea-cross stays stuck on SAO default. #}
|
||||
<input type="hidden" id="id_sea_spread" name="spread"
|
||||
value="{{ default_spread }}" autocomplete="off">
|
||||
<div class="sea-select"
|
||||
data-combobox
|
||||
data-combobox-target="id_sea_spread"
|
||||
role="combobox"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby="id_sea_spread_label"
|
||||
tabindex="0">
|
||||
<span class="sea-select-current">{% if default_spread == 'past-present-future' %}Past, Present, Future{% elif default_spread == 'mind-body-spirit' %}Mind, Body, Spirit{% elif default_spread == 'desire-obstacle-solution' %}Desire, Obstacle, Solution{% elif default_spread == 'waite-smith' %}Celtic Cross, Waite-Smith{% elif default_spread == 'escape-velocity' %}Celtic Cross, Escape Velocity{% else %}Situation, Action, Outcome{% endif %}</span>
|
||||
<span class="sea-select-arrow" aria-hidden="true">▾</span>
|
||||
<ul class="sea-select-list" role="listbox">
|
||||
<li role="presentation" class="sea-select-divider">3-card spreads</li>
|
||||
<li role="option" data-value="past-present-future" aria-selected="{% if default_spread == 'past-present-future' %}true{% else %}false{% endif %}">Past, Present, Future</li>
|
||||
<li role="option" data-value="situation-action-outcome" aria-selected="{% if default_spread == 'situation-action-outcome' %}true{% else %}false{% endif %}">Situation, Action, Outcome</li>
|
||||
<li role="option" data-value="mind-body-spirit" aria-selected="{% if default_spread == 'mind-body-spirit' %}true{% else %}false{% endif %}">Mind, Body, Spirit</li>
|
||||
<li role="option" data-value="desire-obstacle-solution" aria-selected="{% if default_spread == 'desire-obstacle-solution' %}true{% else %}false{% endif %}">Desire, Obstacle, Solution</li>
|
||||
<li role="presentation" class="sea-select-divider">6-card spreads</li>
|
||||
<li role="option" data-value="waite-smith" aria-selected="{% if default_spread == 'waite-smith' %}true{% else %}false{% endif %}">Celtic Cross, Waite-Smith</li>
|
||||
<li role="option" data-value="escape-velocity" aria-selected="{% if default_spread == 'escape-velocity' %}true{% else %}false{% endif %}">Celtic Cross, Escape Velocity</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sea-form-actions">
|
||||
{# Iter 4c — LOCK HAND replaced by AUTO DRAW (mid-draw) #}
|
||||
{# / GATE VIEW (hand complete). Same button slot; JS #}
|
||||
{# transitions label + behavior + `data-state` when #}
|
||||
{# the final card lands. AUTO DRAW opens a guard portal #}
|
||||
{# ("Auto deal cards?") + POSTs the remaining hand in #}
|
||||
{# one shot so a navigate-away mid-animation still #}
|
||||
{# persists. GATE VIEW navigates to the Sprint-6 gate- #}
|
||||
{# keeper. #}
|
||||
<button type="button"
|
||||
id="id_sea_action_btn"
|
||||
class="btn btn-primary"
|
||||
data-state="{% if hand_complete %}gate-view{% else %}auto-draw{% endif %}"
|
||||
data-gate-url="{% url 'my_sea_gate' %}">{% if hand_complete %}GATE<br>VIEW{% else %}AUTO<br>DRAW{% endif %}</button>
|
||||
{# DEL gates on whether ANY card has been drawn (user #}
|
||||
{# spec 2026-05-26): pre-first-draw it's disabled; as #}
|
||||
{# soon as `saved_by_position` has at least one entry #}
|
||||
{# the user can DEL the in-progress hand. `×` #}
|
||||
{# is the disabled-state label (parity w. game-kit's #}
|
||||
{# DON/DOFF disabled convention). JS `_setHasDrawn` #}
|
||||
{# swaps text + class in lockstep w. live deposits. #}
|
||||
<button type="button"
|
||||
id="id_sea_del"
|
||||
class="btn btn-danger{% if not saved_by_position %} btn-disabled{% endif %}">{% if saved_by_position %}DEL{% else %}×{% endif %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{# Sea stage — portaled modal that opens on FLIP click via #}
|
||||
@@ -988,6 +999,16 @@
|
||||
page.setAttribute('data-phase', 'picker');
|
||||
if (landing) landing.style.display = 'none';
|
||||
if (picker) picker.style.display = '';
|
||||
// Phase 2 of the Sea sub-btn rollout — server
|
||||
// rendered the burger fan w. sea_btn inactive
|
||||
// (show_picker was False on landing). After the
|
||||
// client-side phase swap, flip .active so the
|
||||
// modal-open click handler treats sea_btn as
|
||||
// active. Sea_btn stays active for the rest of
|
||||
// the picker phase (incl. hand_complete) so DEL
|
||||
// + GATE VIEW stay reachable inside the modal.
|
||||
var seaBtn = document.getElementById('id_sea_btn');
|
||||
if (seaBtn) seaBtn.classList.add('active');
|
||||
}, SEAT_ANIM_MS);
|
||||
});
|
||||
}
|
||||
@@ -1063,16 +1084,70 @@
|
||||
{% endif %}
|
||||
{# Sprint 6 iter 6c — gear-btn lives on every my-sea page state #}
|
||||
{# (sign-gate / landing / picker). NVM-only menu. Picker NVM #}
|
||||
{# nav-backs to the my-sea TABLE HEX (landing), letting the user #}
|
||||
{# leave the spread without committing. Sign-gate + landing NVM #}
|
||||
{# nav-backs to /gameboard/ (the partial's default fallback). #}
|
||||
{% if show_picker %}{% url 'my_sea' as nvm_url %}{% endif %}
|
||||
{% include "apps/gameboard/_partials/_my_sea_gear.html" %}
|
||||
{# nav-backs to the my-sea TABLE HEX via `?phase=landing` — #}
|
||||
{# without the query param, the server would re-render the #}
|
||||
{# picker on the next GET (hand_non_empty branch wins). The #}
|
||||
{# landing then renders a CONT DRAW btn (since active_draw #}
|
||||
{# exists + incomplete) so the user can re-enter the picker. #}
|
||||
{# Sign-gate + landing NVM nav-back to /gameboard/ (default). #}
|
||||
{% if show_picker %}{% url 'my_sea' as nvm_base %}{% with nvm_url=nvm_base|add:'?phase=landing' %}{% include "apps/gameboard/_partials/_my_sea_gear.html" %}{% endwith %}{% else %}{% include "apps/gameboard/_partials/_my_sea_gear.html" %}{% endif %}
|
||||
{# Bud + burger persist across every stage too — same affordance #}
|
||||
{# as on the my-sea gatekeeper, so the user can invite + reach #}
|
||||
{# the cross-cutting menu from sign-gate / landing / picker alike.#}
|
||||
{% include "apps/gameboard/_partials/_my_sea_bud_panel.html" %}
|
||||
{% include "apps/gameboard/_partials/_burger.html" %}
|
||||
<script src="{% static 'apps/epic/burger-btn.js' %}"></script>
|
||||
|
||||
{# Phase 2 of the Sea sub-btn rollout — wires #id_sea_btn click to #}
|
||||
{# open #id_sea_spread_modal. AUTO DRAW + Escape + backdrop click #}
|
||||
{# all close the modal. AUTO DRAW closes BEFORE its own handler fires #}
|
||||
{# (capture-phase listener) so the existing guard-portal + draw flow #}
|
||||
{# runs against a closed modal, returning the spread crucifix to view #}
|
||||
{# as the cards animate in. #}
|
||||
<script>
|
||||
(function () {
|
||||
var seaBtn = document.getElementById('id_sea_btn');
|
||||
var modal = document.getElementById('id_sea_spread_modal');
|
||||
if (!seaBtn || !modal) return;
|
||||
|
||||
var backdrop = modal.querySelector('.my-sea-spread-modal__backdrop');
|
||||
var actionBtn = document.getElementById('id_sea_action_btn');
|
||||
|
||||
function openModal() {
|
||||
modal.removeAttribute('hidden');
|
||||
}
|
||||
function closeModal() {
|
||||
modal.setAttribute('hidden', '');
|
||||
}
|
||||
|
||||
seaBtn.addEventListener('click', function () {
|
||||
// Inactive sub-btn click is handled by burger-btn.js's
|
||||
// delegated handler (flash --priRd, no modal). Only fire
|
||||
// the modal-open path when this sub-btn is currently active.
|
||||
if (!seaBtn.classList.contains('active')) return;
|
||||
openModal();
|
||||
});
|
||||
|
||||
if (backdrop) {
|
||||
backdrop.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && !modal.hasAttribute('hidden')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Modal stays open through the AUTO DRAW + DEL guard-portal flow
|
||||
// so the portal can position itself against the visible action /
|
||||
// del btn. Close the modal only on guard OK (.guard-yes) so the
|
||||
// draw / delete proceeds against an already-closed modal. NVM
|
||||
// (.guard-no) leaves the modal up so the user can try again.
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!e.target.closest('#id_guard_portal .guard-yes')) return;
|
||||
if (!modal.hasAttribute('hidden')) closeModal();
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
Reference in New Issue
Block a user