PICK SKY: natal wheel polish — house/sign fill fixes, button layout, localStorage FT
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

- Fix D3 arc coordinate offset (add π/2 to all arc angles — D3 subtracts it
  internally, causing fills to render 90° CW from label midpoints)
- Fix house-12 wrap-around: normalise nextCusp += 360 when it crosses 0°,
  eliminating the 330° ghost arc that buried house fill/number layers
- Draw all house fills before cusp lines + numbers (z-order fix)
- SCSS: sign/element fills corrected to rgba(var(--priXx, R, G, B), α) —
  CSS vars are raw RGB tuples so bare var() in fill was invalid
- brighten Stone/Air/Water fallback colours; raise house fill opacities
- Button layout: SAVE SKY moves into form column (full-width, pinned bottom);
  NVM becomes a btn-sm circle anchored on the modal's top-right corner via
  .natus-modal-wrap (position:relative, outside overflow:hidden modal);
  entrance animation moved to wrapper so NVM rides the fade+slide
- Form fields wrapped in .natus-form-main (scrollable); portrait layout
  switches form-col to flex-row so form spans most width, SAVE SKY on right
- Modal max-height 92→96vh, max-width 840→920px, SVG cap 400→480px
- FT: PickSkyLocalStorageTest (2 tests) — form fields restored after NVM
  and after page refresh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-15 00:49:14 -04:00
parent 6248d95bf3
commit 9beb21bffe
4 changed files with 426 additions and 175 deletions

View File

@@ -10,6 +10,7 @@
data-preview-url="{% url 'epic:natus_preview' room.id %}"
data-save-url="{% url 'epic:natus_save' room.id %}">
<div class="natus-modal-wrap">
<div class="natus-modal">
<header class="natus-modal-header">
@@ -21,57 +22,66 @@
{# ── Form column ──────────────────────────────────────── #}
<div class="natus-form-col">
<form id="id_natus_form" autocomplete="off">
<div class="natus-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="natus-form-main">
<form id="id_natus_form" autocomplete="off">
<div class="natus-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="natus-field natus-place-field">
<label for="id_nf_place">Birth place</label>
<div class="natus-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 class="natus-field">
<label for="id_nf_date">Birth date</label>
<input id="id_nf_date" name="date" type="date" required>
</div>
<div id="id_nf_suggestions" class="natus-suggestions" hidden></div>
</div>
<div class="natus-field natus-coords">
<div>
<label>Latitude</label>
<input id="id_nf_lat" name="lat" type="text"
placeholder="—" readonly tabindex="-1">
<div class="natus-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>
<label>Longitude</label>
<input id="id_nf_lon" name="lon" type="text"
placeholder="—" readonly tabindex="-1">
<div class="natus-field natus-place-field">
<label for="id_nf_place">Birth place</label>
<div class="natus-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="natus-suggestions" hidden></div>
</div>
</div>
<div class="natus-field">
<label for="id_nf_tz">Timezone</label>
<input id="id_nf_tz" name="tz" type="text"
placeholder="auto-detected from location">
<small id="id_nf_tz_hint"></small>
</div>
<div class="natus-field natus-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>
</form>
<div class="natus-field">
<label for="id_nf_tz">Timezone</label>
<input id="id_nf_tz" name="tz" type="text"
placeholder="auto-detected from location">
<small id="id_nf_tz_hint"></small>
</div>
</form>
<div id="id_natus_status" class="natus-status"></div>
</div>{# /.natus-form-main #}
<button type="button" id="id_natus_confirm" class="btn btn-primary" disabled>
Save Sky
</button>
<div id="id_natus_status" class="natus-status"></div>
</div>
{# ── Wheel column ─────────────────────────────────────── #}
@@ -81,14 +91,12 @@
</div>{# /.natus-modal-body #}
<footer class="natus-modal-footer">
<button type="button" id="id_natus_cancel" class="btn btn-cancel">NVM</button>
<button type="button" id="id_natus_confirm" class="btn btn-primary" disabled>
Save Sky
</button>
</footer>
</div>{# /.natus-modal #}
{# NVM: circle btn centered on the top-right corner of the modal #}
<button type="button" id="id_natus_cancel" class="btn btn-cancel btn-sm">NVM</button>
</div>{# /.natus-modal-wrap #}
</div>{# /.natus-overlay #}
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
@@ -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();
})();
</script>