diff --git a/custom_components/houseplan/const.py b/custom_components/houseplan/const.py index ddbe6a3..b510f5b 100755 --- a/custom_components/houseplan/const.py +++ b/custom_components/houseplan/const.py @@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory FILES_URL = "/houseplan_files/files" FILES_DIR = "houseplan/files" CONF_ADMIN_ONLY = "admin_only" -VERSION = "1.15.3" +VERSION = "1.15.4" DEFAULT_CONFIG: dict = { "spaces": [], diff --git a/custom_components/houseplan/frontend/houseplan-card.js b/custom_components/houseplan/frontend/houseplan-card.js index 4296743..dfcd2ec 100755 --- a/custom_components/houseplan/frontend/houseplan-card.js +++ b/custom_components/houseplan/frontend/houseplan-card.js @@ -315,10 +315,10 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow } .dev { position: absolute; - box-sizing: border-box; /* рамка 1px не должна смещать центр значка от точки привязки */ + /* центр квадрата (включая рамку 1px) точно на точке привязки: -(size/2 + border) */ width: 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%; background: var(--hp-bg); 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 { --mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62); + display: flex; + align-items: center; + justify-content: center; + line-height: 0; } .dev:active { cursor: grabbing; @@ -1311,4 +1315,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow - `}}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",""); + `}}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",""); diff --git a/custom_components/houseplan/manifest.json b/custom_components/houseplan/manifest.json index 4dd26b3..23c6305 100755 --- a/custom_components/houseplan/manifest.json +++ b/custom_components/houseplan/manifest.json @@ -16,5 +16,5 @@ "issue_tracker": "https://github.com/Matysh/houseplan-card/issues", "requirements": [], "single_config_entry": true, - "version": "1.15.3" + "version": "1.15.4" } diff --git a/demo/smoke_icon_center.mjs b/demo/smoke_icon_center.mjs index 38b3628..c9d4c03 100644 --- a/demo/smoke_icon_center.mjs +++ b/demo/smoke_icon_center.mjs @@ -11,9 +11,15 @@ const bad = await page.evaluate(() => { const ax = lr.left + (parseFloat(d.style.left) / 100) * lr.width; const ay = lr.top + (parseFloat(d.style.top) / 100) * lr.height; const r = d.getBoundingClientRect(); - const offX = r.left + r.width / 2 - ax, offY = r.top + r.height / 2 - ay; - if (Math.abs(offX) > 0.6 || Math.abs(offY) > 0.6) - bad.push({ icon: d.querySelector('ha-icon')?.getAttribute('icon'), offX: +offX.toFixed(2), offY: +offY.toFixed(2) }); + const cx = r.left + r.width / 2, cy = r.top + r.height / 2; + const offX = cx - ax, offY = cy - ay; + // 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; }); diff --git a/demo/srv/assets/houseplan-card.js b/demo/srv/assets/houseplan-card.js index c8b313c..08d8892 100755 --- a/demo/srv/assets/houseplan-card.js +++ b/demo/srv/assets/houseplan-card.js @@ -315,10 +315,10 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow } .dev { position: absolute; - box-sizing: border-box; /* рамка 1px не должна смещать центр значка от точки привязки */ + /* центр квадрата (включая рамку 1px) точно на точке привязки: -(size/2 + border) */ width: 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%; background: var(--hp-bg); 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 { --mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62); + display: flex; + align-items: center; + justify-content: center; + line-height: 0; } .dev:active { cursor: grabbing; @@ -1311,4 +1315,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow - `}}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",""); + `}}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",""); diff --git a/demo/srv/demo.html b/demo/srv/demo.html index 81da0d8..c62eb0d 100644 --- a/demo/srv/demo.html +++ b/demo/srv/demo.html @@ -27,9 +27,13 @@ class HaIcon extends HTMLElement{ _r(){ const p=(window.__ICONS||{})[this.getAttribute('icon')]||''; if(!this.shadowRoot) this.attachShadow({mode:'open'}); - this.shadowRoot.innerHTML=''+ + // Faithful to HA's ha-icon: host is display:block with a large inherited line-height, + // which pushes the (smaller) svg off-centre unless the consumer neutralises it. + this.shadowRoot.innerHTML=''+ ''; } } diff --git a/dist/houseplan-card.js b/dist/houseplan-card.js index 4296743..dfcd2ec 100755 --- a/dist/houseplan-card.js +++ b/dist/houseplan-card.js @@ -315,10 +315,10 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow } .dev { position: absolute; - box-sizing: border-box; /* рамка 1px не должна смещать центр значка от точки привязки */ + /* центр квадрата (включая рамку 1px) точно на точке привязки: -(size/2 + border) */ width: 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%; background: var(--hp-bg); 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 { --mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62); + display: flex; + align-items: center; + justify-content: center; + line-height: 0; } .dev:active { cursor: grabbing; @@ -1311,4 +1315,4 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow - `}}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",""); + `}}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",""); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 16ae9ee..d53f876 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,17 @@ # 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 `` 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) - 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 diff --git a/docs/STATUS.md b/docs/STATUS.md index 0955d33..39b7269 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -13,7 +13,7 @@ | 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 | | 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 | @@ -55,6 +55,9 @@ (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 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 diff --git a/docs/TESTING.md b/docs/TESTING.md index f6e99c3..d229cd4 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -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] - [ ] 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 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 - [ ] Settings persist across reload and other browsers (server-side) diff --git a/package.json b/package.json index 772cbbd..4542f93 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "houseplan-card", - "version": "1.15.3", + "version": "1.15.4", "description": "Interactive house plan Lovelace card for Home Assistant", "license": "MIT", "type": "module", diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts index 3d14bf7..03f0eb6 100755 --- a/src/houseplan-card.ts +++ b/src/houseplan-card.ts @@ -25,7 +25,7 @@ import './editor'; import { cardStyles } from './styles'; 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_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering const LS_ZOOM = 'houseplan_card_zoom_v1'; diff --git a/src/styles.ts b/src/styles.ts index 6acc6bd..cb488e0 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -312,10 +312,10 @@ export const cardStyles = css` } .dev { position: absolute; - box-sizing: border-box; /* рамка 1px не должна смещать центр значка от точки привязки */ + /* центр квадрата (включая рамку 1px) точно на точке привязки: -(size/2 + border) */ width: 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%; background: var(--hp-bg); border: 1px solid var(--hp-line); @@ -331,6 +331,10 @@ export const cardStyles = css` } .dev ha-icon { --mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.62); + display: flex; + align-items: center; + justify-content: center; + line-height: 0; } .dev:active { cursor: grabbing;