MY SKY: full-page layout polish — aperture pinning, wheel-above-form, centred wheel
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

- sky_view passes page_class="page-sky" so the footer pins correctly
- _natus.scss: page-sky aperture block (mirrors page-wallet pattern);
  sky-page stacks wheel above form via flex order + page-level scroll;
  wheel col uses aspect-ratio:1/1 so it takes natural square size without
  compressing to fit the form
- natus-wheel.js: _layout() sets viewBox + preserveAspectRatio="xMidYMid meet"
  so the wheel is always centred inside the SVG element regardless of its
  aspect ratio (fixes left-alignment in the dashboard applet)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-04-16 14:40:52 -04:00
parent bd9a2fdae3
commit 8a24021739
5 changed files with 187 additions and 0 deletions

View File

@@ -317,6 +317,7 @@ def sky_view(request):
"saved_sky": request.user.sky_chart_data, "saved_sky": request.user.sky_chart_data,
"saved_birth_dt": request.user.sky_birth_dt, "saved_birth_dt": request.user.sky_birth_dt,
"saved_birth_place": request.user.sky_birth_place, "saved_birth_place": request.user.sky_birth_place,
"page_class": "page-sky",
}) })

View File

@@ -118,6 +118,10 @@ const NatusWheel = (() => {
const size = Math.min(rect.width || 400, rect.height || 400); const size = Math.min(rect.width || 400, rect.height || 400);
_cx = size / 2; _cx = size / 2;
_cy = size / 2; _cy = size / 2;
// viewBox pins the coordinate system to size×size; preserveAspectRatio
// centres it inside the SVG element regardless of its aspect ratio.
svgEl.setAttribute('viewBox', `0 0 ${size} ${size}`);
svgEl.setAttribute('preserveAspectRatio', 'xMidYMid meet');
_r = size * 0.46; // leave a small margin _r = size * 0.46; // leave a small margin
R = { R = {

View File

@@ -5,6 +5,7 @@ natus (natal chart) interface where the user can save their personal sky
to their account (stored on the User model, independent of any game room). to their account (stored on the User model, independent of any game room).
""" """
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from apps.applets.models import Applet from apps.applets.models import Applet
@@ -13,6 +14,35 @@ from apps.lyric.models import User
from .base import FunctionalTest from .base import FunctionalTest
# Minimal chart fixture — matches the NatusWheel data shape.
_CHART_FIXTURE = {
"planets": {
"Sun": {"sign": "Pisces", "degree": 340.0, "retrograde": False},
"Moon": {"sign": "Gemini", "degree": 72.0, "retrograde": False},
"Mercury": {"sign": "Aquarius", "degree": 310.0, "retrograde": False},
"Venus": {"sign": "Aries", "degree": 10.0, "retrograde": False},
"Mars": {"sign": "Capricorn", "degree": 280.0, "retrograde": False},
"Jupiter": {"sign": "Cancer", "degree": 100.0, "retrograde": False},
"Saturn": {"sign": "Capricorn", "degree": 290.0, "retrograde": True},
"Uranus": {"sign": "Capricorn", "degree": 285.0, "retrograde": False},
"Neptune": {"sign": "Capricorn", "degree": 283.0, "retrograde": False},
"Pluto": {"sign": "Scorpio", "degree": 218.0, "retrograde": False},
},
"houses": {
"cusps": [10, 40, 70, 100, 130, 160, 190, 220, 250, 280, 310, 340],
"asc": 10.0, "mc": 100.0,
},
"elements": {"Fire": 1, "Water": 2, "Stone": 4, "Air": 1, "Time": 0, "Space": 1},
"aspects": [],
"distinctions": {
"1": 1, "2": 0, "3": 0, "4": 1, "5": 0, "6": 0,
"7": 0, "8": 0, "9": 0, "10": 4, "11": 0, "12": 0,
},
"house_system": "O",
"timezone": "America/New_York",
}
class MySkyAppletTest(FunctionalTest): class MySkyAppletTest(FunctionalTest):
"""My Sky applet appears on the dashboard and links to the sky page.""" """My Sky applet appears on the dashboard and links to the sky page."""
@@ -116,3 +146,55 @@ class MySkyLocalStorageTest(FunctionalTest):
self.assertEqual(values["lon"], "-0.1278") self.assertEqual(values["lon"], "-0.1278")
self.assertEqual(values["place"], "London, UK") self.assertEqual(values["place"], "London, UK")
self.assertEqual(values["tz"], "Europe/London") self.assertEqual(values["tz"], "Europe/London")
class MySkyAppletWheelTest(FunctionalTest):
"""Saved natal chart renders as an interactive wheel inside the My Sky applet."""
def setUp(self):
super().setUp()
Applet.objects.get_or_create(
slug="my-sky",
defaults={"name": "My Sky", "grid_cols": 6, "grid_rows": 6, "context": "dashboard"},
)
self.gamer = User.objects.create(email="stargazer@test.io")
self.gamer.sky_chart_data = _CHART_FIXTURE
self.gamer.sky_birth_place = "Lindenwold, NJ, US"
self.gamer.save()
# ── T3 ───────────────────────────────────────────────────────────────────
def test_saved_sky_wheel_renders_with_tooltips_in_applet(self):
"""When the user has saved sky data, the natal wheel appears in the My Sky
applet with working element-ring and planet tooltips."""
self.create_pre_authenticated_session("stargazer@test.io")
self.browser.get(self.live_server_url)
# 1. Wheel SVG is drawn inside the applet
self.wait_for(lambda: self.assertTrue(
self.browser.find_element(
By.CSS_SELECTOR, "#id_applet_my_sky .nw-root"
)
))
# 2. Hovering an element-ring slice shows the tooltip
slice_el = self.browser.find_element(
By.CSS_SELECTOR, "#id_applet_my_sky .nw-element-group"
)
ActionChains(self.browser).move_to_element(slice_el).perform()
self.wait_for(lambda: self.assertEqual(
self.browser.find_element(By.ID, "id_natus_tooltip")
.value_of_css_property("display"),
"block",
))
# 3. Hovering a planet also shows the tooltip
planet_el = self.browser.find_element(
By.CSS_SELECTOR, "#id_applet_my_sky .nw-planet-group"
)
ActionChains(self.browser).move_to_element(planet_el).perform()
self.wait_for(lambda: self.assertEqual(
self.browser.find_element(By.ID, "id_natus_tooltip")
.value_of_css_property("display"),
"block",
))

View File

@@ -507,6 +507,87 @@ body[class*="-light"] #id_natus_tooltip {
.tt-title--pu { color: rgba(var(--priPu), 1); } .tt-title--pu { color: rgba(var(--priPu), 1); }
} }
// ── My Sky dashboard applet ───────────────────────────────────────────────────
#id_applet_my_sky {
display: flex;
flex-direction: column;
h2 { flex-shrink: 0; }
.natus-svg {
flex: 1;
min-height: 0;
max-width: none;
max-height: none;
align-self: center;
}
}
// ── Sky full page (aperture + column layout) ──────────────────────────────────
html:has(body.page-sky) {
overflow: hidden;
}
body.page-sky {
overflow: hidden;
.container {
overflow: hidden;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.row {
flex-shrink: 0;
}
}
// Sky page fills the aperture; its content can scroll past the bottom edge
.sky-page {
position: relative;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
}
// Stack wheel above form; allow body to grow past viewport (page scrolls, not body)
.sky-page .natus-modal-body {
flex-direction: column;
flex-shrink: 0;
}
// Wheel takes its natural square size from its width — never shrinks for the form
.sky-page .natus-wheel-col {
order: -1;
flex: 0 0 auto;
width: 100%;
aspect-ratio: 1 / 1;
max-width: 480px;
max-height: 480px;
align-self: center;
}
// Form col runs horizontally below the wheel (same compact pattern as narrow-portrait modal)
.sky-page .natus-form-col {
flex: 0 0 auto;
flex-direction: row;
align-items: flex-end;
border-right: none;
border-top: 0.1rem solid rgba(var(--terUser), 0.12);
}
.sky-page .natus-form-main {
flex: 1;
min-width: 0;
overflow-y: visible;
}
// ── Sidebar z-index sink (landscape sidebars must go below backdrop) ─────────── // ── Sidebar z-index sink (landscape sidebars must go below backdrop) ───────────
@media (orientation: landscape) { @media (orientation: landscape) {

View File

@@ -1,6 +1,25 @@
{% load static %}
<section <section
id="id_applet_my_sky" id="id_applet_my_sky"
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
> >
<h2><a href="{% url 'sky' %}">My Sky</a></h2> <h2><a href="{% url 'sky' %}">My Sky</a></h2>
{% if request.user.sky_chart_data %}
<svg id="id_my_sky_svg" class="natus-svg"></svg>
{{ request.user.sky_chart_data|json_script:"id_my_sky_data" }}
{% endif %}
</section> </section>
{% if request.user.sky_chart_data %}
<div id="id_natus_tooltip" class="tt" style="display:none;"></div>
<script src="{% static 'apps/gameboard/d3.min.js' %}"></script>
<script src="{% static 'apps/gameboard/natus-wheel.js' %}"></script>
<script>
(function () {
'use strict';
const data = JSON.parse(document.getElementById('id_my_sky_data').textContent);
const svgEl = document.getElementById('id_my_sky_svg');
NatusWheel.preload().then(function () { NatusWheel.draw(svgEl, data); });
})();
</script>
{% endif %}