Compare commits
5 Commits
2c2ec16f08
...
ce4cb03af7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce4cb03af7 | ||
|
|
d5e4fc53f0 | ||
|
|
94cd9db3a4 | ||
|
|
1f874de459 | ||
|
|
75301ca84d |
@@ -46,6 +46,11 @@
|
||||
btn.classList.add('active');
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
fan.setAttribute('aria-hidden', 'false');
|
||||
// Handoff: once the sky is saved (#id_sky_btn .active = the burger has
|
||||
// already pulsed its --priTk cue), opening the fan pulses the Sky
|
||||
// sub-btn the same way — "now click me to reopen your sky".
|
||||
var skyBtn = document.getElementById('id_sky_btn');
|
||||
if (skyBtn && skyBtn.classList.contains('active')) _pulseGlow(skyBtn);
|
||||
}
|
||||
|
||||
function _close() {
|
||||
@@ -291,13 +296,57 @@
|
||||
}
|
||||
window.bindVoiceBtn = bindVoiceBtn;
|
||||
|
||||
// ── Sky sub-btn (CAST SKY reopen) + sky-saved glow ──────────────────────
|
||||
// Once the sky is saved the burger pulses thrice in --priTk (a finite cue
|
||||
// "your sky lives here") and the Sky sub-btn goes .active → an active click
|
||||
// reopens the saved wheel via the sky overlay's exposed window.openSkyFelt.
|
||||
// The pulse fires here on load when the server flagged a saved sky
|
||||
// (#id_burger_btn[data-sky-glow]); the sky overlay also calls pulseSkyGlow()
|
||||
// at the SAVE moment (no reload) so the cue plays the instant you save.
|
||||
// Thrice --priTk pulse (.sky-saved-glow) on a target btn — same cadence as
|
||||
// .flash-inactive. The burger gets it the moment the sky is saved; the Sky
|
||||
// sub-btn gets it on the NEXT burger-open (the cue hands off burger → sub-btn
|
||||
// so the user is led from "look at the burger" to "click the Sky btn").
|
||||
function _pulseGlow(el) {
|
||||
if (!el) return;
|
||||
var pulses = 3, onMs = 220, offMs = 160;
|
||||
(function pulse(n) {
|
||||
if (n <= 0) return;
|
||||
el.classList.add('sky-saved-glow');
|
||||
setTimeout(function () {
|
||||
el.classList.remove('sky-saved-glow');
|
||||
setTimeout(function () { pulse(n - 1); }, offMs);
|
||||
}, onMs);
|
||||
}(pulses));
|
||||
}
|
||||
function pulseSkyGlow() { _pulseGlow(document.getElementById('id_burger_btn')); }
|
||||
window.pulseSkyGlow = pulseSkyGlow;
|
||||
|
||||
function bindSkyBtn() {
|
||||
var skyBtn = document.getElementById('id_sky_btn');
|
||||
if (!skyBtn) return;
|
||||
// Active click → reopen the saved wheel. No stopPropagation: the burger's
|
||||
// delegated handler then closes the fan (its .active behaviour). Inactive
|
||||
// clicks fall through to that handler's 2-pulse flash.
|
||||
skyBtn.addEventListener('click', function () {
|
||||
if (!skyBtn.classList.contains('active')) return;
|
||||
if (window.openSkyFelt) window.openSkyFelt();
|
||||
});
|
||||
// Server flagged a saved sky on this load → play the thrice cue once.
|
||||
var burger = document.getElementById('id_burger_btn');
|
||||
if (burger && burger.dataset.skyGlow) pulseSkyGlow();
|
||||
}
|
||||
window.bindSkyBtn = bindSkyBtn;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
bindBurger();
|
||||
bindVoiceBtn();
|
||||
bindSkyBtn();
|
||||
});
|
||||
} else {
|
||||
bindBurger();
|
||||
bindVoiceBtn();
|
||||
bindSkyBtn();
|
||||
}
|
||||
}());
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
var paneDefault = roomMenu && roomMenu.querySelector('.room-menu-default');
|
||||
var paneScroll = roomMenu && roomMenu.querySelector('.room-menu-scroll');
|
||||
var paneAtlas = roomMenu && roomMenu.querySelector('.room-menu-atlas');
|
||||
var paneSky = roomMenu && roomMenu.querySelector('.room-menu-sky');
|
||||
var GEARLESS = { yarn: true, post: true, pulse: true };
|
||||
var onReelhouse = false;
|
||||
|
||||
@@ -98,6 +99,7 @@
|
||||
if (paneDefault) paneDefault.style.display = pane === paneDefault ? 'contents' : 'none';
|
||||
if (paneScroll) paneScroll.style.display = pane === paneScroll ? '' : 'none';
|
||||
if (paneAtlas) paneAtlas.style.display = pane === paneAtlas ? '' : 'none';
|
||||
if (paneSky) paneSky.style.display = pane === paneSky ? '' : 'none';
|
||||
}
|
||||
function closeRoomMenu() {
|
||||
if (roomMenu) roomMenu.style.display = 'none';
|
||||
@@ -105,6 +107,14 @@
|
||||
}
|
||||
function updateGear() {
|
||||
if (!gearBtn) return;
|
||||
// CAST SKY felt open → the gear is the sky NVM pane (returns to the
|
||||
// hex), regardless of scroll position: the aperture is pinned to the
|
||||
// hex pane while the felt is up, so onReelhouse stays false anyway.
|
||||
if (document.documentElement.classList.contains('sky-open')) {
|
||||
gearBtn.classList.remove('gear-disabled');
|
||||
showPane(paneSky);
|
||||
return;
|
||||
}
|
||||
var disabled = onReelhouse && GEARLESS[current];
|
||||
gearBtn.classList.toggle('gear-disabled', !!disabled);
|
||||
if (disabled) { closeRoomMenu(); return; }
|
||||
@@ -112,6 +122,10 @@
|
||||
else if (current === 'scroll') showPane(paneScroll);
|
||||
else if (current === 'atlas') showPane(paneAtlas);
|
||||
}
|
||||
// Exposed so the CAST SKY felt (sky overlay JS) can re-sync the gear pane
|
||||
// the moment it opens/closes (html.sky-open toggles outside any scroll/
|
||||
// view event that would otherwise drive updateGear).
|
||||
window.RoomViews.syncGear = updateGear;
|
||||
|
||||
function setActiveView(view) {
|
||||
current = view;
|
||||
|
||||
@@ -29,9 +29,19 @@ var SigSelect = (function () {
|
||||
var _reservedFloats = {}; // key: role → portal <i> element (thumbs-up, frozen)
|
||||
var _cursorPortal = null;
|
||||
|
||||
// CAST SKY click → page reload. Reassignable via setReload for tests so
|
||||
// they can spy without actually reloading the Jasmine runner.
|
||||
var _reload = function () { window.location.reload(); };
|
||||
// CAST SKY click crosses the SIG_SELECT → SKY_SELECT server-render boundary
|
||||
// (the felt + phase-stack + sea-inject only exist once table_status is
|
||||
// SKY_SELECT), so it reloads — but drops a sessionStorage flag first so the
|
||||
// felt OPENS on arrival. That kills the old click→reload→click-again double-
|
||||
// take: one click now yields the sky form (the _sky_overlay.html init reads
|
||||
// the flag + clears it). sessionStorage (not a URL param) so the open survives
|
||||
// the reload reliably regardless of redirect/query timing. DRAW SEA can inject
|
||||
// in-place because it stays WITHIN the SKY_SELECT page; CAST SKY can't, so this
|
||||
// is the closest seamless equivalent. Reassignable via setReload for tests.
|
||||
var _reload = function () {
|
||||
try { sessionStorage.setItem('sky-autoopen', '1'); } catch (e) {}
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
function getCsrf() {
|
||||
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
||||
|
||||
@@ -2480,6 +2480,66 @@ class PickSkyRenderingTest(TestCase):
|
||||
self.assertNotContains(response, 'id="id_sky_delete_btn"')
|
||||
|
||||
|
||||
class PickSkyUnifiedFeltTest(TestCase):
|
||||
"""CAST SKY unified with the my_sky apparatus: the dark Gaussian modal
|
||||
(.sky-backdrop + .sky-modal-wrap) is replaced by a --duoUser felt .sky-page
|
||||
rendered INSIDE .room-hex-pane (my_sea-style, mirroring the Sig Select
|
||||
SigSelectUnifiedStageTest precedent). Founder is PC (levity), sky not yet
|
||||
confirmed → the felt form is the active surface."""
|
||||
|
||||
def setUp(self):
|
||||
self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self)
|
||||
self.room.table_status = Room.SKY_SELECT
|
||||
self.room.save()
|
||||
self.sig_card = TarotCard.objects.get(
|
||||
deck_variant=self.earthman, arcana="MIDDLE", suit="BRANDS", number=11
|
||||
)
|
||||
pc_seat = TableSeat.objects.get(room=self.room, role="PC")
|
||||
pc_seat.significator = self.sig_card
|
||||
pc_seat.save()
|
||||
self.url = reverse("epic:room", kwargs={"room_id": self.room.id})
|
||||
|
||||
def test_sky_felt_in_hex_pane_no_dark_backdrop(self):
|
||||
content = self.client.get(self.url).content.decode()
|
||||
# Felt modifier on the hex pane; the old dark Gaussian chrome is gone.
|
||||
self.assertIn("has-sky-stage", content)
|
||||
self.assertNotIn("sky-backdrop", content)
|
||||
self.assertNotIn("sky-modal-wrap", content)
|
||||
# The --duoUser felt wrapper (shared .sky-page class) is present.
|
||||
self.assertIn("sky-page", content)
|
||||
|
||||
def test_sky_overlay_lives_inside_hex_pane(self):
|
||||
content = self.client.get(self.url).content.decode()
|
||||
# The overlay sits INSIDE the hex pane, before the scroll/views pane —
|
||||
# so it fills the hex (my_sea-style) instead of floating over the page.
|
||||
hex_pos = content.find("room-hex-pane")
|
||||
overlay_pos = content.find("id_sky_overlay")
|
||||
scroll_pos = content.find("room-scroll-pane")
|
||||
self.assertNotEqual(overlay_pos, -1)
|
||||
self.assertLess(hex_pos, overlay_pos)
|
||||
self.assertLess(overlay_pos, scroll_pos)
|
||||
|
||||
def test_sky_felt_absent_once_confirmed(self):
|
||||
# A confirmed sky flips the hex to DRAW SEA — the felt form must not
|
||||
# render alongside the sea overlay.
|
||||
pc_seat = TableSeat.objects.get(room=self.room, role="PC")
|
||||
Character.objects.create(seat=pc_seat, confirmed_at=timezone.now())
|
||||
content = self.client.get(self.url).content.decode()
|
||||
self.assertNotIn("has-sky-stage", content)
|
||||
|
||||
def test_gear_has_sky_nvm_pane_returning_to_hex(self):
|
||||
# NVM moved off the felt into the room gear's own sky pane; its NVM
|
||||
# returns to the table-hex (epic:room), where the server re-renders
|
||||
# DRAW SEA (if saved) or CAST SKY (if not).
|
||||
content = self.client.get(self.url).content.decode()
|
||||
self.assertIn("room-menu-sky", content)
|
||||
hex_url = reverse("epic:room", kwargs={"room_id": self.room.id})
|
||||
sky_pane = content[content.find("room-menu-sky"):]
|
||||
sky_pane = sky_pane[: sky_pane.find("</div>")]
|
||||
self.assertIn(hex_url, sky_pane)
|
||||
self.assertIn("NVM", sky_pane)
|
||||
|
||||
|
||||
# ── SEA_SELECT rendering ──────────────────────────────────────────────────────
|
||||
|
||||
class PickSeaRenderingTest(TestCase):
|
||||
@@ -2524,6 +2584,37 @@ class PickSeaRenderingTest(TestCase):
|
||||
response = self.client.get(self.url)
|
||||
self.assertContains(response, "id_sea_overlay")
|
||||
|
||||
# ── Burger Sky sub-btn reopen + glow (Phase 5) ──────────────────────────
|
||||
def test_sky_btn_active_and_glow_flag_when_confirmed(self):
|
||||
self._confirm_sky()
|
||||
response = self.client.get(self.url)
|
||||
self.assertTrue(response.context["sky_btn_active"])
|
||||
content = response.content.decode()
|
||||
# Burger carries the load-pulse flag; the Sky sub-btn goes active.
|
||||
self.assertIn("data-sky-glow", content)
|
||||
# The felt stays in the DOM (hidden) so the burger can reopen the wheel.
|
||||
self.assertIn("id_sky_overlay", content)
|
||||
|
||||
def test_sky_btn_inactive_and_no_glow_when_not_confirmed(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertFalse(response.context.get("sky_btn_active"))
|
||||
self.assertNotIn("data-sky-glow", response.content.decode())
|
||||
|
||||
def test_saved_sky_json_primes_reopen_when_confirmed(self):
|
||||
char = self._confirm_sky()
|
||||
char.chart_data = {"planets": {"Sun": {"sign": "Gemini"}}}
|
||||
char.save()
|
||||
response = self.client.get(self.url)
|
||||
# The confirmed chart is handed to the felt so the burger reopen draws
|
||||
# the saved wheel without a fresh PySwiss round-trip.
|
||||
self.assertIn("Gemini", response.context["saved_sky_json"])
|
||||
|
||||
def test_saved_sky_json_is_null_literal_when_not_confirmed(self):
|
||||
# The felt's `const _savedSky = {{ saved_sky_json|...|safe }};` must be
|
||||
# valid JS even with no saved sky — a bare `null`, never empty.
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.context["saved_sky_json"], "null")
|
||||
|
||||
def test_sea_overlay_select_defaults_to_waite_smith_for_levity(self):
|
||||
self._confirm_sky()
|
||||
response = self.client.get(self.url)
|
||||
|
||||
@@ -648,6 +648,16 @@ def _role_select_context(room, user, seat_param=None):
|
||||
sky_confirmed = confirmed_char is not None
|
||||
ctx["sky_confirmed"] = sky_confirmed
|
||||
ctx["user_seat_role"] = _canonical_seat.role if _canonical_seat else ''
|
||||
# Burger-fan Sky sub-btn: ACTIVE once the sky is saved (confirmed) — the
|
||||
# burger then pulses thrice (--priTk) on load + the sub-btn re-opens the
|
||||
# saved wheel. `saved_sky_json` primes that reopen so the felt draws the
|
||||
# confirmed chart without a fresh PySwiss round-trip (mirrors My Sky).
|
||||
ctx["sky_btn_active"] = sky_confirmed
|
||||
ctx["saved_sky_json"] = (
|
||||
json.dumps(confirmed_char.chart_data)
|
||||
if (sky_confirmed and confirmed_char.chart_data)
|
||||
else "null"
|
||||
)
|
||||
if sky_confirmed:
|
||||
# Fall back to seat.significator for Characters created before the sync was added
|
||||
ctx["my_tray_sig"] = confirmed_char.significator or _canonical_seat.significator
|
||||
|
||||
@@ -96,4 +96,21 @@ class RobustCompressorTestRunner(DiscoverRunner):
|
||||
"WHERE datname = %s AND pid <> pg_backend_pid()",
|
||||
[test_db_name],
|
||||
)
|
||||
super().teardown_databases(old_config, **kwargs)
|
||||
# On local Windows + SQLite, super() ends in os.remove(test_db.sqlite3),
|
||||
# which raises PermissionError [WinError 32] when another handle still
|
||||
# holds the file (a concurrent run, a lingering connection, AV / Search
|
||||
# indexer). Retry briefly, then leave the stale file for the next run to
|
||||
# overwrite rather than crash an otherwise-green run. CI is Postgres on
|
||||
# Linux — DROP DATABASE, no file remove, and Linux unlinks open files
|
||||
# without error — so this loop never triggers there. Mirrors
|
||||
# _robust_save's PermissionError retry.
|
||||
for attempt in range(10):
|
||||
try:
|
||||
super().teardown_databases(old_config, **kwargs)
|
||||
return
|
||||
except PermissionError:
|
||||
if attempt == 9:
|
||||
print("teardown_databases: test DB file still locked after "
|
||||
"retries — leaving it for the next run to overwrite.")
|
||||
return
|
||||
time.sleep(0.1)
|
||||
|
||||
@@ -92,19 +92,29 @@ class PickSeaAsyncTransitionTest(ChannelsFunctionalTest):
|
||||
""")
|
||||
|
||||
def test_pick_sea_btn_visible_after_sky_confirm(self):
|
||||
"""Confirming sky reloads the room to the hex w. DRAW SEA replacing
|
||||
CAST SKY; the sea overlay is NOT auto-opened."""
|
||||
"""Confirming sky lands the room on the hex w. DRAW SEA in place of CAST
|
||||
SKY; the sea overlay is NOT auto-opened. (A direct-POST confirm has no
|
||||
captured chart on this browser, so _onSkyConfirmed reloads to the server-
|
||||
rendered DRAW SEA hex; the felt-save path eases there via the cascade.)"""
|
||||
self.create_pre_authenticated_session("founder@test.io")
|
||||
self.browser.get(self.room_url)
|
||||
|
||||
# Sky not yet confirmed — CAST SKY btn present.
|
||||
# Sky not yet confirmed — CAST SKY is the live (non --out) phase btn.
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
|
||||
|
||||
self._confirm_sky()
|
||||
|
||||
# Page reloads → hex shows DRAW SEA in place of CAST SKY.
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sea_btn"))
|
||||
self.assertEqual(self.browser.find_elements(By.ID, "id_pick_sky_btn"), [])
|
||||
# DRAW SEA becomes the live phase btn; CAST SKY stays in the phase-stack
|
||||
# but goes --out (the buttons cross-fade in one grid cell now, so the
|
||||
# stale CAST SKY is hidden via the class, not removed from the DOM).
|
||||
self.wait_for(lambda: self.assertNotIn(
|
||||
"hex-phase-btn--out",
|
||||
self.browser.find_element(By.ID, "id_pick_sea_btn").get_attribute("class"),
|
||||
))
|
||||
self.assertIn(
|
||||
"hex-phase-btn--out",
|
||||
self.browser.find_element(By.ID, "id_pick_sky_btn").get_attribute("class"),
|
||||
)
|
||||
|
||||
# Sea overlay is NOT auto-opened — it only appears once the gamer
|
||||
# clicks DRAW SEA.
|
||||
|
||||
@@ -95,32 +95,12 @@ class PickSkyLocalStorageTest(FunctionalTest):
|
||||
""")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# T1 — fields survive NVM (close + reopen, same page load) #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_form_fields_repopulated_after_nvm(self):
|
||||
self.create_pre_authenticated_session(self.founder_email)
|
||||
self.browser.get(self.room_url)
|
||||
|
||||
self._open_overlay()
|
||||
self._fill_form()
|
||||
|
||||
# Close via NVM
|
||||
self.browser.find_element(By.ID, "id_sky_cancel").click()
|
||||
|
||||
# Reopen
|
||||
self._open_overlay()
|
||||
|
||||
values = self._field_values()
|
||||
self.assertEqual(values["date"], "2008-05-27")
|
||||
self.assertEqual(values["lat"], "38.3754")
|
||||
self.assertEqual(values["lon"], "-76.6955")
|
||||
self.assertEqual(values["place"], "Morganza, MD")
|
||||
self.assertEqual(values["tz"], "America/New_York")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# T2 — fields survive a page refresh #
|
||||
# Fields survive a page refresh #
|
||||
# ------------------------------------------------------------------ #
|
||||
# (The in-felt NVM was removed in the 2026-06-07 CAST SKY unification —
|
||||
# NVM now lives in the room gear menu and RELOADS back to the hex, so
|
||||
# the close-and-reopen-without-reload path no longer exists. localStorage
|
||||
# durability across a reload is exactly what this refresh case asserts.)
|
||||
|
||||
def test_form_fields_repopulated_after_page_refresh(self):
|
||||
self.create_pre_authenticated_session(self.founder_email)
|
||||
@@ -145,11 +125,12 @@ class PickSkyLocalStorageTest(FunctionalTest):
|
||||
|
||||
|
||||
class PickSkyDelTest(FunctionalTest):
|
||||
"""CAST SKY overlay gets a DEL btn at the wheel center: clicking opens the
|
||||
global guard portal; OK clears the wheel SVG, resets the form fields, &
|
||||
purges the localStorage entry that would otherwise rehydrate the form on
|
||||
the next overlay open / page refresh. No server hit (the wheel here is
|
||||
purely a preview — un-saved data lives only in localStorage)."""
|
||||
"""CAST SKY felt gets a DEL btn at the wheel center AFTER SAVE (the wheel
|
||||
only paints once saved now — my_sky parity, 2026-06-07): clicking DEL opens
|
||||
the global guard portal; OK clears the wheel SVG, resets the form fields,
|
||||
drops the saved state (body.sky-saved), & purges the localStorage entry that
|
||||
would otherwise rehydrate the form. The save/delete round-trips are mocked
|
||||
so the test stays client-focused."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -173,13 +154,13 @@ class PickSkyDelTest(FunctionalTest):
|
||||
self.create_pre_authenticated_session(self.founder_email)
|
||||
self.browser.get(self.room_url)
|
||||
|
||||
# Open CAST SKY modal
|
||||
# Open CAST SKY felt
|
||||
btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_pick_sky_btn"))
|
||||
self.browser.execute_script("arguments[0].click()", btn)
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_sky_overlay"))
|
||||
|
||||
# Mock /sky/preview so schedulePreview resolves & SkyWheel.draw paints
|
||||
# children into #id_sky_svg without hitting PySwiss.
|
||||
# Mock /sky/preview (enables SAVE + captures the chart), /sky/save (reveal
|
||||
# the wheel) & /sky/delete (DEL purge) so the test stays client-focused.
|
||||
self.browser.execute_script("""
|
||||
const FIXTURE = """ + _json.dumps(_PICK_SKY_CHART_FIXTURE) + """;
|
||||
window._origFetch = window.fetch;
|
||||
@@ -187,11 +168,17 @@ class PickSkyDelTest(FunctionalTest):
|
||||
if (typeof url === 'string' && url.includes('/sky/preview')) {
|
||||
return Promise.resolve({ok:true, json:()=>Promise.resolve(FIXTURE)});
|
||||
}
|
||||
if (typeof url === 'string' && url.includes('/sky/save')) {
|
||||
return Promise.resolve({ok:true, json:()=>Promise.resolve({id:1, confirmed:true})});
|
||||
}
|
||||
if (typeof url === 'string' && url.includes('/sky/delete')) {
|
||||
return Promise.resolve({ok:true, json:()=>Promise.resolve({deleted:true})});
|
||||
}
|
||||
return window._origFetch(url, opts);
|
||||
};
|
||||
""")
|
||||
|
||||
# Fill form → triggers schedulePreview → wheel renders
|
||||
# Fill form → triggers schedulePreview (no draw pre-save — my_sky parity)
|
||||
self.browser.execute_script("""
|
||||
document.getElementById('id_nf_date').value = '2008-05-27';
|
||||
document.getElementById('id_nf_lat').value = '38.3754';
|
||||
@@ -203,10 +190,21 @@ class PickSkyDelTest(FunctionalTest):
|
||||
);
|
||||
""")
|
||||
|
||||
# Wait for the wheel to render (svg has children)
|
||||
# SAVE SKY enables once the (mocked) preview resolves → click it → the
|
||||
# wheel reveals + the DEL btn injects (post-save, not on preview).
|
||||
save_btn = self.browser.find_element(By.ID, "id_sky_confirm")
|
||||
self.wait_for(lambda: self.assertFalse(save_btn.get_attribute("disabled")))
|
||||
# JS-click — the felt fills the aperture and SAVE sits in the felt's own
|
||||
# scroll, so Selenium's scroll-into-view trips; the click itself is glue.
|
||||
self.browser.execute_script("arguments[0].click()", save_btn)
|
||||
|
||||
# Wait for the wheel to render (svg has children) + saved state engaged.
|
||||
self.wait_for(lambda: self.assertTrue(
|
||||
self.browser.find_elements(By.CSS_SELECTOR, "#id_sky_svg > *")
|
||||
))
|
||||
self.assertIn("sky-saved", self.browser.execute_script(
|
||||
"return document.body.className;"
|
||||
))
|
||||
|
||||
# localStorage was populated by _saveForm during typing
|
||||
ls_key = self._ls_key()
|
||||
@@ -214,12 +212,14 @@ class PickSkyDelTest(FunctionalTest):
|
||||
f"return localStorage.getItem({_json.dumps(ls_key)});"
|
||||
))
|
||||
|
||||
# DEL btn → guard portal → OK
|
||||
del_btn = self.browser.find_element(By.ID, "id_sky_delete_btn")
|
||||
del_btn.click()
|
||||
# DEL btn → guard portal → OK (JS-click — same felt-scroll glue as SAVE)
|
||||
del_btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_sky_delete_btn"))
|
||||
self.browser.execute_script("arguments[0].click()", del_btn)
|
||||
portal = self.wait_for(lambda: self.browser.find_element(By.ID, "id_guard_portal"))
|
||||
self.wait_for(lambda: self.assertIn("active", portal.get_attribute("class")))
|
||||
portal.find_element(By.CSS_SELECTOR, ".guard-yes").click()
|
||||
self.browser.execute_script(
|
||||
"arguments[0].click()", portal.find_element(By.CSS_SELECTOR, ".guard-yes")
|
||||
)
|
||||
|
||||
# After OK: SVG empty, form fields blank, localStorage entry purged,
|
||||
# and the DEL btn itself is gone — re-injection only happens on the
|
||||
@@ -232,6 +232,11 @@ class PickSkyDelTest(FunctionalTest):
|
||||
self.browser.find_elements(By.ID, "id_sky_delete_btn"),
|
||||
"DEL btn should be removed from the DOM after clear",
|
||||
)
|
||||
# The felt returns to form-alone — the saved state is dropped so the
|
||||
# wheel page collapses + the scroll-snap disengages.
|
||||
self.assertNotIn("sky-saved", self.browser.execute_script(
|
||||
"return document.body.className;"
|
||||
))
|
||||
values = self.browser.execute_script("""
|
||||
return {
|
||||
date: document.getElementById('id_nf_date').value,
|
||||
|
||||
@@ -250,6 +250,143 @@ describe("Burger", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Sky sub-btn reopen + sky-saved glow (CAST SKY unification, 2026-06-07).
|
||||
// Once the sky is saved the burger pulses thrice in --priTk (.sky-saved-glow,
|
||||
// a finite cue) and the Sky sub-btn goes .active → an active click reopens the
|
||||
// saved wheel via the sky overlay's exposed window.openSkyFelt.
|
||||
describe("sky sub-btn reopen + sky-saved glow", () => {
|
||||
let burgerBtn, skyBtn, origOpen;
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
burgerBtn = document.createElement("button");
|
||||
burgerBtn.id = "id_burger_btn";
|
||||
document.body.appendChild(burgerBtn);
|
||||
skyBtn = document.createElement("button");
|
||||
skyBtn.id = "id_sky_btn";
|
||||
skyBtn.className = "burger-fan-btn";
|
||||
document.body.appendChild(skyBtn);
|
||||
origOpen = window.openSkyFelt;
|
||||
window.openSkyFelt = jasmine.createSpy("openSkyFelt");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
window.openSkyFelt = origOpen;
|
||||
burgerBtn.remove();
|
||||
skyBtn.remove();
|
||||
});
|
||||
|
||||
describe("pulseSkyGlow()", () => {
|
||||
it("adds .sky-saved-glow to the burger on the first pulse", () => {
|
||||
window.pulseSkyGlow();
|
||||
expect(burgerBtn.classList.contains("sky-saved-glow")).toBe(true);
|
||||
});
|
||||
|
||||
it("clears the glow after the ON window (~220ms)", () => {
|
||||
window.pulseSkyGlow();
|
||||
jasmine.clock().tick(240);
|
||||
expect(burgerBtn.classList.contains("sky-saved-glow")).toBe(false);
|
||||
});
|
||||
|
||||
it("pulses a third time (still glowing into the 3rd ON window)", () => {
|
||||
window.pulseSkyGlow();
|
||||
// Two full cycles: 2 × (220 ON + 160 OFF) = 760ms → 3rd ON begins.
|
||||
jasmine.clock().tick(770);
|
||||
expect(burgerBtn.classList.contains("sky-saved-glow")).toBe(true);
|
||||
});
|
||||
|
||||
it("settles back to default after the 3 pulses (~1.2s total)", () => {
|
||||
window.pulseSkyGlow();
|
||||
jasmine.clock().tick(1300);
|
||||
expect(burgerBtn.classList.contains("sky-saved-glow")).toBe(false);
|
||||
});
|
||||
|
||||
it("no-ops when the burger btn is absent", () => {
|
||||
burgerBtn.remove();
|
||||
expect(() => window.pulseSkyGlow()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("bindSkyBtn()", () => {
|
||||
it("active Sky-btn click reopens the saved wheel (window.openSkyFelt)", () => {
|
||||
skyBtn.classList.add("active");
|
||||
bindSkyBtn();
|
||||
skyBtn.click();
|
||||
expect(window.openSkyFelt).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("inactive Sky-btn click does NOT reopen (left to the flash stub)", () => {
|
||||
bindSkyBtn(); // skyBtn has no .active
|
||||
skyBtn.click();
|
||||
expect(window.openSkyFelt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("pulses the burger on load when the server flagged a saved sky", () => {
|
||||
burgerBtn.dataset.skyGlow = "1";
|
||||
bindSkyBtn();
|
||||
expect(burgerBtn.classList.contains("sky-saved-glow")).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT pulse on load without the data-sky-glow flag", () => {
|
||||
bindSkyBtn();
|
||||
expect(burgerBtn.classList.contains("sky-saved-glow")).toBe(false);
|
||||
});
|
||||
|
||||
it("no-ops when the Sky btn is absent", () => {
|
||||
skyBtn.remove();
|
||||
expect(() => bindSkyBtn()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Burger → Sky-btn pulse handoff (CAST SKY cascade, 2026-06-07). Once the sky
|
||||
// is saved the burger pulses --priTk; the cue then HANDS OFF to the Sky sub-btn,
|
||||
// which pulses the same way on the next burger-OPEN ("now click me to reopen").
|
||||
describe("burger → sky-btn pulse handoff", () => {
|
||||
let burgerBtn, fan, skyBtn, ac;
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
burgerBtn = document.createElement("button");
|
||||
burgerBtn.id = "id_burger_btn";
|
||||
document.body.appendChild(burgerBtn);
|
||||
fan = document.createElement("div");
|
||||
fan.id = "id_burger_fan";
|
||||
document.body.appendChild(fan);
|
||||
skyBtn = document.createElement("button");
|
||||
skyBtn.id = "id_sky_btn";
|
||||
skyBtn.className = "burger-fan-btn";
|
||||
document.body.appendChild(skyBtn);
|
||||
ac = bindBurger();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (ac) ac.abort();
|
||||
jasmine.clock().uninstall();
|
||||
[burgerBtn, fan, skyBtn].forEach((el) => el && el.remove());
|
||||
});
|
||||
|
||||
it("pulses the Sky sub-btn on burger-open once the sky is saved (.active)", () => {
|
||||
skyBtn.classList.add("active");
|
||||
burgerBtn.click(); // open the fan
|
||||
expect(skyBtn.classList.contains("sky-saved-glow")).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT pulse the Sky sub-btn on open before the sky is saved", () => {
|
||||
burgerBtn.click();
|
||||
expect(skyBtn.classList.contains("sky-saved-glow")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not pulse on burger-CLOSE", () => {
|
||||
skyBtn.classList.add("active");
|
||||
burgerBtn.click(); // open → pulses
|
||||
jasmine.clock().tick(2000); // let the 3-pulse cycle finish
|
||||
burgerBtn.click(); // close
|
||||
expect(skyBtn.classList.contains("sky-saved-glow")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Voice persistence across my_sea reloads — bindVoiceBtn remembers the active
|
||||
// room in sessionStorage + silently re-joins it on the next page if voice is
|
||||
// still available (2026-05-29).
|
||||
|
||||
@@ -189,6 +189,24 @@
|
||||
0 0 1.2rem 0.3rem rgba(var(--ninUser), 0.35);
|
||||
}
|
||||
|
||||
// ── Sky-saved glow (CAST SKY → SAVE SKY) ──────────────────────────────
|
||||
//
|
||||
// Once the sky is saved, the burger pulses thrice in --priTk (border + icon)
|
||||
// to cue "your sky lives here now — reopen it via the Sky sub-btn". burger-
|
||||
// btn.js toggles .sky-saved-glow three times (finite), in rhyme with the
|
||||
// 2-pulse --priRd .flash-inactive + the voice pulse above. The Sky sub-btn's
|
||||
// own .active state (real cloud icon, opacity 1) is handled by the generic
|
||||
// .burger-fan-btn.active rules. Shared by the burger (save-moment cue) AND the
|
||||
// Sky sub-btn (the handoff cue on the next burger-open).
|
||||
#id_burger_btn.sky-saved-glow,
|
||||
#id_sky_btn.sky-saved-glow {
|
||||
color: rgba(var(--priTk), 1);
|
||||
border-color: rgba(var(--priTk), 1);
|
||||
box-shadow:
|
||||
0 0 0.5rem 0.1rem rgba(var(--priTk), 0.75),
|
||||
0 0 1.2rem 0.3rem rgba(var(--priTk), 0.35);
|
||||
}
|
||||
|
||||
// ── Voice affordance glow + pulse (Phase 3, my-sea voice) ─────────────
|
||||
//
|
||||
// Distinct from the sea-btn's --priYl `.glow-handoff` draw nudge: voice uses
|
||||
|
||||
@@ -91,6 +91,18 @@ html.sea-open #id_aperture_fill {
|
||||
}
|
||||
}
|
||||
|
||||
// While the CAST SKY felt is summoned (html.sky-open) the outer aperture must
|
||||
// NOT scroll down to the reelhouse carousel (ATLAS/SCROLL/YARN/POST/PULSE) —
|
||||
// the felt's OWN form↔wheel scroll-snap (.sky-page--room) is the only scroll
|
||||
// in play. Pin the aperture to the hex pane (which the felt fills); restored
|
||||
// the instant the felt closes (sky-open removed). overflow:hidden alone leaves
|
||||
// the aperture parked on whichever pane it was showing — and CAST SKY is only
|
||||
// reachable from the hex pane, so it pins to the hex every time.
|
||||
html.sky-open .room-aperture.is-scrollable {
|
||||
overflow-y: hidden;
|
||||
scroll-snap-type: none;
|
||||
}
|
||||
|
||||
.room-scroll-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -968,6 +980,29 @@ html .room-gate-page.room-gate-page .position-strip .gate-slot { pointer-events:
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// Hex phase-button stack — CAST SKY + DRAW SEA share ONE grid cell so they can
|
||||
// cross-fade IN PLACE during the post-save cascade (the sky-overlay JS toggles
|
||||
// .hex-phase-btn--out). The grid sizes to the larger btn; .table-center centers
|
||||
// it. Only the SKY_SELECT phase renders both; other phases use a lone button.
|
||||
.hex-phase-stack {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hex-phase-stack > .hex-phase-btn {
|
||||
grid-area: 1 / 1; // stack both buttons in the same cell
|
||||
transition: opacity 0.45s ease, transform 0.45s ease;
|
||||
}
|
||||
|
||||
// Eased-out state — invisible + inert, but still in layout so the partner btn
|
||||
// can cross-fade in (display:none can't transition).
|
||||
.hex-phase-btn--out {
|
||||
opacity: 0;
|
||||
transform: scale(0.7);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// "Gravity settling . . ." / "Levity appraising . . ." shown after a polarity
|
||||
// group confirms their sigs while the other group is still selecting.
|
||||
// Pulsing opacity signals active waiting without being jarring.
|
||||
|
||||
@@ -9,8 +9,61 @@
|
||||
|
||||
html.sky-open {
|
||||
overflow: hidden;
|
||||
// NB: the modal-era `#id_aperture_fill { opacity: 1 }` backdrop is GONE here.
|
||||
// It's a full-cover --duoUser div at z-90; the old dark modal sat above it
|
||||
// (z-120), but the inline felt sits at z-5, so lighting the fill painted an
|
||||
// opaque green sheet OVER the felt + form (the fill's opacity transition is
|
||||
// why the form "flashed then vanished"). The felt is its own --duoUser
|
||||
// surface + covers the hex on its own, so the fill stays transparent now.
|
||||
}
|
||||
|
||||
#id_aperture_fill { opacity: 1; }
|
||||
// ── In-room CAST SKY felt (unified with my_sky / sky.html) ────────────────────
|
||||
// The .sky-page apparatus rendered INSIDE .room-hex-pane (my_sea-style, like
|
||||
// .sig-overlay) instead of the old fixed dark modal. The hex pane becomes a
|
||||
// positioning context only while the felt shows, so the absolute-filling felt
|
||||
// scopes to the pane (not the viewport) and the position-strip's root stacking
|
||||
// stays untouched in every other phase. Hidden until the CAST SKY btn adds
|
||||
// html.sky-open; reuses every dashboard .sky-page form/wheel rule below.
|
||||
.room-hex-pane.has-sky-stage {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// NB: chained to (0,2,0) — the base `.sky-page { position: relative }` block
|
||||
// lives LATER in this file, so a bare `.sky-page--room` (0,1,0) loses the tie on
|
||||
// source order and the felt stays position:relative → it collapses to width 0 as
|
||||
// a flex child of the hex-pane (the form vanishes onto a 0-wide column). The
|
||||
// chain wins regardless of order. [[feedback-scss-import-order-specificity]]
|
||||
.sky-page.sky-page--room {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
// Within the hex-pane stacking context (matches .sig-overlay's z:5), above
|
||||
// the hex/seats it covers but below the root-level position strip (z-130).
|
||||
z-index: 5;
|
||||
// Hidden until opened — pointer-events off so the hidden felt can't eat the
|
||||
// CAST SKY btn click beneath it.
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html.sky-open .sky-page.sky-page--room {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
// While the felt is up, hide the position strip (a z-130 hex-pane sibling that
|
||||
// would otherwise float its circles over the form) so the felt reads as a clean
|
||||
// homogeneous surface — restored the instant the felt closes.
|
||||
html.sky-open .position-strip {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
// Post-save cascade ease-out — the felt fades to transparent over ~0.5s (the JS
|
||||
// _FELT_FADE timer) BEFORE sky-open is removed, so the table-hex is revealed with
|
||||
// a soft dissolve instead of a hard snap. Visibility is still on (sky-open) for
|
||||
// the duration of the fade; the JS removes sky-open once the opacity lands at 0.
|
||||
.sky-page.sky-page--cascade-out {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
// ── Backdrop ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -250,6 +250,143 @@ describe("Burger", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Sky sub-btn reopen + sky-saved glow (CAST SKY unification, 2026-06-07).
|
||||
// Once the sky is saved the burger pulses thrice in --priTk (.sky-saved-glow,
|
||||
// a finite cue) and the Sky sub-btn goes .active → an active click reopens the
|
||||
// saved wheel via the sky overlay's exposed window.openSkyFelt.
|
||||
describe("sky sub-btn reopen + sky-saved glow", () => {
|
||||
let burgerBtn, skyBtn, origOpen;
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
burgerBtn = document.createElement("button");
|
||||
burgerBtn.id = "id_burger_btn";
|
||||
document.body.appendChild(burgerBtn);
|
||||
skyBtn = document.createElement("button");
|
||||
skyBtn.id = "id_sky_btn";
|
||||
skyBtn.className = "burger-fan-btn";
|
||||
document.body.appendChild(skyBtn);
|
||||
origOpen = window.openSkyFelt;
|
||||
window.openSkyFelt = jasmine.createSpy("openSkyFelt");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
window.openSkyFelt = origOpen;
|
||||
burgerBtn.remove();
|
||||
skyBtn.remove();
|
||||
});
|
||||
|
||||
describe("pulseSkyGlow()", () => {
|
||||
it("adds .sky-saved-glow to the burger on the first pulse", () => {
|
||||
window.pulseSkyGlow();
|
||||
expect(burgerBtn.classList.contains("sky-saved-glow")).toBe(true);
|
||||
});
|
||||
|
||||
it("clears the glow after the ON window (~220ms)", () => {
|
||||
window.pulseSkyGlow();
|
||||
jasmine.clock().tick(240);
|
||||
expect(burgerBtn.classList.contains("sky-saved-glow")).toBe(false);
|
||||
});
|
||||
|
||||
it("pulses a third time (still glowing into the 3rd ON window)", () => {
|
||||
window.pulseSkyGlow();
|
||||
// Two full cycles: 2 × (220 ON + 160 OFF) = 760ms → 3rd ON begins.
|
||||
jasmine.clock().tick(770);
|
||||
expect(burgerBtn.classList.contains("sky-saved-glow")).toBe(true);
|
||||
});
|
||||
|
||||
it("settles back to default after the 3 pulses (~1.2s total)", () => {
|
||||
window.pulseSkyGlow();
|
||||
jasmine.clock().tick(1300);
|
||||
expect(burgerBtn.classList.contains("sky-saved-glow")).toBe(false);
|
||||
});
|
||||
|
||||
it("no-ops when the burger btn is absent", () => {
|
||||
burgerBtn.remove();
|
||||
expect(() => window.pulseSkyGlow()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("bindSkyBtn()", () => {
|
||||
it("active Sky-btn click reopens the saved wheel (window.openSkyFelt)", () => {
|
||||
skyBtn.classList.add("active");
|
||||
bindSkyBtn();
|
||||
skyBtn.click();
|
||||
expect(window.openSkyFelt).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("inactive Sky-btn click does NOT reopen (left to the flash stub)", () => {
|
||||
bindSkyBtn(); // skyBtn has no .active
|
||||
skyBtn.click();
|
||||
expect(window.openSkyFelt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("pulses the burger on load when the server flagged a saved sky", () => {
|
||||
burgerBtn.dataset.skyGlow = "1";
|
||||
bindSkyBtn();
|
||||
expect(burgerBtn.classList.contains("sky-saved-glow")).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT pulse on load without the data-sky-glow flag", () => {
|
||||
bindSkyBtn();
|
||||
expect(burgerBtn.classList.contains("sky-saved-glow")).toBe(false);
|
||||
});
|
||||
|
||||
it("no-ops when the Sky btn is absent", () => {
|
||||
skyBtn.remove();
|
||||
expect(() => bindSkyBtn()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Burger → Sky-btn pulse handoff (CAST SKY cascade, 2026-06-07). Once the sky
|
||||
// is saved the burger pulses --priTk; the cue then HANDS OFF to the Sky sub-btn,
|
||||
// which pulses the same way on the next burger-OPEN ("now click me to reopen").
|
||||
describe("burger → sky-btn pulse handoff", () => {
|
||||
let burgerBtn, fan, skyBtn, ac;
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
burgerBtn = document.createElement("button");
|
||||
burgerBtn.id = "id_burger_btn";
|
||||
document.body.appendChild(burgerBtn);
|
||||
fan = document.createElement("div");
|
||||
fan.id = "id_burger_fan";
|
||||
document.body.appendChild(fan);
|
||||
skyBtn = document.createElement("button");
|
||||
skyBtn.id = "id_sky_btn";
|
||||
skyBtn.className = "burger-fan-btn";
|
||||
document.body.appendChild(skyBtn);
|
||||
ac = bindBurger();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (ac) ac.abort();
|
||||
jasmine.clock().uninstall();
|
||||
[burgerBtn, fan, skyBtn].forEach((el) => el && el.remove());
|
||||
});
|
||||
|
||||
it("pulses the Sky sub-btn on burger-open once the sky is saved (.active)", () => {
|
||||
skyBtn.classList.add("active");
|
||||
burgerBtn.click(); // open the fan
|
||||
expect(skyBtn.classList.contains("sky-saved-glow")).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT pulse the Sky sub-btn on open before the sky is saved", () => {
|
||||
burgerBtn.click();
|
||||
expect(skyBtn.classList.contains("sky-saved-glow")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not pulse on burger-CLOSE", () => {
|
||||
skyBtn.classList.add("active");
|
||||
burgerBtn.click(); // open → pulses
|
||||
jasmine.clock().tick(2000); // let the 3-pulse cycle finish
|
||||
burgerBtn.click(); // close
|
||||
expect(skyBtn.classList.contains("sky-saved-glow")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Voice persistence across my_sea reloads — bindVoiceBtn remembers the active
|
||||
// room in sessionStorage + silently re-joins it on the next page if voice is
|
||||
// still available (2026-05-29).
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{# sub-btns are inactive; clicking an inactive sub-btn flashes a brief #}
|
||||
{# --priRd glow (twice, fast cadence) — burger-btn.js owns the delegated #}
|
||||
{# click + flash. #}
|
||||
<button id="id_burger_btn" type="button" aria-label="Open burger menu" aria-expanded="false"{% if sea_first_draw_pending %} class="glow-handoff"{% endif %}>
|
||||
<button id="id_burger_btn" type="button" aria-label="Open burger menu" aria-expanded="false"{% if sea_first_draw_pending %} class="glow-handoff"{% endif %}{% if sky_btn_active %} data-sky-glow="1"{% endif %}>
|
||||
<i class="fa-solid fa-burger"></i>
|
||||
</button>
|
||||
<div id="id_burger_fan" aria-hidden="true">
|
||||
@@ -23,7 +23,10 @@
|
||||
<i class="fa-solid fa-headset burger-fan-icon--on"></i>
|
||||
<i class="fa-solid fa-ban burger-fan-icon--off"></i>
|
||||
</button>
|
||||
<button id="id_sky_btn" type="button" class="burger-fan-btn" aria-label="Sky">
|
||||
{# Sky sub-btn → reopens the saved CAST SKY wheel. `.active` once the sky is #}
|
||||
{# saved (sky_btn_active, set by epic.room_view); burger-btn.js binds the #}
|
||||
{# active click → window.openSkyFelt(). Inactive elsewhere (flash stub). #}
|
||||
<button id="id_sky_btn" type="button" class="burger-fan-btn{% if sky_btn_active %} active{% endif %}" aria-label="Sky">
|
||||
<i class="fa-solid fa-cloud burger-fan-icon--on"></i>
|
||||
<i class="fa-solid fa-ban burger-fan-icon--off"></i>
|
||||
</button>
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if scroll_filter %}
|
||||
{# Sky pane — shown by room-views.js (updateGear) while the CAST SKY felt is #}
|
||||
{# open (html.sky-open). NVM returns to the table-hex: epic:room re-renders #}
|
||||
{# the hex with DRAW SEA if the sky was saved (confirmed), else CAST SKY. #}
|
||||
<div class="room-menu-sky" style="display:none">
|
||||
<a href="{% url 'epic:room' room.id %}" class="btn btn-cancel">NVM</a>
|
||||
</div>
|
||||
<div class="room-menu-scroll" style="display:none">
|
||||
<form id="id_scroll_filter_form">
|
||||
<label><input type="checkbox" name="labels" value="frame" checked> Frame</label>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
{% load static %}
|
||||
{# CAST SKY overlay — natal chart entry + D3 wheel preview #}
|
||||
{# Included in room.html when table_status == "SKY_SELECT" #}
|
||||
{# Opens when user clicks #id_pick_sky_btn; html.sky-open controls #}
|
||||
{# visibility via CSS — backdrop-filter blur + centred modal. #}
|
||||
{# CAST SKY felt — natal chart entry + D3 wheel, unified with the my_sky / #}
|
||||
{# sky.html apparatus (2026-06-07). Renders INSIDE .room-hex-pane on edge-to- #}
|
||||
{# edge --duoUser felt (no dark Gaussian backdrop / modal), my_sea-style. The #}
|
||||
{# .sky-page wrapper REUSES sky.html's dashboard form/wheel rules; the #}
|
||||
{# .sky-page--room modifier scopes the in-room fill + open/close visibility. #}
|
||||
{# Opens when the user clicks #id_pick_sky_btn (html.sky-open). Pre-save the #}
|
||||
{# form shows alone; on SAVE the felt flips into scroll-snap (form shunts down, #}
|
||||
{# wheel takes page 1). NVM lives in the room gear menu (not on the felt). #}
|
||||
|
||||
<div class="sky-backdrop"></div>
|
||||
<div class="sky-overlay"
|
||||
<div class="sky-page sky-page--room"
|
||||
id="id_sky_overlay"
|
||||
data-preview-url="{% url 'epic:sky_preview' room.id %}"
|
||||
data-save-url="{% url 'epic:sky_save' room.id %}"
|
||||
@@ -13,98 +16,84 @@
|
||||
data-sea-partial-url="{% url 'epic:sea_partial' room.id %}"
|
||||
data-user-seat-role="{{ user_seat_role }}">
|
||||
|
||||
<div class="sky-modal-wrap">
|
||||
<div class="sky-modal">
|
||||
<div class="sky-modal-body">
|
||||
|
||||
<header class="sky-modal-header">
|
||||
<h2>SKY <span>SELECT</span></h2>
|
||||
<p>Enter your birth details to generate your natal chart.</p>
|
||||
</header>
|
||||
{# ── Form column ──────────────────────────────────────── #}
|
||||
<div class="sky-form-col">
|
||||
|
||||
<div class="sky-modal-body">
|
||||
{# form-main scrolls independently; confirm btn stays pinned below it #}
|
||||
<div class="sky-form-main">
|
||||
<form id="id_sky_form" autocomplete="off">
|
||||
|
||||
{# ── Form column ──────────────────────────────────────── #}
|
||||
<div class="sky-form-col">
|
||||
<div class="sky-field">
|
||||
<label for="id_nf_date">Birth date</label>
|
||||
<input id="id_nf_date" name="date" type="date" required>
|
||||
</div>
|
||||
|
||||
{# form-main scrolls independently; confirm btn stays pinned below it #}
|
||||
<div class="sky-form-main">
|
||||
<form id="id_sky_form" autocomplete="off">
|
||||
<div class="sky-field">
|
||||
<label for="id_nf_time">Birth time</label>
|
||||
<input id="id_nf_time" name="time" type="time" value="12:00">
|
||||
<small>Local time at birth place. Use 12:00 if unknown.</small>
|
||||
</div>
|
||||
|
||||
<div class="sky-field">
|
||||
<label for="id_nf_date">Birth date</label>
|
||||
<input id="id_nf_date" name="date" type="date" required>
|
||||
<div class="sky-field sky-place-field">
|
||||
<label for="id_nf_place">Birth place</label>
|
||||
<div class="sky-place-wrap">
|
||||
<input id="id_nf_place" name="place" type="text"
|
||||
placeholder="Start typing a city…"
|
||||
autocomplete="off">
|
||||
<button type="button" id="id_nf_geolocate"
|
||||
class="btn btn-secondary btn-sm"
|
||||
title="Use device location">
|
||||
<i class="fa-solid fa-location-crosshairs"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="id_nf_suggestions" class="sky-suggestions" hidden></div>
|
||||
</div>
|
||||
|
||||
<div class="sky-field">
|
||||
<label for="id_nf_time">Birth time</label>
|
||||
<input id="id_nf_time" name="time" type="time" value="12:00">
|
||||
<small>Local time at birth place. Use 12:00 if unknown.</small>
|
||||
<div class="sky-field sky-coords">
|
||||
<div>
|
||||
<label>Latitude</label>
|
||||
<input id="id_nf_lat" name="lat" type="text"
|
||||
placeholder="—" readonly tabindex="-1">
|
||||
</div>
|
||||
|
||||
<div class="sky-field sky-place-field">
|
||||
<label for="id_nf_place">Birth place</label>
|
||||
<div class="sky-place-wrap">
|
||||
<input id="id_nf_place" name="place" type="text"
|
||||
placeholder="Start typing a city…"
|
||||
autocomplete="off">
|
||||
<button type="button" id="id_nf_geolocate"
|
||||
class="btn btn-secondary btn-sm"
|
||||
title="Use device location">
|
||||
<i class="fa-solid fa-location-crosshairs"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="id_nf_suggestions" class="sky-suggestions" hidden></div>
|
||||
<div>
|
||||
<label>Longitude</label>
|
||||
<input id="id_nf_lon" name="lon" type="text"
|
||||
placeholder="—" readonly tabindex="-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sky-field sky-coords">
|
||||
<div>
|
||||
<label>Latitude</label>
|
||||
<input id="id_nf_lat" name="lat" type="text"
|
||||
placeholder="—" readonly tabindex="-1">
|
||||
</div>
|
||||
<div>
|
||||
<label>Longitude</label>
|
||||
<input id="id_nf_lon" name="lon" type="text"
|
||||
placeholder="—" readonly tabindex="-1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="sky-field">
|
||||
<label for="id_nf_tz">Timezone</label>
|
||||
<input id="id_nf_tz" name="tz" type="text"
|
||||
placeholder="auto-detected from coordinates"
|
||||
readonly tabindex="-1">
|
||||
</div>
|
||||
|
||||
<div class="sky-field">
|
||||
<label for="id_nf_tz">Timezone</label>
|
||||
<input id="id_nf_tz" name="tz" type="text"
|
||||
placeholder="auto-detected from coordinates"
|
||||
readonly tabindex="-1">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</form>
|
||||
<div id="id_sky_status" class="sky-status"></div>
|
||||
</div>{# /.sky-form-main #}
|
||||
|
||||
<div id="id_sky_status" class="sky-status"></div>
|
||||
</div>{# /.sky-form-main #}
|
||||
<button type="button" id="id_sky_confirm" class="btn btn-primary" disabled>
|
||||
Save Sky
|
||||
</button>
|
||||
|
||||
<button type="button" id="id_sky_confirm" class="btn btn-primary" disabled>
|
||||
Save Sky
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{# ── Wheel column ─────────────────────────────────────── #}
|
||||
{# DEL btn is JS-injected after the wheel paints (see the SAVE #}
|
||||
{# success handler) — keeping it out of the template means a #}
|
||||
{# pre-save form can never show a DEL action against a non- #}
|
||||
{# existent wheel. #}
|
||||
<div class="sky-wheel-col">
|
||||
<svg id="id_sky_svg" class="sky-svg"></svg>
|
||||
</div>
|
||||
|
||||
{# ── Wheel column ─────────────────────────────────────── #}
|
||||
{# DEL btn is JS-injected after the wheel paints (see schedule #}
|
||||
{# Preview success handler) — keeping it out of the template #}
|
||||
{# means a blank CAST SKY modal can never show a DEL action #}
|
||||
{# against a non-existent wheel. #}
|
||||
<div class="sky-wheel-col">
|
||||
<svg id="id_sky_svg" class="sky-svg"></svg>
|
||||
</div>
|
||||
</div>{# /.sky-modal-body #}
|
||||
|
||||
</div>{# /.sky-modal-body #}
|
||||
|
||||
</div>{# /.sky-modal #}
|
||||
|
||||
{# NVM: circle btn centered on the top-right corner of the modal #}
|
||||
<button type="button" id="id_sky_cancel" class="btn btn-cancel btn-sm">NVM</button>
|
||||
|
||||
</div>{# /.sky-modal-wrap #}
|
||||
</div>{# /.sky-overlay #}
|
||||
</div>{# /.sky-page--room #}
|
||||
|
||||
<script src="{% static 'apps/gameboard/d3.min.js' %}"></script>
|
||||
<script src="{% static 'apps/gameboard/sky-wheel.js' %}"></script>
|
||||
@@ -117,6 +106,8 @@
|
||||
const svgEl = document.getElementById('id_sky_svg');
|
||||
const statusEl = document.getElementById('id_sky_status');
|
||||
const confirmBtn = document.getElementById('id_sky_confirm');
|
||||
// NVM now lives in the room gear menu (the felt refactor dropped the in-modal
|
||||
// cancel btn) — guarded everywhere so a null lookup never throws.
|
||||
const cancelBtn = document.getElementById('id_sky_cancel');
|
||||
const geoBtn = document.getElementById('id_nf_geolocate');
|
||||
const placeInput = document.getElementById('id_nf_place');
|
||||
@@ -142,7 +133,7 @@
|
||||
|
||||
// Preload zodiac SVG icons eagerly — they'll be cached before any draw() call.
|
||||
// To swap an icon, replace the .svg file in zodiac-signs/ and hard-refresh.
|
||||
SkyWheel.preload();
|
||||
const _preloadReady = SkyWheel.preload();
|
||||
|
||||
// ── localStorage persistence ──────────────────────────────────────────────
|
||||
// Key scoped to room so multiple rooms don't clobber each other.
|
||||
@@ -175,10 +166,32 @@
|
||||
|
||||
// ── Open / Close ──────────────────────────────────────────────────────────
|
||||
|
||||
// While the felt is up the burger Text sub-btn must be inert: its swipe
|
||||
// machine (room-views.js) would otherwise drive DOWN into the reelhouse — but
|
||||
// the aperture is scroll-locked, so the page half-loads a carousel view it
|
||||
// can't actually reach (confusing UX). We toggle #id_text_btn OFF on open and
|
||||
// restore its server-set baseline on close.
|
||||
let _textBtnWasActive = false;
|
||||
function _disableTextBtn() {
|
||||
const tb = document.getElementById('id_text_btn');
|
||||
if (!tb) return;
|
||||
_textBtnWasActive = tb.classList.contains('active');
|
||||
tb.classList.remove('active');
|
||||
}
|
||||
function _restoreTextBtn() {
|
||||
const tb = document.getElementById('id_text_btn');
|
||||
if (tb && _textBtnWasActive) tb.classList.add('active');
|
||||
}
|
||||
|
||||
function openSky() {
|
||||
document.documentElement.classList.add('sky-open');
|
||||
_disableTextBtn();
|
||||
// Re-sync the room gear to the sky NVM pane (room-views.js owns the gear
|
||||
// pane-swap; sky-open toggles outside any scroll/view event it watches).
|
||||
if (window.RoomViews && window.RoomViews.syncGear) window.RoomViews.syncGear();
|
||||
// If the wheel is empty but the form has enough data (restored from
|
||||
// localStorage), kick off a fresh preview so the animation plays.
|
||||
// localStorage), kick off a fresh preview so SAVE re-enables (no draw
|
||||
// until saved — my_sky parity).
|
||||
if (!svgEl.querySelector('*') && _formReady()) {
|
||||
schedulePreview();
|
||||
}
|
||||
@@ -186,13 +199,14 @@
|
||||
|
||||
function closeSky() {
|
||||
document.documentElement.classList.remove('sky-open');
|
||||
_restoreTextBtn();
|
||||
hideSuggestions();
|
||||
if (window.RoomViews && window.RoomViews.syncGear) window.RoomViews.syncGear();
|
||||
}
|
||||
|
||||
const pickSkyBtn = document.getElementById('id_pick_sky_btn');
|
||||
if (pickSkyBtn) pickSkyBtn.addEventListener('click', openSky);
|
||||
cancelBtn.addEventListener('click', closeSky);
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeSky(); });
|
||||
if (cancelBtn) cancelBtn.addEventListener('click', closeSky);
|
||||
|
||||
// ── Status helper ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -347,12 +361,18 @@
|
||||
|
||||
setStatus('');
|
||||
confirmBtn.disabled = false;
|
||||
if (svgEl.querySelector('*')) {
|
||||
SkyWheel.redraw(data);
|
||||
} else {
|
||||
SkyWheel.draw(svgEl, data);
|
||||
// my_sky parity — pre-save the form shows ALONE on the felt; the wheel
|
||||
// only paints AFTER SAVE SKY (_activateSavedState). _lastChartData is
|
||||
// captured above so SAVE has the chart payload + can reveal the wheel.
|
||||
// Once saved, live edits re-draw in place (the wheel page stays live).
|
||||
if (_skySaved()) {
|
||||
if (svgEl.querySelector('*')) {
|
||||
SkyWheel.redraw(data);
|
||||
} else {
|
||||
SkyWheel.draw(svgEl, data);
|
||||
}
|
||||
_ensureDelBtn();
|
||||
}
|
||||
_ensureDelBtn();
|
||||
})
|
||||
.catch(err => {
|
||||
if (seq !== _fetchSeq) return;
|
||||
@@ -361,6 +381,10 @@
|
||||
});
|
||||
}
|
||||
|
||||
function _skySaved() {
|
||||
return document.body.classList.contains('sky-saved');
|
||||
}
|
||||
|
||||
// ── Save ──────────────────────────────────────────────────────────────────
|
||||
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
@@ -389,10 +413,13 @@
|
||||
return r.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data.confirmed) {
|
||||
setStatus('Sky saved!');
|
||||
}
|
||||
// Confirmed state is driven by the room:sky_confirmed WS event
|
||||
setStatus('Sky saved!');
|
||||
// my_sky parity — reveal the wheel on the felt instead of reloading.
|
||||
// The form shunts to page 2; the wheel slides in as page 1 and we ease
|
||||
// to it. The gamer lingers on their saved wheel; the gear NVM returns
|
||||
// to the hex (which the server re-renders with DRAW SEA — the save
|
||||
// already set confirmed_at, so `sky_confirmed` is true server-side).
|
||||
_activateSavedState();
|
||||
})
|
||||
.catch(err => {
|
||||
setStatus(`Save failed: ${err.message}`, 'error');
|
||||
@@ -400,16 +427,134 @@
|
||||
});
|
||||
});
|
||||
|
||||
// ── Sky confirmed → close sky & reload to land on hex w. DRAW SEA ──────
|
||||
//
|
||||
// The gamer should witness the table hex (now showing DRAW SEA in place of
|
||||
// CAST SKY) before opting into the sea overlay. We reload the room page —
|
||||
// the server-side template will re-render with `sky_confirmed=True` so the
|
||||
// hex's btn flips automatically, and the user clicks DRAW SEA to continue.
|
||||
// ── Reveal the saved wheel on the felt (no reload) ────────────────────────
|
||||
// Adds body.sky-saved so _sky.scss flips the felt into scroll-snap (wheel
|
||||
// page 1 via .sky-wheel-col order:-1, form page 2), draws the wheel from the
|
||||
// captured chart, injects DEL, then pins to the form section + eases to the
|
||||
// wheel so the reveal animates in (mirrors sky.html's _activateSavedState).
|
||||
|
||||
function _activateSavedState() {
|
||||
if (!_lastChartData) return;
|
||||
const wasSaved = document.body.classList.contains('sky-saved');
|
||||
document.body.classList.add('sky-saved');
|
||||
if (svgEl.querySelector('*')) {
|
||||
SkyWheel.redraw(_lastChartData);
|
||||
} else {
|
||||
SkyWheel.draw(svgEl, _lastChartData);
|
||||
}
|
||||
_ensureDelBtn();
|
||||
if (!wasSaved) {
|
||||
// First reveal: pin to the form section so the wheel slides in from above
|
||||
// (the form "shunts down"), then begin the post-save cascade. #id_sky_btn
|
||||
// activation + the burger glow ride the cascade (not the save instant) so
|
||||
// they land WITH the table-hex reveal — see _cascadeFeltOut.
|
||||
const formCol = overlay.querySelector('.sky-form-col');
|
||||
if (formCol) overlay.scrollTop = formCol.offsetTop;
|
||||
_scrollApertureToTop();
|
||||
_startSaveCascade();
|
||||
} else {
|
||||
// Re-assert (WS re-fire / reopen-from-saved): keep the reopen affordance
|
||||
// live; no cascade (the table-hex already advanced on the first save).
|
||||
const skyBtn = document.getElementById('id_sky_btn');
|
||||
if (skyBtn) skyBtn.classList.add('active');
|
||||
_scrollApertureToTop();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Post-save cascade ───────────────────────────────────────────────────────
|
||||
// After SAVE the gamer lingers on the freshly-drawn wheel ~3s; THEN the felt
|
||||
// eases OUT to reveal the table-hex, the burger glow fires (your sky now lives
|
||||
// in the burger fan), and 3s later the DRAW SEA btn eases IN — with the sea
|
||||
// overlay injected so it's live with no reload. Each beat is its own timer.
|
||||
const _CASCADE_LINGER = 3000, _FELT_FADE = 500, _SEA_DELAY = 3000;
|
||||
|
||||
function _startSaveCascade() {
|
||||
setTimeout(_cascadeFeltOut, _CASCADE_LINGER);
|
||||
}
|
||||
|
||||
function _cascadeFeltOut() {
|
||||
overlay.classList.add('sky-page--cascade-out'); // CSS fades the felt out
|
||||
setTimeout(() => {
|
||||
document.documentElement.classList.remove('sky-open');
|
||||
overlay.classList.remove('sky-page--cascade-out');
|
||||
_restoreTextBtn();
|
||||
if (window.RoomViews && window.RoomViews.syncGear) window.RoomViews.syncGear();
|
||||
// CAST SKY is stale → ease it out; light the burger reopen affordance +
|
||||
// fire the glow, all concurrent with the table-hex reveal.
|
||||
const skyPhaseBtn = document.getElementById('id_pick_sky_btn');
|
||||
if (skyPhaseBtn) skyPhaseBtn.classList.add('hex-phase-btn--out');
|
||||
const skyBtn = document.getElementById('id_sky_btn');
|
||||
if (skyBtn) skyBtn.classList.add('active');
|
||||
if (window.pulseSkyGlow) window.pulseSkyGlow();
|
||||
setTimeout(_cascadeDrawSeaIn, _SEA_DELAY);
|
||||
}, _FELT_FADE);
|
||||
}
|
||||
|
||||
function _cascadeDrawSeaIn() {
|
||||
const seaPhaseBtn = document.getElementById('id_pick_sea_btn');
|
||||
if (seaPhaseBtn) seaPhaseBtn.classList.remove('hex-phase-btn--out'); // eases in
|
||||
_injectSeaOverlay();
|
||||
}
|
||||
|
||||
// Fetch + inject the DRAW SEA overlay so it's live without a reload. The sea
|
||||
// partial carries its own inline <script> (openSea binding on #id_pick_sea_btn
|
||||
// + SeaDeal.reinit) — innerHTML won't execute it, so each <script> is re-
|
||||
// created. Idempotent + best-effort (a gear NVM / refresh recovers via the
|
||||
// server-rendered confirmed overlay).
|
||||
function _injectSeaOverlay() {
|
||||
const url = overlay.dataset.seaPartialUrl;
|
||||
const container = document.getElementById('id_sea_inject');
|
||||
if (!url || !container || container.dataset.injected) return;
|
||||
fetch(url, { credentials: 'same-origin' })
|
||||
.then((r) => { if (!r.ok) throw new Error(r.status); return r.text(); })
|
||||
.then((html) => {
|
||||
container.dataset.injected = '1';
|
||||
const tpl = document.createElement('template');
|
||||
tpl.innerHTML = html;
|
||||
Array.prototype.slice.call(tpl.content.childNodes).forEach((node) => {
|
||||
if (node.nodeName === 'SCRIPT') {
|
||||
const s = document.createElement('script');
|
||||
if (node.src) s.src = node.src;
|
||||
else s.textContent = node.textContent;
|
||||
container.appendChild(s);
|
||||
} else {
|
||||
container.appendChild(node);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => { /* transient — server render recovers on next load */ });
|
||||
}
|
||||
|
||||
// Ease the felt's scroll back to the wheel page (top) after a save —
|
||||
// ease-out cubic, ~280ms. No-op if already at the top.
|
||||
function _scrollApertureToTop() {
|
||||
if (overlay.scrollTop === 0) return;
|
||||
const start = overlay.scrollTop;
|
||||
const startTime = performance.now();
|
||||
const DURATION = 280;
|
||||
const ease = (t) => { const u = 1 - t; return 1 - u * u * u; };
|
||||
function step(now) {
|
||||
const t = Math.min(1, (now - startTime) / DURATION);
|
||||
overlay.scrollTop = Math.max(0, start * (1 - ease(t)));
|
||||
if (t < 1) requestAnimationFrame(step);
|
||||
}
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
// ── Sky confirmed (self-targeted WS) ──────────────────────────────────────
|
||||
// Two cases:
|
||||
// • This browser captured the chart (the gamer saved via the felt) → the
|
||||
// save .then() already ran the cascade; this WS re-fire is idempotent
|
||||
// (re-asserts the saved state, no second cascade, no reload).
|
||||
// • No captured chart (a direct POST confirm, or another browser of the same
|
||||
// seat) → the cascade can't run, so reload to land on the server-rendered
|
||||
// DRAW SEA hex (the pre-cascade behaviour, preserved for these paths).
|
||||
function _onSkyConfirmed() {
|
||||
closeSky();
|
||||
window.location.reload();
|
||||
if (_lastChartData) {
|
||||
_activateSavedState();
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
// ── DEL btn — JS-injected after the wheel paints; absent on a blank modal
|
||||
@@ -456,6 +601,9 @@
|
||||
_lastChartData = null;
|
||||
confirmBtn.disabled = true;
|
||||
setStatus('');
|
||||
// Drop the saved state so the felt returns to form-alone (wheel page
|
||||
// collapses, scroll-snap disengages) — the seat's Character was purged.
|
||||
document.body.classList.remove('sky-saved');
|
||||
try { localStorage.removeItem(LS_KEY); } catch (_) {}
|
||||
if (_delBtn) {
|
||||
_delBtn.remove();
|
||||
@@ -472,10 +620,19 @@
|
||||
return m ? m[1] : '';
|
||||
}
|
||||
|
||||
// ── Restore persisted form data ────────────────────────────────────────────
|
||||
// Called after all functions are defined. Wheel draw is deferred to
|
||||
// openSky() so the animation plays when the modal opens, not silently
|
||||
// in the background on page load.
|
||||
// ── Reopen affordance ──────────────────────────────────────────────────────
|
||||
// Exposed so the burger fan's #id_sky_btn (active once the sky is saved) can
|
||||
// reopen the saved wheel. burger-btn.js calls window.openSkyFelt().
|
||||
window.openSkyFelt = openSky;
|
||||
|
||||
// ── Restore persisted form data + prime a saved (confirmed) wheel ───────────
|
||||
// Pre-save the wheel draw is deferred to openSky (form shows alone). Once the
|
||||
// sky is confirmed the server hands us the saved chart (saved_sky_json) so the
|
||||
// felt is primed in its saved state — body.sky-saved engages the scroll-snap
|
||||
// and the wheel is drawn ready, so the burger reopen lands straight on it
|
||||
// (mirrors My Sky's saved-on-load draw).
|
||||
|
||||
const _savedSky = {{ saved_sky_json|default:"null"|safe }};
|
||||
|
||||
// WS: server broadcasts sky_confirmed when any gamer confirms their sky.
|
||||
// Only act when the event's seat_role matches this browser's seat.
|
||||
@@ -487,5 +644,38 @@
|
||||
});
|
||||
|
||||
_restoreForm();
|
||||
|
||||
if (_savedSky) {
|
||||
_lastChartData = _savedSky;
|
||||
document.body.classList.add('sky-saved');
|
||||
confirmBtn.disabled = false;
|
||||
_preloadReady.then(() => {
|
||||
if (!svgEl.querySelector('*')) SkyWheel.draw(svgEl, _savedSky);
|
||||
_ensureDelBtn();
|
||||
});
|
||||
}
|
||||
|
||||
// Reload-into-open: the SIG_SELECT → SKY_SELECT transition (sig-select.js's
|
||||
// CAST SKY click) drops a sessionStorage flag then reloads here, so the felt
|
||||
// OPENS on arrival — one click yields the form instead of the old click→reload
|
||||
// →click-again. Read once + clear so a later manual refresh doesn't re-open.
|
||||
try {
|
||||
if (sessionStorage.getItem('sky-autoopen') === '1') {
|
||||
sessionStorage.removeItem('sky-autoopen');
|
||||
openSky();
|
||||
}
|
||||
} catch (_) { /* sessionStorage unavailable — non-fatal */ }
|
||||
|
||||
// ── STUB: lock the form once character creation completes ───────────────────
|
||||
// Character creation ends after DRAW SEA → the Voronoi map (roadmap step 21),
|
||||
// which isn't built yet. When it lands the server will flag the finished state
|
||||
// and the re-opened sky becomes READ-ONLY (view your wheel, can't re-cast). For
|
||||
// now this is an inert hook — `_skyLocked` is always false until that phase
|
||||
// exists. TODO: drive `_skyLocked` from a server ctx flag + lock the gear DEL.
|
||||
const _skyLocked = false;
|
||||
if (_skyLocked) {
|
||||
form.querySelectorAll('input').forEach((el) => { el.disabled = true; });
|
||||
confirmBtn.disabled = true;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{# so the green-felt _sig_select_overlay fills it (my_sea-style), covering #}
|
||||
{# the hex/seats behind. Dismissing the overlay (this gamer's sigs done) #}
|
||||
{# reveals the hex + waiting message underneath. #}
|
||||
<div class="room-pane room-hex-pane{% if room.table_status == "SIG_SELECT" and user_polarity and not polarity_done and viewer_cost_current %} has-sig-stage{% endif %}">
|
||||
<div class="room-pane room-hex-pane{% if room.table_status == "SIG_SELECT" and user_polarity and not polarity_done and viewer_cost_current %} has-sig-stage{% endif %}{% if room.table_status == "SKY_SELECT" and not sky_confirmed and viewer_cost_current %} has-sky-stage{% endif %}">
|
||||
<div class="room-shell">
|
||||
<div id="id_game_table" class="room-table">
|
||||
{# SCAN SIGS advances the whole table past role-select — gated on #}
|
||||
@@ -83,11 +83,15 @@
|
||||
onclick="window.location.href='{% url 'epic:room_gate' room.id %}{% if current_slot %}?seat={{ current_slot }}{% endif %}'">GATE<br>VIEW</button>
|
||||
{% else %}
|
||||
{% if room.table_status == "SKY_SELECT" %}
|
||||
{% if sky_confirmed %}
|
||||
<button id="id_pick_sea_btn" class="btn btn-primary">DRAW<br>SEA</button>
|
||||
{% else %}
|
||||
<button id="id_pick_sky_btn" class="btn btn-primary">CAST<br>SKY</button>
|
||||
{% endif %}
|
||||
{# Both phase btns render so the post-save cascade can #}
|
||||
{# cross-fade CAST SKY → DRAW SEA in place (no reload). The #}
|
||||
{# server sets --out on the inactive one; sky-select.js's #}
|
||||
{# cascade swaps them. On a confirmed reload the server lands #}
|
||||
{# DRAW SEA visible + CAST SKY out, same as the cascade end. #}
|
||||
<div class="hex-phase-stack">
|
||||
<button id="id_pick_sky_btn" class="btn btn-primary hex-phase-btn{% if sky_confirmed %} hex-phase-btn--out{% endif %}">CAST<br>SKY</button>
|
||||
<button id="id_pick_sea_btn" class="btn btn-primary hex-phase-btn{% if not sky_confirmed %} hex-phase-btn--out{% endif %}">DRAW<br>SEA</button>
|
||||
</div>
|
||||
{% elif room.table_status == "SIG_SELECT" %}
|
||||
<button id="id_pick_sky_btn" class="btn btn-primary" style="display:none">CAST<br>SKY</button>
|
||||
{% if polarity_done %}
|
||||
@@ -121,6 +125,16 @@
|
||||
{% if room.table_status == "SIG_SELECT" and user_polarity and not polarity_done and viewer_cost_current %}
|
||||
{% include "apps/gameboard/_partials/_sig_select_overlay.html" %}
|
||||
{% endif %}
|
||||
{# CAST SKY felt — natal chart entry, my_sea-style --duoUser felt that #}
|
||||
{# fills the hex pane (replaces the old root-level dark modal). Hidden #}
|
||||
{# until the CAST SKY btn (or, once saved, the burger Sky sub-btn) adds #}
|
||||
{# html.sky-open. Rendered through the confirmed state too so the saved #}
|
||||
{# wheel stays re-openable from the burger; `has-sky-stage` (the active #}
|
||||
{# entry surface) is dropped once confirmed — the hex then shows DRAW #}
|
||||
{# SEA and the felt sits hidden, primed with the saved chart. #}
|
||||
{% if room.table_status == "SKY_SELECT" and viewer_cost_current %}
|
||||
{% include "apps/gameboard/_partials/_sky_overlay.html" %}
|
||||
{% endif %}
|
||||
{# Position circles scroll away WITH the hex (they live inside the hex #}
|
||||
{# pane, not at room-page root). Neither the aperture nor the pane sets #}
|
||||
{# z-index/transform, so the strip's z-130 still resolves in the root #}
|
||||
@@ -150,20 +164,26 @@
|
||||
{# their trigger-btn ids in JS) must not render alongside it. (The Sig #}
|
||||
{# Select stage now lives INSIDE the hex pane above — my_sea-style felt.) #}
|
||||
|
||||
{# Sky (Pick Sky) overlay — natal chart entry #}
|
||||
{% if room.table_status == "SKY_SELECT" and not sky_confirmed and viewer_cost_current %}
|
||||
{% include "apps/gameboard/_partials/_sky_overlay.html" %}
|
||||
{% endif %}
|
||||
{# Sky tooltip: sibling of .sky-overlay, not inside .sky-modal-wrap (which has transform) #}
|
||||
{% if room.table_status == "SKY_SELECT" and not sky_confirmed and viewer_cost_current %}
|
||||
{# Sky tooltips — rendered at room-page root so they escape the felt's #}
|
||||
{# overflow clip (the wheel's planet-hover tooltips are position:fixed). #}
|
||||
{# Present through the confirmed state too (the burger reopen shows the #}
|
||||
{# saved wheel, whose planet hovers need these portals). #}
|
||||
{% if room.table_status == "SKY_SELECT" and viewer_cost_current %}
|
||||
<div id="id_sky_tooltip" class="tt" style="display:none;"></div>
|
||||
<div id="id_sky_tooltip_2" class="tt" style="display:none;"></div>
|
||||
{% endif %}
|
||||
|
||||
{# Sea (Pick Sea) overlay — Celtic Cross spread entry #}
|
||||
{# Sea (Pick Sea) overlay — Celtic Cross spread entry. Server-rendered #}
|
||||
{# once the sky is confirmed (the reload/refresh path). #}
|
||||
{% if room.table_status == "SKY_SELECT" and sky_confirmed and viewer_cost_current %}
|
||||
{% include "apps/gameboard/_partials/_sea_overlay.html" %}
|
||||
{% endif %}
|
||||
{# No-reload path: the post-save cascade fetches sea_partial + injects the #}
|
||||
{# DRAW SEA overlay HERE (executing its inline init) so DRAW SEA is live #}
|
||||
{# without a reload. Empty until the cascade's _injectSeaOverlay runs. #}
|
||||
{% if room.table_status == "SKY_SELECT" and not sky_confirmed and viewer_cost_current %}
|
||||
<div id="id_sea_inject"></div>
|
||||
{% endif %}
|
||||
|
||||
{# Gamer-needed stub — a seat lapsed past its renewal grace and was #}
|
||||
{# auto-BYE'd, so the table no longer fills all six. Minimal stub #}
|
||||
|
||||
Reference in New Issue
Block a user