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
16 changes: 16 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ 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 — 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
**`ha-icon-picker`** when that element is registered, else falls back to the
native `.tf` text input (same defensive pattern as the v0.16.4 `ha-textfield`
fix — a missing custom element renders invisible). `set hass` / `_rerender`
now propagate `hass` to `ha-icon-picker` too (selector broadened to
`ha-entity-picker, ha-icon-picker`).
- Swapped the 7 `_tf('Icon (mdi:…)', …)` calls for `_iconField(…)` across stat /
power / cover / linear-gauge / switch / light cards + the entities-card row
editor. Config key + card rendering unchanged (still `icon`).
- Verified in Chromium both ways: with `ha-icon-picker` defined → the dropdown
is used and selecting an icon fires `config-changed`; with it undefined →
native `Icon (mdi:…)` input fallback, no errors. `bash build.sh` +
`node test/smoke.js` green. VERSION 0.16.4 → 0.16.5.

### 2026-07-21 — Editor text fields: drop `ha-textfield` for native `<input>` (v0.16.4)
- **User (screenshots):** couldn't change the power card title — the visual
editor was **missing every text field** (Title, Name, Icon, Unit, Decimals,
Expand Down
37 changes: 27 additions & 10 deletions 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.4';
const VERSION = '0.16.5';

// ── Named accent presets ──────────────────────────────────────────
// Selectable in every card editor; a card may also use a raw hex value.
Expand Down Expand Up @@ -431,7 +431,7 @@

set hass(hass) {
this._hass = hass;
this.shadowRoot.querySelectorAll('ha-entity-picker').forEach((p) => {
this.shadowRoot.querySelectorAll('ha-entity-picker, ha-icon-picker').forEach((p) => {
p.hass = hass;
});
}
Expand Down Expand Up @@ -512,6 +512,23 @@
return el;
}

// Icon field. Uses HA's searchable icon picker (`ha-icon-picker`) when that
// element is registered in the frontend, otherwise falls back to a native
// text input — some frontend builds don't load it, and a missing custom
// element would render as an invisible node.
_iconField(value, onChange, label = 'Icon') {
if (customElements.get('ha-icon-picker')) {
const el = document.createElement('ha-icon-picker');
if (this._hass) el.hass = this._hass;
el.value = value ?? '';
el.label = label;
el.style.width = '100%';
el.addEventListener('value-changed', (e) => onChange(e.detail.value));
return el;
}
return this._tf(label + ' (mdi:…)', value, onChange);
}

_select(label, options, value, onChange) {
const wrap = document.createElement('div');
wrap.className = 'field';
Expand Down Expand Up @@ -662,7 +679,7 @@
// Re-render the editor and re-bind hass to freshly created entity pickers.
_rerender() {
this._render();
if (this._hass) this.shadowRoot.querySelectorAll('ha-entity-picker').forEach((p) => { p.hass = this._hass; });
if (this._hass) this.shadowRoot.querySelectorAll('ha-entity-picker, ha-icon-picker').forEach((p) => { p.hass = this._hass; });
}

// Generic dynamic list editor (add / remove / reorder rows).
Expand Down Expand Up @@ -1067,7 +1084,7 @@
this._titleField(),
this._picker('Entity (required)', c.entity, (v) => this._patch('entity', v)),
this._tf('Name (optional)', c.name, (v) => this._patch('name', v)),
this._tf('Icon (mdi:…)', c.icon, (v) => this._patch('icon', v)),
this._iconField(c.icon, (v) => this._patch('icon', v)),
this._tf('Unit override', c.unit, (v) => this._patch('unit', v)),
this._tf('Decimals', c.decimals, (v) => this._patch('decimals', v), { type: 'number' }),
this._accentField(c.accent, (v) => this._patch('accent', v)),
Expand Down Expand Up @@ -1649,7 +1666,7 @@
this._titleField(),
this._picker('Power entity (required)', c.entity, (v) => this._patch('entity', v), { domains: ['sensor'] }),
this._tf('Name (optional)', c.name, (v) => this._patch('name', v)),
this._tf('Icon (mdi:…)', c.icon, (v) => this._patch('icon', v)),
this._iconField(c.icon, (v) => this._patch('icon', v)),
this._tf('Unit override', c.unit, (v) => this._patch('unit', v)),
this._tf('Decimals', c.decimals, (v) => this._patch('decimals', v), { type: 'number' }),
this._accentField(c.accent, (v) => this._patch('accent', v)),
Expand Down Expand Up @@ -2172,7 +2189,7 @@
this._titleField(),
this._picker('Entity (required)', c.entity, (v) => this._patch('entity', v)),
this._tf('Name (optional)', c.name, (v) => this._patch('name', v)),
this._tf('Icon (mdi:…)', c.icon, (v) => this._patch('icon', v)),
this._iconField(c.icon, (v) => this._patch('icon', v)),
this._tf('Unit override', c.unit, (v) => this._patch('unit', v)),
this._tf('Minimum', c.min, (v) => this._patch('min', v), { type: 'number' }),
this._tf('Maximum', c.max, (v) => this._patch('max', v), { type: 'number' }),
Expand Down Expand Up @@ -2446,7 +2463,7 @@
nameRow.append(nameSel, nameTf);

// Icon + secondary source.
const iconTf = this._tf('Icon (mdi:…)', row.icon, (v) => set({ icon: v }));
const iconTf = this._iconField(row.icon, (v) => set({ icon: v }));
const sec = row.secondary || '';
const secIsEntity = sec.indexOf('.') > -1;
const secPicker = this._picker('Secondary entity', secIsEntity ? sec : '', (v) => set({ secondary: v || undefined }));
Expand Down Expand Up @@ -3206,7 +3223,7 @@
this._titleField(),
this._picker('Entity (required)', c.entity, (v) => this._patch('entity', v), { domains: ['switch', 'input_boolean', 'fan', 'light', 'automation', 'script', 'humidifier', 'siren'] }),
this._tf('Name (optional)', c.name, (v) => this._patch('name', v)),
this._tf('Icon (mdi:…)', c.icon, (v) => this._patch('icon', v)),
this._iconField(c.icon, (v) => this._patch('icon', v)),
this._accentField(c.accent, (v) => this._patch('accent', v)),
this._select('Secondary line', [
{ value: '', label: 'None' },
Expand Down Expand Up @@ -3334,7 +3351,7 @@
this._titleField(),
this._picker('Light entity (required)', c.entity, (v) => this._patch('entity', v), { domains: ['light'] }),
this._tf('Name (optional)', c.name, (v) => this._patch('name', v)),
this._tf('Icon (mdi:…)', c.icon, (v) => this._patch('icon', v)),
this._iconField(c.icon, (v) => this._patch('icon', v)),
this._accentField(c.accent, (v) => this._patch('accent', v)),
this._switch('Show brightness slider', c.slider !== false, (v) => this._patch('slider', v)),
this._switch('Use the bulb colour', c.use_color !== false, (v) => this._patch('use_color', v)),
Expand Down Expand Up @@ -3672,7 +3689,7 @@
this._titleField(),
this._picker('Cover entity (required)', c.entity, (v) => this._patch('entity', v), { domains: ['cover'] }),
this._tf('Name (optional)', c.name, (v) => this._patch('name', v)),
this._tf('Icon (mdi:…)', c.icon, (v) => this._patch('icon', v)),
this._iconField(c.icon, (v) => this._patch('icon', v)),
this._accentField(c.accent, (v) => this._patch('accent', v)),
this._switch('Show position slider', c.slider !== false, (v) => this._patch('slider', v)),
this._hint('Buttons call cover.open/close/stop; the slider calls cover.set_cover_position.')
Expand Down
2 changes: 1 addition & 1 deletion src/prism-cover-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
this._titleField(),
this._picker('Cover entity (required)', c.entity, (v) => this._patch('entity', v), { domains: ['cover'] }),
this._tf('Name (optional)', c.name, (v) => this._patch('name', v)),
this._tf('Icon (mdi:…)', c.icon, (v) => this._patch('icon', v)),
this._iconField(c.icon, (v) => this._patch('icon', v)),
this._accentField(c.accent, (v) => this._patch('accent', v)),
this._switch('Show position slider', c.slider !== false, (v) => this._patch('slider', v)),
this._hint('Buttons call cover.open/close/stop; the slider calls cover.set_cover_position.')
Expand Down
2 changes: 1 addition & 1 deletion src/prism-entities-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
nameRow.append(nameSel, nameTf);

// Icon + secondary source.
const iconTf = this._tf('Icon (mdi:…)', row.icon, (v) => set({ icon: v }));
const iconTf = this._iconField(row.icon, (v) => set({ icon: v }));
const sec = row.secondary || '';
const secIsEntity = sec.indexOf('.') > -1;
const secPicker = this._picker('Secondary entity', secIsEntity ? sec : '', (v) => set({ secondary: v || undefined }));
Expand Down
2 changes: 1 addition & 1 deletion src/prism-light-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
this._titleField(),
this._picker('Light entity (required)', c.entity, (v) => this._patch('entity', v), { domains: ['light'] }),
this._tf('Name (optional)', c.name, (v) => this._patch('name', v)),
this._tf('Icon (mdi:…)', c.icon, (v) => this._patch('icon', v)),
this._iconField(c.icon, (v) => this._patch('icon', v)),
this._accentField(c.accent, (v) => this._patch('accent', v)),
this._switch('Show brightness slider', c.slider !== false, (v) => this._patch('slider', v)),
this._switch('Use the bulb colour', c.use_color !== false, (v) => this._patch('use_color', v)),
Expand Down
2 changes: 1 addition & 1 deletion src/prism-linear-gauge-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
this._titleField(),
this._picker('Entity (required)', c.entity, (v) => this._patch('entity', v)),
this._tf('Name (optional)', c.name, (v) => this._patch('name', v)),
this._tf('Icon (mdi:…)', c.icon, (v) => this._patch('icon', v)),
this._iconField(c.icon, (v) => this._patch('icon', v)),
this._tf('Unit override', c.unit, (v) => this._patch('unit', v)),
this._tf('Minimum', c.min, (v) => this._patch('min', v), { type: 'number' }),
this._tf('Maximum', c.max, (v) => this._patch('max', v), { type: 'number' }),
Expand Down
2 changes: 1 addition & 1 deletion src/prism-power-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
this._titleField(),
this._picker('Power entity (required)', c.entity, (v) => this._patch('entity', v), { domains: ['sensor'] }),
this._tf('Name (optional)', c.name, (v) => this._patch('name', v)),
this._tf('Icon (mdi:…)', c.icon, (v) => this._patch('icon', v)),
this._iconField(c.icon, (v) => this._patch('icon', v)),
this._tf('Unit override', c.unit, (v) => this._patch('unit', v)),
this._tf('Decimals', c.decimals, (v) => this._patch('decimals', v), { type: 'number' }),
this._accentField(c.accent, (v) => this._patch('accent', v)),
Expand Down
23 changes: 20 additions & 3 deletions 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.4';
const VERSION = '0.16.5';

// ── Named accent presets ──────────────────────────────────────────
// Selectable in every card editor; a card may also use a raw hex value.
Expand Down Expand Up @@ -427,7 +427,7 @@

set hass(hass) {
this._hass = hass;
this.shadowRoot.querySelectorAll('ha-entity-picker').forEach((p) => {
this.shadowRoot.querySelectorAll('ha-entity-picker, ha-icon-picker').forEach((p) => {
p.hass = hass;
});
}
Expand Down Expand Up @@ -508,6 +508,23 @@
return el;
}

// Icon field. Uses HA's searchable icon picker (`ha-icon-picker`) when that
// element is registered in the frontend, otherwise falls back to a native
// text input — some frontend builds don't load it, and a missing custom
// element would render as an invisible node.
_iconField(value, onChange, label = 'Icon') {
if (customElements.get('ha-icon-picker')) {
const el = document.createElement('ha-icon-picker');
if (this._hass) el.hass = this._hass;
el.value = value ?? '';
el.label = label;
el.style.width = '100%';
el.addEventListener('value-changed', (e) => onChange(e.detail.value));
return el;
}
return this._tf(label + ' (mdi:…)', value, onChange);
}

_select(label, options, value, onChange) {
const wrap = document.createElement('div');
wrap.className = 'field';
Expand Down Expand Up @@ -658,7 +675,7 @@
// Re-render the editor and re-bind hass to freshly created entity pickers.
_rerender() {
this._render();
if (this._hass) this.shadowRoot.querySelectorAll('ha-entity-picker').forEach((p) => { p.hass = this._hass; });
if (this._hass) this.shadowRoot.querySelectorAll('ha-entity-picker, ha-icon-picker').forEach((p) => { p.hass = this._hass; });
}

// Generic dynamic list editor (add / remove / reorder rows).
Expand Down
2 changes: 1 addition & 1 deletion src/prism-stat-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
this._titleField(),
this._picker('Entity (required)', c.entity, (v) => this._patch('entity', v)),
this._tf('Name (optional)', c.name, (v) => this._patch('name', v)),
this._tf('Icon (mdi:…)', c.icon, (v) => this._patch('icon', v)),
this._iconField(c.icon, (v) => this._patch('icon', v)),
this._tf('Unit override', c.unit, (v) => this._patch('unit', v)),
this._tf('Decimals', c.decimals, (v) => this._patch('decimals', v), { type: 'number' }),
this._accentField(c.accent, (v) => this._patch('accent', v)),
Expand Down
2 changes: 1 addition & 1 deletion src/prism-switch-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
this._titleField(),
this._picker('Entity (required)', c.entity, (v) => this._patch('entity', v), { domains: ['switch', 'input_boolean', 'fan', 'light', 'automation', 'script', 'humidifier', 'siren'] }),
this._tf('Name (optional)', c.name, (v) => this._patch('name', v)),
this._tf('Icon (mdi:…)', c.icon, (v) => this._patch('icon', v)),
this._iconField(c.icon, (v) => this._patch('icon', v)),
this._accentField(c.accent, (v) => this._patch('accent', v)),
this._select('Secondary line', [
{ value: '', label: 'None' },
Expand Down
Loading