My Sea iter 4b polish: Brief banner uses standard portaled .note-banner (Gaussian glass atop h2); next-free-draw datetime in dedicated <time> slot (not "Invalid Date"); DEL guard reuses shared #id_guard_portal from base.html — TDD

UX refactor on top of iter 4b (b76d3c5) per user direction:

(1) Brief banner — replaced custom `.my-sea-brief` markup + SCSS w. a call to `Brief.showBanner` from note.js. Now matches the my-notes / my-sign default-deck-warning Briefs exactly: standard `.note-banner` portaled atop the h2 w. Gaussian-glass backdrop-filter blur. Tagged `.my-sea-locked-banner` for FT disambiguation only — no visual override.

(2) Brief timestamp — fix for "Invalid Date" rendering in note.js's `<time class="note-banner__timestamp">` slot. Previously passed `created_at: ''` to `Brief.showBanner` → `new Date('')` returns Invalid Date → `toLocaleDateString` renders "Invalid Date". Now passes the next-free-draw ISO timestamp as `created_at` (server emits via `|date:'c'`). After Brief.showBanner returns, the `_showFreeDrawLockedBrief` JS overwrites the rendered text w. the more detailed `D, M j @ g:i A` format ("Wed, May 20 @ 11:57 PM") — leaves the ISO `datetime=` attribute intact for accessibility. The `line_text` no longer carries the timestamp inline (it's redundant w. the dedicated slot).

(3) DEL guard portal — replaced custom `#id_my_sea_del_portal` fullscreen modal + `.my-sea-del-portal` SCSS w. a call to `window.showGuard` from base.html, targeting the shared `#id_guard_portal`. Same Gaussian-glass tooltip the room gear-menu DEL flow uses: no backdrop, positioned above the anchor button, standard `.btn-confirm OK` + `.btn-cancel NVM` pair. Bundled a non-breaking `options.yesLabel` extension to `show()` in base.html for future destructive flows that need a custom YES label (defaults to 'OK', resets on dismiss/confirm) — my-sea doesn't use it per user direction (the `.btn-confirm` class implies "OK"; destructive intent belongs on the trigger button, which is `.btn-danger DEL`).

Tests: 30 iter-4b ITs (model + lock + delete + saved-draw view branches) + 5 iter-4b FTs all green; IT/FT assertions updated to target the shared portal markup (`#id_guard_portal.active`, `.guard-yes`, `.guard-no`, `.note-banner.my-sea-locked-banner`).

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-20 00:12:52 -04:00
parent b76d3c5dff
commit c1a8133345
5 changed files with 165 additions and 204 deletions

View File

@@ -1116,26 +1116,45 @@ class MySeaViewWithSavedDrawTest(TestCase):
self.assertNotIn("my-sea-sign-gate", html) self.assertNotIn("my-sea-sign-gate", html)
self.assertIn('data-phase="picker"', html) self.assertIn('data-phase="picker"', html)
def test_view_renders_brief_banner_when_active_draw_exists(self): def test_view_triggers_brief_banner_when_active_draw_exists(self):
# Brief is rendered client-side via Brief.showBanner (standard
# `.note-banner` w. Gaussian-glass bg, portaled atop the h2 —
# same UX as my-notes / my-sign default-deck-warning Briefs).
# Server emits a `window._showFreeDrawLockedBrief("<iso>")` call
# gated on active_draw; ISO timestamp (`|date:'c'`) is re-used
# as both `created_at` AND the source for the human-formatted
# display string note.js renders in the `.note-banner__timestamp`
# slot — single source of truth, no "Invalid Date" on bad input.
response = self.client.get(reverse("my_sea")) response = self.client.get(reverse("my_sea"))
self.assertContains(response, "my-sea-brief") # Match the call form w. opening quote — the bare token
self.assertContains(response, "my-sea-brief__timestamp") # `_showFreeDrawLockedBrief(` also appears in the function
self.assertContains(response, "my-sea-brief__nvm") # definition emitted unconditionally inside the picker IIFE.
self.assertContains(response, 'window._showFreeDrawLockedBrief("')
# The ISO format produced by Django's `|date:'c'` starts with the
# full year + ISO-style T separator — pin a representative token.
from django.utils import timezone
from datetime import timedelta
expected_year = (timezone.now() + timedelta(hours=24)).strftime("%Y")
self.assertContains(response, '_showFreeDrawLockedBrief("' + expected_year)
def test_brief_banner_hidden_without_active_draw(self): def test_view_does_not_trigger_brief_banner_without_active_draw(self):
# Markup is rendered unconditionally so JS can un-hide it on LOCK # Definition of `_showFreeDrawLockedBrief` is always emitted;
# HAND POST success without a page reload. When no active_draw, # only the CALL is gated on active_draw. Pin the call form.
# the wrapping div carries `[hidden]` so the banner is invisible.
from apps.gameboard.models import MySeaDraw from apps.gameboard.models import MySeaDraw
MySeaDraw.objects.all().delete() MySeaDraw.objects.all().delete()
response = self.client.get(reverse("my_sea")) response = self.client.get(reverse("my_sea"))
self.assertContains(response, '<div class="my-sea-brief" hidden>') self.assertNotContains(response, 'window._showFreeDrawLockedBrief("')
def test_view_renders_del_guard_portal_when_active_draw_exists(self): def test_view_wires_del_button_to_shared_guard_portal_when_active_draw(self):
# No my-sea-specific guard markup — the picker IIFE calls
# `window.showGuard(delBtn, "Are you sure?", confirmFn)` which
# targets the shared #id_guard_portal from base.html (same
# tooltip the room gear-menu uses; standard OK/NVM button pair).
# Server emits the call site; we pin the call form + the delete
# URL it POSTs to.
response = self.client.get(reverse("my_sea")) response = self.client.get(reverse("my_sea"))
self.assertContains(response, 'id="id_my_sea_del_portal"') self.assertContains(response, "window.showGuard(")
self.assertContains(response, "my-sea-del-portal__confirm") self.assertContains(response, reverse("my_sea_delete"))
self.assertContains(response, "my-sea-del-portal__nvm")
def test_saved_hand_renders_as_filled_slots_in_picker(self): def test_saved_hand_renders_as_filled_slots_in_picker(self):
# Each saved position's slot is server-rendered as `--filled` w. # Each saved position's slot is server-rendered as `--filled` w.

View File

@@ -1096,35 +1096,45 @@ class MySeaLockHandTest(FunctionalTest):
# ── Test 3 ─────────────────────────────────────────────────────────────── # ── Test 3 ───────────────────────────────────────────────────────────────
def test_saved_draw_renders_brief_banner_with_next_free_draw_timestamp(self): def test_saved_draw_renders_brief_banner_with_next_free_draw_timestamp(self):
"""Post-lock UX: a Look!-formatted Brief banner above the picker """Post-lock UX: a Look!-formatted Brief banner appears atop the
informs the user when the next free draw is available + offers a h2 (standard portaled `.note-banner` w. Gaussian-glass bg, same
NVM button to dismiss. Mirrors the Brief banner shape from the styling as my-notes / my-sign default-deck-warning Briefs). The
Baltimorean Note unlock + the my-sign default-deck warning.""" next-free-draw timestamp lives in the dedicated `.note-banner__
timestamp` `<time>` slot (note.js's standard datetime element),
formatted by JS to `D, M j @ g:i A` shape — e.g. "Wed, May 20 @
11:57 PM". Tagged `.my-sea-locked-banner` so this FT disambiguates
from any other Briefs that may stack on the page."""
self._save_draw_for_user() self._save_draw_for_user()
self.create_pre_authenticated_session(self.email) self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/gameboard/my-sea/") self.browser.get(self.live_server_url + "/gameboard/my-sea/")
brief = self.wait_for( brief = self.wait_for(
lambda: self.browser.find_element( lambda: self.browser.find_element(
By.CSS_SELECTOR, ".my-sea-brief" By.CSS_SELECTOR, ".note-banner.my-sea-locked-banner"
) )
) )
text = brief.text text = brief.text
self.assertIn("Look!", text) self.assertIn("Look!", text)
self.assertIn("free draw", text.lower()) self.assertIn("free draw", text.lower())
# The timestamp is rendered inside a dedicated child so the JS # Timestamp slot owns the next-free-draw datetime. The "@" token
# NVM-dismiss handler can find + style it independently of the # in the `D, M j @ g:i A` format is a stable assertion target;
# surrounding copy. # also pin the year to confirm the source ISO parsed correctly
ts = brief.find_element(By.CSS_SELECTOR, ".my-sea-brief__timestamp") # (would render "Invalid Date" if note.js got an empty string).
self.assertTrue(ts.text.strip(), "brief should render a non-empty next-free-draw timestamp") ts = brief.find_element(By.CSS_SELECTOR, ".note-banner__timestamp")
# NVM button is present (Jasmine pins the dismiss-on-click). ts_text = ts.text
brief.find_element(By.CSS_SELECTOR, ".my-sea-brief__nvm") self.assertIn("@", ts_text)
self.assertNotIn("Invalid", ts_text)
# NVM dismiss button is wired by note.js itself.
brief.find_element(By.CSS_SELECTOR, ".note-banner__nvm")
# ── Test 4 ─────────────────────────────────────────────────────────────── # ── Test 4 ───────────────────────────────────────────────────────────────
def test_del_click_opens_guard_portal_with_uniform_confirm_copy(self): def test_del_click_opens_shared_guard_portal(self):
"""DEL on a locked hand opens `#id_my_sea_del_portal` — uniform """DEL on a locked hand opens the shared `#id_guard_portal` from
'Are you sure?' copy (no conditional quota wording; the Brief base.html (same Gaussian-glass tooltip the room gear-menu uses)
banner carries that info separately) w. CONFIRM + NVM buttons.""" w. uniform 'Are you sure?' copy + the standard `.btn-confirm OK`
+ `.btn-cancel NVM` button pair. The Brief banner above carries
the quota-specific info, so the portal stays text-free of
conditional wording."""
self._save_draw_for_user() self._save_draw_for_user()
self.create_pre_authenticated_session(self.email) self.create_pre_authenticated_session(self.email)
self.browser.get(self.live_server_url + "/gameboard/my-sea/") self.browser.get(self.live_server_url + "/gameboard/my-sea/")
@@ -1137,22 +1147,21 @@ class MySeaLockHandTest(FunctionalTest):
delbtn.click() delbtn.click()
portal = self.wait_for( portal = self.wait_for(
lambda: self.browser.find_element( lambda: self.browser.find_element(
By.CSS_SELECTOR, "#id_my_sea_del_portal" By.CSS_SELECTOR, "#id_guard_portal.active"
) )
) )
self.wait_for(lambda: self.assertTrue(portal.is_displayed())) self.wait_for(lambda: self.assertTrue(portal.is_displayed()))
text = portal.text.lower() self.assertIn("sure", portal.text.lower())
self.assertIn("sure", text) portal.find_element(By.CSS_SELECTOR, ".guard-yes")
portal.find_element(By.CSS_SELECTOR, ".my-sea-del-portal__confirm") portal.find_element(By.CSS_SELECTOR, ".guard-no")
portal.find_element(By.CSS_SELECTOR, ".my-sea-del-portal__nvm")
# ── Test 5 ─────────────────────────────────────────────────────────────── # ── Test 5 ───────────────────────────────────────────────────────────────
def test_del_confirm_clears_saved_draw_and_returns_to_landing(self): def test_del_confirm_clears_saved_draw_and_returns_to_landing(self):
"""Clicking the portal's CONFIRM POSTs to the delete endpoint """Clicking the portal's OK (`.guard-yes`) POSTs to the delete
→ server wipes the MySeaDraw row → reload lands on the FREE DRAW endpoint → server wipes the MySeaDraw row → reload lands on the
landing again (no saved hand, no Brief banner, FREE DRAW btn FREE DRAW landing again (no saved hand, no Brief banner, FREE
present).""" DRAW btn present)."""
from apps.gameboard.models import MySeaDraw from apps.gameboard.models import MySeaDraw
self._save_draw_for_user() self._save_draw_for_user()
self.assertEqual(MySeaDraw.objects.filter(user=self.gamer).count(), 1) self.assertEqual(MySeaDraw.objects.filter(user=self.gamer).count(), 1)
@@ -1166,7 +1175,7 @@ class MySeaLockHandTest(FunctionalTest):
picker.find_element(By.CSS_SELECTOR, "#id_sea_del").click() picker.find_element(By.CSS_SELECTOR, "#id_sea_del").click()
confirm = self.wait_for( confirm = self.wait_for(
lambda: self.browser.find_element( lambda: self.browser.find_element(
By.CSS_SELECTOR, ".my-sea-del-portal__confirm" By.CSS_SELECTOR, "#id_guard_portal.active .guard-yes"
) )
) )
confirm.click() confirm.click()

View File

@@ -505,74 +505,9 @@ body.page-gameboard {
} }
// ── Iter 4b: Brief banner + DEL guard portal ───────────────────────────────── // ── Iter 4b: Brief banner + DEL guard portal ─────────────────────────────────
// Both reuse shared chrome: the Brief is `.note-banner` from note.js
// Brief banner — Look!-formatted strip above the picker whenever a saved // (portaled atop h2 w. Gaussian glass); the DEL guard is `#id_guard_portal`
// draw occupies the user's free-quota slot. Shape mirrors .my-sea-sign- // from base.html (the same one the room gear-menu DEL uses, positioned
// gate but sized as a banner (full-width, single line + actions row). // above the anchor button w. Gaussian glass + no backdrop). The picker IIFE
.my-sea-brief { // invokes it via `window.showGuard(delBtn, "Are you sure?", confirmFn,
display: flex; // null, {yesLabel: "DEL"})`. No my-sea-specific SCSS needed.
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.5rem 1rem;
margin: 0.5rem 0 1rem;
background-color: rgba(var(--secUser), 0.65);
border: 0.1rem solid rgba(var(--terUser), 0.6);
border-radius: 0.4rem;
color: rgba(var(--terUser), 1);
font-size: 0.95rem;
&[hidden] { display: none; }
.my-sea-brief__line {
margin: 0;
flex: 1;
}
.my-sea-brief__timestamp {
font-weight: bold;
color: rgba(var(--ninUser), 1);
}
.my-sea-brief__nvm {
flex: 0 0 auto;
}
}
// DEL guard portal — fixed-position centered modal w. a uniform
// 'Are you sure?' prompt. CONFIRM POSTs to /gameboard/my-sea/delete;
// NVM closes the portal. The Brief banner above carries the quota-
// specific copy so this stays free of conditional text.
.my-sea-del-portal {
position: fixed;
inset: 0;
z-index: 1100;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
&[hidden] { display: none; }
.my-sea-del-portal__panel {
background-color: rgba(var(--secUser), 0.95);
border: 0.15rem solid rgba(var(--terUser), 0.8);
border-radius: 0.5rem;
padding: 1.25rem 1.75rem;
min-width: 18rem;
text-align: center;
color: rgba(var(--terUser), 1);
}
.my-sea-del-portal__line {
margin: 0 0 1rem;
font-size: 1.05rem;
font-weight: bold;
}
.my-sea-del-portal__actions {
display: flex;
gap: 0.75rem;
justify-content: center;
}
}

View File

@@ -205,39 +205,12 @@
{% include "apps/gameboard/_partials/_sea_stage.html" %} {% include "apps/gameboard/_partials/_sea_stage.html" %}
</div> </div>
{# Iter 4b — Look!-formatted Brief banner above the picker. #} {# Iter 4b — DEL guard reuses the shared `#id_guard_portal` #}
{# Always rendered when the picker is rendered, but hidden #} {# from base.html (the same one the room's gear-menu DEL btn #}
{# unless a saved draw occupies the user's free-quota slot #} {# uses). Gaussian-glass tooltip positioned above the DEL btn,#}
{# (server) OR LOCK HAND just fired (client un-hides on the #} {# no backdrop. The picker IIFE below invokes it via #}
{# fetch response w. the next-free-draw timestamp). Avoiding #} {# `window.showGuard(delBtn, "Are you sure?", confirmFn, null,#}
{# a full page reload on LOCK lets the iter-4a FTs keep their #} {# {yesLabel: "DEL"})` when DEL is clicked post-lock. #}
{# picker element refs valid post-lock. #}
<div class="my-sea-brief"{% if not active_draw %} hidden{% endif %}>
<p class="my-sea-brief__line">
Look!&mdash;your free draw is locked in for the next 24 hours. Next free draw available at
<time class="my-sea-brief__timestamp"
datetime="{% if next_free_draw_at %}{{ next_free_draw_at|date:'c' }}{% endif %}">{% if next_free_draw_at %}{{ next_free_draw_at|date:'D, M j @ g:i A' }}{% endif %}</time>.
</p>
<button type="button" class="btn btn-cancel my-sea-brief__nvm">NVM</button>
</div>
{# Iter 4b — DEL guard portal. Uniform 'Are you sure?' copy #}
{# regardless of quota state (the Brief banner above carries #}
{# the quota-specific info). Always rendered (hidden by #}
{# default); DEL click un-hides when picker is `_locked`. #}
{# CONFIRM POSTs to /gameboard/my-sea/delete; NVM dismisses. #}
<div id="id_my_sea_del_portal" class="my-sea-del-portal" hidden>
<div class="my-sea-del-portal__panel">
<p class="my-sea-del-portal__line">Are you sure?</p>
<div class="my-sea-del-portal__actions">
<button type="button"
class="btn btn-cancel my-sea-del-portal__nvm">NVM</button>
<button type="button"
class="btn btn-danger my-sea-del-portal__confirm"
data-delete-url="{% url 'my_sea_delete' %}">CONFIRM</button>
</div>
</div>
</div>
{# Sprint 5 iter 4a — shuffled deck (levity + gravity halves, #} {# Sprint 5 iter 4a — shuffled deck (levity + gravity halves, #}
{# sig excluded) embedded as JSON; JS reads on init and #} {# sig excluded) embedded as JSON; JS reads on init and #}
{# pops from the relevant pile on each deposit. #} {# pops from the relevant pile on each deposit. #}
@@ -492,19 +465,39 @@
// ── DEL semantics differ by lock state ────────────────── // ── DEL semantics differ by lock state ──────────────────
// Pre-lock: DEL resets the in-progress hand client-side // Pre-lock: DEL resets the in-progress hand client-side
// (iter 4a behaviour — no server round-trip). // (iter 4a behaviour — no server round-trip).
// Post-lock: DEL opens `#id_my_sea_del_portal` guard portal // Post-lock: DEL invokes the shared `#id_guard_portal`
// (iter 4b). The portal CONFIRM POSTs to // from base.html via `window.showGuard`, w. a
// /gameboard/my-sea/delete; NVM closes the // "DEL" YES-label override (the room's gear-
// portal. // menu DEL flow uses the same portal). On YES
// we POST to /gameboard/my-sea/delete then
// navigate back to the page (server returns
// 204; we redirect manually to land on the
// FREE DRAW landing).
if (delBtn) { if (delBtn) {
delBtn.addEventListener('click', function (e) { delBtn.addEventListener('click', function (e) {
e.stopPropagation(); e.stopPropagation();
var portal = document.getElementById('id_my_sea_del_portal'); if (!_locked) {
if (_locked && portal) { _resetHand();
portal.hidden = false;
return; return;
} }
_resetHand(); if (!window.showGuard) return;
// Trigger btn (DEL, `.btn-danger`) opens the shared
// guard portal; the portal's confirm button is the
// standard `.btn-confirm` "OK" + `.btn-cancel` "NVM"
// pair — matches the room gear-menu DEL flow exactly.
window.showGuard(
delBtn,
'Are you sure?',
function () {
fetch('{% url "my_sea_delete" %}', {
method: 'POST',
credentials: 'same-origin',
headers: {'X-CSRFToken': _csrf()},
}).then(function (r) {
if (r.ok) window.location.reload();
});
}
);
}); });
} }
if (lockBtn) { if (lockBtn) {
@@ -538,11 +531,8 @@
return m ? decodeURIComponent(m[1]) : ''; return m ? decodeURIComponent(m[1]) : '';
} }
function _formatTimestamp(iso) { function _formatTimestamp(iso) {
// Mirror the server-side `D, M j @ g:i A` format used in // Mirror the server-side `D, M j @ g:i A` format
// the template's pre-rendered next-free-draw timestamp // (e.g., "Thu, May 21 @ 2:41 AM").
// (e.g., "Thu, May 21 @ 2:41 AM"). Keeps the post-LOCK
// visual consistent with a fresh-page-load saved-draw
// render.
var d = new Date(iso); var d = new Date(iso);
if (isNaN(d)) return ''; if (isNaN(d)) return '';
var DAYS = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; var DAYS = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
@@ -556,16 +546,40 @@
return DAYS[d.getDay()] + ', ' + MONTHS[d.getMonth()] return DAYS[d.getDay()] + ', ' + MONTHS[d.getMonth()]
+ ' ' + d.getDate() + ' @ ' + h + ':' + mm + ' ' + ampm; + ' ' + d.getDate() + ' @ ' + h + ':' + mm + ' ' + ampm;
} }
function _revealBrief(nextFreeDrawIso) { window._showFreeDrawLockedBrief = function (iso) {
var brief = document.querySelector('.my-sea-brief'); // Standard Brief banner — portaled atop the h2 w.
if (!brief) return; // Gaussian-glass bg (see [[note.js]] showBanner). The
var ts = brief.querySelector('.my-sea-brief__timestamp'); // next-free-draw moment is passed as an ISO string +
if (ts && nextFreeDrawIso) { // re-used as `created_at` so note.js's `<time
ts.setAttribute('datetime', nextFreeDrawIso); // class="note-banner__timestamp">` slot renders the
ts.textContent = _formatTimestamp(nextFreeDrawIso); // datetime instead of "Invalid Date" (which it does
// for empty/invalid input). The `line_text` carries
// only the contextual prose now — the dedicated slot
// owns the timestamp display.
if (!window.Brief || !Brief.showBanner) return;
Brief.showBanner({
title: 'Free draw locked',
line_text:
'Look!&mdash;your free draw is locked in. ' +
'Next free draw available at:',
post_url: '{% url "gameboard" %}',
created_at: iso,
kind: 'NUDGE',
});
var banner = document.querySelector('.note-banner');
if (banner) {
banner.classList.add('my-sea-locked-banner');
// note.js renders the timestamp as `toLocaleDateString`
// (e.g., "May 20, 2026") — short-form, no time. Our
// use case wants the full `D, M j @ g:i A` shape
// (e.g., "Wed, May 20 @ 11:57 PM") so the user sees
// both the date AND the precise unlock hour. Overwrite
// the rendered text in-place (leaves the `datetime=`
// attribute intact for accessibility tooling).
var ts = banner.querySelector('.note-banner__timestamp');
if (ts && iso) ts.textContent = _formatTimestamp(iso);
} }
brief.hidden = false; };
}
function _postLock(hand) { function _postLock(hand) {
fetch('{% url "my_sea_lock" %}', { fetch('{% url "my_sea_lock" %}', {
method: 'POST', method: 'POST',
@@ -582,49 +596,11 @@
return r.ok ? r.json() : null; return r.ok ? r.json() : null;
}).then(function (body) { }).then(function (body) {
if (body && body.next_free_draw_at) { if (body && body.next_free_draw_at) {
_revealBrief(body.next_free_draw_at); window._showFreeDrawLockedBrief(body.next_free_draw_at);
} }
}); });
} }
// ── DEL guard portal wiring ────────────────────────────
var portal = document.getElementById('id_my_sea_del_portal');
if (portal) {
var nvmBtn = portal.querySelector('.my-sea-del-portal__nvm');
var confirmBtn = portal.querySelector('.my-sea-del-portal__confirm');
if (nvmBtn) {
nvmBtn.addEventListener('click', function (e) {
e.stopPropagation();
portal.hidden = true;
});
}
if (confirmBtn) {
confirmBtn.addEventListener('click', function (e) {
e.stopPropagation();
var url = confirmBtn.dataset.deleteUrl || '';
if (!url) return;
fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {'X-CSRFToken': _csrf()},
}).then(function (r) {
if (r.ok) window.location.reload();
});
});
}
}
// ── Brief banner NVM ──────────────────────────────────
var brief = document.querySelector('.my-sea-brief');
if (brief) {
var briefNvm = brief.querySelector('.my-sea-brief__nvm');
if (briefNvm) {
briefNvm.addEventListener('click', function () {
brief.hidden = true;
});
}
}
function syncLabels(spread) { function syncLabels(spread) {
var labels = POSITION_LABELS[spread] || {}; var labels = POSITION_LABELS[spread] || {};
cross.querySelectorAll('.sea-pos-label').forEach(function (el) { cross.querySelectorAll('.sea-pos-label').forEach(function (el) {
@@ -731,6 +707,22 @@
{# Tagged w. .my-sea-intro-banner so FTs disambiguate from #} {# Tagged w. .my-sea-intro-banner so FTs disambiguate from #}
{# any other Briefs on the page. #} {# any other Briefs on the page. #}
<script src="{% static 'apps/dashboard/note.js' %}"></script> <script src="{% static 'apps/dashboard/note.js' %}"></script>
{% if active_draw %}
{# Iter 4b — saved-draw Brief. Standard portaled banner via #}
{# Brief.showBanner (Gaussian-glass bg, atop-h2 positioning); #}
{# the on-LOCK-success path inside the picker IIFE calls the #}
{# same `window._showFreeDrawLockedBrief` so a freshly-locked #}
{# hand gets the identical UX without a page reload. Pass an #}
{# ISO timestamp (`|date:'c'`) so note.js's `<time>` slot #}
{# parses cleanly instead of rendering "Invalid Date". #}
<script>
document.addEventListener('DOMContentLoaded', function () {
if (window._showFreeDrawLockedBrief) {
window._showFreeDrawLockedBrief("{{ next_free_draw_at|date:'c' }}");
}
});
</script>
{% endif %}
{% if show_backup_intro_banner %} {% if show_backup_intro_banner %}
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {

View File

@@ -141,6 +141,10 @@
_cb = callback; _cb = callback;
_onDismiss = onDismiss || null; _onDismiss = onDismiss || null;
portal.querySelector('.guard-message').innerHTML = message; portal.querySelector('.guard-message').innerHTML = message;
// Optional override for the YES-button label (e.g., "DEL" for
// a destructive-named action). Resets to "OK" inside dismiss/
// doConfirm so the next show() starts from the default.
portal.querySelector('.guard-yes').textContent = options.yesLabel || 'OK';
portal.classList.add('active'); portal.classList.add('active');
var rect = anchor.getBoundingClientRect(); var rect = anchor.getBoundingClientRect();
var pw = portal.offsetWidth; var pw = portal.offsetWidth;
@@ -165,6 +169,7 @@
if (!portal) return; if (!portal) return;
var od = _onDismiss; var od = _onDismiss;
portal.classList.remove('active'); portal.classList.remove('active');
portal.querySelector('.guard-yes').textContent = 'OK';
_cb = null; _cb = null;
_onDismiss = null; _onDismiss = null;
if (od) od(); if (od) od();
@@ -173,6 +178,7 @@
function doConfirm() { function doConfirm() {
var cb = _cb; var cb = _cb;
portal.classList.remove('active'); portal.classList.remove('active');
portal.querySelector('.guard-yes').textContent = 'OK';
_cb = null; _cb = null;
_onDismiss = null; _onDismiss = null;
if (cb) cb(); if (cb) cb();