diff --git a/CLAUDE.md b/CLAUDE.md index a83a83b..0c8b889 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -132,6 +132,26 @@ 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 — Animated cards: render-guard fix for restart "bouncing" (v0.16.3) +- **User:** the wind card animation is broken — "a bouncing line right in the + wind rose." Root cause: HA pushes `hass` to every card on **every** state + change, and these cards rebuild their whole `innerHTML` in `set hass` → + `_render()`. Recreating the DOM restarts every CSS animation from frame 0, so + the wind streak's multi-second sweep snapped back to its start on each tick = + a line bouncing across the compass. Same latent bug in **rain** (falling + drops / wave) and **lux** (26s ray-spin / disc-pulse), which also animate + continuously — fixed all three. +- Fix: a **render guard**. Each card computes a `sig` (JSON of the displayed + values — wind: speed/bearing/gust/unit/name; rain: frac/event/intensity/ + periodChips/intLine/name; lux: value/night/band/name/unit) and early-returns + from `_render` when it's unchanged, so the DOM (and its animations) persist + across unrelated ticks. `setConfig` resets `this._sig = null` to force a + rebuild on config change. Constructor seeds `this._sig = null`. +- Verified: `bash build.sh` + `node test/smoke.js` green; a Node harness test + confirmed an identical `hass` push skips the rebuild while a changed wind + value still re-renders. VERSION 0.16.2 → 0.16.3. (In-browser visual check + still blocked — sandbox Chromium has no outbound network.) + ### 2026-07-21 — Forecast NWS: explicit Home/Custom location (v0.16.2) + animated-icon alignment fix (v0.16.1) - **v0.16.1:** user reported the forecast icons are misaligned **when animated**. Root cause in the shared flat `weatherIcon`: the cloud `` carried BOTH its diff --git a/prism.js b/prism.js index 493249e..ec8220c 100644 --- a/prism.js +++ b/prism.js @@ -20,7 +20,7 @@ if (window.PrismUI && window.PrismUI.version) return; // already loaded - const VERSION = '0.16.2'; + const VERSION = '0.16.3'; // ── Named accent presets ────────────────────────────────────────── // Selectable in every card editor; a card may also use a raw hex value. @@ -4072,6 +4072,7 @@ this.attachShadow({ mode: 'open' }); this._config = null; this._hass = null; + this._sig = null; } setConfig(config) { @@ -4079,6 +4080,7 @@ throw new Error('prism-wind-card: set `entity` (a weather or wind-speed entity) or `speed_entity`.'); } this._config = { show_gust: true, animate: true, ...config }; + this._sig = null; // force a rebuild on the next render if (this._hass) this._render(); } @@ -4113,6 +4115,14 @@ const cardinal = hasDir ? cardinalOf(bearing) : ''; const name = c.name || (this._hass.states[c.entity] ? this._hass.states[c.entity].attributes.friendly_name : (c.entity || c.speed_entity)); + // Home Assistant pushes `hass` on every state change, so _render runs + // constantly. Only rebuild the DOM when a displayed value actually + // changes — otherwise the recreated streak spans restart their CSS sweep + // animation on every tick (a "bouncing line" across the rose). + const sig = JSON.stringify([speed, bearing, gust, unit, name]); + if (sig === this._sig) return; + this._sig = sig; + // Compass geometry. const W = 120, cx = 60, cy = 60, R = 52; let ticks = ''; @@ -5113,11 +5123,13 @@ this.attachShadow({ mode: 'open' }); this._config = null; this._hass = null; + this._sig = null; } setConfig(config) { if (!config.event_entity) throw new Error('prism-rain-card: `event_entity` is required.'); this._config = { animate: true, accent: 'blue', ...config }; + this._sig = null; // force a rebuild on the next render if (this._hass) this._render(); } @@ -5192,6 +5204,13 @@ ? `${rate.label}` : `${P.esc(fmtRain(intensity, iUnit))} ${P.esc(iUnit)} · ${rate.label}`; + // Home Assistant pushes `hass` on every state change. Rebuild only when a + // displayed value changes, so the falling-drop / wave CSS animations + // aren't restarted from scratch on every unrelated tick. + const sig = JSON.stringify([frac, event, intensity, periodChips, intLine, name]); + if (sig === this._sig) return; + this._sig = sig; + this.shadowRoot.innerHTML = `