diff --git a/CLAUDE.md b/CLAUDE.md
index 15611d6..59dce74 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -132,6 +132,35 @@ showcase updates itself. (Manual local regen still works too; the sandbox needs
Newest first. One short entry per working session: what shipped + open threads.
+### 2026-07-21 — Sun card: moon-phase view after sunset (v0.17.0)
+- **User:** refactor the sun card to also show moon phases / rise / set, with an
+ option to show the moon info after sunset. Kept the daytime sun-path arc as-is
+ and added a **night moon view** gated by `show_moon` (default true) + a moon
+ sensor being present.
+- `prism-sun-card` now branches in `_render`: daytime → `_sunView` (the existing
+ arc, extracted verbatim into a method); after sunset (state `below_horizon` /
+ elevation ≤ 0) with `show_moon` on and the moon entity available → `_moonView`.
+ The moon view draws a **moon-phase glyph** via `moonLitPath(cx,cy,r,k)` (outer
+ semicircle on the lit side + a terminator half-ellipse of x-radius
+ `|cos(2πk)|·r`; waxing lights the right), with new-moon = nothing lit and
+ full-moon = full circle special-cased. Shows illumination % (from an optional
+ sensor, else computed `(1-cos2πk)/2`), the phase label, the next sunrise, and a
+ moonrise/moonset subrow when those sensors are configured. `moonInfo()` maps
+ the 8 HA phase names (or a numeric phase) → `{label, k}`.
+- Config adds `show_moon`, `moon_entity` (default `sensor.moon`), `moonrise_entity`,
+ `moonset_entity`, `moon_illumination_entity`. Editor gains a **Moon** section
+ (switch + 4 pickers + hint). `getStubConfig` sets `moon_entity` when
+ `sensor.moon` exists. Moon colours are theme-safe (`#cdd6e5` lit + `--_text-2`
+ outline, `--_surface-2` unlit disk), moon icons use `--_text-2` (not accent).
+- Wired: smoke gains a `sun.night` + `sensor.moon`/moonrise/moonset fixtures and
+ 2 cases; preview gains a `sun.after_sunset` + moon mocks and a "Sun & Moon"
+ demo (renders offline, so the showcase picks it up). README table + quick-start,
+ docs/cards.md option table + example. VERSION 0.16.5 → 0.17.0.
+- Verified in real Chromium across all **8 phases**: moon view active at night,
+ correct lit geometry (first/last quarter = half-width, gibbous > half, full =
+ circle, crescents thin), illumination 0→100%, phase labels, moonrise/moonset
+ subrow, zero console errors. `bash build.sh` + `node test/smoke.js` green.
+
### 2026-07-21 — Editor icon fields: HA icon-picker dropdown w/ fallback (v0.16.5)
- **User:** can the visual editor use the icon picker dropdown? Added a shared
`_iconField(value, onChange, label?)` on `PrismEditor`: uses HA's searchable
diff --git a/README.md b/README.md
index 6288d38..951edf8 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ Prism is two things working together:
| **Wind** | `custom:prism-wind-card` | Wind tile: dominant speed, a flat compass rose with an accent direction arrow, Beaufort descriptor, gusts, and animated flat wind-streak accents that intensify with the wind. Reads a `weather.*` entity or individual sensors. |
| **Weather** | `custom:prism-weather-card` | Current-conditions tile: big temperature, a flat animated condition icon, feels-like + today's high/low, and humidity / wind / pressure chips. Reads a `weather.*` entity. |
| **Forecast** | `custom:prism-forecast-card` | Daily or hourly forecast strip: a flat condition icon per period, high/low temps, and precipitation-chance chips. Reads a `weather.*` entity **or** the free US National Weather Service API (weather.gov). |
-| **Sun** | `custom:prism-sun-card` | Sun-path arc: sunrise/sunset times, the sun riding its arc (a moon below the horizon at night), the traveled portion filled with the accent, and a live "sets in / rises in" countdown. Reads `sun.sun`. |
+| **Sun** | `custom:prism-sun-card` | Sun-path arc: sunrise/sunset times, the sun riding its arc, the traveled portion filled with the accent, and a live "sets in / rises in" countdown. After sunset it can flip to a moon-phase view (phase, illumination, moonrise/moonset). Reads `sun.sun` (+ optional `sensor.moon`). |
| **UV** | `custom:prism-uv-card` | UV-index tile: a big value coloured by its WHO risk band, the category + sun-protection advice, and a flat UV-ramp scale (green→purple) with a value marker. Reads a UV sensor or a `weather.*` `uv_index`. |
| **Rain** | `custom:prism-rain-card` | Animated rain-gauge tile: a measuring cylinder that fills with the event total, raindrops falling in at a rate set by the intensity, the event amount + a rain-rate descriptor, and chips for hourly / 24h / weekly / monthly. |
| **Air Quality** | `custom:prism-aqi-card` | AQI tile: a big value coloured by its US EPA category, the category + health advice, and a flat Good→Hazardous scale with a value marker. Reads an AQI sensor. |
@@ -229,10 +229,15 @@ title: UV Index
```yaml
# Sun — sunrise/sunset arc with a live countdown. Reads sun.sun.
+# After sunset it can show the moon phase (needs the Moon integration).
type: custom:prism-sun-card
entity: sun.sun
title: Sun
accent: amber
+show_moon: true # after sunset: moon phase + illumination
+moon_entity: sensor.moon # optional (defaults to sensor.moon)
+# moonrise_entity: sensor.moonrise # optional
+# moonset_entity: sensor.moonset # optional
```
```yaml
diff --git a/docs/cards.md b/docs/cards.md
index 617232b..b178fd1 100644
--- a/docs/cards.md
+++ b/docs/cards.md
@@ -542,7 +542,9 @@ units: us
## `custom:prism-sun-card`
-Flat **sun-path arc**: a dome of the sky from sunrise to sunset with the sun riding along it (a moon below the horizon at night), the **traveled portion filled with the accent**, **sunrise/sunset times** at each end, and a live **"sets in / rises in"** countdown in the middle.
+Flat **sun-path arc**: a dome of the sky from sunrise to sunset with the sun riding along it, the **traveled portion filled with the accent**, **sunrise/sunset times** at each end, and a live **"sets in / rises in"** countdown in the middle.
+
+Optionally, **after sunset** it flips to a **moon view**: a moon-phase glyph drawn to the current illuminated fraction, the phase name + **illumination %**, the next **sunrise**, and — when their sensors are configured — **moonrise / moonset**.
| Key | Type | Default | Notes |
|-----|------|---------|-------|
@@ -550,8 +552,13 @@ Flat **sun-path arc**: a dome of the sky from sunrise to sunset with the sun rid
| `name` | string | friendly name | Used in the accessible label. |
| `title` | string | — | Card header. |
| `accent` | string | `amber` | Colours the traveled arc and the sun. |
+| `show_moon` | bool | `true` | After sunset, show the moon view instead of the resting sun arc. |
+| `moon_entity` | string | `sensor.moon` | Moon-phase sensor (the Moon integration's `sensor.moon`). Its state is a phase name like `waxing_gibbous`, or a numeric phase. |
+| `moonrise_entity` | string | — | Optional sensor with a moonrise timestamp. |
+| `moonset_entity` | string | — | Optional sensor with a moonset timestamp. |
+| `moon_illumination_entity` | string | — | Optional illumination-% sensor. If unset, illumination is computed from the phase. |
-Daytime progress uses the next setting as today's sunset and approximates today's sunrise from the next rising; at night the arc rests and a moon sits on the horizon. Tap opens more-info.
+Daytime progress uses the next setting as today's sunset and approximates today's sunrise from the next rising. At night, with `show_moon` on and a moon sensor available, the card shows the moon phase; otherwise the arc rests with a moon on the horizon. Tap opens more-info.
```yaml
type: custom:prism-sun-card
@@ -560,6 +567,18 @@ title: Sun
accent: amber
```
+```yaml
+# With moon phase after sunset (needs the Moon integration for sensor.moon).
+type: custom:prism-sun-card
+entity: sun.sun
+title: Sun & Moon
+accent: amber
+show_moon: true
+moon_entity: sensor.moon
+moonrise_entity: sensor.moonrise # optional
+moonset_entity: sensor.moonset # optional
+```
+
---
## `custom:prism-uv-card`
diff --git a/prism.js b/prism.js
index e74d082..d7742e4 100644
--- a/prism.js
+++ b/prism.js
@@ -20,7 +20,7 @@
if (window.PrismUI && window.PrismUI.version) return; // already loaded
- const VERSION = '0.16.5';
+ const VERSION = '0.17.0';
// ── Named accent presets ──────────────────────────────────────────
// Selectable in every card editor; a card may also use a raw hex value.
@@ -4805,9 +4805,14 @@
/**
* prism-sun-card
* Flat sun-path tile: an arc of the sky from sunrise to sunset with the sun
- * riding along it (a moon below the horizon at night), the traveled portion
- * filled with the accent, sunrise/sunset times at each end, and a live
- * "sets in / rises in" countdown. Reads the `sun.sun` entity.
+ * riding along it, the traveled portion filled with the accent, sunrise/sunset
+ * times at each end, and a live "sets in / rises in" countdown. Reads `sun.sun`.
+ *
+ * Optionally, after sunset it flips to a **moon view**: a moon-phase glyph
+ * drawn to the current illuminated fraction, the phase name + illumination, the
+ * next sunrise, and (when sensors are configured) moonrise / moonset. Reads a
+ * moon-phase sensor (`sensor.moon` from the Moon integration) plus optional
+ * moonrise / moonset / illumination sensors.
*
* type: custom:prism-sun-card
*/
@@ -4823,6 +4828,50 @@
return h ? `${h}h ${mm}m` : `${mm}m`;
}
+ // Moon phase names (HA `sensor.moon`) → label + phase fraction k
+ // (0 = new, 0.25 = first quarter, 0.5 = full, 0.75 = last quarter).
+ const MOON = {
+ new_moon: { label: 'New Moon', k: 0.0 },
+ waxing_crescent: { label: 'Waxing Crescent', k: 0.125 },
+ first_quarter: { label: 'First Quarter', k: 0.25 },
+ waxing_gibbous: { label: 'Waxing Gibbous', k: 0.375 },
+ full_moon: { label: 'Full Moon', k: 0.5 },
+ waning_gibbous: { label: 'Waning Gibbous', k: 0.625 },
+ last_quarter: { label: 'Last Quarter', k: 0.75 },
+ waning_crescent: { label: 'Waning Crescent', k: 0.875 },
+ };
+ const illumFromK = (k) => (1 - Math.cos(2 * Math.PI * k)) / 2;
+
+ // Resolve a moon-phase state into { label, k }. Accepts a phase name, or a
+ // numeric value (0–1 fraction or 0–100 percent → nearest named phase feel).
+ function moonInfo(state) {
+ const s = String(state == null ? '' : state).toLowerCase().trim();
+ if (MOON[s]) return MOON[s];
+ const key = s.replace(/[\s-]+/g, '_');
+ if (MOON[key]) return MOON[key];
+ const n = parseFloat(s);
+ if (!isNaN(n)) {
+ const k = n > 1 ? (n / 100) % 1 : n % 1; // percent or fraction → cycle pos
+ // Nearest named phase for the label.
+ let best = 'new_moon', bd = Infinity;
+ for (const name in MOON) { const d = Math.abs(MOON[name].k - k); if (d < bd) { bd = d; best = name; } }
+ return { label: MOON[best].label, k };
+ }
+ return null;
+ }
+
+ // SVG path for the illuminated portion of a moon disk at phase fraction k.
+ function moonLitPath(cx, cy, r, k) {
+ const b = Math.cos(2 * Math.PI * k); // 1 (new) … -1 (full)
+ const rx = Math.abs(b) * r; // terminator ellipse x-radius
+ const waxing = k < 0.5; // northern hemisphere: lit on the right
+ const outerSweep = waxing ? 1 : 0;
+ const termSweep = waxing ? (b > 0 ? 0 : 1) : (b > 0 ? 1 : 0);
+ return `M${cx.toFixed(1)},${(cy - r).toFixed(1)}`
+ + ` A${r},${r} 0 0 ${outerSweep} ${cx.toFixed(1)},${(cy + r).toFixed(1)}`
+ + ` A${rx.toFixed(1)},${r} 0 0 ${termSweep} ${cx.toFixed(1)},${(cy - r).toFixed(1)} Z`;
+ }
+
// ── Editor ────────────────────────────────────────────────────────
class PrismSunCardEditor extends P.PrismEditor {
_fields(stack) {
@@ -4832,7 +4881,14 @@
this._picker('Sun entity', c.entity, (v) => this._patch('entity', v), { domains: ['sun'] }),
this._tf('Name (optional)', c.name, (v) => this._patch('name', v)),
this._accentField(c.accent, (v) => this._patch('accent', v)),
- this._hint('Defaults to sun.sun. Uses its next_rising / next_setting / elevation attributes.')
+ this._hint('Defaults to sun.sun. Uses its next_rising / next_setting / elevation attributes.'),
+ this._section('Moon'),
+ this._switch('Show moon after sunset', c.show_moon !== false, (v) => this._patch('show_moon', v)),
+ this._picker('Moon phase sensor', c.moon_entity, (v) => this._patch('moon_entity', v), { domains: ['sensor'] }),
+ this._picker('Moonrise sensor (optional)', c.moonrise_entity, (v) => this._patch('moonrise_entity', v), { domains: ['sensor'] }),
+ this._picker('Moonset sensor (optional)', c.moonset_entity, (v) => this._patch('moonset_entity', v), { domains: ['sensor'] }),
+ this._picker('Illumination sensor (optional)', c.moon_illumination_entity, (v) => this._patch('moon_illumination_entity', v), { domains: ['sensor'] }),
+ this._hint('Defaults to sensor.moon (Moon integration) for the phase. Moonrise/moonset need their own sensors; illumination is computed from the phase if no sensor is set.')
);
}
}
@@ -4848,7 +4904,7 @@
}
setConfig(config) {
- this._config = { entity: 'sun.sun', accent: 'amber', ...config };
+ this._config = { entity: 'sun.sun', accent: 'amber', show_moon: true, ...config };
if (this._hass) this._render();
}
@@ -4858,14 +4914,24 @@
getGridOptions() { return { rows: 3, columns: 6, min_rows: 3, min_columns: 4 }; }
static getConfigElement() { return document.createElement('prism-sun-card-editor'); }
- static getStubConfig() { return { entity: 'sun.sun', accent: 'amber' }; }
+ static getStubConfig(hass) {
+ const hasMoon = hass && hass.states && hass.states['sensor.moon'];
+ return { entity: 'sun.sun', accent: 'amber', ...(hasMoon ? { moon_entity: 'sensor.moon' } : {}) };
+ }
- _moreInfo() {
+ _moreInfo(id) {
this.dispatchEvent(new CustomEvent('hass-more-info', {
- detail: { entityId: this._config.entity }, bubbles: true, composed: true,
+ detail: { entityId: id || this._config.entity }, bubbles: true, composed: true,
}));
}
+ _num(id) {
+ return id && this._hass.states[id] ? parseFloat(this._hass.states[id].state) : NaN;
+ }
+ _time(id) {
+ return id && this._hass.states[id] ? Date.parse(this._hass.states[id].state) : NaN;
+ }
+
_render() {
if (!this._config || !this._hass) return;
const c = this._config;
@@ -4880,8 +4946,49 @@
const elev = parseFloat(a.elevation);
const isDay = st ? (st.state === 'above_horizon' || (!isNaN(elev) && elev > 0)) : false;
- // Daylight progress: today's sunset is the next setting; approximate
- // today's sunrise as the next rising minus a day (good enough for a viz).
+ // Should we render the moon view? Only after sunset, when enabled and a
+ // moon-phase sensor is available.
+ const moonId = c.moon_entity || 'sensor.moon';
+ const moonSt = this._hass.states[moonId];
+ const showMoon = c.show_moon !== false && !isDay && !!moonSt;
+
+ const styles = `
+ `;
+
+ const inner = showMoon
+ ? this._moonView(moonSt, rise, c, name)
+ : this._sunView(a, st, rise, set, now, isDay, accent, name);
+
+ this.shadowRoot.innerHTML = `
+ ${styles}
+