mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
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
95 lines
4.3 KiB
TypeScript
95 lines
4.3 KiB
TypeScript
/**
|
|
* The moon's only foothold in the initial View graph (#661, C7): whether the
|
|
* lazy `moon-runtime` chunk is wanted, and one load per page for every card on
|
|
* it. It is wanted when the moon is switched on on a View surface and it is
|
|
* not daytime — with any background (#718 K1): over "Follow the Sun" the moon
|
|
* lives in the environment, over a static one in its own sky layer — and when
|
|
* General settings open, for the status line (#718 K6). Everything else —
|
|
* astronomy, art, element, layer, its 30 s ticker, the status — lives in the
|
|
* chunk.
|
|
*
|
|
* The contract of the other lazy runtimes, in the fewest bytes (the initial
|
|
* graph sits at its budget): a chunk from another build is never installed and
|
|
* never asked for again; a failed load leaves no moon and is retried at most
|
|
* every 30 s, on the next request, from the content-hashed URL with a fresh
|
|
* query so the browser's cached failure is not replayed.
|
|
*/
|
|
import { nothing, type TemplateResult } from 'lit';
|
|
import { ENTRY_BUILD_FINGERPRINT } from './editor-runtime-loader';
|
|
import { dayCycleFingerprint, resolveDayCycle, type DayCycleState } from './sun';
|
|
|
|
type MoonRuntime = typeof import('./moon-runtime');
|
|
export type MoonHost = import('./moon-runtime').MoonHost;
|
|
|
|
const MOON_RETRY_ASSET = '__HOUSEPLAN_MOON_RETRY_ASSET__';
|
|
let runtime: MoonRuntime | null = null;
|
|
/** The load in flight: every caller meanwhile waits for the same one. */
|
|
let loading: Promise<void> | null = null;
|
|
/** Earliest next attempt; 0 before the first, Infinity after a foreign build. */
|
|
let nextAttempt = 0;
|
|
|
|
const moonOn = (settings: unknown): boolean => (settings as { moon?: unknown } | null | undefined)?.moon === true;
|
|
|
|
/**
|
|
* `use` gets the chunk: at once when it is here, else when the page-wide load
|
|
* brings it. A failure, a load still barred by the 30 s retry or a chunk of
|
|
* another build never call it.
|
|
*/
|
|
export function withMoon(use: (moon: MoonRuntime) => void): void {
|
|
if (runtime) { use(runtime); return; }
|
|
if (!loading && Date.now() >= nextAttempt) {
|
|
const retry = nextAttempt > 0;
|
|
nextAttempt = Date.now() + 30_000;
|
|
loading = (retry
|
|
? import(/* @vite-ignore */ new URL(`${MOON_RETRY_ASSET}?${nextAttempt}`, import.meta.url).href) as Promise<MoonRuntime>
|
|
: import('./moon-runtime')
|
|
).then((module) => {
|
|
loading = null;
|
|
if (module.MOON_RUNTIME_FINGERPRINT === ENTRY_BUILD_FINGERPRINT) runtime = module;
|
|
else nextAttempt = Infinity;
|
|
}, () => { loading = null; });
|
|
}
|
|
void loading?.then(() => { if (runtime) use(runtime); });
|
|
}
|
|
|
|
/**
|
|
* #718 K1/K5: the phase the moon uses where no environment is drawn — a static
|
|
* background (global or the space's own), the moon switched on, a View
|
|
* surface (`viewWeight` of #101 above 0). Anything else computes nothing.
|
|
*/
|
|
export function moonSkyState(
|
|
settings: unknown, daynight: boolean, viewWeight: number, hass: unknown, now: Date | number = new Date(),
|
|
): DayCycleState | null {
|
|
return !daynight && viewWeight > 0 && moonOn(settings) ? resolveDayCycle(hass, now) : null;
|
|
}
|
|
|
|
/**
|
|
* #718 K5: what the 30 s clock ticker compares — the whole environment (its
|
|
* sun moves every minute), but only the phase of the moon's sky, so a static
|
|
* background re-renders at 08:00 and 18:00, not every minute.
|
|
*/
|
|
export function dayCycleClock(
|
|
env: DayCycleState | null, sky: DayCycleState | null,
|
|
): [DayCycleState | null, string] {
|
|
return [env ?? sky, env ? dayCycleFingerprint(env) : sky ? sky.phase : ''];
|
|
}
|
|
|
|
/**
|
|
* The moon element. `state` is the day-cycle sample (null: no moon);
|
|
* `settings` the global settings, where only an explicit `moon: true` counts
|
|
* (C1). Without `sky` the element goes into `renderDayCycleEnvironment`; with
|
|
* it (the View weight) it comes in its own layer for a static background.
|
|
* Nothing until the chunk is here; the card that asked re-renders when it
|
|
* arrives.
|
|
*/
|
|
export function moonLayer(
|
|
host: MoonHost | undefined, settings: unknown, state: DayCycleState | null, sky?: number,
|
|
): TemplateResult | typeof nothing {
|
|
if (!host || !state || !moonOn(settings)) return nothing;
|
|
if (!runtime && state.phase !== 'day') withMoon(() => host.requestUpdate());
|
|
if (!runtime) return nothing;
|
|
return sky === undefined
|
|
? runtime.renderMoon(host, settings, state.phase)
|
|
: runtime.renderMoonSky(host, settings, state.phase, sky);
|
|
}
|