feat(moon): the moon with any background, and its status in General settings (#718)

The owner decided on 30.09 that the moon is not part of the "Follow the Sun"
environment but a switch of its own: with a static background (global or a
space's own) the card showed no moon even with the switch on, and the switch
said nothing about why the moon was missing right now.

With a static background there is no environment, so the moon stands in its
own layer, `.hp-moon-sky`: the first child of `.stage` / `.hp-static-stage`,
the whole scene, no z-index, filter or will-change, under the plan by DOM
order, fading with the #101 View weight. Inside is the very #661 element, so
place, size, art and fades are unchanged, and a background switch moves it to
its new parent in the same render without a flicker. The phase comes from the
same `resolveDayCycle`, computed only while the moon is on and on View; without
`sun.sun` both cards keep their 30 s clock ticker and re-render only when the
phase changes (the environment is still compared by its whole fingerprint).

General settings get a second caption line under the moon switch
(`data-moon-status`): one snapshot per opening, judged by the lazy chunk as if
the switch were on, first reason wins (no home, day, below 3°, under 3 %),
numbers rounded and clamped below the threshold they missed. `moonStatus`
decides "shown" with the same `moonShownAt` as the element. It lives in a
WeakMap beside the draft, so it never makes the dialog dirty; a closed
opening's result is dropped. The dialog loads the chunk through the gate's
loader (`withMoon`), now shared by every caller while a load is in flight, so
there is still one fingerprint check and one retry token.

Bundle (same build, against origin/dev): initial View 300 072 -> 300 248 B gzip
(+176 B, under the 500 B of the spec; budget and ceiling not raised); lazy
editor 238 558 -> 238 991 B (+433 B, the line and English strings); lazy moon
11 385 -> 11 712 B (+327 B, layer CSS and status). `src/moon.ts` stays out of
the initial and the editor graph; bundle-budget now refuses an editor/moon
overlap. Monolith metrics: hostRefs 4 885 -> 4 888 — the three `host.` reads of
`src/editors/moon-status.ts` (hass, `_settingsDialog`, requestUpdate) through
its own three-member interface, not the editor port; the other five metrics
are unchanged. houseplan-editor-runtime.ts grows by two lines (import, call).

Tests: AC9/AC10/AC15 and the sky layer in test/moon.test.mjs (the #661
"static -> nothing" check inverted), AC14 and the opening lifecycle in
test/moon-settings.test.mjs, smokes demo/smoke_moon_static.mjs (AC1-AC6; AC1
and AC3 were red on dev) and demo/smoke_moon_status.mjs (AC11/AC12), AC7 in
smoke_daycycle_layer_budget. Golden: two new scenes
(static-bg-moon-gibbous-white-light, static-bg-moon-crescent-south-dark,
matrix v70), the harness checks the moon's parent by background and waits for
the status line in the General settings frames. Four new mutants; the clock
ticker one is a browser guard (201 at the guideline of 200).

Issue: #718
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-10-01 05:25:21 +00:00
committed by claude[bot]
parent 712d41b6d6
commit b998b0b34a
33 changed files with 1553 additions and 146 deletions
+7 -3
View File
@@ -25,7 +25,7 @@ import {
import { DEFAULT_ICON_RULES, compileIconRules } from './rules';
import { t, type Lang } from './i18n';
import { bgModeOf, resolveDayCycle } from './sun';
import { moonLayer, type MoonHost } from './moon-gate';
import { moonLayer, moonSkyState, type MoonHost } from './moon-gate';
import { dayCycleStageVars, renderDayCycleEnvironment } from './day-cycle-render';
import type { DevItem, OpeningCfg, ServerConfig } from './types';
import { floorMinusBodies, physicalBodyParts, polyclipPathD } from './physical-geometry';
@@ -670,9 +670,12 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
});
// The static card paints the same four-phase environment as full View.
// Wedges stay full-card-only; the decorative background is independent.
// #718: a static background gets the moon in its own sky layer instead.
const spaceSettings = (o.cfg.spaces.find((sp: any) => sp.id === o.spaceId) as any)?.settings || {};
const dayCycle = bgModeOf(o.cfg?.settings, spaceSettings) === 'daynight'
? resolveDayCycle(planHass, o.dayCycleNow ?? new Date()) : null;
const daynight = bgModeOf(o.cfg?.settings, spaceSettings) === 'daynight';
const dayCycleNow = o.dayCycleNow ?? new Date();
const dayCycle = daynight ? resolveDayCycle(planHass, dayCycleNow) : null;
const moonSky = moonSkyState(o.cfg?.settings, daynight, 1, planHass, dayCycleNow);
const stageBg = stageBgOf(o.cfg?.settings, disp);
// Opaque plan paper, same contract as the full card (docs/DECOR-EDITOR.md §3.3):
@@ -931,6 +934,7 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
?inert=${!!o.inert}
style="aspect-ratio:${vb[2]}/${vb[3]}${stageBg ? ';background:' + stageBg : ''};--hp-cell-visual-scale:${gridVisualScale(cellCm)};--wall-fill:${colors.wall_fill.c};--wall-fill-op:${colors.wall_fill.a}${dayCycle ? `;${dayCycleStageVars(dayCycle)}` : ''}">
${renderDayCycleEnvironment(dayCycle, 1, moonLayer(o.moonHost, o.cfg?.settings, dayCycle))}
${moonLayer(o.moonHost, o.cfg?.settings, moonSky, 1)}
${dayCycle && paperShapes.length ? svg`<svg class="hp-paper-outline-svg"
viewBox="${vb[0]} ${vb[1]} ${vb[2]} ${vb[3]}"
preserveAspectRatio="xMidYMid meet" aria-hidden="true" pointer-events="none">