2026-03-02 13:57:03 -05:00
|
|
|
{% load compress %}
|
|
|
|
|
{% load static %}
|
2026-03-07 15:05:49 -05:00
|
|
|
|
2026-03-02 13:57:03 -05:00
|
|
|
|
2026-01-13 20:58:05 -05:00
|
|
|
<!DOCTYPE html>
|
2026-03-02 13:57:03 -05:00
|
|
|
<html lang="en">
|
2026-01-13 20:58:05 -05:00
|
|
|
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<meta name="author" content="Disco DeDisco">
|
|
|
|
|
<meta name="robots" content="noindex, nofollow">
|
2026-01-29 15:21:54 -05:00
|
|
|
<title>Earthman RPG | {% block title_text %}{% endblock title_text %}</title>
|
2026-03-02 13:57:03 -05:00
|
|
|
{% compress css %}
|
|
|
|
|
<link type="text/x-scss" rel="stylesheet" href="{% static 'scss/core.scss' %}">
|
|
|
|
|
{% endcompress %}
|
2026-03-09 14:40:34 -04:00
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
|
2026-01-13 20:58:05 -05:00
|
|
|
</head>
|
|
|
|
|
|
2026-03-06 18:14:01 -05:00
|
|
|
<body class="{{ user_palette }} {{ page_class|default:'' }}">
|
2026-01-13 20:58:05 -05:00
|
|
|
<div class="container">
|
2026-03-07 15:05:49 -05:00
|
|
|
{% include "core/_partials/_navbar.html" %}
|
2026-01-30 17:23:07 -05:00
|
|
|
|
2026-04-04 13:49:48 -04:00
|
|
|
<div class="row">
|
|
|
|
|
<div class="col-lg-6">
|
|
|
|
|
<h2 >{% block header_text %}{% endblock header_text %}</h2>
|
|
|
|
|
{% block extra_header %}
|
|
|
|
|
{% endblock extra_header %}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-30 17:23:07 -05:00
|
|
|
{% if messages %}
|
brief sprint C3.b+c+d+e: share-post Line+Brief async, magic-link / invalid-link banners use Brief styling, .alert-* retired — TDD
Closes the C3 brief sprint. Three event sources (note unlock, share invite, login messages) now route through the Brief slide-down, & the legacy .alert-success/.alert-warning rendering in base.html is retired.
C3.b — share-post async Line + Brief:
- billboard.share_post detects Accept: application/json. JSON path appends a Line (text="Shared with X at <isoformat>", isoformat carries microseconds so two rapid shares of the same email don't collide on Line.unique_together(post,text)), spawns a Brief(kind=SHARE_INVITE) for the sharer, and returns {brief: brief.to_banner_dict() | None, line_text, recipient_display}. Sharer-shares-with-themselves stays a silent no-op (response carries brief: null). Legacy form-submit path preserved for non-AJAX (still redirects + flashes the privacy-safe message — kept for older FTs / no-JS fallback).
- billboard.Brief.to_banner_dict() (moved from dashboard.views helper to a model method) shapes the JSON the banner JS consumes.
- post.html: share form intercepted by JS — fetches POST w. Accept:application/json, then appends `data.line_text` as the next row in #id_post_table, calls Brief.showBanner(data.brief), and (when registered) appends a fresh `<span class="post-recipient">` to the new #id_post_recipients box. No page reload — the alert-success flash is gone.
- 10 new ITs (SharePostAsyncTest + SharePostLegacyRedirectTest) cover the JSON path, line append, brief creation w. SHARE_INVITE kind, registered/unregistered recipient behaviour, sharer-self skip, line dedupe via timestamp, and that the legacy form-submit redirect path still works.
- functional_tests.test_sharing line numbering updated: the share now records its own Line so the alice-reply lands at row 3 instead of 2.
C3.c+d — magic-link confirmation + invalid-link error use Brief banner styling:
- base.html's {% if messages %} block stops rendering .alert-success/.alert-warning divs. Instead each message renders as a transient Brief-styled banner: <div class="note-banner note-banner--message note-banner--{{level_tag}}"> with .note-banner__body / __description carrying the message text and a .btn-cancel NVM that removes the banner via inline onclick. No DB Brief row; no FYI; no square. Same Gaussian-glass look as note-unlock + share-invite Briefs.
- _note.scss adds the note-banner--message variant (full-opacity description) + note-banner--error/--warning border-color override (priRd 0.6) so the invalid-link banner reads as red/abandon.
C3.e — .alert-success/.alert-warning retired in markup; the SCSS class blocks aren't referenced anywhere else in templates so they sit dormant (left in place — base form styling keeps .form-control etc. working; no need to ripple into _base.scss).
Banner JS (note.js / Brief module) was untouched in C3.b+c+d — the Brief.showBanner contract from C3.a already handles all three kinds (NOTE_UNLOCK / USER_POST / SHARE_INVITE) by reading kind off the brief; the message-banner path doesn't go through showBanner because there's no Brief row.
Tests: 218 dashboard+billboard+api ITs + 322 lyric+dashboard+billboard ITs + 2 sharing FTs + 9 my_notes FTs + 1 Jasmine FT all green. Existing lyric.test_views login message-text assertions unchanged (they pull from messages framework — not the rendered HTML — so the markup swap doesn't affect them).
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 18:15:43 -04:00
|
|
|
{% for message in messages %}
|
|
|
|
|
{# Transient Brief-styled banner — no DB row, no FYI/square. #}
|
|
|
|
|
{# Slides in under the navbar h2 w. the same Gaussian-glass #}
|
|
|
|
|
{# look as the Brief notification banner; NVM dismisses. #}
|
|
|
|
|
<div class="note-banner note-banner--message note-banner--{{ message.level_tag }}">
|
|
|
|
|
<div class="note-banner__body">
|
|
|
|
|
<p class="note-banner__description">{{ message }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button type="button" class="btn btn-cancel note-banner__nvm"
|
|
|
|
|
onclick="this.parentElement.remove()">NVM</button>
|
2026-01-30 17:23:07 -05:00
|
|
|
</div>
|
brief sprint C3.b+c+d+e: share-post Line+Brief async, magic-link / invalid-link banners use Brief styling, .alert-* retired — TDD
Closes the C3 brief sprint. Three event sources (note unlock, share invite, login messages) now route through the Brief slide-down, & the legacy .alert-success/.alert-warning rendering in base.html is retired.
C3.b — share-post async Line + Brief:
- billboard.share_post detects Accept: application/json. JSON path appends a Line (text="Shared with X at <isoformat>", isoformat carries microseconds so two rapid shares of the same email don't collide on Line.unique_together(post,text)), spawns a Brief(kind=SHARE_INVITE) for the sharer, and returns {brief: brief.to_banner_dict() | None, line_text, recipient_display}. Sharer-shares-with-themselves stays a silent no-op (response carries brief: null). Legacy form-submit path preserved for non-AJAX (still redirects + flashes the privacy-safe message — kept for older FTs / no-JS fallback).
- billboard.Brief.to_banner_dict() (moved from dashboard.views helper to a model method) shapes the JSON the banner JS consumes.
- post.html: share form intercepted by JS — fetches POST w. Accept:application/json, then appends `data.line_text` as the next row in #id_post_table, calls Brief.showBanner(data.brief), and (when registered) appends a fresh `<span class="post-recipient">` to the new #id_post_recipients box. No page reload — the alert-success flash is gone.
- 10 new ITs (SharePostAsyncTest + SharePostLegacyRedirectTest) cover the JSON path, line append, brief creation w. SHARE_INVITE kind, registered/unregistered recipient behaviour, sharer-self skip, line dedupe via timestamp, and that the legacy form-submit redirect path still works.
- functional_tests.test_sharing line numbering updated: the share now records its own Line so the alice-reply lands at row 3 instead of 2.
C3.c+d — magic-link confirmation + invalid-link error use Brief banner styling:
- base.html's {% if messages %} block stops rendering .alert-success/.alert-warning divs. Instead each message renders as a transient Brief-styled banner: <div class="note-banner note-banner--message note-banner--{{level_tag}}"> with .note-banner__body / __description carrying the message text and a .btn-cancel NVM that removes the banner via inline onclick. No DB Brief row; no FYI; no square. Same Gaussian-glass look as note-unlock + share-invite Briefs.
- _note.scss adds the note-banner--message variant (full-opacity description) + note-banner--error/--warning border-color override (priRd 0.6) so the invalid-link banner reads as red/abandon.
C3.e — .alert-success/.alert-warning retired in markup; the SCSS class blocks aren't referenced anywhere else in templates so they sit dormant (left in place — base form styling keeps .form-control etc. working; no need to ripple into _base.scss).
Banner JS (note.js / Brief module) was untouched in C3.b+c+d — the Brief.showBanner contract from C3.a already handles all three kinds (NOTE_UNLOCK / USER_POST / SHARE_INVITE) by reading kind off the brief; the message-banner path doesn't go through showBanner because there's no Brief row.
Tests: 218 dashboard+billboard+api ITs + 322 lyric+dashboard+billboard ITs + 2 sharing FTs + 9 my_notes FTs + 1 Jasmine FT all green. Existing lyric.test_views login message-text assertions unchanged (they pull from messages framework — not the rendered HTML — so the markup swap doesn't affect them).
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 18:15:43 -04:00
|
|
|
{% endfor %}
|
2026-01-30 17:23:07 -05:00
|
|
|
{% endif %}
|
2026-01-29 15:21:54 -05:00
|
|
|
|
2026-05-08 19:14:50 -04:00
|
|
|
{# Anchor for Brief.showBanner — banner inserts as nextSibling so #}
|
|
|
|
|
{# it lands at the top of page content on every base.html-extending #}
|
|
|
|
|
{# page, regardless of where (or whether) <h2> is positioned. #}
|
|
|
|
|
<div id="id_brief_banner_anchor"></div>
|
|
|
|
|
|
2026-02-08 21:43:58 -05:00
|
|
|
{% block content %}
|
|
|
|
|
{% endblock content %}
|
2026-01-13 20:58:05 -05:00
|
|
|
|
|
|
|
|
</div>
|
2026-03-07 15:05:49 -05:00
|
|
|
|
|
|
|
|
{% include "core/_partials/_footer.html" %}
|
2026-01-25 22:40:57 -05:00
|
|
|
|
2026-03-15 01:17:09 -04:00
|
|
|
{% if user.is_authenticated %}
|
|
|
|
|
<button id="id_kit_btn" data-kit-url="{% url 'kit_bag' %}" aria-label="Open Kit Bag">
|
|
|
|
|
<i class="fa-solid fa-briefcase"></i>
|
|
|
|
|
</button>
|
|
|
|
|
{% endif %}
|
|
|
|
|
<dialog id="id_kit_bag_dialog"></dialog>
|
|
|
|
|
|
2026-03-23 19:31:57 -04:00
|
|
|
<div id="id_guard_portal">
|
|
|
|
|
<span class="guard-message"></span>
|
|
|
|
|
<div class="guard-actions">
|
|
|
|
|
<button class="btn btn-confirm guard-yes" type="button">OK</button>
|
|
|
|
|
<button class="btn btn-cancel guard-no" type="button">NVM</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-04 15:13:16 -05:00
|
|
|
{% block scripts %}
|
|
|
|
|
{% endblock scripts %}
|
2026-03-23 19:31:57 -04:00
|
|
|
<script>
|
2026-05-09 14:01:16 -04:00
|
|
|
// h2 letter splitter — wrap each character of every .row .col-lg-6 h2
|
|
|
|
|
// word-span in its own <span> so .scss can use justify-content:
|
|
|
|
|
// space-between to fill the 45/55 slot. text-justify: inter-character
|
|
|
|
|
// would do this in pure CSS but iOS Safari + Firefox silently fall
|
|
|
|
|
// back to inter-word for Latin text, leaving letters clustered at the
|
|
|
|
|
// slot's start.
|
|
|
|
|
(function () {
|
|
|
|
|
var spans = document.querySelectorAll('.row .col-lg-6 h2 > span');
|
|
|
|
|
for (var i = 0; i < spans.length; i++) {
|
|
|
|
|
var span = spans[i];
|
|
|
|
|
if (span.dataset.lettersSplit === '1') continue;
|
|
|
|
|
var text = (span.textContent || '').trim();
|
|
|
|
|
if (!text) continue;
|
|
|
|
|
span.dataset.lettersSplit = '1';
|
|
|
|
|
span.setAttribute('aria-label', text);
|
|
|
|
|
span.textContent = '';
|
|
|
|
|
for (var j = 0; j < text.length; j++) {
|
|
|
|
|
var letter = document.createElement('span');
|
|
|
|
|
letter.setAttribute('aria-hidden', 'true');
|
|
|
|
|
letter.textContent = text[j];
|
|
|
|
|
span.appendChild(letter);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}());
|
2026-05-18 18:22:08 -04:00
|
|
|
|
|
|
|
|
// iOS Safari auto-zooms when focusing an <input>/<textarea>/<select>
|
|
|
|
|
// whose font-size is < 16px, and does NOT auto-zoom back out on blur.
|
iOS focus-zoom prevention — input font-size floor @ 16px ; JS fallback strengthened
Belt-and-suspenders for the iOS Safari auto-zoom-on-input quirk: Mobile Safari zooms the viewport when an `<input>`/`<textarea>`/`<select>` is focused & its computed font-size < 16px, and never zooms back out on blur. Two layers ; PRIMARY — SCSS prevention: new `input, textarea, select, [contenteditable] { font-size: unquote("max(16px, 1em)") }` in core.scss (Sass can't reconcile px/em units in compile-time max() so unquote() passes the CSS max() through verbatim — modern browsers handle natively). 1em inherits parent, max() floors at 16. ALSO floored `.form-control-lg` in _base.scss — was `font-size: 1.125rem`, which at rem=14 (small portrait, clamp(14px, 2.4vmin, 22px) hits its floor) computes to 15.75px → **0.25px** under iOS's 16px threshold → the "ever so slightly" zoom on New Game + New Post applets the user reported (both use `.form-control.form-control-lg`, specificity 0,2,0 beats my element-level 0,0,1 rule). Floor: `unquote("max(16px, 1.125rem)")` ; SECONDARY — JS fallback in base.html: rewritten from `setAttribute('content', ...)` toggle to full meta-element remove+re-add, which modern iOS handles more reliably than attribute mutations on the existing meta. Triggers on document-level `focusout` (bubbles natively, no capture-phase needed) for `input/textarea/select`; injects fresh viewport meta w. `maximum-scale=1.0, user-scalable=no` for 300ms (iOS reads as zoom violation → snaps to 1:1), then swaps back to the cached base content so pinch-zoom remains available elsewhere ; user observed horizontal scrollbar appearing when the page zoomed — that's the symptom the user actually cared about (broken layout, not aesthetic zoom). w. SCSS floor in place the zoom shouldn't trigger to begin with; the JS is purely for inputs that slip through (future custom controls, shadow DOM, etc.) ; iOS-specific behavior — Selenium+Firefox doesn't replicate the auto-zoom so no FT layer added. Verified by user manual iPhone test (post-fix retest pending after force-refresh)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:32:45 -04:00
|
|
|
// Belt-and-suspenders: primary prevention is the global `font-size:
|
|
|
|
|
// max(16px, 1em)` rule in core.scss; this JS is a fallback for inputs
|
|
|
|
|
// that slip through (custom controls, shadow DOM, etc.). Modern iOS
|
|
|
|
|
// doesn't reliably react to a simple `setAttribute('content', ...)`
|
|
|
|
|
// tweak on the existing meta — removing and re-appending the meta
|
|
|
|
|
// element entirely is more dependable.
|
2026-05-18 18:22:08 -04:00
|
|
|
(function () {
|
iOS focus-zoom prevention — input font-size floor @ 16px ; JS fallback strengthened
Belt-and-suspenders for the iOS Safari auto-zoom-on-input quirk: Mobile Safari zooms the viewport when an `<input>`/`<textarea>`/`<select>` is focused & its computed font-size < 16px, and never zooms back out on blur. Two layers ; PRIMARY — SCSS prevention: new `input, textarea, select, [contenteditable] { font-size: unquote("max(16px, 1em)") }` in core.scss (Sass can't reconcile px/em units in compile-time max() so unquote() passes the CSS max() through verbatim — modern browsers handle natively). 1em inherits parent, max() floors at 16. ALSO floored `.form-control-lg` in _base.scss — was `font-size: 1.125rem`, which at rem=14 (small portrait, clamp(14px, 2.4vmin, 22px) hits its floor) computes to 15.75px → **0.25px** under iOS's 16px threshold → the "ever so slightly" zoom on New Game + New Post applets the user reported (both use `.form-control.form-control-lg`, specificity 0,2,0 beats my element-level 0,0,1 rule). Floor: `unquote("max(16px, 1.125rem)")` ; SECONDARY — JS fallback in base.html: rewritten from `setAttribute('content', ...)` toggle to full meta-element remove+re-add, which modern iOS handles more reliably than attribute mutations on the existing meta. Triggers on document-level `focusout` (bubbles natively, no capture-phase needed) for `input/textarea/select`; injects fresh viewport meta w. `maximum-scale=1.0, user-scalable=no` for 300ms (iOS reads as zoom violation → snaps to 1:1), then swaps back to the cached base content so pinch-zoom remains available elsewhere ; user observed horizontal scrollbar appearing when the page zoomed — that's the symptom the user actually cared about (broken layout, not aesthetic zoom). w. SCSS floor in place the zoom shouldn't trigger to begin with; the JS is purely for inputs that slip through (future custom controls, shadow DOM, etc.) ; iOS-specific behavior — Selenium+Firefox doesn't replicate the auto-zoom so no FT layer added. Verified by user manual iPhone test (post-fix retest pending after force-refresh)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:32:45 -04:00
|
|
|
var origMeta = document.querySelector('meta[name="viewport"]');
|
|
|
|
|
if (!origMeta) return;
|
|
|
|
|
var baseContent = origMeta.getAttribute('content');
|
2026-05-18 18:22:08 -04:00
|
|
|
document.addEventListener('focusout', function (e) {
|
|
|
|
|
if (!e.target.matches || !e.target.matches('input, textarea, select')) return;
|
iOS focus-zoom prevention — input font-size floor @ 16px ; JS fallback strengthened
Belt-and-suspenders for the iOS Safari auto-zoom-on-input quirk: Mobile Safari zooms the viewport when an `<input>`/`<textarea>`/`<select>` is focused & its computed font-size < 16px, and never zooms back out on blur. Two layers ; PRIMARY — SCSS prevention: new `input, textarea, select, [contenteditable] { font-size: unquote("max(16px, 1em)") }` in core.scss (Sass can't reconcile px/em units in compile-time max() so unquote() passes the CSS max() through verbatim — modern browsers handle natively). 1em inherits parent, max() floors at 16. ALSO floored `.form-control-lg` in _base.scss — was `font-size: 1.125rem`, which at rem=14 (small portrait, clamp(14px, 2.4vmin, 22px) hits its floor) computes to 15.75px → **0.25px** under iOS's 16px threshold → the "ever so slightly" zoom on New Game + New Post applets the user reported (both use `.form-control.form-control-lg`, specificity 0,2,0 beats my element-level 0,0,1 rule). Floor: `unquote("max(16px, 1.125rem)")` ; SECONDARY — JS fallback in base.html: rewritten from `setAttribute('content', ...)` toggle to full meta-element remove+re-add, which modern iOS handles more reliably than attribute mutations on the existing meta. Triggers on document-level `focusout` (bubbles natively, no capture-phase needed) for `input/textarea/select`; injects fresh viewport meta w. `maximum-scale=1.0, user-scalable=no` for 300ms (iOS reads as zoom violation → snaps to 1:1), then swaps back to the cached base content so pinch-zoom remains available elsewhere ; user observed horizontal scrollbar appearing when the page zoomed — that's the symptom the user actually cared about (broken layout, not aesthetic zoom). w. SCSS floor in place the zoom shouldn't trigger to begin with; the JS is purely for inputs that slip through (future custom controls, shadow DOM, etc.) ; iOS-specific behavior — Selenium+Firefox doesn't replicate the auto-zoom so no FT layer added. Verified by user manual iPhone test (post-fix retest pending after force-refresh)
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:32:45 -04:00
|
|
|
var oldMeta = document.querySelector('meta[name="viewport"]');
|
|
|
|
|
if (oldMeta) oldMeta.remove();
|
|
|
|
|
var snapMeta = document.createElement('meta');
|
|
|
|
|
snapMeta.setAttribute('name', 'viewport');
|
|
|
|
|
snapMeta.setAttribute('content', baseContent + ', maximum-scale=1.0, user-scalable=no');
|
|
|
|
|
document.head.appendChild(snapMeta);
|
|
|
|
|
setTimeout(function () {
|
|
|
|
|
snapMeta.remove();
|
|
|
|
|
var revertMeta = document.createElement('meta');
|
|
|
|
|
revertMeta.setAttribute('name', 'viewport');
|
|
|
|
|
revertMeta.setAttribute('content', baseContent);
|
|
|
|
|
document.head.appendChild(revertMeta);
|
|
|
|
|
}, 300);
|
2026-05-18 18:22:08 -04:00
|
|
|
});
|
|
|
|
|
}());
|
2026-05-09 14:01:16 -04:00
|
|
|
</script>
|
|
|
|
|
<script>
|
2026-03-23 19:31:57 -04:00
|
|
|
(function () {
|
|
|
|
|
var portal = null;
|
|
|
|
|
var _cb = null;
|
|
|
|
|
var _onDismiss = null;
|
|
|
|
|
|
2026-04-05 16:54:03 -04:00
|
|
|
function show(anchor, message, callback, onDismiss, options) {
|
2026-03-23 19:31:57 -04:00
|
|
|
if (!portal) return;
|
2026-04-05 16:54:03 -04:00
|
|
|
options = options || {};
|
2026-03-23 19:31:57 -04:00
|
|
|
_cb = callback;
|
|
|
|
|
_onDismiss = onDismiss || null;
|
|
|
|
|
portal.querySelector('.guard-message').innerHTML = message;
|
|
|
|
|
portal.classList.add('active');
|
|
|
|
|
var rect = anchor.getBoundingClientRect();
|
|
|
|
|
var pw = portal.offsetWidth;
|
|
|
|
|
var rawLeft = rect.left + rect.width / 2;
|
|
|
|
|
var cleft = Math.max(pw / 2 + 8, Math.min(rawLeft, window.innerWidth - pw / 2 - 8));
|
|
|
|
|
portal.style.left = Math.round(cleft) + 'px';
|
2026-04-05 01:23:20 -04:00
|
|
|
var cardCenterY = rect.top + rect.height / 2;
|
2026-04-05 16:54:03 -04:00
|
|
|
// Default: upper half → below (avoids viewport top edge for navbar/fixed buttons).
|
|
|
|
|
// invertY: upper half → above (for modal grids where tooltip should fly away from centre).
|
|
|
|
|
var showBelow = (cardCenterY < window.innerHeight / 2);
|
|
|
|
|
if (options.invertY) showBelow = !showBelow;
|
|
|
|
|
if (showBelow) {
|
2026-03-23 19:31:57 -04:00
|
|
|
portal.style.top = Math.round(rect.bottom) + 'px';
|
|
|
|
|
portal.style.transform = 'translate(-50%, 0.5rem)';
|
2026-04-05 16:00:52 -04:00
|
|
|
} else {
|
|
|
|
|
portal.style.top = Math.round(rect.top) + 'px';
|
|
|
|
|
portal.style.transform = 'translate(-50%, calc(-100% - 0.5rem))';
|
2026-03-23 19:31:57 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dismiss() {
|
|
|
|
|
if (!portal) return;
|
|
|
|
|
var od = _onDismiss;
|
|
|
|
|
portal.classList.remove('active');
|
|
|
|
|
_cb = null;
|
|
|
|
|
_onDismiss = null;
|
|
|
|
|
if (od) od();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function doConfirm() {
|
|
|
|
|
var cb = _cb;
|
|
|
|
|
portal.classList.remove('active');
|
|
|
|
|
_cb = null;
|
|
|
|
|
_onDismiss = null;
|
|
|
|
|
if (cb) cb();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
|
|
|
portal = document.getElementById('id_guard_portal');
|
|
|
|
|
if (!portal) return;
|
|
|
|
|
portal.querySelector('.guard-yes').addEventListener('click', doConfirm);
|
|
|
|
|
portal.querySelector('.guard-no').addEventListener('click', dismiss);
|
|
|
|
|
document.addEventListener('keydown', function (e) {
|
|
|
|
|
if (e.key === 'Escape') dismiss();
|
|
|
|
|
});
|
|
|
|
|
// Outside-click to dismiss — capture phase + stopPropagation
|
|
|
|
|
// prevents the click from cascading to backdrop listeners (e.g. closeFan)
|
|
|
|
|
document.addEventListener('click', function (e) {
|
|
|
|
|
if (!portal.classList.contains('active')) return;
|
|
|
|
|
if (portal.contains(e.target)) return;
|
2026-04-05 01:23:20 -04:00
|
|
|
// If clicking a card, let the event through so the card's
|
|
|
|
|
// own handler immediately opens the guard on the new target.
|
|
|
|
|
// For any other outside click, stop propagation to prevent
|
|
|
|
|
// the backdrop from also closing the fan.
|
|
|
|
|
if (!e.target.closest('.card')) e.stopPropagation();
|
2026-03-23 19:31:57 -04:00
|
|
|
dismiss();
|
|
|
|
|
}, true);
|
|
|
|
|
// Intercept [data-confirm] buttons (capture phase, before form submits)
|
|
|
|
|
document.addEventListener('click', function (e) {
|
|
|
|
|
var btn = e.target.closest('[data-confirm]');
|
|
|
|
|
if (!btn) return;
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
|
var form = btn.closest('form');
|
|
|
|
|
show(btn, btn.dataset.confirm, function () {
|
|
|
|
|
if (form) form.submit();
|
2026-04-05 16:00:52 -04:00
|
|
|
else if (btn.dataset.href) window.location.href = btn.dataset.href;
|
2026-03-23 19:31:57 -04:00
|
|
|
});
|
|
|
|
|
}, true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
window.showGuard = show;
|
|
|
|
|
}());
|
|
|
|
|
</script>
|
2026-03-04 15:13:16 -05:00
|
|
|
<script src="{% static "vendor/htmx.min.js" %}"></script>
|
Django Channels role-select sprint: turn_changed, roles_revealed, role_select_start consumer handlers; WS URL changed from room_slug to room_id UUID; TableSeat model - room, gamer, slot_number, role, role_revealed, seat_position fields; Room.table_status field with ROLE_SELECT, SIG_SELECT, IN_GAME choices; migration 0006_table_status_and_table_seat; pick_roles and select_role views; _role_select_context helper; _notify_turn_changed, _notify_roles_revealed, _notify_role_select_start notifiers; all gate-mutation views now call _notify_gate_update; ChannelsFunctionalTest base class with serve_static, screenshot, dump helpers; SQLite TEST NAME set to file path for ChannelsLiveServerTestCase; InMemoryChannelLayer added to test CHANNEL_LAYERS settings; FT 5 and FT 6 now passing - active seat arc and turn advance via WS, no page refresh; room.js, gatekeeper.js, role-select.js added to apps/epic/static; applets.js, game-kit.js, dashboard.js, wallet.js relocated to app-scoped static dirs; room.html: hex table, table-seat arcs, card-stack, inventory panel, role-card hand, WS scripts; _room.scss: room-shell flex layout, .table-hex polygon clip-path, .table-seat and .seat-card-arc, .card-stack eligible/ineligible states, .card flip animation, .inv-role-card stacked hand, .role-select-backdrop; gear btn and room menu always position: fixed; 375 tests, 0 skipped
2026-03-17 00:24:23 -04:00
|
|
|
<script src="{% static "apps/applets/applets.js" %}"></script>
|
2026-05-13 00:27:39 -04:00
|
|
|
<script src="{% static "apps/applets/row-lock.js" %}"></script>
|
Django Channels role-select sprint: turn_changed, roles_revealed, role_select_start consumer handlers; WS URL changed from room_slug to room_id UUID; TableSeat model - room, gamer, slot_number, role, role_revealed, seat_position fields; Room.table_status field with ROLE_SELECT, SIG_SELECT, IN_GAME choices; migration 0006_table_status_and_table_seat; pick_roles and select_role views; _role_select_context helper; _notify_turn_changed, _notify_roles_revealed, _notify_role_select_start notifiers; all gate-mutation views now call _notify_gate_update; ChannelsFunctionalTest base class with serve_static, screenshot, dump helpers; SQLite TEST NAME set to file path for ChannelsLiveServerTestCase; InMemoryChannelLayer added to test CHANNEL_LAYERS settings; FT 5 and FT 6 now passing - active seat arc and turn advance via WS, no page refresh; room.js, gatekeeper.js, role-select.js added to apps/epic/static; applets.js, game-kit.js, dashboard.js, wallet.js relocated to app-scoped static dirs; room.html: hex table, table-seat arcs, card-stack, inventory panel, role-card hand, WS scripts; _room.scss: room-shell flex layout, .table-hex polygon clip-path, .table-seat and .seat-card-arc, .card-stack eligible/ineligible states, .card flip animation, .inv-role-card stacked hand, .role-select-backdrop; gear btn and room menu always position: fixed; 375 tests, 0 skipped
2026-03-17 00:24:23 -04:00
|
|
|
<script src="{% static "apps/dashboard/game-kit.js" %}"></script>
|
2026-03-04 15:13:16 -05:00
|
|
|
<script>
|
2026-04-02 14:51:08 -04:00
|
|
|
document.cookie = 'user_tz=' + Intl.DateTimeFormat().resolvedOptions().timeZone + '; path=/; SameSite=Lax';
|
2026-03-04 15:13:16 -05:00
|
|
|
document.body.addEventListener('htmx:configRequest', function(evt) {
|
|
|
|
|
evt.detail.headers['X-CSRFToken'] = getCookie('csrftoken');
|
|
|
|
|
});
|
|
|
|
|
function getCookie(name) {
|
|
|
|
|
let val = null;
|
|
|
|
|
if (document.cookie) {
|
|
|
|
|
for (let c of document.cookie.split(';')) {
|
|
|
|
|
c = c.trim();
|
|
|
|
|
if (c.startsWith(name + '=')) {
|
|
|
|
|
val = decodeURIComponent(c.substring(name.length + 1));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return val;
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
2026-01-03 19:20:41 -05:00
|
|
|
</html>
|