fix v1.15.4: device icon glyph centering — real ha-icon is block+line-height (glyph sat ~1.8px low); .dev ha-icon now zero-line-height flex. Revert v1.15.3 border-box (shrank badge); anchor centered via margin. Demo stub made faithful; smoke guards glyph-in-badge. Verified live.

This commit is contained in:
Matysh
2026-07-08 17:44:41 +03:00
parent 927e7d7054
commit cc27a6b2fa
13 changed files with 64 additions and 22 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
FILES_URL = "/houseplan_files/files" FILES_URL = "/houseplan_files/files"
FILES_DIR = "houseplan/files" FILES_DIR = "houseplan/files"
CONF_ADMIN_ONLY = "admin_only" CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.15.3" VERSION = "1.15.4"
DEFAULT_CONFIG: dict = { DEFAULT_CONFIG: dict = {
"spaces": [], "spaces": [],
@@ -315,10 +315,10 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
} }
.dev { .dev {
position: absolute; position: absolute;
box-sizing: border-box; /* рамка 1px не должна смещать центр значка от точки привязки */ /* центр квадрата (включая рамку 1px) точно на точке привязки: -(size/2 + border) */
width: var(--icon-size, 2.5cqw); width: var(--icon-size, 2.5cqw);
height: var(--icon-size, 2.5cqw); height: var(--icon-size, 2.5cqw);
margin: calc(var(--icon-size, 2.5cqw) / -2) 0 0 calc(var(--icon-size, 2.5cqw) / -2); margin: calc(var(--icon-size, 2.5cqw) / -2 - 1px) 0 0 calc(var(--icon-size, 2.5cqw) / -2 - 1px);
border-radius: 22%; border-radius: 22%;
background: var(--hp-bg); background: var(--hp-bg);
border: 1px solid var(--hp-line); border: 1px solid var(--hp-line);
@@ -334,6 +334,10 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
} }
.dev ha-icon { .dev ha-icon {
--mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62); --mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62);
display: flex;
align-items: center;
justify-content: center;
line-height: 0;
} }
.dev:active { .dev:active {
cursor: grabbing; cursor: grabbing;
@@ -1311,4 +1315,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
</button> </button>
</div> </div>
</div> </div>
</div>`}}Yt.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_markup:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Yt.styles=Lt,customElements.get("houseplan-card")||customElements.define("houseplan-card",Yt),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.15.3 ","background:#3ea6ff;color:#04121f;font-weight:700",""); </div>`}}Yt.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_markup:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Yt.styles=Lt,customElements.get("houseplan-card")||customElements.define("houseplan-card",Yt),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.15.4 ","background:#3ea6ff;color:#04121f;font-weight:700","");
+1 -1
View File
@@ -16,5 +16,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues", "issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [], "requirements": [],
"single_config_entry": true, "single_config_entry": true,
"version": "1.15.3" "version": "1.15.4"
} }
+9 -3
View File
@@ -11,9 +11,15 @@ const bad = await page.evaluate(() => {
const ax = lr.left + (parseFloat(d.style.left) / 100) * lr.width; const ax = lr.left + (parseFloat(d.style.left) / 100) * lr.width;
const ay = lr.top + (parseFloat(d.style.top) / 100) * lr.height; const ay = lr.top + (parseFloat(d.style.top) / 100) * lr.height;
const r = d.getBoundingClientRect(); const r = d.getBoundingClientRect();
const offX = r.left + r.width / 2 - ax, offY = r.top + r.height / 2 - ay; const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
if (Math.abs(offX) > 0.6 || Math.abs(offY) > 0.6) const offX = cx - ax, offY = cy - ay;
bad.push({ icon: d.querySelector('ha-icon')?.getAttribute('icon'), offX: +offX.toFixed(2), offY: +offY.toFixed(2) }); // glyph centred within the badge
const icon = d.querySelector('ha-icon');
const svg = icon?.shadowRoot?.querySelector('svg') || icon?.shadowRoot?.querySelector('ha-svg-icon')?.shadowRoot?.querySelector('svg');
let gX = 0, gY = 0;
if (svg) { const sr = svg.getBoundingClientRect(); gX = sr.left + sr.width / 2 - cx; gY = sr.top + sr.height / 2 - cy; }
if (Math.abs(offX) > 0.6 || Math.abs(offY) > 0.6 || Math.abs(gX) > 0.6 || Math.abs(gY) > 0.6)
bad.push({ icon: icon?.getAttribute('icon'), offX: +offX.toFixed(2), offY: +offY.toFixed(2), glyphX: +gX.toFixed(2), glyphY: +gY.toFixed(2) });
} }
return bad; return bad;
}); });
+7 -3
View File
@@ -315,10 +315,10 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
} }
.dev { .dev {
position: absolute; position: absolute;
box-sizing: border-box; /* рамка 1px не должна смещать центр значка от точки привязки */ /* центр квадрата (включая рамку 1px) точно на точке привязки: -(size/2 + border) */
width: var(--icon-size, 2.5cqw); width: var(--icon-size, 2.5cqw);
height: var(--icon-size, 2.5cqw); height: var(--icon-size, 2.5cqw);
margin: calc(var(--icon-size, 2.5cqw) / -2) 0 0 calc(var(--icon-size, 2.5cqw) / -2); margin: calc(var(--icon-size, 2.5cqw) / -2 - 1px) 0 0 calc(var(--icon-size, 2.5cqw) / -2 - 1px);
border-radius: 22%; border-radius: 22%;
background: var(--hp-bg); background: var(--hp-bg);
border: 1px solid var(--hp-line); border: 1px solid var(--hp-line);
@@ -334,6 +334,10 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
} }
.dev ha-icon { .dev ha-icon {
--mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62); --mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62);
display: flex;
align-items: center;
justify-content: center;
line-height: 0;
} }
.dev:active { .dev:active {
cursor: grabbing; cursor: grabbing;
@@ -1311,4 +1315,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
</button> </button>
</div> </div>
</div> </div>
</div>`}}Yt.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_markup:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Yt.styles=Lt,customElements.get("houseplan-card")||customElements.define("houseplan-card",Yt),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.15.2 ","background:#3ea6ff;color:#04121f;font-weight:700",""); </div>`}}Yt.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_markup:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Yt.styles=Lt,customElements.get("houseplan-card")||customElements.define("houseplan-card",Yt),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.15.3 ","background:#3ea6ff;color:#04121f;font-weight:700","");
+7 -3
View File
@@ -27,9 +27,13 @@ class HaIcon extends HTMLElement{
_r(){ _r(){
const p=(window.__ICONS||{})[this.getAttribute('icon')]||''; const p=(window.__ICONS||{})[this.getAttribute('icon')]||'';
if(!this.shadowRoot) this.attachShadow({mode:'open'}); if(!this.shadowRoot) this.attachShadow({mode:'open'});
this.shadowRoot.innerHTML='<style>:host{display:inline-flex;align-items:center;justify-content:center;'+ // Faithful to HA's ha-icon: host is display:block with a large inherited line-height,
'width:var(--mdc-icon-size,24px);height:var(--mdc-icon-size,24px);vertical-align:middle}'+ // which pushes the (smaller) svg off-centre unless the consumer neutralises it.
'svg{width:100%;height:100%;fill:currentColor;display:block}</style>'+ this.shadowRoot.innerHTML='<style>:host{display:block;line-height:1.8;'+
'width:var(--mdc-icon-size,24px);height:var(--mdc-icon-size,24px);'+
'fill:currentColor;vertical-align:middle}'+
'svg{width:var(--mdc-icon-size,24px);height:var(--mdc-icon-size,24px);fill:currentColor;'+
'display:inline-block;vertical-align:baseline}</style>'+
'<svg viewBox="0 0 24 24"><path d="'+p+'"></path></svg>'; '<svg viewBox="0 0 24 24"><path d="'+p+'"></path></svg>';
} }
} }
+7 -3
View File
@@ -315,10 +315,10 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
} }
.dev { .dev {
position: absolute; position: absolute;
box-sizing: border-box; /* рамка 1px не должна смещать центр значка от точки привязки */ /* центр квадрата (включая рамку 1px) точно на точке привязки: -(size/2 + border) */
width: var(--icon-size, 2.5cqw); width: var(--icon-size, 2.5cqw);
height: var(--icon-size, 2.5cqw); height: var(--icon-size, 2.5cqw);
margin: calc(var(--icon-size, 2.5cqw) / -2) 0 0 calc(var(--icon-size, 2.5cqw) / -2); margin: calc(var(--icon-size, 2.5cqw) / -2 - 1px) 0 0 calc(var(--icon-size, 2.5cqw) / -2 - 1px);
border-radius: 22%; border-radius: 22%;
background: var(--hp-bg); background: var(--hp-bg);
border: 1px solid var(--hp-line); border: 1px solid var(--hp-line);
@@ -334,6 +334,10 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
} }
.dev ha-icon { .dev ha-icon {
--mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62); --mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62);
display: flex;
align-items: center;
justify-content: center;
line-height: 0;
} }
.dev:active { .dev:active {
cursor: grabbing; cursor: grabbing;
@@ -1311,4 +1315,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
</button> </button>
</div> </div>
</div> </div>
</div>`}}Yt.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_markup:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Yt.styles=Lt,customElements.get("houseplan-card")||customElements.define("houseplan-card",Yt),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.15.3 ","background:#3ea6ff;color:#04121f;font-weight:700",""); </div>`}}Yt.properties={hass:{attribute:!1},_config:{state:!0},_space:{state:!0},_layout:{state:!0},_devices:{state:!0},_tip:{state:!0},_selId:{state:!0},_toast:{state:!0},_serverCfg:{state:!0},_markup:{state:!0},_tool:{state:!0},_path:{state:!0},_cursorPt:{state:!0},_areaSel:{state:!0},_nameSel:{state:!0},_roomDialog:{state:!0},_spaceDialog:{state:!0},_infoCard:{state:!0},_rulesDialog:{state:!0},_importDialog:{state:!0},_markerDialog:{state:!0},_zoom:{state:!0},_view:{state:!0}},Yt.styles=Lt,customElements.get("houseplan-card")||customElements.define("houseplan-card",Yt),window.customCards=window.customCards||[],window.customCards.find(t=>"houseplan-card"===t.type)||window.customCards.push({type:"houseplan-card",name:"House Plan Card",description:"Interactive house plan: spaces, rooms and devices with live states and drag layout."}),console.info("%c HOUSEPLAN-CARD %c v1.15.4 ","background:#3ea6ff;color:#04121f;font-weight:700","");
+12
View File
@@ -1,5 +1,17 @@
# Changelog # Changelog
## v1.15.4 — 2026-07-08 (fix: device icon vertical centering — proper root cause)
- **Root cause of the off-centre icon, found in the live app** (the demo stub hid it):
HA's real `<ha-icon>` host is `display:block` with a large inherited `line-height`
(~22 px for a ~12 px glyph), so the SVG sat ~1.8 px **below** the badge centre.
Fix: `.dev ha-icon` is now a zero-line-height flex box — the glyph centres exactly.
- Reverted the v1.15.3 `box-sizing: border-box` (it shrank the badge by 2 px and made the
vertical offset *more* visible — "worse"). The 1 px anchor drift is instead corrected by
the centering margin (`-(size/2 + 1px)`), keeping the original badge size.
- Verified in the real dashboard: anchor offset 0, glyph offset 0, badge size unchanged.
- Demo `ha-icon` stub made faithful to HA (block + line-height) so `smoke_icon_center`
now actually reproduces and guards this; the smoke also asserts glyph-in-badge centering.
## v1.15.3 — 2026-07-08 (fix: device icon 1px off its anchor point) ## v1.15.3 — 2026-07-08 (fix: device icon 1px off its anchor point)
- Device icon badges were sitting **1 px down-and-right of their true point**: `.dev` - Device icon badges were sitting **1 px down-and-right of their true point**: `.dev`
used the default `content-box`, so the 1 px border made the rendered square 2 px used the default `content-box`, so the 1 px border made the rendered square 2 px
+4 -1
View File
@@ -13,7 +13,7 @@
| Item | State | | Item | State |
|---|---| |---|---|
| Version | **v1.15.3** everywhere (manifest, const.py, package.json, CARD_VERSION) | | Version | **v1.15.4** everywhere (manifest, const.py, package.json, CARD_VERSION) |
| GitHub | https://github.com/Matysh/houseplan-card — branch `main`, releases v1.9.3…v1.11.2 | | GitHub | https://github.com/Matysh/houseplan-card — branch `main`, releases v1.9.3…v1.11.2 |
| CI | `.github/workflows/validate.yml` (hacs + hassfest + frontend + backend) — **fully green** since v1.11.1; `release.yml` auto-attaches the card bundle (needs `permissions: contents: write`, fixed) | | CI | `.github/workflows/validate.yml` (hacs + hassfest + frontend + backend) — **fully green** since v1.11.1; `release.yml` auto-attaches the card bundle (needs `permissions: contents: write`, fixed) |
| HACS | Works as custom repository (id 1290210112 on the home instance). **Inclusion PR: https://github.com/hacs/default/pull/9004** (queue ≈2 months as of 2026-07). Lesson: #8995 was auto-closed by hacs-bot — the PR body MUST be their exact template with every checkbox ticked and all 3 links (release, HACS action run, hassfest run); a custom body gets closed without discussion | | HACS | Works as custom repository (id 1290210112 on the home instance). **Inclusion PR: https://github.com/hacs/default/pull/9004** (queue ≈2 months as of 2026-07). Lesson: #8995 was auto-closed by hacs-bot — the PR body MUST be their exact template with every checkbox ticked and all 3 links (release, HACS action run, hassfest run); a custom body gets closed without discussion |
@@ -55,6 +55,9 @@
(fridges/TRVs/chip `*_device_temperature`/diagnostic); now thermometer/air-monitor only. (fridges/TRVs/chip `*_device_temperature`/diagnostic); now thermometer/air-monitor only.
- **v1.15.3** — fix: device icon badge sat 1 px off its anchor (content-box + 1 px - **v1.15.3** — fix: device icon badge sat 1 px off its anchor (content-box + 1 px
border); `box-sizing: border-box` centres it exactly on the device point. border); `box-sizing: border-box` centres it exactly on the device point.
- **v1.15.4** — fix: real `ha-icon` (block + big line-height) put the glyph ~1.8 px low;
`.dev ha-icon` is now a zero-line-height flex box. Reverted v1.15.3 border-box (shrank the
badge). Verified live. Demo stub made faithful so the smoke guards it.
## Where things live ## Where things live
+1
View File
@@ -57,6 +57,7 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
smart-plug chip temperatures (`*_device_temperature`) and diagnostic-category temps are excluded [auto] smart-plug chip temperatures (`*_device_temperature`) and diagnostic-category temps are excluded [auto]
- [ ] Space dialog is 500 px wide; the comfort-bounds inputs are compact (56 px) - [ ] Space dialog is 500 px wide; the comfort-bounds inputs are compact (56 px)
- [ ] Device icon badge is centred exactly on its point (no 1 px down-right drift) [auto] - [ ] Device icon badge is centred exactly on its point (no 1 px down-right drift) [auto]
- [ ] Device glyph is centred within its badge (no vertical drift — real ha-icon is block+line-height) [auto]
- [ ] Room hover highlight still works when custom borders/fills are on - [ ] Room hover highlight still works when custom borders/fills are on
- [ ] Settings persist across reload and other browsers (server-side) - [ ] Settings persist across reload and other browsers (server-side)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "houseplan-card", "name": "houseplan-card",
"version": "1.15.3", "version": "1.15.4",
"description": "Interactive house plan Lovelace card for Home Assistant", "description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
+1 -1
View File
@@ -25,7 +25,7 @@ import './editor';
import { cardStyles } from './styles'; import { cardStyles } from './styles';
import { langOf, t, type I18nKey } from './i18n'; import { langOf, t, type I18nKey } from './i18n';
const CARD_VERSION = '1.15.3'; const CARD_VERSION = '1.15.4';
const LS_KEY = 'houseplan_card_layout_v1'; const LS_KEY = 'houseplan_card_layout_v1';
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
const LS_ZOOM = 'houseplan_card_zoom_v1'; const LS_ZOOM = 'houseplan_card_zoom_v1';
+6 -2
View File
@@ -312,10 +312,10 @@ export const cardStyles = css`
} }
.dev { .dev {
position: absolute; position: absolute;
box-sizing: border-box; /* рамка 1px не должна смещать центр значка от точки привязки */ /* центр квадрата (включая рамку 1px) точно на точке привязки: -(size/2 + border) */
width: var(--icon-size, 2.5cqw); width: var(--icon-size, 2.5cqw);
height: var(--icon-size, 2.5cqw); height: var(--icon-size, 2.5cqw);
margin: calc(var(--icon-size, 2.5cqw) / -2) 0 0 calc(var(--icon-size, 2.5cqw) / -2); margin: calc(var(--icon-size, 2.5cqw) / -2 - 1px) 0 0 calc(var(--icon-size, 2.5cqw) / -2 - 1px);
border-radius: 22%; border-radius: 22%;
background: var(--hp-bg); background: var(--hp-bg);
border: 1px solid var(--hp-line); border: 1px solid var(--hp-line);
@@ -331,6 +331,10 @@ export const cardStyles = css`
} }
.dev ha-icon { .dev ha-icon {
--mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62); --mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62);
display: flex;
align-items: center;
justify-content: center;
line-height: 0;
} }
.dev:active { .dev:active {
cursor: grabbing; cursor: grabbing;