Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<g>` carried BOTH its
Expand Down
30 changes: 29 additions & 1 deletion prism.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -4072,13 +4072,15 @@
this.attachShadow({ mode: 'open' });
this._config = null;
this._hass = null;
this._sig = null;
}

setConfig(config) {
if (!config.entity && !config.speed_entity) {
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();
}

Expand Down Expand Up @@ -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 = '';
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -5192,6 +5204,13 @@
? `<span class="rate l${rate.level}">${rate.label}</span>`
: `${P.esc(fmtRain(intensity, iUnit))} ${P.esc(iUnit)} · <span class="rate l${rate.level}">${rate.label}</span>`;

// 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 = `
<style>
${P.TOKEN_STYLE}
Expand Down Expand Up @@ -5615,11 +5634,13 @@
this.attachShadow({ mode: 'open' });
this._config = null;
this._hass = null;
this._sig = null;
}

setConfig(config) {
if (!config.entity) throw new Error('prism-lux-card: `entity` is required.');
this._config = { animate: true, ...config };
this._sig = null; // force a rebuild on the next render
if (this._hass) this._render();
}

Expand Down Expand Up @@ -5660,6 +5681,13 @@
const value = has ? P.fmtNumber(Math.round(lux), 0) : '—';
const unit = (st && st.attributes.unit_of_measurement) || 'lx';

// Home Assistant pushes `hass` on every state change. Rebuild only when
// the reading changes, so the ray-spin / disc-pulse CSS animations aren't
// restarted from scratch on every unrelated tick.
const sig = JSON.stringify([value, night, band.label, name, unit]);
if (sig === this._sig) return;
this._sig = sig;

// Sun geometry (viewBox 96). Disc + 12 rays scale with the light level.
const CX = 48, CY = 48;
const r = 12 + frac * 6;
Expand Down
9 changes: 9 additions & 0 deletions src/prism-lux-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@
this.attachShadow({ mode: 'open' });
this._config = null;
this._hass = null;
this._sig = null;
}

setConfig(config) {
if (!config.entity) throw new Error('prism-lux-card: `entity` is required.');
this._config = { animate: true, ...config };
this._sig = null; // force a rebuild on the next render
if (this._hass) this._render();
}

Expand Down Expand Up @@ -95,6 +97,13 @@
const value = has ? P.fmtNumber(Math.round(lux), 0) : '—';
const unit = (st && st.attributes.unit_of_measurement) || 'lx';

// Home Assistant pushes `hass` on every state change. Rebuild only when
// the reading changes, so the ray-spin / disc-pulse CSS animations aren't
// restarted from scratch on every unrelated tick.
const sig = JSON.stringify([value, night, band.label, name, unit]);
if (sig === this._sig) return;
this._sig = sig;

// Sun geometry (viewBox 96). Disc + 12 rays scale with the light level.
const CX = 48, CY = 48;
const r = 12 + frac * 6;
Expand Down
9 changes: 9 additions & 0 deletions src/prism-rain-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,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();
}

Expand Down Expand Up @@ -132,6 +134,13 @@
? `<span class="rate l${rate.level}">${rate.label}</span>`
: `${P.esc(fmtRain(intensity, iUnit))} ${P.esc(iUnit)} · <span class="rate l${rate.level}">${rate.label}</span>`;

// 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 = `
<style>
${P.TOKEN_STYLE}
Expand Down
2 changes: 1 addition & 1 deletion src/prism-shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,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.
Expand Down
10 changes: 10 additions & 0 deletions src/prism-wind-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,15 @@
this.attachShadow({ mode: 'open' });
this._config = null;
this._hass = null;
this._sig = null;
}

setConfig(config) {
if (!config.entity && !config.speed_entity) {
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();
}

Expand Down Expand Up @@ -163,6 +165,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 = '';
Expand Down
Loading