mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 22:49:16 +00:00
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:
@@ -15,6 +15,7 @@ import {
|
||||
formCard, segmented, subsection, textLink, toggleRow, unitInput,
|
||||
} from './form-kit';
|
||||
import { forgetGeneralBaseline, generalDirty, generalProblems, type GeneralSettingsDraft } from './general-form-state';
|
||||
import { moonStatusOf, moonStatusText } from './moon-status';
|
||||
import { langOf, type I18nKey } from '../i18n';
|
||||
import { settingsT, type SettingsI18nKey } from '../i18n/settings';
|
||||
import { supportT } from '../i18n/support';
|
||||
@@ -76,6 +77,8 @@ export function renderGeneralSettingsDialog(this: HouseplanEditorRuntime): Templ
|
||||
};
|
||||
const glowProblem = problemFor('gs-glow-radius');
|
||||
const northProblem = problemFor('gs-north');
|
||||
// #718 K7: the second caption line, once the moon chunk judged this opening.
|
||||
const moonStatus = moonStatusOf(host);
|
||||
const zigbeeHelp = hasTopologyTranslation(lang, 'help') && hasTopologyTranslation(lang, 'help_aria')
|
||||
? html`<hp-help data-help-key="topology.help" .text=${topologyT(lang, 'help')} .ariaLabel=${topologyT(lang, 'help_aria')}></hp-help>`
|
||||
: nothing;
|
||||
@@ -227,7 +230,10 @@ export function renderGeneralSettingsDialog(this: HouseplanEditorRuntime): Templ
|
||||
${renderSunRayOriginSegment(d.sunRayOrigin, (key) => t(key), (sunRayOrigin) => set({ sunRayOrigin }))}
|
||||
${toggleRow({
|
||||
id: 'gs-moon', icon: 'mdi:moon-waning-crescent',
|
||||
title: st('gs.moon'), caption: st('gs.moon_hint'),
|
||||
title: st('gs.moon'),
|
||||
caption: moonStatus
|
||||
? html`${st('gs.moon_hint')}<span style="display:block" data-moon-status=${moonStatus.reason}>${moonStatusText(moonStatus, st)}</span>`
|
||||
: st('gs.moon_hint'),
|
||||
checked: d.moon, onChange: (v) => set({ moon: v }),
|
||||
})}`,
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* #718 K7: the «Now: …» line under the moon switch in General settings.
|
||||
*
|
||||
* One snapshot per opening — `now`, `hass.config`, `sun.sun` — judged by the
|
||||
* lazy moon chunk as if the switch were on: the moon no longer depends on the
|
||||
* background, so one status serves the whole installation, whatever the
|
||||
* switch, the background segment or the spaces say. It lives beside the
|
||||
* draft, never in it (`generalDraftKey` does not see it), so the line arriving
|
||||
* does not make the dialog dirty. The result of an opening that was closed in
|
||||
* the meantime is dropped; until the chunk is here, or when it failed, there
|
||||
* is no line and the hint alone stays.
|
||||
*/
|
||||
import { withMoon } from '../moon-gate';
|
||||
import { resolveDayCycle, sunStateOf } from '../sun';
|
||||
import type { MoonStatus, MoonStatusReason } from '../moon';
|
||||
import type { SettingsI18nKey } from '../i18n/settings';
|
||||
|
||||
interface MoonStatusHost {
|
||||
hass?: unknown;
|
||||
_settingsDialog: unknown;
|
||||
requestUpdate(): void;
|
||||
}
|
||||
|
||||
interface Opening { status?: MoonStatus }
|
||||
|
||||
const openings = new WeakMap<object, Opening>();
|
||||
|
||||
/** Called by `_openSettingsDialog` once the draft exists: asks for this opening's status. */
|
||||
export function openMoonStatus(host: MoonStatusHost, now: Date = new Date()): void {
|
||||
const opening: Opening = {};
|
||||
openings.set(host, opening);
|
||||
const hass = host.hass as { config?: unknown } | undefined;
|
||||
const state = resolveDayCycle(hass, now);
|
||||
// With `source === 'sun'` this is the very elevation `dayCycleSunOf` read.
|
||||
const sun = sunStateOf(hass)?.elevation ?? null;
|
||||
withMoon((moon) => {
|
||||
if (openings.get(host) !== opening || !host._settingsDialog) return;
|
||||
opening.status = moon.moonStatus(hass?.config, state, sun, now);
|
||||
host.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
/** The status of the current opening, once the chunk has judged it. */
|
||||
export function moonStatusOf(host: object): MoonStatus | undefined {
|
||||
return openings.get(host)?.status;
|
||||
}
|
||||
|
||||
/** Literal keys (#502): the dead-key gate does not read a key glued from the reason. */
|
||||
const MOON_STATUS_KEYS: Readonly<Record<MoonStatusReason, SettingsI18nKey>> = {
|
||||
shown: 'gs.moon_status_shown',
|
||||
no_home: 'gs.moon_status_no_home',
|
||||
day_sun: 'gs.moon_status_day_sun',
|
||||
day_clock: 'gs.moon_status_day_clock',
|
||||
low: 'gs.moon_status_low',
|
||||
new: 'gs.moon_status_new',
|
||||
};
|
||||
|
||||
/** A signed whole number: the minus is U+2212, no plus; −0 reads as 0. */
|
||||
const signed = (value = 0): string => (value < 0 ? `−${-value}` : String(Math.abs(value)));
|
||||
|
||||
/** The line's text; `st` is the dialog's `settingsT` for its language. */
|
||||
export function moonStatusText(
|
||||
status: MoonStatus, st: (key: SettingsI18nKey, vars: Record<string, string>) => string,
|
||||
): string {
|
||||
return st(MOON_STATUS_KEYS[status.reason], {
|
||||
alt: signed(status.alt), pct: signed(status.pct), sun: signed(status.sun),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user