diff --git a/src/apps/gameboard/static/apps/gameboard/natus-wheel.js b/src/apps/gameboard/static/apps/gameboard/natus-wheel.js index e95ff92..8c923a2 100644 --- a/src/apps/gameboard/static/apps/gameboard/natus-wheel.js +++ b/src/apps/gameboard/static/apps/gameboard/natus-wheel.js @@ -10,7 +10,7 @@ * { * planets: { Sun: { sign, degree, retrograde }, … }, * houses: { cusps: [f×12], asc: f, mc: f }, - * elements: { Fire: n, Water: n, Earth: n, Air: n, Time: n, Space: n }, + * elements: { Fire: n, Water: n, Stone: n, Air: n, Time: n, Space: n }, * aspects: [{ planet1, planet2, type, angle, orb }, …], * distinctions: { "1": n, …, "12": n }, * house_system: "O", @@ -28,15 +28,15 @@ const NatusWheel = (() => { const SIGNS = [ { name: 'Aries', symbol: '♈', element: 'Fire' }, - { name: 'Taurus', symbol: '♉', element: 'Earth' }, + { name: 'Taurus', symbol: '♉', element: 'Stone' }, { name: 'Gemini', symbol: '♊', element: 'Air' }, { name: 'Cancer', symbol: '♋', element: 'Water' }, { name: 'Leo', symbol: '♌', element: 'Fire' }, - { name: 'Virgo', symbol: '♍', element: 'Earth' }, + { name: 'Virgo', symbol: '♍', element: 'Stone' }, { name: 'Libra', symbol: '♎', element: 'Air' }, { name: 'Scorpio', symbol: '♏', element: 'Water' }, { name: 'Sagittarius', symbol: '♐', element: 'Fire' }, - { name: 'Capricorn', symbol: '♑', element: 'Earth' }, + { name: 'Capricorn', symbol: '♑', element: 'Stone' }, { name: 'Aquarius', symbol: '♒', element: 'Air' }, { name: 'Pisces', symbol: '♓', element: 'Water' }, ]; @@ -46,22 +46,15 @@ const NatusWheel = (() => { Jupiter: '♃', Saturn: '♄', Uranus: '♅', Neptune: '♆', Pluto: '♇', }; - const ASPECT_COLOURS = { + // Aspect stroke colors remain in JS — they are data-driven, not stylistic. + const ASPECT_COLORS = { Conjunction: 'var(--priYl, #f0e060)', Sextile: 'var(--priGn, #60c080)', Square: 'var(--priRd, #c04040)', Trine: 'var(--priGn, #60c080)', Opposition: 'var(--priRd, #c04040)', }; - - const ELEMENT_COLOURS = { - Fire: 'var(--terUser, #c04040)', - Earth: 'var(--priGn, #60c080)', - Air: 'var(--priYl, #f0e060)', - Water: 'var(--priBl, #4080c0)', - Time: 'var(--quaUser, #808080)', - Space: 'var(--quiUser, #a0a0a0)', - }; + // Element fill colors live in _natus.scss (.nw-sign--* / .nw-element--*). const HOUSE_LABELS = [ '', 'Self', 'Worth', 'Education', 'Family', 'Creation', 'Ritual', @@ -80,13 +73,14 @@ const NatusWheel = (() => { /** Convert ecliptic longitude to SVG angle. * - * Ecliptic 0° (Aries) sits at the Ascendant position (left, 9 o'clock in - * standard chart convention). SVG angles are clockwise from 12 o'clock, so: - * svg_angle = -(ecliptic - asc) - 90° (in radians) - * We subtract 90° because D3 arcs start at 12 o'clock. + * American convention: ASC sits at 9 o'clock (left). SVG 0° is 3 o'clock + * and increases clockwise, so ecliptic (counter-clockwise, ASC-relative) + * maps to SVG via: + * svg_angle = -(ecliptic - asc) - 180° (in radians) + * The −180° offset places ASC exactly at the left (9 o'clock) position. */ function _toAngle(degree, asc) { - return (-(degree - asc) - 90) * Math.PI / 180; + return (-(degree - asc) - 180) * Math.PI / 180; } function _css(varName, fallback) { @@ -140,16 +134,14 @@ const NatusWheel = (() => { axisGroup.append('line') .attr('x1', x1).attr('y1', y1) .attr('x2', x2).attr('y2', y2) - .attr('stroke', _css('--secUser', '#c0a060')) - .attr('stroke-width', 1.5) - .attr('opacity', 0.7); + .attr('class', 'nw-axis-line'); axisGroup.append('text') .attr('x', _cx + (R.ascMcR + 12) * Math.cos(a)) .attr('y', _cy + (R.ascMcR + 12) * Math.sin(a)) .attr('text-anchor', 'middle') .attr('dominant-baseline', 'middle') .attr('font-size', `${_r * 0.055}px`) - .attr('fill', _css('--secUser', '#c0a060')) + .attr('class', 'nw-axis-label') .text(label); }); } @@ -167,26 +159,16 @@ const NatusWheel = (() => { // D3 arc expects startAngle < endAngle in its own convention; we swap // because our _toAngle goes counter-clockwise const [sa, ea] = startA > endA ? [endA, startA] : [startA, endA]; - // Fill wedge - const fill = { - Fire: _css('--terUser', '#7a3030'), - Earth: _css('--priGn', '#306030'), - Air: _css('--quaUser', '#606030'), - Water: _css('--priUser', '#304070'), - }[sign.element]; sigGroup.append('path') .attr('transform', `translate(${_cx},${_cy})`) .attr('d', arc({ innerRadius: R.signInner, outerRadius: R.signOuter, - startAngle: sa, - endAngle: ea, + startAngle: sa + Math.PI / 2, + endAngle: ea + Math.PI / 2, })) - .attr('fill', fill) - .attr('opacity', 0.35) - .attr('stroke', _css('--quaUser', '#444')) - .attr('stroke-width', 0.5); + .attr('class', `nw-sign--${sign.element.toLowerCase()}`); // Symbol at midpoint const midA = (sa + ea) / 2; @@ -195,8 +177,8 @@ const NatusWheel = (() => { .attr('y', _cy + R.labelR * Math.sin(midA)) .attr('text-anchor', 'middle') .attr('dominant-baseline', 'middle') - .attr('font-size', `${_r * 0.072}px`) - .attr('fill', _css('--secUser', '#c8b060')) + .attr('font-size', `${_r * 0.095}px`) + .attr('class', 'nw-sign-label') .text(sign.symbol); }); } @@ -206,46 +188,48 @@ const NatusWheel = (() => { const arc = d3.arc(); const houseGroup = g.append('g').attr('class', 'nw-houses'); - cusps.forEach((cusp, i) => { - const nextCusp = cusps[(i + 1) % 12]; - const startA = _toAngle(cusp, asc); - const endA = _toAngle(nextCusp, asc); + // Pre-compute angles; normalise the last house's nextCusp across 360° wrap. + const houses = cusps.map((cusp, i) => { + let nextCusp = cusps[(i + 1) % 12]; + if (nextCusp <= cusp) nextCusp += 360; // close the circle for house 12 + const startA = _toAngle(cusp, asc); + const endA = _toAngle(nextCusp, asc); + // _toAngle is strictly decreasing with degree after normalisation, + // so startA > endA always — D3 arc needs sa < ea. + const sa = endA, ea = startA; + return { i, startA, sa, ea, midA: (sa + ea) / 2 }; + }); - // Cusp radial line + // 1. Fills first so cusp lines + numbers are never buried beneath them. + houses.forEach(({ i, sa, ea }) => { + houseGroup.append('path') + .attr('transform', `translate(${_cx},${_cy})`) + .attr('d', arc({ + innerRadius: R.houseInner, + outerRadius: R.houseOuter, + startAngle: sa + Math.PI / 2, + endAngle: ea + Math.PI / 2, + })) + .attr('class', i % 2 === 0 ? 'nw-house-fill--even' : 'nw-house-fill--odd'); + }); + + // 2. Cusp lines + house numbers on top. + houses.forEach(({ i, startA, midA }) => { houseGroup.append('line') .attr('x1', _cx + R.houseInner * Math.cos(startA)) .attr('y1', _cy + R.houseInner * Math.sin(startA)) .attr('x2', _cx + R.signInner * Math.cos(startA)) .attr('y2', _cy + R.signInner * Math.sin(startA)) - .attr('stroke', _css('--quaUser', '#555')) - .attr('stroke-width', 0.8); + .attr('class', 'nw-house-cusp'); - // House number at midpoint of house arc - const [sa, ea] = startA > endA ? [endA, startA] : [startA, endA]; - const midA = (sa + ea) / 2; houseGroup.append('text') .attr('x', _cx + R.houseNumR * Math.cos(midA)) .attr('y', _cy + R.houseNumR * Math.sin(midA)) .attr('text-anchor', 'middle') .attr('dominant-baseline', 'middle') .attr('font-size', `${_r * 0.05}px`) - .attr('fill', _css('--quiUser', '#888')) - .attr('opacity', 0.8) + .attr('class', 'nw-house-num') .text(i + 1); - - // Faint fill strip - houseGroup.append('path') - .attr('transform', `translate(${_cx},${_cy})`) - .attr('d', arc({ - innerRadius: R.houseInner, - outerRadius: R.houseOuter, - startAngle: sa, - endAngle: ea, - })) - .attr('fill', (i % 2 === 0) - ? _css('--quaUser', '#3a3a3a') - : _css('--quiUser', '#2e2e2e')) - .attr('opacity', 0.15); }); } @@ -261,11 +245,8 @@ const NatusWheel = (() => { const circle = planetGroup.append('circle') .attr('cx', _cx + R.planetR * Math.cos(ascAngle)) .attr('cy', _cy + R.planetR * Math.sin(ascAngle)) - .attr('r', _r * 0.038) - .attr('fill', pdata.retrograde - ? _css('--terUser', '#7a3030') - : _css('--priUser', '#304070')) - .attr('opacity', 0.6); + .attr('r', _r * 0.05) + .attr('class', pdata.retrograde ? 'nw-planet-circle--rx' : 'nw-planet-circle'); // Symbol const label = planetGroup.append('text') @@ -273,8 +254,9 @@ const NatusWheel = (() => { .attr('y', _cy + R.planetR * Math.sin(ascAngle)) .attr('text-anchor', 'middle') .attr('dominant-baseline', 'middle') - .attr('font-size', `${_r * 0.068}px`) - .attr('fill', _css('--ninUser', '#e0d0a0')) + .attr('dy', '0.1em') + .attr('font-size', `${_r * 0.09}px`) + .attr('class', 'nw-planet-label') .text(PLANET_SYMBOLS[name] || name[0]); // Retrograde indicator @@ -285,29 +267,30 @@ const NatusWheel = (() => { .attr('text-anchor', 'middle') .attr('dominant-baseline', 'middle') .attr('font-size', `${_r * 0.040}px`) - .attr('fill', _css('--terUser', '#c04040')) .attr('class', 'nw-rx') .text('℞'); } // Animate from ASC → final position (staggered) + // circle uses cx/cy; text uses x/y — must be separate transitions. const interpAngle = d3.interpolate(ascAngle, finalA); - [circle, label].forEach(el => { - el.transition() - .delay(idx * 40) - .duration(600) - .ease(d3.easeQuadOut) - .attrTween('cx', () => t => _cx + R.planetR * Math.cos(interpAngle(t))) - .attrTween('cy', () => t => _cy + R.planetR * Math.sin(interpAngle(t))); - }); + const transition = () => d3.transition() + .delay(idx * 40) + .duration(600) + .ease(d3.easeQuadOut); + + circle.transition(transition()) + .attrTween('cx', () => t => _cx + R.planetR * Math.cos(interpAngle(t))) + .attrTween('cy', () => t => _cy + R.planetR * Math.sin(interpAngle(t))); + + label.transition(transition()) + .attrTween('x', () => t => _cx + R.planetR * Math.cos(interpAngle(t))) + .attrTween('y', () => t => _cy + R.planetR * Math.sin(interpAngle(t))); // Retrograde ℞ — move together with planet if (pdata.retrograde) { planetGroup.select('.nw-rx:last-child') - .transition() - .delay(idx * 40) - .duration(600) - .ease(d3.easeQuadOut) + .transition(transition()) .attrTween('x', () => t => _cx + (R.planetR + _r * 0.055) * Math.cos(interpAngle(t))) .attrTween('y', () => t => _cy + (R.planetR + _r * 0.055) * Math.sin(interpAngle(t))); } @@ -316,7 +299,7 @@ const NatusWheel = (() => { function _drawAspects(g, data) { const asc = data.houses.asc; - const aspectGroup = g.append('g').attr('class', 'nw-aspects').attr('opacity', 0.45); + const aspectGroup = g.append('g').attr('class', 'nw-aspects'); // Build degree lookup const degrees = {}; @@ -331,17 +314,17 @@ const NatusWheel = (() => { .attr('y1', _cy + R.aspectR * Math.sin(a1)) .attr('x2', _cx + R.aspectR * Math.cos(a2)) .attr('y2', _cy + R.aspectR * Math.sin(a2)) - .attr('stroke', ASPECT_COLOURS[type] || '#888') + .attr('stroke', ASPECT_COLORS[type] || '#888') .attr('stroke-width', type === 'Opposition' || type === 'Square' ? 1.2 : 0.8); }); } function _drawElements(g, data) { const el = data.elements; - const total = (el.Fire || 0) + (el.Earth || 0) + (el.Air || 0) + (el.Water || 0); + const total = (el.Fire || 0) + (el.Stone || 0) + (el.Air || 0) + (el.Water || 0); if (total === 0) return; - const pieData = ['Fire', 'Earth', 'Air', 'Water'].map(k => ({ + const pieData = ['Fire', 'Stone', 'Air', 'Water'].map(k => ({ key: k, value: el[k] || 0, })); @@ -356,10 +339,7 @@ const NatusWheel = (() => { .data(pie) .join('path') .attr('d', arc) - .attr('fill', d => ELEMENT_COLOURS[d.data.key]) - .attr('opacity', 0.7) - .attr('stroke', _css('--quaUser', '#444')) - .attr('stroke-width', 0.5); + .attr('class', d => `nw-element--${d.data.key.toLowerCase()}`); // Time + Space emergent counts as text ['Time', 'Space'].forEach((key, i) => { @@ -371,8 +351,7 @@ const NatusWheel = (() => { .attr('text-anchor', 'middle') .attr('dominant-baseline', 'middle') .attr('font-size', `${_r * 0.045}px`) - .attr('fill', ELEMENT_COLOURS[key]) - .attr('opacity', 0.8) + .attr('class', `nw-element-label--${key.toLowerCase()}`) .text(`${key[0]}${count}`); }); } @@ -389,15 +368,12 @@ const NatusWheel = (() => { // Outer circle border g.append('circle') .attr('cx', _cx).attr('cy', _cy).attr('r', R.signOuter) - .attr('fill', 'none') - .attr('stroke', _css('--quaUser', '#555')) - .attr('stroke-width', 1); + .attr('class', 'nw-outer-ring'); // Inner filled disc (aspect area background) g.append('circle') .attr('cx', _cx).attr('cy', _cy).attr('r', R.elementOuter) - .attr('fill', _css('--quaUser', '#252525')) - .attr('opacity', 0.4); + .attr('class', 'nw-inner-disc'); _drawAspects(g, data); _drawElements(g, data); diff --git a/src/functional_tests/test_pick_sky.py b/src/functional_tests/test_pick_sky.py new file mode 100644 index 0000000..51d3f47 --- /dev/null +++ b/src/functional_tests/test_pick_sky.py @@ -0,0 +1,125 @@ +"""Functional tests for the PICK SKY overlay — natal chart entry.""" + +from selenium.webdriver.common.by import By + +from apps.applets.models import Applet +from apps.epic.models import GateSlot, Room +from apps.lyric.models import User + +from .base import FunctionalTest + + +def _make_sky_select_room(): + """Minimal SKY_SELECT room — just enough for the overlay to render.""" + email = "founder@test.io" + gamer, _ = User.objects.get_or_create(email=email) + room = Room.objects.create(name="Sky Test Room", table_status=Room.SKY_SELECT, owner=gamer) + # Put the founder in slot 1 so the view recognises them as a participant + slot = room.gate_slots.get(slot_number=1) + slot.gamer = gamer + slot.status = GateSlot.FILLED + slot.save() + room.gate_status = Room.OPEN + room.save() + return room, gamer, email + + +class PickSkyLocalStorageTest(FunctionalTest): + """PICK SKY form fields persist to localStorage.""" + + def setUp(self): + super().setUp() + Applet.objects.get_or_create( + slug="new-game", defaults={"name": "New Game", "context": "gameboard"} + ) + Applet.objects.get_or_create( + slug="my-games", defaults={"name": "My Games", "context": "gameboard"} + ) + self.room, self.founder, self.founder_email = _make_sky_select_room() + self.room_url = ( + self.live_server_url + f"/gameboard/room/{self.room.id}/" + ) + + def _open_overlay(self): + 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_natus_overlay") + ) + + def _fill_form(self): + """Set date, lat, lon directly (bypasses Nominatim network call).""" + self.browser.execute_script( + "document.getElementById('id_nf_date').value = '1990-02-28';" + "document.getElementById('id_nf_lat').value = '39.8244';" + "document.getElementById('id_nf_lon').value = '-74.9970';" + "document.getElementById('id_nf_place').value = 'Lindenwold, NJ';" + "document.getElementById('id_nf_tz').value = 'America/New_York';" + ) + # Fire input events so the save listener triggers + self.browser.execute_script(""" + ['id_nf_date','id_nf_lat','id_nf_lon','id_nf_place','id_nf_tz'].forEach(id => { + const el = document.getElementById(id); + el.dispatchEvent(new Event('input', {bubbles: true})); + }); + """) + + def _field_values(self): + return self.browser.execute_script(""" + return { + date: document.getElementById('id_nf_date').value, + lat: document.getElementById('id_nf_lat').value, + lon: document.getElementById('id_nf_lon').value, + place: document.getElementById('id_nf_place').value, + tz: document.getElementById('id_nf_tz').value, + }; + """) + + # ------------------------------------------------------------------ # + # 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_natus_cancel").click() + + # Reopen + self._open_overlay() + + values = self._field_values() + self.assertEqual(values["date"], "1990-02-28") + self.assertEqual(values["lat"], "39.8244") + self.assertEqual(values["lon"], "-74.9970") + self.assertEqual(values["place"], "Lindenwold, NJ") + self.assertEqual(values["tz"], "America/New_York") + + # ------------------------------------------------------------------ # + # T2 — fields survive a page refresh # + # ------------------------------------------------------------------ # + + def test_form_fields_repopulated_after_page_refresh(self): + self.create_pre_authenticated_session(self.founder_email) + self.browser.get(self.room_url) + + self._open_overlay() + self._fill_form() + + # Refresh the page + self.browser.refresh() + + self._open_overlay() + + values = self._field_values() + self.assertEqual(values["date"], "1990-02-28") + self.assertEqual(values["lat"], "39.8244") + self.assertEqual(values["lon"], "-74.9970") + self.assertEqual(values["place"], "Lindenwold, NJ") + self.assertEqual(values["tz"], "America/New_York") diff --git a/src/static_src/scss/_natus.scss b/src/static_src/scss/_natus.scss index 80d304f..6317037 100644 --- a/src/static_src/scss/_natus.scss +++ b/src/static_src/scss/_natus.scss @@ -62,34 +62,42 @@ html.natus-open .natus-overlay { // ── Modal panel ─────────────────────────────────────────────────────────────── -.natus-modal { - pointer-events: auto; - display: flex; - flex-direction: column; +// Thin wrapper: position:relative so the NVM circle can sit on the corner +// without being clipped by the modal's overflow:hidden. +.natus-modal-wrap { + position: relative; + pointer-events: none; // overlay handles pointer-events; children re-enable width: 92vw; - max-width: 840px; - max-height: 92vh; - border: 0.1rem solid rgba(var(--terUser), 0.25); - border-radius: 0.5rem; - background: rgba(var(--priUser), 1); - overflow: hidden; + max-width: 920px; - // Fade + slide in + // Fade + slide in — wraps modal AND NVM btn so both animate together opacity: 0; transform: translateY(1rem); transition: opacity 0.2s ease, transform 0.2s ease; } -html.natus-open .natus-modal { +html.natus-open .natus-modal-wrap { opacity: 1; transform: translateY(0); } +.natus-modal { + pointer-events: auto; + display: flex; + flex-direction: column; + width: 100%; // fills .natus-modal-wrap + max-height: 96vh; + border: 0.1rem solid rgba(var(--terUser), 0.25); + border-radius: 0.5rem; + overflow: hidden; +} + // ── Header ──────────────────────────────────────────────────────────────────── .natus-modal-header { flex-shrink: 0; padding: 0.6rem 1rem; + background: rgba(var(--priUser), 1); border-bottom: 0.1rem solid rgba(var(--terUser), 0.15); display: flex; flex-direction: row; @@ -121,17 +129,33 @@ html.natus-open .natus-modal { overflow: hidden; } -// Form column — fixed width, scrollable +// Form column — fixed width; form-main scrolls, confirm btn pinned at bottom .natus-form-col { flex: 0 0 240px; - overflow-y: auto; + overflow: hidden; padding: 0.9rem 1rem; + background: rgba(var(--priUser), 1); border-right: 0.1rem solid rgba(var(--terUser), 0.12); display: flex; flex-direction: column; gap: 0.65rem; } +// Scrollable inner container (form fields + status) +.natus-form-main { + flex: 1; + min-height: 0; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 0.65rem; +} + +// Confirm btn inside form-col — full width, pinned at column bottom +.natus-form-col > #id_natus_confirm { + flex-shrink: 0; +} + // Wheel column — fills remaining space .natus-wheel-col { flex: 1; @@ -140,7 +164,7 @@ html.natus-open .natus-modal { align-items: center; justify-content: center; padding: 0.75rem; - background: rgba(var(--priUser), 0.5); + // background: rgba(var(--duoUser), 1); position: relative; } @@ -149,8 +173,8 @@ html.natus-open .natus-modal { width: 100%; height: 100%; aspect-ratio: 1 / 1; - max-width: 400px; - max-height: 400px; + max-width: 480px; + max-height: 480px; } // ── Form fields ─────────────────────────────────────────────────────────────── @@ -270,22 +294,29 @@ html.natus-open .natus-modal { } } -// ── Footer ──────────────────────────────────────────────────────────────────── +// ── NVM corner btn ──────────────────────────────────────────────────────────── +// Absolutely pinned to top-right corner of .natus-modal-wrap. +// transform: translate(50%,-50%) centres the circle on the corner point. +// Lives outside .natus-modal so overflow:hidden doesn't clip it. -.natus-modal-footer { - flex-shrink: 0; - padding: 0.6rem 1rem; - border-top: 0.1rem solid rgba(var(--terUser), 0.15); - display: flex; - justify-content: flex-end; - gap: 0.5rem; +#id_natus_cancel { + position: absolute; + top: 0; + right: 0; + transform: translate(50%, -50%); + z-index: 10; + margin: 0; + pointer-events: auto; } // ── Narrow / portrait ───────────────────────────────────────────────────────── @media (max-width: 600px) { + .natus-modal-wrap { + width: 92vw; + } + .natus-modal { - width: 96vw; max-height: 96vh; } @@ -294,10 +325,28 @@ html.natus-open .natus-modal { overflow-y: auto; } + // Form col stacks above wheel; internally becomes a flex-row so + // form-main gets most of the width and confirm btn sits to its right. .natus-form-col { flex: 0 0 auto; + flex-direction: row; + align-items: flex-end; border-right: none; border-bottom: 0.1rem solid rgba(var(--terUser), 0.12); + overflow: visible; // form-main handles its own scroll + gap: 0.5rem; + } + + .natus-form-main { + flex: 1; + min-width: 0; + overflow-y: auto; + max-height: 40vh; + } + + .natus-form-col > #id_natus_confirm { + flex-shrink: 0; + align-self: flex-end; } .natus-wheel-col { @@ -305,6 +354,55 @@ html.natus-open .natus-modal { } } +// ── SVG wheel element styles ────────────────────────────────────────────────── +// Colors and opacity live here; geometry (cx/cy/r/font-size) stays in JS. + +.nw-outer-ring { + fill: none; + stroke: rgba(var(--quaUser), 0.6); + stroke-width: 1px; +} + +.nw-inner-disc { + fill: rgba(var(--quaUser), 0.6); +} + +// Axes (ASC / DSC / MC / IC) +.nw-axis-line { stroke: rgba(var(--secUser), 1); stroke-width: 1.5px; } +.nw-axis-label { fill: rgba(var(--secUser), 1); } + +// Sign ring — vars are RGB tuples ("R, G, B"), must be wrapped in rgba() +.nw-sign--fire { fill: rgba(var(--priRd, 192, 64, 64), 0.75); stroke: rgba(var(--quaUser), 1); stroke-width: 0.5px; } +.nw-sign--stone { fill: rgba(var(--priFs, 122, 96, 64), 0.75); stroke: rgba(var(--quaUser), 1); stroke-width: 0.5px; } +.nw-sign--air { fill: rgba(var(--priCy, 64, 144, 176), 0.75); stroke: rgba(var(--quaUser), 1); stroke-width: 0.5px; } +.nw-sign--water { fill: rgba(var(--priId, 80, 80, 160), 0.75); stroke: rgba(var(--quaUser), 1); stroke-width: 0.5px; } +.nw-sign-label { fill: rgba(var(--secUser), 1); } + +// House ring +.nw-house-cusp { stroke: rgba(var(--quaUser), 0.8); stroke-width: 0.8px; } +.nw-house-num { fill: rgba(var(--quiUser), 1); } +.nw-house-fill--even { fill: rgba(var(--quaUser), 0.45); } +.nw-house-fill--odd { fill: rgba(var(--quiUser), 0.35); } + +// Planets +.nw-planet-circle { fill: rgba(var(--quiUser), 1); } +.nw-planet-circle--rx { fill: rgba(var(--quiUser), 1); } +.nw-planet-label { fill: rgba(var(--quaUser), 1); stroke: rgba(var(--quaUser), 0.6); stroke-width: 0.4px; paint-order: stroke fill; } +.nw-rx { fill: rgba(var(--terUser), 1); } + +// Aspects +.nw-aspects { opacity: 0.8; } + +// Element pie +.nw-element--fire { fill: rgba(var(--priRd, 192, 64, 64), 0.92); stroke: rgba(var(--quaUser), 1); stroke-width: 0.5px; } +.nw-element--stone { fill: rgba(var(--priFs, 122, 96, 64), 0.92); stroke: rgba(var(--quaUser), 1); stroke-width: 0.5px; } +.nw-element--air { fill: rgba(var(--priCy, 64, 144, 176), 0.92); stroke: rgba(var(--quaUser), 1); stroke-width: 0.5px; } +.nw-element--water { fill: rgba(var(--priId, 80, 80, 160), 0.92); stroke: rgba(var(--quaUser), 1); stroke-width: 0.5px; } + +// Time / Space emergent labels +.nw-element-label--time { fill: rgba(var(--priYl, 192, 160, 48), 1); } +.nw-element-label--space { fill: rgba(var(--priGn, 64, 96, 64), 1); } + // ── Sidebar z-index sink (landscape sidebars must go below backdrop) ─────────── @media (orientation: landscape) { diff --git a/src/templates/apps/gameboard/_partials/_natus_overlay.html b/src/templates/apps/gameboard/_partials/_natus_overlay.html index d83c963..64b0449 100644 --- a/src/templates/apps/gameboard/_partials/_natus_overlay.html +++ b/src/templates/apps/gameboard/_partials/_natus_overlay.html @@ -10,6 +10,7 @@ data-preview-url="{% url 'epic:natus_preview' room.id %}" data-save-url="{% url 'epic:natus_save' room.id %}"> +
@@ -21,57 +22,66 @@ {# ── Form column ──────────────────────────────────────── #}
-
-
- - -
+ {# form-main scrolls independently; confirm btn stays pinned below it #} +
+ -
- - - Local time at birth place. Use 12:00 if unknown. -
- -
- -
- - +
+ +
- -
-
-
- - +
+ + + Local time at birth place. Use 12:00 if unknown.
-
- - + +
+ +
+ + +
+
-
-
- - - -
+
+
+ + +
+
+ + +
+
- +
+ + + +
+ + + +
+
{# /.natus-form-main #} + + -
{# ── Wheel column ─────────────────────────────────────── #} @@ -81,14 +91,12 @@
{# /.natus-modal-body #} -
- - -
-
{# /.natus-modal #} + + {# NVM: circle btn centered on the top-right corner of the modal #} + + +
{# /.natus-modal-wrap #}
{# /.natus-overlay #} @@ -122,10 +130,44 @@ const PLACE_DELAY = 400; // ms — Nominatim polite rate const CHART_DELAY = 300; // ms — chart preview debounce + // ── localStorage persistence ────────────────────────────────────────────── + // Key scoped to room so multiple rooms don't clobber each other. + + const LS_KEY = 'natus-form:' + SAVE_URL; + + function _saveForm() { + const data = { + date: document.getElementById('id_nf_date').value, + time: document.getElementById('id_nf_time').value, + place: placeInput.value, + lat: latInput.value, + lon: lonInput.value, + tz: tzInput.value, + }; + try { localStorage.setItem(LS_KEY, JSON.stringify(data)); } catch (_) {} + } + + function _restoreForm() { + let data; + try { data = JSON.parse(localStorage.getItem(LS_KEY) || 'null'); } catch (_) {} + if (!data) return; + if (data.date) document.getElementById('id_nf_date').value = data.date; + if (data.time) document.getElementById('id_nf_time').value = data.time; + if (data.place) placeInput.value = data.place; + if (data.lat) latInput.value = data.lat; + if (data.lon) lonInput.value = data.lon; + if (data.tz) { tzInput.value = data.tz; tzHint.textContent = 'Auto-detected from coordinates.'; } + } + // ── Open / Close ────────────────────────────────────────────────────────── function openNatus() { document.documentElement.classList.add('natus-open'); + // If the wheel is empty but the form has enough data (restored from + // localStorage), kick off a fresh preview so the animation plays. + if (!svgEl.querySelector('*') && _formReady()) { + schedulePreview(); + } } function closeNatus() { @@ -199,6 +241,7 @@ latInput.value = parseFloat(place.lat).toFixed(4); lonInput.value = parseFloat(place.lon).toFixed(4); hideSuggestions(); + _saveForm(); schedulePreview(); } @@ -219,7 +262,8 @@ }) .then(r => r.json()) .then(data => { placeInput.value = _cityName(data.address) || data.display_name || ''; }) - .catch(() => {}); + .catch(() => {}) + .finally(() => _saveForm()); setStatus(''); schedulePreview(); }, @@ -242,6 +286,7 @@ // Trigger on date / time / tz changes (coords come via selectPlace / geolocation) form.addEventListener('input', (e) => { if (e.target === placeInput) return; // place triggers via selectPlace + _saveForm(); clearTimeout(_chartDebounce); _chartDebounce = setTimeout(schedulePreview, CHART_DELAY); }); @@ -336,5 +381,12 @@ const m = document.cookie.match(/csrftoken=([^;]+)/); return m ? m[1] : ''; } + + // ── Restore persisted form data ──────────────────────────────────────────── + // Called after all functions are defined. Wheel draw is deferred to + // openNatus() so the animation plays when the modal opens, not silently + // in the background on page load. + + _restoreForm(); })();