From d732ed6ab68eca5268577c06a5a7dcce28323cfd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 23:13:58 +0000 Subject: [PATCH] =?UTF-8?q?Editor=20icon=20fields:=20use=20HA=20icon-picke?= =?UTF-8?q?r=20dropdown=20with=20native=20fallback=20=E2=80=94=20v0.16.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a shared _iconField() helper on PrismEditor that renders HA's searchable ha-icon-picker when that element is registered in the frontend, falling back to the native text input otherwise (same defensive pattern as the ha-textfield fix — a missing custom element would render invisible). set hass / _rerender now also propagate hass to ha-icon-picker. Switch the seven "Icon (mdi:…)" text fields (stat, power, cover, linear-gauge, switch, light cards + the entities-card row editor) to _iconField. The `icon` config key and card rendering are unchanged. Verified in Chromium both ways: with ha-icon-picker defined the dropdown is used and picking an icon fires config-changed; without it the native input fallback renders. VERSION 0.16.4 -> 0.16.5. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014cpNPVU8j1iiU9CnXgr5JY --- CLAUDE.md | 16 +++++++++++++++ prism.js | 37 +++++++++++++++++++++++++--------- src/prism-cover-card.js | 2 +- src/prism-entities-card.js | 2 +- src/prism-light-card.js | 2 +- src/prism-linear-gauge-card.js | 2 +- src/prism-power-card.js | 2 +- src/prism-shared.js | 23 ++++++++++++++++++--- src/prism-stat-card.js | 2 +- src/prism-switch-card.js | 2 +- 10 files changed, 70 insertions(+), 20 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 86afce9..15611d6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `` (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, diff --git a/prism.js b/prism.js index 357b778..e74d082 100644 --- a/prism.js +++ b/prism.js @@ -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. @@ -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; }); } @@ -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'; @@ -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). @@ -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)), @@ -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)), @@ -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' }), @@ -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 })); @@ -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' }, @@ -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)), @@ -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.') diff --git a/src/prism-cover-card.js b/src/prism-cover-card.js index b4b6efe..c0d2e06 100644 --- a/src/prism-cover-card.js +++ b/src/prism-cover-card.js @@ -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.') diff --git a/src/prism-entities-card.js b/src/prism-entities-card.js index d574b4c..7e4e855 100644 --- a/src/prism-entities-card.js +++ b/src/prism-entities-card.js @@ -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 })); diff --git a/src/prism-light-card.js b/src/prism-light-card.js index 88a7d31..057c9ce 100644 --- a/src/prism-light-card.js +++ b/src/prism-light-card.js @@ -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)), diff --git a/src/prism-linear-gauge-card.js b/src/prism-linear-gauge-card.js index 4b77d98..cc3358a 100644 --- a/src/prism-linear-gauge-card.js +++ b/src/prism-linear-gauge-card.js @@ -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' }), diff --git a/src/prism-power-card.js b/src/prism-power-card.js index 93d4473..1570bd6 100644 --- a/src/prism-power-card.js +++ b/src/prism-power-card.js @@ -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)), diff --git a/src/prism-shared.js b/src/prism-shared.js index 535f69d..059efc5 100644 --- a/src/prism-shared.js +++ b/src/prism-shared.js @@ -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. @@ -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; }); } @@ -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'; @@ -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). diff --git a/src/prism-stat-card.js b/src/prism-stat-card.js index 5ab1e68..d4363b0 100644 --- a/src/prism-stat-card.js +++ b/src/prism-stat-card.js @@ -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)), diff --git a/src/prism-switch-card.js b/src/prism-switch-card.js index 2ac3766..7ca5b0f 100644 --- a/src/prism-switch-card.js +++ b/src/prism-switch-card.js @@ -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' },