Compare commits

...
Author SHA1 Message Date
claude[bot] cc09e8f827 docs: review document for #426
Проверка (CI) / Классификация изменённых файлов (push) Successful in 24s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Successful in 50s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 45s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 17s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 24s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 13m24s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 6m56s
Issue: #426
User-Visible: no
2026-09-03 06:38:29 +00:00
Sergey Matyunin a6eb3d6cfa test: accept room tooltip settings golden
Issue: #426
User-Visible: no
Release: v1.71.0-beta.2
Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33722471494
2026-09-03 09:25:14 +03:00
Sergey Matyunin 223951c042 chore: refresh config schema manifest
Issue: #426
User-Visible: no
2026-09-03 09:16:22 +03:00
Sergey Matyunin 9d1e5fdbba docs: refresh screenshot source fingerprint
Issue: #426
User-Visible: no
2026-09-03 09:12:50 +03:00
Sergey Matyunin 8013f7d052 feat: add room hover tooltip setting
Issue: #426
User-Visible: yes
2026-09-03 09:06:57 +03:00
claude[bot] ac99a2ff08 docs: review document for #426
Issue: #426
User-Visible: no
2026-09-03 05:52:52 +00:00
Sergey Matyunin 587c20699c docs: specify room hover tooltip toggle
Issue: #426
User-Visible: no
2026-09-03 08:47:07 +03:00
59 changed files with 1602 additions and 631 deletions
@@ -1,125 +1,125 @@
{
"schema": 1,
"fingerprint": "816ff9c893e23b7441e73210f85024741ff6c5bf8dabd2fb0df9e8b8055bc693",
"fingerprint": "b69bdfe3df225cf1b96fea0159a03a620e38cb94ec6106f9ee3b0e57cdefd183",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-Dwkibvgo.js",
"houseplan-assets/houseplan-card-CxS6HLVV.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 291031,
"initialViewGzipBytes": 290942,
"lazyFiles": [
"houseplan-assets/backdrop-pick-DWD0ZQTc.js",
"houseplan-assets/de-DJX-vLW1.js",
"houseplan-assets/editor-Dvui5H3b.js",
"houseplan-assets/fr-DVmfM6Kw.js",
"houseplan-assets/houseplan-editor-runtime-9AEP51ly.js",
"houseplan-assets/houseplan-onboarding-runtime-CnVK5oOC.js"
"houseplan-assets/backdrop-pick-I85bKcdl.js",
"houseplan-assets/de-CDRfP4mw.js",
"houseplan-assets/editor-Bze_fgW0.js",
"houseplan-assets/fr-B64rhazj.js",
"houseplan-assets/houseplan-editor-runtime-X23eakN9.js",
"houseplan-assets/houseplan-onboarding-runtime-C3A8efgN.js"
],
"lazyGzipBytes": 214972,
"lazyGzipBytes": 215441,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-DWD0ZQTc.js",
"houseplan-assets/editor-Dvui5H3b.js",
"houseplan-assets/houseplan-editor-runtime-9AEP51ly.js"
"houseplan-assets/backdrop-pick-I85bKcdl.js",
"houseplan-assets/editor-Bze_fgW0.js",
"houseplan-assets/houseplan-editor-runtime-X23eakN9.js"
],
"lazyEditorGzipBytes": 159157,
"lazyEditorGzipBytes": 159770,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-DWD0ZQTc.js",
"houseplan-assets/houseplan-onboarding-runtime-CnVK5oOC.js"
"houseplan-assets/backdrop-pick-I85bKcdl.js",
"houseplan-assets/houseplan-onboarding-runtime-C3A8efgN.js"
],
"lazyOnboardingGzipBytes": 14132,
"lazyOnboardingGzipBytes": 14129,
"lazyLocaleFiles": [
"houseplan-assets/de-DJX-vLW1.js",
"houseplan-assets/fr-DVmfM6Kw.js"
"houseplan-assets/de-CDRfP4mw.js",
"houseplan-assets/fr-B64rhazj.js"
],
"lazyLocaleGzipBytes": 48859,
"lazyLocaleGzipBytes": 48717,
"files": [
{
"path": "houseplan-assets/backdrop-pick-DWD0ZQTc.js",
"sha256": "455450cbc7b74f052fb112680af9ba4be145af8c3a1061907ec541b63957f22f",
"path": "houseplan-assets/backdrop-pick-I85bKcdl.js",
"sha256": "7becca609cbb95b1ba278743fce5b3e93c7adf8a7bd9107783931f29279ac5df",
"rawBytes": 20974,
"gzipBytes": 7176,
"gzipBytes": 7175,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dwkibvgo.js"
"houseplan-assets/houseplan-card-CxS6HLVV.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-DJX-vLW1.js",
"sha256": "73d198680c17602addf41855748533ed47233e81e9bdc02a786034aeac0e4e63",
"rawBytes": 81798,
"gzipBytes": 24701,
"path": "houseplan-assets/de-CDRfP4mw.js",
"sha256": "2f90acc29e659e771dea42290b74ceb54d8ab9a7fae9cfdbd7ce6bc4bda0b70b",
"rawBytes": 81632,
"gzipBytes": 24630,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-Dvui5H3b.js",
"sha256": "11578ded60410e4e879c9342ee74c8a0968477ea2bae98eb18480a0ad02d303c",
"path": "houseplan-assets/editor-Bze_fgW0.js",
"sha256": "2e2513efca4fa8d9019c3601693fb3ff3b9e050ba3fe054b190f81d286540dda",
"rawBytes": 3826,
"gzipBytes": 1580,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dwkibvgo.js"
"houseplan-assets/houseplan-card-CxS6HLVV.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-DVmfM6Kw.js",
"sha256": "167bed2822ce7e2b38d47e8dc1d73648d10c04d469979bf05e414000b53487b4",
"rawBytes": 83973,
"gzipBytes": 24158,
"path": "houseplan-assets/fr-B64rhazj.js",
"sha256": "6a1a18220938a8d4631586a9dc7f8ef8243b77a23089777d0917975300960a82",
"rawBytes": 83754,
"gzipBytes": 24087,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-Dwkibvgo.js",
"sha256": "d16deb0fc6f591040a2f97d51102e3e355da35cb3d1b69aa79472406c28d3ed2",
"rawBytes": 1033612,
"gzipBytes": 290234,
"path": "houseplan-assets/houseplan-card-CxS6HLVV.js",
"sha256": "1607bc6eeb275b340391ff10bdb7c2c5f62e59d5d2a43adbdb412698a0a024d1",
"rawBytes": 1032923,
"gzipBytes": 290148,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-DJX-vLW1.js",
"houseplan-assets/editor-Dvui5H3b.js",
"houseplan-assets/fr-DVmfM6Kw.js",
"houseplan-assets/houseplan-editor-runtime-9AEP51ly.js",
"houseplan-assets/houseplan-onboarding-runtime-CnVK5oOC.js"
"houseplan-assets/de-CDRfP4mw.js",
"houseplan-assets/editor-Bze_fgW0.js",
"houseplan-assets/fr-B64rhazj.js",
"houseplan-assets/houseplan-editor-runtime-X23eakN9.js",
"houseplan-assets/houseplan-onboarding-runtime-C3A8efgN.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-9AEP51ly.js",
"sha256": "c1b8f90458afedddbbdde98570c517d6e66dfcee08ad4764b1ac2473bdf89fb4",
"rawBytes": 574225,
"gzipBytes": 150401,
"path": "houseplan-assets/houseplan-editor-runtime-X23eakN9.js",
"sha256": "53f256fffc8e6f02618e56dbf56f25258356c2ca221ee9918ccbc57d9e590f6b",
"rawBytes": 575918,
"gzipBytes": 151015,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-DWD0ZQTc.js",
"houseplan-assets/houseplan-card-Dwkibvgo.js"
"houseplan-assets/backdrop-pick-I85bKcdl.js",
"houseplan-assets/houseplan-card-CxS6HLVV.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-CnVK5oOC.js",
"sha256": "bd169896116229e76b37243106bde64755589017639890d36dd19282c872db7a",
"path": "houseplan-assets/houseplan-onboarding-runtime-C3A8efgN.js",
"sha256": "a5433ef2715f424016b31a2ae3c3c30f2cff05549230fd0ab16db3159834de3f",
"rawBytes": 28088,
"gzipBytes": 6956,
"gzipBytes": 6954,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-DWD0ZQTc.js",
"houseplan-assets/houseplan-card-Dwkibvgo.js"
"houseplan-assets/backdrop-pick-I85bKcdl.js",
"houseplan-assets/houseplan-card-CxS6HLVV.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "730163666440793e44be0ee5482f7080931370f94555a1d52072d3a54698447e",
"sha256": "f9d93e5aa355fc91dc8fad57e7669a0ac663e8b538f42ea671a1a319fe214c3e",
"rawBytes": 1183,
"gzipBytes": 797,
"gzipBytes": 794,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-Dwkibvgo.js"
"houseplan-assets/houseplan-card-CxS6HLVV.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="816ff9c893e23b7441e73210f85024741ff6c5bf8dabd2fb0df9e8b8055bc693";import{b as e,l as o,t,df as s,A as a,dg as i,dh as l,di as n,E as r,c}from"./houseplan-card-Dwkibvgo.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b69bdfe3df225cf1b96fea0159a03a620e38cb94ec6106f9ee3b0e57cdefd183";import{b as e,l as o,t,dg as s,A as a,dh as i,di as l,dj as n,E as r,c}from"./houseplan-card-CxS6HLVV.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>f[e.name]||e.name}
.computeLabel=${e=>h[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(h.slice(0,3))}
${d(f.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${d(h.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="816ff9c893e23b7441e73210f85024741ff6c5bf8dabd2fb0df9e8b8055bc693";try{await import("./houseplan-assets/houseplan-card-Dwkibvgo.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b69bdfe3df225cf1b96fea0159a03a620e38cb94ec6106f9ee3b0e57cdefd183";try{await import("./houseplan-assets/houseplan-card-CxS6HLVV.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
@@ -140,6 +140,9 @@ def _global_settings(value: object) -> dict[str, Any]:
out = _copy_keys(value, ("glow_radius_cm", "bg_color", "north_deg", "bg_mode", "sun_rays"))
if not isinstance(value, dict):
return out
show_room_tooltip = value.get("show_room_tooltip")
if isinstance(show_room_tooltip, bool):
out["show_room_tooltip"] = show_room_tooltip
fill_colors = value.get("fill_colors")
if isinstance(fill_colors, dict):
out["fill_colors"] = {
@@ -1935,6 +1935,7 @@ CONFIG_SCHEMA = vol.All(
vol.Optional("north_deg"): _north_deg,
vol.Optional("bg_mode"): _BG_MODE,
vol.Optional("sun_rays"): bool,
vol.Optional("show_room_tooltip"): bool,
# Removed from the UI/runtime in 2026-08-08. Keep accepting the
# legacy field so an existing stored config can still load; the
# frontend ignores it and removes it on the next settings save.
+4 -4
View File
@@ -1,11 +1,11 @@
{
"schema": 1,
"matrixVersion": 54,
"acceptedAt": "2026-09-02T05:20:33.824Z",
"sourceFingerprint": "baf59953cfd6eb3543f7da5af21787b20dc76f136b74782ae917f811d2fc1ab5",
"acceptedAt": "2026-09-03T06:25:02.855Z",
"sourceFingerprint": "b69bdfe3df225cf1b96fea0159a03a620e38cb94ec6106f9ee3b0e57cdefd183",
"chromium": "151.0.7922.34",
"witnesses": {
"count": 90,
"count": 132,
"floor": 10
},
"scenarios": {
@@ -142,7 +142,7 @@
"device-help-popover-light-ru": "c2146f13d03841c395b0772f1a21de358c62a8703d3d02e7d026fac8ae29955e",
"decor-color-popover-mobile-ru": "c65318327c6e4631c769419e791a749d667b4dfc36f2e3fc46d6fac53180cf1b",
"decor-color-popover-desktop-en": "60de64b1bc072327cb88d2e876732d50d5cb7441c850f52b7af6f4ed5c6de22b",
"general-color-popover-desktop-en": "762c20c3d0499c1ea444e5d980eec4e78c7112455589b5202010f53a8b51099c",
"general-color-popover-desktop-en": "5b47ab921f889c9ad41cab195e6efeed283f014d37f5377714b54ef30282ebf6",
"settings-help-zoom-200-en-light": "bb1192a52bab5ebc6984a5d799a32d05f9ce374ce6b493f5f618b639eedce8d5",
"settings-help-zoom-200-ru-dark": "13e7fdd19017c38a5659394e622ff54ac8a426f9cb6e85b3248864767aa9f42b",
"support-desktop-empty-light-en": "fecabb63521f258acc66ecdf7a438db10c73d0eac39618c55fb51287080f01f5",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

+84
View File
@@ -0,0 +1,84 @@
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const result = await page.evaluate(async () => {
const card = window.__card;
const root = () => card.shadowRoot || card.renderRoot;
const room = () => root().querySelector('.room');
const mouse = (type, x, y) => new PointerEvent(type, {
pointerType: 'mouse', bubbles: true, composed: true, clientX: x, clientY: y,
});
const out = {};
card._openSettingsDialog();
await card.updateComplete;
const settingRow = [...root().querySelectorAll('hp-dialog label.srcrow')]
.find((label) => label.textContent.trim() === 'Show the room information window on hover');
out.localizedControl = !!settingRow?.querySelector('ha-switch,input[type="checkbox"]');
out.defaultOn = card._settingsDialog?.showRoomTooltip === true;
card._settingsDialog = { ...card._settingsDialog, showRoomTooltip: false };
card._settingsDialog = null;
out.cancelKeepsAbsentDefault = !Object.hasOwn(card._serverCfg.settings, 'show_room_tooltip');
room().dispatchEvent(mouse('pointerenter', 180, 180));
room().dispatchEvent(mouse('pointermove', 180, 180));
await card.updateComplete;
out.defaultRoomTip = card._tip?.room === true && !!root().querySelector('.tip');
card._openSettingsDialog();
card._settingsDialog = { ...card._settingsDialog, showRoomTooltip: false };
await card._saveSettingsDialog();
await card.updateComplete;
out.falsePersists = card._serverCfg.settings.show_room_tooltip === false;
out.visibleRoomTipCleared = card._tip === null && !root().querySelector('.tip');
let roomAreaCalls = 0;
const originalRoomArea = card._roomArea.bind(card);
card._roomArea = (...args) => {
roomAreaCalls += 1;
return originalRoomArea(...args);
};
room().dispatchEvent(mouse('pointerenter', 190, 190));
room().dispatchEvent(mouse('pointermove', 190, 190));
await card.updateComplete;
out.disabledRoomTip = card._tip === null && !root().querySelector('.tip');
out.disabledSkipsArea = roomAreaCalls === 0;
out.roomHighlightSurvives = card._hoverRoom !== null
&& !!root().querySelector('.room-hover-fill-layer,.room-hover-outline-layer');
root().querySelector('.dev').dispatchEvent(mouse('pointermove', 200, 200));
await card.updateComplete;
out.deviceTipSurvives = card._tip?.room === false && !!root().querySelector('.tip');
card._openSettingsDialog();
out.reopenShowsOff = card._settingsDialog?.showRoomTooltip === false;
card._settingsDialog = { ...card._settingsDialog, showRoomTooltip: true };
await card._saveSettingsDialog();
await card.updateComplete;
out.trueStoredAsAbsent = !Object.hasOwn(card._serverCfg.settings, 'show_room_tooltip');
out.enableDoesNotRestoreStaleTip = card._tip?.room !== true;
room().dispatchEvent(mouse('pointermove', 210, 210));
await card.updateComplete;
out.roomTipRestoredOnMove = card._tip?.room === true && !!root().querySelector('.tip');
return out;
});
checkAll(result, {
localizedControl: true,
defaultOn: true,
cancelKeepsAbsentDefault: true,
defaultRoomTip: true,
falsePersists: true,
visibleRoomTipCleared: true,
disabledRoomTip: true,
disabledSkipsArea: true,
roomHighlightSurvives: true,
deviceTipSurvives: true,
reopenShowsOff: true,
trueStoredAsAbsent: true,
enableDoesNotRestoreStaleTip: true,
roomTipRestoredOnMove: true,
});
await finish(browser, result);
+58 -58
View File
@@ -1,125 +1,125 @@
{
"schema": 1,
"fingerprint": "816ff9c893e23b7441e73210f85024741ff6c5bf8dabd2fb0df9e8b8055bc693",
"fingerprint": "b69bdfe3df225cf1b96fea0159a03a620e38cb94ec6106f9ee3b0e57cdefd183",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-Dwkibvgo.js",
"houseplan-assets/houseplan-card-CxS6HLVV.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 291031,
"initialViewGzipBytes": 290942,
"lazyFiles": [
"houseplan-assets/backdrop-pick-DWD0ZQTc.js",
"houseplan-assets/de-DJX-vLW1.js",
"houseplan-assets/editor-Dvui5H3b.js",
"houseplan-assets/fr-DVmfM6Kw.js",
"houseplan-assets/houseplan-editor-runtime-9AEP51ly.js",
"houseplan-assets/houseplan-onboarding-runtime-CnVK5oOC.js"
"houseplan-assets/backdrop-pick-I85bKcdl.js",
"houseplan-assets/de-CDRfP4mw.js",
"houseplan-assets/editor-Bze_fgW0.js",
"houseplan-assets/fr-B64rhazj.js",
"houseplan-assets/houseplan-editor-runtime-X23eakN9.js",
"houseplan-assets/houseplan-onboarding-runtime-C3A8efgN.js"
],
"lazyGzipBytes": 214972,
"lazyGzipBytes": 215441,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-DWD0ZQTc.js",
"houseplan-assets/editor-Dvui5H3b.js",
"houseplan-assets/houseplan-editor-runtime-9AEP51ly.js"
"houseplan-assets/backdrop-pick-I85bKcdl.js",
"houseplan-assets/editor-Bze_fgW0.js",
"houseplan-assets/houseplan-editor-runtime-X23eakN9.js"
],
"lazyEditorGzipBytes": 159157,
"lazyEditorGzipBytes": 159770,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-DWD0ZQTc.js",
"houseplan-assets/houseplan-onboarding-runtime-CnVK5oOC.js"
"houseplan-assets/backdrop-pick-I85bKcdl.js",
"houseplan-assets/houseplan-onboarding-runtime-C3A8efgN.js"
],
"lazyOnboardingGzipBytes": 14132,
"lazyOnboardingGzipBytes": 14129,
"lazyLocaleFiles": [
"houseplan-assets/de-DJX-vLW1.js",
"houseplan-assets/fr-DVmfM6Kw.js"
"houseplan-assets/de-CDRfP4mw.js",
"houseplan-assets/fr-B64rhazj.js"
],
"lazyLocaleGzipBytes": 48859,
"lazyLocaleGzipBytes": 48717,
"files": [
{
"path": "houseplan-assets/backdrop-pick-DWD0ZQTc.js",
"sha256": "455450cbc7b74f052fb112680af9ba4be145af8c3a1061907ec541b63957f22f",
"path": "houseplan-assets/backdrop-pick-I85bKcdl.js",
"sha256": "7becca609cbb95b1ba278743fce5b3e93c7adf8a7bd9107783931f29279ac5df",
"rawBytes": 20974,
"gzipBytes": 7176,
"gzipBytes": 7175,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dwkibvgo.js"
"houseplan-assets/houseplan-card-CxS6HLVV.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-DJX-vLW1.js",
"sha256": "73d198680c17602addf41855748533ed47233e81e9bdc02a786034aeac0e4e63",
"rawBytes": 81798,
"gzipBytes": 24701,
"path": "houseplan-assets/de-CDRfP4mw.js",
"sha256": "2f90acc29e659e771dea42290b74ceb54d8ab9a7fae9cfdbd7ce6bc4bda0b70b",
"rawBytes": 81632,
"gzipBytes": 24630,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-Dvui5H3b.js",
"sha256": "11578ded60410e4e879c9342ee74c8a0968477ea2bae98eb18480a0ad02d303c",
"path": "houseplan-assets/editor-Bze_fgW0.js",
"sha256": "2e2513efca4fa8d9019c3601693fb3ff3b9e050ba3fe054b190f81d286540dda",
"rawBytes": 3826,
"gzipBytes": 1580,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dwkibvgo.js"
"houseplan-assets/houseplan-card-CxS6HLVV.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-DVmfM6Kw.js",
"sha256": "167bed2822ce7e2b38d47e8dc1d73648d10c04d469979bf05e414000b53487b4",
"rawBytes": 83973,
"gzipBytes": 24158,
"path": "houseplan-assets/fr-B64rhazj.js",
"sha256": "6a1a18220938a8d4631586a9dc7f8ef8243b77a23089777d0917975300960a82",
"rawBytes": 83754,
"gzipBytes": 24087,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-Dwkibvgo.js",
"sha256": "d16deb0fc6f591040a2f97d51102e3e355da35cb3d1b69aa79472406c28d3ed2",
"rawBytes": 1033612,
"gzipBytes": 290234,
"path": "houseplan-assets/houseplan-card-CxS6HLVV.js",
"sha256": "1607bc6eeb275b340391ff10bdb7c2c5f62e59d5d2a43adbdb412698a0a024d1",
"rawBytes": 1032923,
"gzipBytes": 290148,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-DJX-vLW1.js",
"houseplan-assets/editor-Dvui5H3b.js",
"houseplan-assets/fr-DVmfM6Kw.js",
"houseplan-assets/houseplan-editor-runtime-9AEP51ly.js",
"houseplan-assets/houseplan-onboarding-runtime-CnVK5oOC.js"
"houseplan-assets/de-CDRfP4mw.js",
"houseplan-assets/editor-Bze_fgW0.js",
"houseplan-assets/fr-B64rhazj.js",
"houseplan-assets/houseplan-editor-runtime-X23eakN9.js",
"houseplan-assets/houseplan-onboarding-runtime-C3A8efgN.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-9AEP51ly.js",
"sha256": "c1b8f90458afedddbbdde98570c517d6e66dfcee08ad4764b1ac2473bdf89fb4",
"rawBytes": 574225,
"gzipBytes": 150401,
"path": "houseplan-assets/houseplan-editor-runtime-X23eakN9.js",
"sha256": "53f256fffc8e6f02618e56dbf56f25258356c2ca221ee9918ccbc57d9e590f6b",
"rawBytes": 575918,
"gzipBytes": 151015,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-DWD0ZQTc.js",
"houseplan-assets/houseplan-card-Dwkibvgo.js"
"houseplan-assets/backdrop-pick-I85bKcdl.js",
"houseplan-assets/houseplan-card-CxS6HLVV.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-CnVK5oOC.js",
"sha256": "bd169896116229e76b37243106bde64755589017639890d36dd19282c872db7a",
"path": "houseplan-assets/houseplan-onboarding-runtime-C3A8efgN.js",
"sha256": "a5433ef2715f424016b31a2ae3c3c30f2cff05549230fd0ab16db3159834de3f",
"rawBytes": 28088,
"gzipBytes": 6956,
"gzipBytes": 6954,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-DWD0ZQTc.js",
"houseplan-assets/houseplan-card-Dwkibvgo.js"
"houseplan-assets/backdrop-pick-I85bKcdl.js",
"houseplan-assets/houseplan-card-CxS6HLVV.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "730163666440793e44be0ee5482f7080931370f94555a1d52072d3a54698447e",
"sha256": "f9d93e5aa355fc91dc8fad57e7669a0ac663e8b538f42ea671a1a319fe214c3e",
"rawBytes": 1183,
"gzipBytes": 797,
"gzipBytes": 794,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-Dwkibvgo.js"
"houseplan-assets/houseplan-card-CxS6HLVV.js"
],
"dynamicImports": []
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="816ff9c893e23b7441e73210f85024741ff6c5bf8dabd2fb0df9e8b8055bc693";import{b as e,l as o,t,df as s,A as a,dg as i,dh as l,di as n,E as r,c}from"./houseplan-card-Dwkibvgo.js";class f extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,f={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},h=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b69bdfe3df225cf1b96fea0159a03a620e38cb94ec6106f9ee3b0e57cdefd183";import{b as e,l as o,t,dg as s,A as a,dh as i,di as l,dj as n,E as r,c}from"./houseplan-card-CxS6HLVV.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
.computeLabel=${e=>f[e.name]||e.name}
.computeLabel=${e=>h[e.name]||e.name}
@value-changed=${this._valueChanged}
></ha-form>`;return c`
${d(h.slice(0,3))}
${d(f.slice(0,3))}
${_?c`<div class="default-floor-error" role="alert"
style="color:var(--error-color,#db4437);margin:-4px 0 12px;overflow-wrap:anywhere">
${t(s,"editor.default_floor_missing",{id:_})}
</div>`:a}
${d(h.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}f.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",f);
${d(f.slice(3))}
`}_valueChanged(e){const o={...this._config,...e.detail.value};""===o.floor?delete o.floor:o.floor===this._floorToken&&(o.floor=this._config?.floor);const t=new Event("config-changed",{bubbles:!0,composed:!0});t.detail={config:o},this.dispatchEvent(t)}}h.properties={hass:{attribute:!1},_config:{state:!0},_spaces:{state:!0}},customElements.get("houseplan-card-editor")||customElements.define("houseplan-card-editor",h);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="816ff9c893e23b7441e73210f85024741ff6c5bf8dabd2fb0df9e8b8055bc693";try{await import("./houseplan-assets/houseplan-card-Dwkibvgo.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="b69bdfe3df225cf1b96fea0159a03a620e38cb94ec6106f9ee3b0e57cdefd183";try{await import("./houseplan-assets/houseplan-card-CxS6HLVV.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
+4
View File
@@ -2,6 +2,10 @@
## Unreleased
- General settings can now hide the room information window on mouse hover
without disabling the room highlight or device tooltips; existing plans keep
the window enabled by default ([#426](https://github.com/Matysh/houseplan-card/issues/426)).
## v1.71.0-beta.1 — 2026-09-03
- The Background editor can now upload and reuse private PNG, JPEG, WebP and
+5
View File
@@ -8,6 +8,11 @@
## Не выпущено
- В общих настройках теперь можно отключить информационное окно комнаты при
наведении мышью, не отключая подсветку комнаты и подсказки устройств;
существующие планы сохраняют включённое поведение по умолчанию
([#426](https://github.com/Matysh/houseplan-card/issues/426)).
## v1.71.0-beta.1 — 2026-09-03
- В редакторе подложки теперь можно загружать и повторно использовать закрытые
+14
View File
@@ -63,6 +63,20 @@ writer produce the same request; accepting either would reopen last-writer-wins
data loss. This changes only the WebSocket write contract. Stored config,
model/store versions, exports and read compatibility are unchanged.
## Room hover information preference (#426)
`settings.show_room_tooltip` is an optional global boolean. Absence and any
invalid legacy/future value read as the historical enabled default; only exact
`false` hides the floating room information window. Saving the enabled value
removes the key. The field does not change room highlighting or device
tooltips, and does not require a model/store version migration.
An older frontend ignores the field and temporarily shows the room window. An
older backend preserves it through the existing unknown-settings policy, so a
new frontend restores the disabled behavior after upgrade. Full backup/import
preserves the setting and the privacy-safe support projection includes only a
validated boolean.
## Stable wall identity — model v8 (#282)
Model v8 adds `space.wall_segments[]`, ordered `rooms[].wall_ids[]`, IDs on
+4
View File
@@ -1024,6 +1024,10 @@ separately promised workflows:
- [ ] Room hover adds a subtle accent wash and double contour without changing
the underlying room fill or Glow brightness
- [ ] Room tooltip shows average room temperature and humidity after the area line and before LQI; missing values are omitted [auto: smoke_ux_fixes]
- [ ] General settings can hide only the room tooltip: default/Cancel/save/reopen
semantics, skipped area work, persistent room highlight, unaffected
device tooltip and restoration on the next mouse move
[auto: smoke_room_tooltip_toggle]
- [ ] Average room temperature counts ONLY thermometer/air-monitor devices — fridges, TRV heads,
smart-plug chip temperatures (`*_device_temperature`) and diagnostic-category temps are excluded [manual]
- [ ] Space dialog is 500 px wide; the comfort-bounds inputs are compact (56 px)
+5
View File
@@ -51,6 +51,11 @@ desktop hover without a reload. Space/mode changes, page hiding and remounts
also discard transient hover. Keyboard focus and explicit click/tap surfaces
remain independent of this visual hover gate.
The default-on **Show the room information window on hover** preference applies
only after this pointer-modality gate has enabled real mouse hover. Turning it
off leaves the room highlight and device tooltips unchanged; it does not add a
touch or pen replacement for the room window.
## What “best-effort editors” means
On a coarse-pointer or no-hover device, an editor operation may:
+6 -1
View File
@@ -55,7 +55,7 @@ override them; a room may override its space; a marker may override its room.
| Level | Meaning | Stored data |
|---|---|---|
| Card | One dashboard instance | Initial space, language, icon size, value/LQI display, live state, kiosk and cycle |
| Global settings | Defaults for all spaces | Fill palette, background, Glow radius, north, sun, weather and icon rules |
| Global settings | Defaults for all spaces | Fill palette, background, Glow radius, north, sun, room-hover information and icon rules |
| Space | Floor, yard, garage or building | Plan image, scale, rooms, walls, openings, decor and display settings |
| Room | A closed outline | Name, optional HA area, temperature/humidity source and local fill |
| Wall | A room-contour or independent segment | Stable identity and thickness from 0 to 100 cm; zero-thickness appearance is selected per space |
@@ -196,6 +196,11 @@ View is the state with no editor open. Close the active editor to return to it.
| Kiosk | Actionable as in View | Read-only | Read-only; no editors |
| Static card | Not live or interactive | Render only | Render only |
The room highlight remains available in View and kiosk. To keep that highlight
but hide the floating room summary, turn off **General settings → Show the room
information window on hover**. The option is on by default and does not affect
device tooltips.
Each editor has a stable primary toolbar. Tool parameters and selected-object
actions appear in a context tray over the top of the canvas. On a narrow screen
the tray scrolls horizontally instead of shrinking the plan.
+6 -1
View File
@@ -55,7 +55,7 @@ House Plan — локальная интеграция и две Lovelace-кар
| Уровень | Что это | Что хранит |
|---|---|---|
| Карточка | Один экземпляр `houseplan-card` на дашборде | Стартовое пространство, язык, размер иконок, показ значений/LQI, живые состояния, киоск и автолистание |
| Общие настройки | Настройки всего House Plan | Палитры заливок, фон, радиус света, север, солнце, погода, правила иконок |
| Общие настройки | Настройки всего House Plan | Палитры заливок, фон, радиус света, север, солнце, окно комнаты при наведении, правила иконок |
| Пространство | Этаж, двор, гараж, отдельное строение | Подложку, масштаб сетки, комнаты, стены, проёмы, декор и настройки отображения |
| Комната | Только замкнутый контур | Название, необязательную HA-зону, источники температуры/влажности и локальную заливку |
| Стена | Сегмент контура комнаты или независимая стена | Стабильный ID и толщину 0–100 см; вид стен нулевой толщины задаётся для пространства |
@@ -207,6 +207,11 @@ title: План дома
| Киоск | Настенный экран | Интерактивны как в просмотре | Только отображение | Только отображение; редакторы отсутствуют |
| Статическая карточка | Компактная схема пространства | Только рисунок | Только рисунок | Без состояний и интерактивности |
Подсветка комнаты сохраняется в Просмотре и киоске. Чтобы оставить подсветку,
но убрать всплывающее описание комнаты, выключите **Общие настройки →
Показывать окно с информацией при наведении на комнату**. По умолчанию опция
включена и не влияет на подсказки устройств.
В редакторах сетка продолжается по всему рабочему холсту. В режиме просмотра сетка не показывается.
Основная панель каждого редактора содержит только постоянные инструменты, а
+4
View File
@@ -79,6 +79,10 @@ device state do not depend on that gate. A device action that actually dispatche
briefly scales its painted shell to 95% and back over 200 ms; informational,
editor and no-op paths do not imitate a successful action.
The global `settings.show_room_tooltip` preference controls only the floating
room information window. Missing or invalid values mean enabled; exact `false`
hides the window while room highlight and every device tooltip remain active.
Removed from this mode (they move, not die):
- icon dragging ("drag anywhere", v1.9 — consciously reversed),
- room-label dragging,
+11 -11
View File
@@ -3,7 +3,7 @@
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": "oxipng 10.2.0",
"sourceFingerprint": "cb230a51d6b12aed13e4a2db8e08701fcaefed3b54a6e61c4e0cbb1a3483be7d",
"sourceFingerprint": "985f523e198334c67df7ca550bbb5d2c3822a12d6134ab4a02820dcf211151d2",
"captureScriptSha256": "cadb8e1bcab9f1dcdd7d75b3b90ddcbaaeb2b8c2a098f575a21f39ff70f5c59c",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,7 +15,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "cb230a51d6b12aed13e4a2db8e08701fcaefed3b54a6e61c4e0cbb1a3483be7d",
"sourceSha256": "985f523e198334c67df7ca550bbb5d2c3822a12d6134ab4a02820dcf211151d2",
"imageSha256": "80a70361dc18dd0461568df332062e6482c633af5d280954f8b675701418a76d"
},
"view-touch": {
@@ -26,7 +26,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "cb230a51d6b12aed13e4a2db8e08701fcaefed3b54a6e61c4e0cbb1a3483be7d",
"sourceSha256": "985f523e198334c67df7ca550bbb5d2c3822a12d6134ab4a02820dcf211151d2",
"imageSha256": "4106cc28847047505f46921ff95765d8abdf5b382d9d17d4c5e4ad129dd8f6be"
},
"space-create": {
@@ -37,7 +37,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "cb230a51d6b12aed13e4a2db8e08701fcaefed3b54a6e61c4e0cbb1a3483be7d",
"sourceSha256": "985f523e198334c67df7ca550bbb5d2c3822a12d6134ab4a02820dcf211151d2",
"imageSha256": "617b51b3648498787b5039980c9f3eceb75ba56ed63a1a20e616bc05bc304362"
},
"room-contour-close": {
@@ -48,7 +48,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "cb230a51d6b12aed13e4a2db8e08701fcaefed3b54a6e61c4e0cbb1a3483be7d",
"sourceSha256": "985f523e198334c67df7ca550bbb5d2c3822a12d6134ab4a02820dcf211151d2",
"imageSha256": "1dab6cc3b9d1bf7d8c40f0e5137f8c688683c9b7eabc5167da99d41ecfdd5b79"
},
"plan-context-tray": {
@@ -59,7 +59,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "cb230a51d6b12aed13e4a2db8e08701fcaefed3b54a6e61c4e0cbb1a3483be7d",
"sourceSha256": "985f523e198334c67df7ca550bbb5d2c3822a12d6134ab4a02820dcf211151d2",
"imageSha256": "94ef50762753c0ac6ddc84d2521c4232a3f9ecd89843810d2df61c517592bed7"
},
"device-editor": {
@@ -70,7 +70,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "cb230a51d6b12aed13e4a2db8e08701fcaefed3b54a6e61c4e0cbb1a3483be7d",
"sourceSha256": "985f523e198334c67df7ca550bbb5d2c3822a12d6134ab4a02820dcf211151d2",
"imageSha256": "7a601769de38aa19c2e280f6ff4cf3695b854b1d1c6d1f8de4e6b5e8f7eb1559"
},
"device-display-preview": {
@@ -81,7 +81,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "cb230a51d6b12aed13e4a2db8e08701fcaefed3b54a6e61c4e0cbb1a3483be7d",
"sourceSha256": "985f523e198334c67df7ca550bbb5d2c3822a12d6134ab4a02820dcf211151d2",
"imageSha256": "0939875f8631694f4de0ef4fd01032010ede8c7d49fb7c0a857612d4b3afff93"
},
"background-editor": {
@@ -92,7 +92,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "cb230a51d6b12aed13e4a2db8e08701fcaefed3b54a6e61c4e0cbb1a3483be7d",
"sourceSha256": "985f523e198334c67df7ca550bbb5d2c3822a12d6134ab4a02820dcf211151d2",
"imageSha256": "054170fd9ef45762b602b4d5c9c3b9ea9724858be61af137970c246f485c13bb"
},
"room-card": {
@@ -103,7 +103,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "cb230a51d6b12aed13e4a2db8e08701fcaefed3b54a6e61c4e0cbb1a3483be7d",
"sourceSha256": "985f523e198334c67df7ca550bbb5d2c3822a12d6134ab4a02820dcf211151d2",
"imageSha256": "2ae4a58853d98e10d12456b2078ec2f6a0b597722c8310d7b016abd2bc42561e"
},
"device-info": {
@@ -114,7 +114,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "cb230a51d6b12aed13e4a2db8e08701fcaefed3b54a6e61c4e0cbb1a3483be7d",
"sourceSha256": "985f523e198334c67df7ca550bbb5d2c3822a12d6134ab4a02820dcf211151d2",
"imageSha256": "cb37f7eefd936f98ef44969b21dfe8e40ee27d1f558abe8d53c0223f6488b8a6"
}
},
+290
View File
@@ -0,0 +1,290 @@
# CODE-REVIEW-426-r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/426
- **Этап:** code (код-ревью, PROCESS.md §2.7)
- **Заход:** r1 · блокирующих циклов израсходовано 0 из 4
- **Материал:** `git log --oneline origin/dev..HEAD` / `git diff origin/dev...HEAD`
на ветке `issue/426-room-hover-tooltip-toggle`, HEAD `a6eb3d6cfa492ed1e742231800f31efab7c46e4b`.
- **ТЗ:** `docs/specs/426-room-hover-tooltip-toggle.md` — ревью ТЗ уже
зелёное (`docs/reviews/SPEC-REVIEW-426-r1.md`, SHA `587c2069`), сам текст ТЗ
не менялся между тем ревью и этим (issue содержит ревизию 2, но это
уточнение места editor-only строк без изменения поведения — см. историю
issue).
Это первый заход код-ревью для #426: раздел «дельта/унаследовано из r0» из
§2.9 не применяется (применяется только начиная с r2).
## Скоуп
Шесть коммитов на ветке:
1. `587c2069` docs: specify room hover tooltip toggle (ТЗ)
2. `ac99a2ff` docs: review document for #426 (SPEC-REVIEW артефакт)
3. `8013f7d0` feat: add room hover tooltip setting — **весь продуктовый код**:
`src/types.ts`, `src/logic.ts`, `src/houseplan-card.ts`,
`src/houseplan-editor-runtime.ts`, i18n (4 lazy editor-словаря + перенос
`gs.hint` из initial View в lazy editor-бандл), backend
(`validation.py`, `support_package.py`), тесты (`logic.test.mjs`,
`i18n.test.mjs`, `test_validation.py`, `test_support_package.py`), новый
`demo/smoke_room_tooltip_toggle.mjs`, `scripts/smoke-links.mjs`, docs
(CHANGELOG×2, USER-GUIDE×2, UX-MODES, TOUCH-SUPPORT, CONFIG-COMPATIBILITY,
TESTING), собранные `dist/**`/`custom_components/.../frontend/**`.
4. `9d1e5fdb` docs: refresh screenshot source fingerprint (гейт `check-docs.mjs`)
5. `223951c0` chore: refresh config schema manifest (`scripts/config-schema.json`)
6. `a6eb3d6c` test: accept room tooltip settings golden — приняла новый
`general-color-popover-desktop-en` эталон (единственный изменившийся
golden-кадр).
Продуктовый скоуп совпадает с ТЗ: один глобальный boolean
`settings.show_room_tooltip`, resolver `showRoomTooltipOf`, guard в локальном
`tip`-замыкании комнаты, независимость hover-подсветки/device tooltip,
backend/schema/privacy-projection, i18n, документация, release-артефакты.
## Как проверялось
Ручного тестирования в цикле нет — весь разбор построен на чтении
`git diff origin/dev...HEAD` и на выводе уже прогнанных CI-джобов на этом
дереве.
1. **Продуктовая рамка.** `docs/SCOPE.md` J1 («live spatial overview») —
опция не убирает состояние, только один информационный элемент
вида/интерфейса; никакая строка Core user jobs не запрещает такую точную
настройку показа. View остаётся продуктом для двух персон из трёх —
изменение не трогает hover-подсветку, устройства, действия.
2. **Frontend-контракт** (`src/logic.ts`, `src/types.ts`,
`src/houseplan-card.ts`, `src/houseplan-editor-runtime.ts`) — построчно,
с трассировкой каждого AC1–AC7 к конкретной строке диффа (см. раздел
«Находки» и «Что проверено»).
3. **Backend** (`custom_components/houseplan/validation.py`,
`support_package.py`) и тесты (`tests_backend/test_validation.py`,
`test_support_package.py`) — построчно + логическая проверка, что новые
тесты умеют падать (мутация: убрать `isinstance(..., bool)` → тест
`test_room_tooltip_global_setting_is_strict_boolean_and_round_trips`
перестаёт отклонять `0`/`1`/`"false"`; убрать `isinstance(show_room_tooltip, bool)`
в `support_package.py` → тест на строку `"false"` в
`test_projection_helpers_fail_closed_on_malformed_shapes` перестаёт быть
пустым словарём).
4. **i18n** — сверка всех 4 lazy editor-словарей, `test/i18n.test.mjs`
(жёсткое число `44`, не диапазон — ловит и пропущенный, и лишний ключ).
5. **Docs** — `CONFIG-COMPATIBILITY.md`/`UX-MODES.md`/`TOUCH-SUPPORT.md`/
`USER-GUIDE.{md,ru.md}`/`CHANGELOG.{md,ru.md}` — сверены построчно с
контрактом ТЗ (терминология "Show the room information window on hover" /
"Показывать окно с информацией при наведении на комнату" совпадает с
зафиксированной владельцем строкой).
6. **CI-évidence вместо повторного прогона** (см. «Какие гейты» ниже) —
прочитаны логи трёх Actions-прогонов на этой ветке через `gh run view`.
7. **Golden-эталон** — скачаны обе версии
`general-color-popover-desktop-en.png` (`origin/dev` и `a6eb3d6c`) и
визуально сверены (см. «Находки»/«Что проверено»).
8. `node scripts/smoke-select.mjs --base origin/dev --head HEAD` — получен
список смоков по символам диффа, дальше сверен с логами уже прошедшего
прогона (см. ниже).
## Находки
**High: 0. Medium: 0.** Изменение соответствует ТЗ, все девять AC закрыты
кодом и/или тестом, гейты (см. ниже) зелёные на этом дереве.
Отмечаю две вещи, которые проверил специально, потому что выглядели как
потенциальный риск, но не подтвердились как дефект (Low, не требует правки):
- **`_notePointer` не вызывается на каждый `pointermove`, когда опция
выключена** (`src/houseplan-card.ts:11580-11585`) — до правки `_showTip()`
вызывал `this._notePointer(ev)` на каждый pointermove комнаты; теперь при
`showRoomTooltipOf() === false` цикл выходит раньше и `_notePointer` не
вызывается для pointermove (только для pointerenter через `enterRoom`).
Прочитал `PointerModalityController.note()`
(`src/pointer-modality.ts:78-82`) и `nextPointerModality`
(`src/pointer-modality.ts:20-28`): модальность — защёлка, меняется только
при событии другого `pointerType`, а не тухнет со временем. Поскольку
`pointerenter` уже фиксирует модальность на входе в комнату, повторные
вызовы на move были бы no-op'ами. Регрессии нет — **проверено чтением, не
исполнением**.
- **Optimistic write в `_saveSettingsDialog` не откатывается при ошибке
сохранения** (`src/houseplan-editor-runtime.ts:10243-10256`) —
`this.host._serverCfg` мутируется новым `settings` (включая
`show_room_tooltip`) **до** `await this._saveConfigNow()`; `catch`-ветка
снимает только `busy`, не восстанавливает `_serverCfg`. Формально это
означает, что при сетевой ошибке эффективное значение опции в памяти уже
«новое», хотя запись не подтверждена сервером — то есть именно то, что
AC1.5 запрещает («не выдаёт несохранённый draft за применённое значение»).
Но это **не новое поведение**: тот же паттерн уже действует для `sunRays`,
`bgColor`, `northDeg`, `glow_radius_cm`, `bgMode` в той же функции, и ТЗ
сознательно пишет «по существующему контракту общих настроек» — то есть
автор идёт по прецеденту, а не изобретает новый разрыв. Чинить
существующий контракт всего диалога — вне скоупа #426 (это была бы правка
соседнего поведения, не связанного с room-tooltip). Не завожу отдельный
issue: находка Low на уровне существующего кода, не Medium/High и не
попутный дефект, привнесённый этим диффом.
## Что проверено и корректно
- **AC1 (UI/default).** Один переключатель сразу после `gs.hint`
(`houseplan-editor-runtime.ts:10538-10547`), `_boolInput`, доступное имя из
видимой строки — как и `sunRays`. `_openSettingsDialog` инициализирует
draft через единственный resolver `showRoomTooltipOf(this.host._settings)`
(`:9034-9039`). Юнит-тест на resolver
(`test/logic.test.mjs`) покрывает absent/`undefined`/`null`/`true`/`0`/`1`/
`'false'`/`[]`/`{}` → `true`, только точный `false` → `false` — то есть
ровно табличный план AC1 из ТЗ, тест умеет падать (замена на
`Boolean(value)` даёт `false` для нескольких из этих проб).
- **AC2 (persistence).** Save: `if (d.showRoomTooltip) delete settings.show_room_tooltip; else settings.show_room_tooltip = false;`
(`:10237-10238`) — та же двусторонняя схема, что у `sunRays`. Cancel/Escape
не проходят через `_saveSettingsDialog` (не тронуто диффом — общий
контракт диалога). Backend: `vol.Optional("show_room_tooltip"): bool`
(`validation.py:1938`) отклоняет `None`/`0`/`1`/`"false"`/`[]`/`{}`,
принимает оба boolean с round-trip — тест
`test_room_tooltip_global_setting_is_strict_boolean_and_round_trips`
доказывает и допуск, и отказ.
- **AC3 (tooltip off).** Guard **до** `_roomArea(r)`
(`houseplan-card.ts:11582-11586`): при `false` — `return` раньше вычисления
площади и раньше `_showTip()`; попутно чистит уже показанный
`.tip`, если он был room-tooltip (`if (this._tip?.room) this._tip = null;`).
Save с выключенным значением дополнительно чистит tip сразу после
`_saveConfigNow()` (`:10245`). Смок
`demo/smoke_room_tooltip_toggle.mjs` проверяет оба пути:
`disabledRoomTip`/`disabledSkipsArea` (счётчик вызовов `_roomArea`) и
`visibleRoomTipCleared` после Save — прогнан в CI (см. ниже), `ok`.
- **AC4 (default parity).** Ветка `showRoomTooltipOf() === true` не меняет
путь: тот же вызов `_showTip(e, title, areaText ?? '', showLqi ? ... : null, this._roomTemp(r), this._roomHum(r), true)`
— единственное отличие от старого кода это добавленный `room: boolean`
флаг в конце сигнатуры (используется только для идентификации источника
tip, не влияет на title/area/temp/hum/lqi/positioning). Смок
`defaultRoomTip`/`roomTipRestoredOnMove` подтверждает содержимое и
восстановление после reload/повторного включения.
- **AC5 (hover/device independence).** `enterRoom` (вынесенный из пяти
дублирующихся инлайн-обработчиков `pointerenter`) не содержит нового
guard'а — `_hoverRoom` продолжает устанавливаться независимо от
`show_room_tooltip`. `_showTip()` не тронут: device tooltip
(`houseplan-card.ts:12463+`) вызывает `_showTip(...)` без нового
параметра → `room` по умолчанию `false`, не подавляется новым guard'ом (он
находится только внутри room-специфичного замыкания `tip`, не в общем
`_showTip`). Смок: `roomHighlightSurvives`, `deviceTipSurvives` (`_tip?.room === false`).
- **AC6 (pointer/mode parity).** Новый guard стоит **внутри** уже
существующего `if (this._mode !== 'view') return;`, а модальность
(`_pointerModality.hoverEnabled`) по-прежнему проверяется в `_showTip()`
на true-ветке — touch/pen ничего не меняют. Разобрано и решение выше про
`_notePointer` — проверено чтением, регрессии нет.
- **AC7 (compatibility/privacy).** `CONFIG_SCHEMA` — explicit `bool`,
`ALLOW_EXTRA` сохранён. `support_package._global_settings` копирует ключ
только если `isinstance(show_room_tooltip, bool)`
(`support_package.py:142-144`) — невалидная форма (`"false"`) не
копируется, тест `test_projection_helpers_fail_closed_on_malformed_shapes`
это подтверждает. Отсутствие ключа не материализуется нигде (frontend
save удаляет ключ на `true`, backend не создаёт его при чтении). Таблица
mixed-version в ТЗ соответствует `ALLOW_EXTRA` (старый backend) и
strict-schema (новый backend) поведению, прочитанному в коде.
- **AC8 (i18n/docs/release).** Все 4 словаря обновлены, `gs.hint` перенесён
из initial-View словарей (`src/i18n/{en,de,fr,ru}.json`, удалён) в lazy
editor-словари (`src/i18n/support/{en,de,fr,ru}.json`, добавлен вместе с
`gs.show_room_tooltip`) — снижает initial View graph, как и требовало ТЗ.
`test/i18n.test.mjs` жёстко проверяет `44` ключа (было 42) — тест падает и
при пропуске, и при лишнем ключе. Оба CHANGELOG в одном коммите
(`8013f7d0`) с `User-Visible: yes`. USER-GUIDE EN/RU, UX-MODES,
TOUCH-SUPPORT, CONFIG-COMPATIBILITY обновлены и согласованы терминологией
("Show the room information window on hover" / доменная строка RU).
- **AC9 (гейты и бюджет).** См. раздел «Какие гейты прогнаны» — все
перечисленные в AC9 гейты зелёные на этом SHA (через прямой прогон или
легитимный byte-identical reuse, см. ниже), default golden кадры не
изменились (единственный изменившийся кадр — `general-color-popover-desktop-en`,
который явно является ожидаемым следствием новой строки в диалоге, принят
по правилам §11).
## Golden-эталон: проверка правомерности принятия
Коммит `a6eb3d6c` меняет ровно один кадр, `general-color-popover-desktop-en`,
и несёт `Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33722471494`
— это соответствует требованию §11 «эталоны golden принимаются только через
`npm run golden:accept -- --reviewed` на полном артефакте Linux CI».
Проверил сам прогон (`gh run view 33722471494`): джоб «Golden-кадры против
принятых эталонов» упал (ожидаемо — новый кадр ещё не принят), но перед этим
отработали и позеленели «Фронтенд: типы/юниты/мутанты/бандл»,
«Бэкенд: pytest», «Смоки: все шарды зелёные» — то есть кандидат собран из
полного, валидного дерева, не подогнан вручную.
Скачал обе версии PNG (`origin/dev` и `a6eb3d6c`) и сравнил визуально: разница
— это ровно новая строка-переключатель «Show the room information window on
hover» между `gs.hint` и `Fill: lights`, из-за которой весь низ диалога (в том
числе открытый color-popover) сдвинулся вниз на высоту одной строки. Никакого
постороннего визуального изменения нет. Решение автора не создавать отдельный
новый golden-сценарий (ТЗ, «Release-артефакты») было верным для *нового*
сценария; для *существующего* `general-color-popover-desktop-en`, который
непреднамеренно задело смещение вёрстки, потребовалось (и было сделано)
штатное принятие через reviewed CI-артефакт — никакого расхождения с
процессом.
## Какие гейты прогнаны и почему
Полные наборы (§8, предрелизный гейт) прогонять было не нужно — прогонялись/
подтверждались только гейты, которых касается этот diff, через чтение логов
уже отработавших Actions-прогонов этой ветки:
| Гейт | Статус | Источник |
|---|---|---|
| `npx tsc --noEmit`, `npm test`, `npm run build` + сверка 3 копий бандла | ✅ зелёный, не перегонял | Validate на `a6eb3d6c`, https://github.com/Matysh/houseplan-card/actions/runs/33723133249, джоб «Фронтенд: типы, юниты, мутанты, синхрон бандла» — как указано в постановке задачи, этот прогон уже подтверждён и повторного прогона не требует |
| `node scripts/check-docs.mjs` (source fingerprint) | ✅ | Коммит `9d1e5fdb` обновил `docs/images/screenshots.json`; предполётный docs-джоб зелёный в обоих прогонах |
| Config-schema manifest | ✅ | Коммит `223951c0` обновил `scripts/config-schema.json`, добавив `config.settings.show_room_tooltip` |
| `python -m pytest tests_backend -q` | ✅, не перегонял — byte-identical reuse | Реально выполнен и позеленел в run `33722471494` (джоб «Бэкенд: pytest», 2m9s, включает новые `test_validation.py`/`test_support_package.py`); в финальном run `33723133249` переиспользован («входы побайтово те же», #208) — backend-код между этими прогонами не менялся |
| Golden (`npm run golden:verify`) | ✅ | Run `33723133249`, джоб «Golden-кадры против принятых эталонов» зелёный **после** принятия эталона в `a6eb3d6c`; сам процесс принятия проверен отдельно выше |
| Browser smokes, выбранные по диффу | ✅, не перегонял — byte-identical reuse | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` дал 20 «прямых совпадений» (список ниже) и 18 «слабых связей» (только `_config`); вместо выборочного прогона проверил логи — **весь набор из 215 смоков** реально выполнен в run `33722471494` (3 шарда, все ✓, финальный агрегат «Смоки: все шарды зелёные» ✓), включая явно `demo/smoke_room_tooltip_toggle` (`ok smoke_room_tooltip_toggle` в логе шарда 1); в финальном run `33723133249` переиспользован тем же byte-identical правилом. Это сильнее любой выборки по символам — весь набор, а не только совпавшие по символам файлы |
| `npm run invariants -- --config ...` | Не прогонял — не требуется | Diff не трогает геометрию: нет изменений в rooms/walls/layout/marker.space/open_spans, только новый independent boolean и его resolver |
| Performance-профили | Не прогонял — не требуется | Не названы в AC9 как отдельный профиль; ТЗ («Производительность и безопасность») обоснованно исключает их, guard стоит до `_roomArea()`; perf-smoke job переиспользован (#208) на обоих прогонах как побайтово идентичный |
| `test/single-source-numbers.test.mjs` | Не запускал целево | Diff не добавляет и не дублирует ни одного видимого числового значения — переключатель boolean, площадь/температура/влажность/LQI не тронуты; тест из общего юнит-набора всё равно прошёл в рамках зелёного `npm test` |
### Прямые совпадения по `smoke-select.mjs` — решение по каждой строке
Все нижеперечисленные фактически прогнаны и зелены в CI (см. таблицу выше:
весь набор из 215 смоков выполнялся, а не только эти 20), решение
«прогнать» принято до выборки — фиксирую построчно для аудита:
- `smoke_feedback_v2`, `smoke_touch_tips` (`_hoverRoom`,`_notePointer`,`_tip`) — прямое совпадение, прогнан
- `smoke_room_tooltip_toggle` — новый целевой смок задачи, прогнан
- `smoke_help_affordance`, `smoke_bg_color`, `smoke_color_picker_consumers`, `smoke_dialog_zombie`, `smoke_esc_dialogs`, `smoke_gs_always`, `smoke_ha_controls`, `smoke_sun` (`_settingsDialog`/`_config`) — общие настройки задеты новой строкой в диалоге, прогнан
- `smoke_junction_patch_resilience`, `smoke_multiwall_junction`, `smoke_wall_key_roundtrip`, `smoke_isometric_live_touch`, `smoke_sun_live_bg` (`_hoverRoom`) — не про room-tooltip, но используют тот же геттер; прогнан вместе со всеми
- `smoke_decor`, `smoke_room_climate_placement`, `smoke_room_settings`, `smoke_ux_fixes` (`_tip`) — device/room tooltip соседние сценарии, прогнан
Слабые связи (18, все через один `_config`) не разбирались построчно — общий
прогон всего набора делает это избыточным; ни один из них не относится к
room-tooltip по существу.
## Чего не проверял
- Полный HA-харнесс/performance-профили за пределами того, что уже
переиспользовано CI как byte-identical (#208) — не требуется по AC9 и не
затронуто диффом.
- Ручное открытие приложения в браузере — цикл ревью не предусматривает
ручного тестирования; вместо этого разобран код и прочитаны логи реально
выполненного (не смоделированного) браузерного смок-прогона.
- Мутационная проверка вручную (я не патчил код и не перезапускал тесты
локально) — оценена **логическая** способность тестов упасть по чтению
кода теста и производственного кода одновременно; там, где это
нетривиально (backend bool schema, i18n key count), это явно
зафиксировано выше.
- Полный визуальный дифф остальных 131 неизменившихся golden-кадров — не
нужно, `baselines-index.json` показывает, что изменился ровно один хэш.
## Материал раунда
- Ветка: `issue/426-room-hover-tooltip-toggle`
- SHA: `a6eb3d6cfa492ed1e742231800f31efab7c46e4b`
- Диапазон: `origin/dev..HEAD` (6 коммитов, см. «Скоуп»)
## Вердикт
**Зелёный.** High: 0 · Medium: 0. Готово к мержу.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/426-room-hover-tooltip-toggle`, коммит `a6eb3d6cfa49` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `ac93d978f870507113624993ecfa47e429806218`
```
git log --all --format='%H %T' | grep ac93d978f870
```
- ТЗ `docs/specs/426-room-hover-tooltip-toggle.md`, блоб `66fb319f32ef962372bb998256effaf205a7fbb8`
```
git log --all --find-object=66fb319f32ef962372bb998256effaf205a7fbb8 -- docs/specs/426-room-hover-tooltip-toggle.md
```
+171
View File
@@ -0,0 +1,171 @@
# SPEC-REVIEW-426-r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/426
- **Этап:** spec (ревью ТЗ, PROCESS.md §2.4)
- **Заход:** r1 · блокирующих циклов израсходовано 0 из 4
- **Материал:** `docs/specs/426-room-hover-tooltip-toggle.md` на ветке
`issue/426-room-hover-tooltip-toggle`, SHA `587c20699c4bc1ada405c5c7f9775d34b2b78a39`
(совпадает с ревизией, заявленной автором в issue).
- **Трек:** полный (не `small`) — аналитик назвал нарушенные критерии §5
корректно: новое compatibility-поле в `config.settings` и новый публичный
UX-контракт, изменение проходит через frontend и backend.
## Скоуп
Один глобальный переключатель «Показывать окно с информацией при наведении на
комнату» в «Общих настройках», управляющий только room-tooltip в View
(включая kiosk с мышью); hover-подсветка комнаты и все прочие тултипы не
меняются. Новое optional `settings.show_room_tooltip: boolean`, default `true`
материализуется отсутствием ключа.
## Как проверялось
Ревью ТЗ на этапе `spec` не запускает автотесты/гейты (кода ещё нет) — задача
ревьюера здесь одна: убедиться, что каждое AC однозначно, доказуемо, и что
факты о текущем коде, на которые опирается контракт, не являются
не помеченной догадкой. Поэтому весь разбор — очный аудит соответствия текста
ТЗ реальному состоянию репозитория `587c2069`:
1. `docs/SCOPE.md`, `PROCESS.md`, `AGENTS.md` — трек, обязательные разделы
§7.1, класс изменений, шаблон вердикта.
2. Тело issue #426 и оба комментария (аналитика, занятие, готовность ТЗ) —
сверка с тем, что владелец зафиксировал сам (место настройки, точная
формулировка, default) против того, что ТЗ выдаёт за решённое.
3. `docs/USER-GUIDE.ru.md` (строка 203, 1871) — терминология «подсказка»/
«наведение», состав room-tooltip (название, чистая площадь, температура,
влажность, LQI) — совпадает с описанием AC4.
4. `docs/UX-MODES.md`, `docs/TOUCH-SUPPORT.md` (pointer modality, hover
ownership) — независимость room hover-подсветки от tooltip и контракт
touch/pen уже существуют и не меняются этим ТЗ.
5. Построчная проверка каждого фактического утверждения ТЗ о текущем коде:
- `src/houseplan-card.ts:11578-11589` — локальный `tip`-замыкание на
`pointermove` комнаты, guard `if (this._mode !== 'view') return;`,
безусловный вызов `_showTip()` — подтверждает раздел «Проблема».
- `src/houseplan-card.ts:7282-7294` — `_showTip()` общий для комнаты и
устройства, проверяет `hoverEnabled`/`drag`, ничего не знает про
room-specific флаг — подтверждает риск «не трогать общий guard».
- `src/houseplan-card.ts:1032, 9542, 9659, 9668, 11626-11677` — `_hoverRoom`
и `room-hover-fill-layer`/`outline-layer` независимы от `tip`,
устанавливаются на `pointerenter` — подтверждает AC5.
- `src/houseplan-card.ts:12463-12472` — device tooltip гейтится
`_mode !== 'view' && _mode !== 'devices'`, использует тот же `_showTip()`
— подтверждает «Device tooltip... в View и Редакторе устройств».
- `src/houseplan-editor-runtime.ts:9023-9039, 10213-10253` —
`_openSettingsDialog`/`_saveSettingsDialog` уже хранят `sunRays` по схеме
«явный `true` пишется, falsy удаляется» через `settings: any = {...cfg.settings}`
— подтверждает, что план для `show_room_tooltip` (обратная схема: default
`true` → удаление ключа, `false` → явная запись) технически реализуем тем
же путём, а не гипотетичен.
- `src/houseplan-editor-runtime.ts:10537-10538` — `gs.hint` действительно
первая строка тела диалога — подтверждает «сразу после вводного текста».
- `src/houseplan-editor-runtime.ts:10622-10626` — `<label class="srcrow">`
оборачивает `_boolInput()` и `<span>` для `sunRays` — подтверждает
«доступное имя из видимой строки» как существующий, а не изобретённый
паттерн.
- `custom_components/houseplan/validation.py:1917-1968` — `CONFIG_SCHEMA.settings`
уже принимает `sun_rays: bool` рядом с `extra=vol.ALLOW_EXTRA` —
подтверждает «explicit boolean для нового известного ключа» как
существующий шаблон.
- `custom_components/houseplan/support_package.py:139-140` — `_global_settings`
— явный allowlist `_copy_keys(...)`, куда `show_room_tooltip` предстоit
добавить — подтверждает claim о privacy-проекции.
- `src/space-card.ts:2` — докстринг «READ-ONLY, static schematic» —
подтверждает «`houseplan-space-card` не меняется».
- `scripts/config-field-registry.mjs:26-29` — `sun_rays` (обычное
экспонированное поле, не compatibility-долг) в реестре не числится, только
`show_all`; отсутствие записи о новом поле в `CONFIG-COMPATIBILITY.md`
реестре — верное решение по прецеденту, не пропуск.
- `src/i18n/{en,de,fr,ru}.json` — ровно четыре синхронных словаря,
подтверждает «четыре» без лишнего/недостающего языка.
6. Дубликаты: `gh issue view` для #79 (closed, регрессия заливки), #154
(closed, touch hover sticking), #196 (closed, содержимое тултипа), #152
(open, но про клик/тап-зум, не про информационное окно) — ни один не
пересекается с «отключить только информационное окно».
Материал полный, дельты нет — это первый заход (r1), раздел «Унаследовано из
r0» не применим.
## Находки
Нет High. Нет Medium. Нет Low.
Все девять AC (AC1–AC9) сформулированы как проверяемые утверждения с указанным
способом доказательства (`unit`/`smoke`/`backend`/`commands`), не пересекаются
по ответственности и в сумме покрывают контракт из разделов 1–3: UI/default,
persistence, tooltip off, default parity, hover/device independence,
pointer/mode parity, compatibility/privacy, i18n/docs/release, гейты/бюджет.
План автотестов называет конкретные мутационные пробы для каждого AC (замена
resolver на `Boolean(value)`, guard внутри общего `_showTip()`, скрытие
`_hoverRoom`, хранение `true` вместо удаления ключа, отсутствие очистки
`.tip` при Save) — то есть тесты, которые способны упасть, а не тавтологичные
проверки. Раздел «Принятые предположения» отделяет то, что решил владелец
(место, формулировка, default), от того, что решает автор технически
(ключ конфига, точное место переключателя в вёрстке диалога, отсутствие
per-space override) — ни одна догадка не выдана за факт без пометки.
Продуктовая рамка (SCOPE.md job J1, View как продукт для двух персон из трёх,
инвариант «не трогать другие интерактивные эффекты комнаты» из тела issue)
соблюдена явно разделами «Скоуп»/«Не-скоуп»/«Контракт поведения» и не
противоречит ни одному пункту `docs/UX-MODES.md`/`docs/TOUCH-SUPPORT.md`.
## Что проверено и корректно
- Обязательные разделы §7.1 присутствуют все, включая оба продуктовых
(сценарий; что человек увидит до/после) первыми.
- AC пронумерованы, у каждого явно назван способ доказательства.
- i18n: ключ `gs.show_room_tooltip`, все четыре словаря, формулировки заданы.
- Совместимость: optional boolean, `ALLOW_EXTRA` уже покрывает старый
backend/frontend в обе стороны, миграции store/model version нет и не
требуется — согласуется с `docs/CONFIG-COMPATIBILITY.md` (эта настройка не
compatibility-долг, а обычное новое поле, как `sun_rays`).
- Откат описан явно и без требования флага Labs (persisted boolean — сам себе
выключатель), с уточнением, что произойдёт при вынужденном отступлении.
Требование `docs/PROCESS.md` §2.5 «откат: как выключить или вернуть назад»
выполнено содержательно, а не формальной строкой.
- Release-артефакты называют оба changelog, User Guide EN/RU,
`UX-MODES.md`/`TOUCH-SUPPORT.md`/`CONFIG-COMPATIBILITY.md`, осведомлённость
о доокументационном скриншотном гейте (`check-docs.mjs`/source fingerprint)
и обоснованный отказ от нового golden-сценария (default View визуально не
меняется, диалог общих настроек не входит в принятые golden-сценарии).
- Риски перечислены с конкретной привязкой к AC, который их снимает —
не общие фразы.
## Чего не проверял
- Существование и корректность самого кода реализации — на этапе `spec` кода
ещё нет по правилу (ТЗ пишется до кода), проверять нечего.
- Гейты `typecheck`/`test`/`build`/`no-new-any`/`check-docs`/backend pytest —
не запускались: это гейты реализации (`S6-in-progress`) и код-ревью
(`S7-code-review`), а не ревью ТЗ. Zeленый Validate на `587c2069`
(https://github.com/Matysh/houseplan-card/actions/runs/33720335555)
проверяет только документационный коммит этого SHA, не имеет отношения к
ненаписанному коду issue #426.
- Мутационные пробы плана автотестов не выполнялись физически (кода нет) —
оценена только их логическая способность различить дефект от корректной
реализации, что и требуется от ревью ТЗ.
## Материал раунда
- Ветка: `issue/426-room-hover-tooltip-toggle`
- SHA: `587c20699c4bc1ada405c5c7f9775d34b2b78a39`
- ТЗ: `docs/specs/426-room-hover-tooltip-toggle.md` (та же ревизия)
## Вердикт
**Зелёный.** Готово к переходу в `S5-ready`. High: 0 · Medium: 0.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/426-room-hover-tooltip-toggle`, коммит `587c20699c4b` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `ed72e6cb924cd5e6167c04ba95a73fc0f0bf649d`
```
git log --all --format='%H %T' | grep ed72e6cb924c
```
- ТЗ `docs/specs/426-room-hover-tooltip-toggle.md`, блоб `35a3ad7ff77cf1597f004b94b0799844b56f29f1`
```
git log --all --find-object=35a3ad7ff77cf1597f004b94b0799844b56f29f1 -- docs/specs/426-room-hover-tooltip-toggle.md
```
+313
View File
@@ -0,0 +1,313 @@
# Issue #426 — отключение информационного окна комнаты при наведении
- **Issue:** https://github.com/Matysh/houseplan-card/issues/426
- **Приоритет / тип:** P2 · feature / polish
- **Область:** Общие настройки, View/киоск, room hover, global config,
backend validation/privacy projection, i18n, документация и QA
- **Связи:** #79 (room hover), #154 (pointer modality), #196 (содержимое
тултипа)
- **Ревизия:** 2 (2026-09-03; размещение editor-only строк уточнено по
результату bundle-budget без изменения поведения)
## Сценарий
Администратор использует насыщенный план, на котором информационное окно
комнаты при движении мыши перекрывает полезную часть сцены или просто не нужно.
В **Общих настройках** он выключает опцию **«Показывать окно с информацией при
наведении на комнату»**. После сохранения комнаты по-прежнему подсвечиваются
при наведении, но окно с названием, площадью, температурой, влажностью и LQI не
появляется. Настройка действует на все пространства этой конфигурации и на
других экранах после загрузки конфигурации.
## Что человек увидит до и после
**До:** mouse-hover комнаты всегда показывает информационное окно; отключить
его отдельно от hover-подсветки нельзя.
**После:** в начале диалога «Общие настройки» есть включённый по умолчанию
переключатель. В выключенном состоянии исчезает только информационное окно
комнаты. Подсветка комнаты, тултипы устройств и все действия остаются прежними.
## Проблема и подтверждение по коду
Комната создаёт локальный обработчик `tip` в `src/houseplan-card.ts`; каждое из
пяти представлений геометрии комнаты (`path`/`polygon`/`rect`) передаёт его в
`@pointermove`. Обработчик в View безусловно вычисляет clean-floor area и
вызывает общий `_showTip()`. Сам `_showTip()` правильно проверяет mouse-hover,
pointer modality и drag, но не различает пользовательское намерение показать
или скрыть room-tooltip.
Hover-подсветка уже имеет независимое состояние `_hoverRoom` и отдельные SVG
слои `_renderRoomHoverFill()` / `_renderRoomHoverOutline()`. Поэтому требование
можно выполнить без отключения подсветки и без изменения геометрии комнаты.
Диалог общих настроек и его сохранение принадлежат lazy
`src/houseplan-editor-runtime.ts`; соответствующего draft/config-поля сейчас
нет.
## Скоуп
- Один глобальный переключатель с заданной владельцем русской строкой в
«Общих настройках» и эквивалентами EN/DE/FR.
- Optional boolean `settings.show_room_tooltip` с default `true` при отсутствии
или невалидном legacy/future значении.
- Отключение только room-tooltip во всех пространствах полного
`houseplan-card`, включая View в kiosk-конфигурации на hover-capable экране.
- Сохранение прежней hover-подсветки комнаты и всех некомнатных тултипов.
- Backend validation, безопасная support-package проекция, compatibility docs,
пользовательские руководства, changelog и тестовый контракт.
- Немедленное исчезновение уже показанного room-tooltip после успешного
сохранения выключенного значения.
## Не-скоуп
- Отключение или изменение hover-подсветки комнаты.
- Отключение тултипов устройств, проёмов, кнопок, Help affordance или иных
поверхностей.
- Изменение состава, порядка строк, позиции или оформления информационного
окна комнаты.
- Перенос настройки на уровень пространства/комнаты, локальное значение на
экран или отдельная настройка для kiosk.
- Изменение touch/pen-контракта: на этих указателях transient hover и сейчас не
появляется.
- Интерактивность `houseplan-space-card`: эта карточка остаётся статической и
уже не имеет hover/tooltips.
- Миграция store/model version или переписывание существующих конфигураций при
чтении.
## Контракт поведения
### 1. Общие настройки
1. Сразу после существующей вводной строки диалога находится обычная строка с
переключателем **«Показывать окно с информацией при наведении на комнату»**.
Новая секция, help-иконка и дополнительное предупреждение не нужны.
2. При открытии диалога значение draft равно `true`, если
`settings.show_room_tooltip` не является точным boolean `false`; точный
`false` открывается выключенным.
3. Переключение меняет только draft. Cancel, Escape и закрытие по правилам
диалога не меняют серверную конфигурацию и текущее поведение.
4. Save с выключенной опцией записывает точный
`settings.show_room_tooltip: false`. Save с включённой опцией удаляет ключ,
материализуя default отсутствием, а не `true`.
5. После успешного Save результат действует сразу, без reload. Ошибка записи
оставляет диалог открытым и не выдаёт несохранённый draft за применённое
значение по существующему контракту общих настроек.
### 2. Room hover в View
1. Единственный resolver читает настройку так: только точный `false` означает
«не показывать»; отсутствие, `null`, строка, число и будущая повреждённая
форма fail-safe проецируются в нынешнее `true`.
2. При effective `true` mouse-hover комнаты побайтово/семантически сохраняет
текущий путь: title, clean-floor area, temperature, humidity, LQI, позиция и
обновление вслед за указателем не меняются.
3. При effective `false` pointer enter/move по комнате не создаёт и не рисует
`.tip`, а также не выполняет отложенное вычисление `_roomArea()` только ради
скрытого окна.
4. Независимый `_hoverRoom` продолжает устанавливаться на pointer enter;
`room-hover-fill-layer` и `room-hover-outline-layer` остаются видимыми по
прежним правилам. Pointer leave и все lifecycle/mode/space очистки transient
hover не меняются.
5. Device tooltip продолжает использовать общий `_showTip()` в View и
Редакторе устройств независимо от `show_room_tooltip`. Настройка не может
стать глобальным запретом `.tip`.
6. Успешное сохранение `false` очищает возможное текущее room-tooltip. Повторное
включение не создаёт окно само: оно появляется при следующем настоящем
mouse move над комнатой.
### 3. Режимы и указатели
- Контракт применяется только к комнате в View. Plan, Devices и Background не
получают нового hover-поведения.
- На hover-capable desktop и в kiosk с мышью используется одно global значение.
- Touch/pen и compatibility mouse events продолжают подавляться
`PointerModalityController` независимо от настройки.
- Pan, pinch, drag, click/tap, room-card link, device actions и keyboard paths
не меняются.
## UX и i18n
Добавить ключ `gs.show_room_tooltip` во все четыре синхронизированных lazy
editor-словаря `src/i18n/support/{en,ru,de,fr}.json`. Существующую editor-only
строку `gs.hint` перенести туда же, чтобы новый control не увеличивал initial
View graph:
- RU: `Показывать окно с информацией при наведении на комнату`;
- EN: `Show the room information window on hover`;
- DE: `Rauminformationen beim Darüberfahren anzeigen`;
- FR: `Afficher les informations de la pièce au survol`.
Используется существующий `_boolInput()` и класс строки общих настроек. Control
имеет доступное имя из видимой строки; отдельные tooltip/help и aria-only ключи
не добавляются. Порядок остальных контролов не меняется.
## Модель данных, миграция и совместимость
### Frontend
- `ServerConfig.settings` получает optional
`show_room_tooltip?: boolean`.
- Pure resolver (рабочее имя `showRoomTooltipOf`) является единственным
источником default для runtime и draft диалога.
- `_settingsDialog` получает boolean `showRoomTooltip`; draft не читается
напрямую из UI DOM при Save.
- Model/store version не меняется. Загрузка не материализует default и не
создаёт запись.
### Backend и support package
- `CONFIG_SCHEMA.settings` явно принимает только boolean для нового известного
ключа. `extra=ALLOW_EXTRA` сохраняется для forward compatibility остальных
полей.
- Privacy projection support package переносит только нормализованный boolean
`show_room_tooltip`, без новых пользовательских данных.
- Полный backup/export/import уже переносит global settings как часть config;
отдельный remap или envelope field не нужен.
### Mixed version и downgrade
| Frontend | Backend | Поведение |
|---|---|---|
| old | new | Новый известный boolean игнорируется старой карточкой; room-tooltip показывается, конфиг не повреждается |
| new | old | Старый backend сохраняет поле через существующий `ALLOW_EXTRA`; новая карточка применяет `false` |
| new | new | Выключенное значение сохраняется и подавляет только room-tooltip |
При downgrade пользователь временно снова увидит room-tooltip, но значение
`false` остаётся в конфиге и восстановит поведение после возврата новой версии.
Это мягкая деградация; data migration и блокировка смешанных версий не нужны.
## Затронутые файлы и модули
- `src/types.ts`, `src/logic.ts`, `src/houseplan-card.ts`,
`src/houseplan-editor-runtime.ts`.
- `src/i18n/support/{en,ru,de,fr}.json`, `src/i18n/support.ts`.
- `custom_components/houseplan/validation.py`,
`custom_components/houseplan/support_package.py`.
- `test/logic.test.mjs`, settings/source contract tests,
`tests_backend/test_validation.py`, support-package tests и целевой browser
smoke для room-tooltip/general settings.
- `docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md`, `docs/UX-MODES.md`,
`docs/TOUCH-SUPPORT.md`, `docs/CONFIG-COMPATIBILITY.md`, при необходимости
`docs/TESTING.md`, оба changelog.
- Собранные `dist/**` и `custom_components/houseplan/frontend/**` по обычному
bundle-контракту.
## Критерии приёмки
- **AC1 — UI и default (unit + smoke).** В общих настройках есть ровно один
переключатель с локализованным именем; новый и legacy config без поля
открывает его включённым, точный `false` — выключенным.
- **AC2 — persistence (unit + smoke + backend).** Save выключенного draft
записывает boolean `false`; reopen/reload сохраняют его. Save включённого
draft удаляет ключ. Cancel/Escape не пишут, backend принимает оба canonical
состояния и отклоняет не-boolean новое значение.
- **AC3 — tooltip off (unit + smoke).** При `false` настоящий mouse move над
комнатой не создаёт `.tip`, не вызывает `_roomArea()`, и уже видимое окно
исчезает после успешного Save.
- **AC4 — default parity (smoke).** При absent/`true` room-tooltip сохраняет
нынешние title, area, temperature, humidity, LQI, positioning и движение за
указателем.
- **AC5 — hover/device independence (smoke).** В обоих состояниях остаются
room fill/outline hover layers; при `false` device tooltip в View/Devices
по-прежнему появляется и содержит прежние данные.
- **AC6 — pointer/mode parity (unit + smoke).** Touch/pen и synthetic
compatibility mouse не создают room-tooltip; View/kiosk с настоящей мышью
следуют опции; editor modes, pan/pinch/click и room-card действия не меняются.
- **AC7 — compatibility/privacy (backend + unit).** Full config round-trip и
support projection сохраняют нормализованный boolean; отсутствие ключа не
материализуется; mixed-version/downgrade ведут себя по таблице без изменения
model/store version.
- **AC8 — i18n/docs/release (unit + docs gate).** EN/RU/DE/FR имеют parity,
оба руководства и UX/touch/compatibility docs описывают границу опции, а оба
changelog получают пользовательскую запись в том же коммите.
- **AC9 — гейты и бюджет (commands).** В цикле реализации проходят
`npx tsc --noEmit`, `npm test`, `npm run build`, целевой backend pytest,
`no-new-any`, docs check и выбранный browser smoke. Default-кадры golden не
меняются; initial/editor gzip остаются в текущих бюджетах.
## План автотестов
- Табличный unit для resolver: missing/`undefined`/`null`/invalid/`true` →
`true`, только boolean `false` → `false`.
- Source/dialog contract: draft инициализируется resolver-ом; `false` пишется,
`true` удаляется; ключ UI присутствует во всех локалях.
- Backend pytest: `false`/`true` проходят, строка/число отклоняются; support
projection содержит boolean и не копирует невалидную форму.
- Новый/расширенный Playwright smoke: открыть общие настройки, проверить
default, Cancel, сохранить `false`, reopen/reload, подвигать реальную мышь над
комнатой и устройством, проверить room hover layers и `.tip`, вернуть `true`
и проверить восстановление текущего содержимого room-tooltip.
- Мутационные доказательства: заменить resolver на `Boolean(value)` — падает
AC1/AC4; поставить guard внутри общего `_showTip()` — падает device-часть
AC5; скрыть `_hoverRoom` вместе с tooltip — падает AC5; хранить `true` —
падает AC2/AC7; не очищать текущее окно при Save — падает AC3.
## Release-артефакты
- Пользовательская запись в `docs/CHANGELOG.md` и
`docs/CHANGELOG.ru.md` в product-коммите.
- Обновлённые EN/RU User Guide, `docs/UX-MODES.md`,
`docs/TOUCH-SUPPORT.md` и `docs/CONFIG-COMPATIBILITY.md`.
- Изменение нового control доказывает целевой browser smoke; отдельный новый
golden не нужен, потому что default View визуально не меняется и диалог
общих настроек не является принятым golden-сценарием.
- Любая правка `src/**` обновляет source fingerprint документационных
скриншотов через каноническую приёмку; неожиданный raster diff блокирует
завершение задачи.
- Performance/security artifacts не добавляются: один boolean resolver на
room pointermove не создаёт frame-loop или сетевого пути; штатные bundle
budget и prerelease performance gates остаются обязательными.
## Производительность и безопасность
Resolver выполняет одну строгую boolean-проверку до вычисления площади; при
выключенной опции работа на pointermove уменьшается. Он не входит в render loop
и не меняет геометрию, кэши или сетевые запросы. Новый boolean не содержит
персональных данных; support package переносит его только как allowlisted
presentation preference.
## Риски
- **Случайно выключить device tooltip.** `_tip` общий для комнат и устройств;
guard в `_showTip()` был бы слишком широким. Снимается room-specific guard и
независимой проверкой device tooltip в AC5.
- **Сломать default старых конфигов.** `Boolean(undefined)` дал бы `false`.
Снимается pure resolver-ом «только точный false выключает» и таблицей AC1.
- **Показать несохранённый draft как live-настройку.** Диалог редактирует копию,
поэтому runtime читает только server config, а не `_settingsDialog`; Cancel и
failure проверяются AC2.
- **Оставить уже видимый tooltip после выключения.** Пассивный pointer может не
дать нового события. Успешный Save явно очищает transient tip, AC3 фиксирует
это поведение.
- **Потерять `false` в mixed-version цикле.** Старый backend сохраняет unknown
settings, а old frontend не должен реконструировать весь settings-объект без
spread. Compatibility-таблица и round-trip AC7 делают границу явной.
- **Добавить скрытую стоимость pointermove.** Guard ставится до `_roomArea()` и
climate/LQI чтений; bundle/performance gates подтверждают отсутствие роста
горячего пути.
## Откат
Feature flag не нужен: persisted boolean уже сам является выключателем. Для
аварийного продуктового отката удаляются строка UI и room-specific runtime
guard, но backend acceptance/type и support projection временно сохраняются.
Старый runtime безопасно вернёт прежний всегда-включённый tooltip, а сохранённые
`false` не повредятся и не заблокируют config writes.
Если поле требуется убрать окончательно, отдельная проверяемая data-fix удаляет
`settings.show_room_tooltip` из сохранённых конфигураций; только после этого
можно убрать явный schema/support contract. Повышать model/store version или
переписывать все конфиги для обычного rollback нельзя.
## Принятые предположения
- «Общие настройки» означает одно server-persisted значение для всей
конфигурации, а не per-space/per-room/localStorage.
- Отключается только информационное окно комнаты; hover wash/outline остаются.
- Опция действует и в kiosk при наличии настоящей мыши; touch-only kiosk уже не
показывает hover по текущему контракту.
- Переключатель расположен сразу после вводного текста диалога, без новой
секции, help и предупреждения.
- Канонический ключ — `settings.show_room_tooltip`; default хранится отсутствием,
а явное значение требуется только для `false`.
- `houseplan-space-card` не меняется, потому что уже не имеет интерактивности.
+1
View File
@@ -164,6 +164,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#294](https://github.com/Matysh/houseplan-card/issues/294) Esc завершает текущую цепочку стен без удаления геометрии | [294-wall-esc-detach.md](294-wall-esc-detach.md) |
| [#419](https://github.com/Matysh/houseplan-card/issues/419) Безопасная уборка Area-снапшота при пустом или усечённом HA-реестре | [419-area-snapshot-roster-guard.md](419-area-snapshot-roster-guard.md) |
| [#421](https://github.com/Matysh/houseplan-card/issues/421) Отрицательные доказательства для трёх защитных проверок | [421-negative-test-proofs.md](421-negative-test-proofs.md) |
| [#426](https://github.com/Matysh/houseplan-card/issues/426) Отключение информационного окна комнаты при наведении | [426-room-hover-tooltip-toggle.md](426-room-hover-tooltip-toggle.md) |
## P3
+4
View File
@@ -407,6 +407,10 @@
"opaque": "<function _north_deg>",
"required": false
},
"config.settings.show_room_tooltip": {
"required": false,
"type": "bool"
},
"config.settings.sun_rays": {
"required": false,
"type": "bool"
+7
View File
@@ -27,6 +27,13 @@
/** @type {SmokeLink[]} */
export const SMOKE_LINKS = [
{
symbols: ['showRoomTooltipOf'],
smokes: ['smoke_room_tooltip_toggle.mjs'],
because: 'the production-bundle scenario proves the global-settings draft and persistence, '
+ 'room-only suppression before area work, immediate stale-room-tip cleanup, preserved room '
+ 'highlight/device tooltip and default-on restoration (#426)',
},
{
symbols: [
'applyDevicePlacement', 'devicePlacement', 'sameDevicePlacement',
+21 -32
View File
@@ -39,7 +39,7 @@ import {
spaceDisplayOf, resolveEffectiveRoomFill, fillColorsOf, DEFAULT_FILL_COLORS,
customFillOf, roomCustomFillOf, DEFAULT_CUSTOM_FILL,
type FillColors, type FillColorEntry, type ResolvedRoomFill, runServiceFor, RUN_TARGET_DOMAINS,
DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY, stageBgOf,
DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY, stageBgOf, showRoomTooltipOf,
DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay,
referencedContentUrls,
DISPLAY_MODES, TAP_ACTIONS, SPACE_FILL_UI_MODES, ROOM_FILL_MODES,
@@ -1026,6 +1026,7 @@ export class HouseplanCard extends LitElement {
lqi?: number | null;
temp?: number | null;
hum?: number | null;
room?: boolean;
} | null = null;
/** Room whose physical perimeter is highlighted in View. The explicit
* overlay is needed because thick wall bodies paint above room shapes. */
@@ -2219,6 +2220,7 @@ export class HouseplanCard extends LitElement {
colors: FillColors; glowRadius: number; bgColor: string | null;
/** sun on the plan (docs/SUN.md) */
northDeg: number | null; bgMode: 'static' | 'daynight'; sunRays: boolean;
showRoomTooltip: boolean;
busy: boolean;
} | null = null;
private _supportDialog: SupportDialogState | null = null;
@@ -7286,11 +7288,12 @@ export class HouseplanCard extends LitElement {
lqi?: number | null,
temp?: number | null,
hum?: number | null,
room = false,
): void {
this._notePointer(ev);
if (!this._pointerModality.hoverEnabled) return;
if (this._drag || this._deviceDrag) return;
this._tip = { x: ev.clientX, y: ev.clientY, title, meta, lqi, temp, hum };
this._tip = { x: ev.clientX, y: ev.clientY, title, meta, lqi, temp, hum, room };
}
// ================= ROOM MARKUP EDITOR =================
@@ -11577,6 +11580,10 @@ export class HouseplanCard extends LitElement {
let areaText: string | null | undefined;
const tip = (e: PointerEvent) => {
if (this._mode !== 'view') return;
if (!showRoomTooltipOf(this._settings)) {
if (this._tip?.room) this._tip = null;
return;
}
if (areaText === undefined) areaText = this._roomArea(r);
this._showTip(
e,
@@ -11585,8 +11592,15 @@ export class HouseplanCard extends LitElement {
showLqi ? this._roomLqi(r.area) : null,
this._roomTemp(r),
this._roomHum(r),
true,
);
};
const enterRoom = (event: PointerEvent) => {
this._notePointer(event);
if (this._pointerModality.hoverEnabled) {
this._hoverRoom = { space: space.id, room: r };
}
};
const myPoly = polyOf(r);
// A room's ordinary solid stroke must not run beneath its zero
// wall overlay — suppress it and draw a trimmed outline. This
@@ -11623,57 +11637,32 @@ export class HouseplanCard extends LitElement {
? svg`<path class="${cls}" style="${style}" fill-rule="evenodd"
data-hp="room" data-id=${hpId} data-area=${hpArea}
d="${[obstaclePath, ...holes.map(pathD)].join(' ')}"
@pointerenter=${(event: PointerEvent) => {
this._notePointer(event);
if (this._pointerModality.hoverEnabled) {
this._hoverRoom = { space: space.id, room: r };
}
}}
@pointerenter=${enterRoom}
@pointermove=${tip}
@pointerleave=${() => this._clearTransientHover()}></path>`
: holes.length && fillPoly
? svg`<path class="${cls}" style="${style}" fill-rule="evenodd"
data-hp="room" data-id=${hpId} data-area=${hpArea}
d="${[fillPoly, ...holes].map(pathD).join(' ')}"
@pointerenter=${(event: PointerEvent) => {
this._notePointer(event);
if (this._pointerModality.hoverEnabled) {
this._hoverRoom = { space: space.id, room: r };
}
}}
@pointerenter=${enterRoom}
@pointermove=${tip}
@pointerleave=${() => this._clearTransientHover()}></path>`
: fillPoly && fillPoly !== myPoly
? svg`<polygon class="${cls}" style="${style}" points="${fillPoly.map((p) => p.join(',')).join(' ')}"
data-hp="room" data-id=${hpId} data-area=${hpArea}
@pointerenter=${(event: PointerEvent) => {
this._notePointer(event);
if (this._pointerModality.hoverEnabled) {
this._hoverRoom = { space: space.id, room: r };
}
}}
@pointerenter=${enterRoom}
@pointermove=${tip}
@pointerleave=${() => this._clearTransientHover()}></polygon>`
: r.poly
? svg`<polygon class="${cls}" style="${style}" points="${r.poly.map((p) => p.join(',')).join(' ')}"
data-hp="room" data-id=${hpId} data-area=${hpArea}
@pointerenter=${(event: PointerEvent) => {
this._notePointer(event);
if (this._pointerModality.hoverEnabled) {
this._hoverRoom = { space: space.id, room: r };
}
}}
@pointerenter=${enterRoom}
@pointermove=${tip}
@pointerleave=${() => this._clearTransientHover()}></polygon>`
: svg`<rect class="${cls}" style="${style}"
data-hp="room" data-id=${hpId} data-area=${hpArea}
x="${r.x}" y="${r.y}" width="${r.w}" height="${r.h}" rx="${Math.min(r.w!, r.h!) * 0.03}"
@pointerenter=${(event: PointerEvent) => {
this._notePointer(event);
if (this._pointerModality.hoverEnabled) {
this._hoverRoom = { space: space.id, room: r };
}
}}
@pointerenter=${enterRoom}
@pointermove=${tip}
@pointerleave=${() => this._clearTransientHover()}></rect>`;
const trimmed = edgeCuts.length && myPoly
+19 -6
View File
@@ -33,7 +33,7 @@ import {
spaceDisplayOf, resolveEffectiveRoomFill, fillColorsOf, DEFAULT_FILL_COLORS,
customFillOf, roomCustomFillOf, DEFAULT_CUSTOM_FILL,
type FillColors, type FillColorEntry, type ResolvedRoomFill, runServiceFor, RUN_TARGET_DOMAINS,
DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY, stageBgOf,
DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY, stageBgOf, showRoomTooltipOf,
DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay,
referencedContentUrls,
DISPLAY_MODES, TAP_ACTIONS, SPACE_FILL_UI_MODES, ROOM_FILL_MODES,
@@ -1112,8 +1112,8 @@ export interface HouseplanEditorHostPort {
_sentPos: Map<string, DeviceLayout[string] | null>;
_serverCfg: ServerConfig | null;
_serverStorage: boolean;
_settings: { exclude_integrations?: string[]; group_lights?: boolean; show_all?: boolean; filter_seeded?: boolean; icon_rules?: { pattern: string; icon: string; }[]; };
_settingsDialog: { colors: FillColors; glowRadius: number; bgColor: string | null; northDeg: number | null; bgMode: "static" | "daynight"; sunRays: boolean; busy: boolean; } | null;
_settings: { exclude_integrations?: string[]; group_lights?: boolean; show_all?: boolean; filter_seeded?: boolean; icon_rules?: { pattern: string; icon: string; }[]; show_room_tooltip?: boolean; };
_settingsDialog: { colors: FillColors; glowRadius: number; bgColor: string | null; northDeg: number | null; bgMode: "static" | "daynight"; sunRays: boolean; showRoomTooltip: boolean; busy: boolean; } | null;
_supportDialog: SupportDialogState | null;
_showAll: boolean;
_showHidden: boolean;
@@ -1134,7 +1134,7 @@ export interface HouseplanEditorHostPort {
_t: (key: I18nKey, vars?: Record<string, string | number>) => string;
_undoDevicePosition: () => void;
_thickWallCuts: () => number[][];
_tip: { x: number; y: number; title: string; meta: string; lqi?: number | null; temp?: number | null; hum?: number | null; } | null;
_tip: { x: number; y: number; title: string; meta: string; lqi?: number | null; temp?: number | null; hum?: number | null; room?: boolean; } | null;
_toggleConfirmationLines: (intent: ResolvedToggleIntent) => string[];
_toggleConfirmationStateText: (target: ResolvedToggleTarget) => string;
_toggleIntent: (device: DevItem, devices?: readonly DevItem[]) => ResolvedToggleIntent | null;
@@ -9034,6 +9034,7 @@ public _openSettingsDialog = (): void => {
northDeg: northDegOf(this.host._settings, {}),
bgMode: bgModeOf(this.host._settings, {}),
sunRays: sunRaysOn(this.host._settings, {}),
showRoomTooltip: showRoomTooltipOf(this.host._settings),
busy: false,
};
};
@@ -10233,12 +10234,15 @@ public _updateDecorStyle(next: DecorStyle): void {
settings.bg_mode = d.bgMode;
if (d.sunRays) settings.sun_rays = true;
else delete settings.sun_rays;
if (d.showRoomTooltip) delete settings.show_room_tooltip;
else settings.show_room_tooltip = false;
// Legacy compatibility: old configs may still contain this accepted
// field, but weather no longer affects sunlight and the UI no longer
// exposes it. Saving general settings cleans the obsolete value up.
delete settings.weather_entity;
this.host._serverCfg = { ...cfg, settings };
await this._saveConfigNow();
if (!d.showRoomTooltip && this.host._tip?.room) this.host._tip = null;
this.host._settingsDialog = null;
this.host.requestUpdate();
this.host._showToast(this.host._t('gs.saved'));
@@ -10534,7 +10538,16 @@ public _renderSettingsDialog(): TemplateResult {
return html`<hp-dialog .hass=${this.host.hass} .title=${this.host._t('gs.title')} icon="mdi:cog-outline" wide
@hp-close=${() => (this.host._settingsDialog = null)}>
<div class="body">
<div class="rhint">${this.host._t('gs.hint')}</div>
<div class="rhint">${supportT(
langOf(this.host.hass, this.host._config?.language), 'gs.hint',
)}</div>
<label class="srcrow">
${this._boolInput(this.host._settingsDialog!.showRoomTooltip, (v) =>
(this.host._settingsDialog = { ...this.host._settingsDialog!, showRoomTooltip: v }))}
<span>${supportT(
langOf(this.host.hass, this.host._config?.language), 'gs.show_room_tooltip',
)}</span>
</label>
<label class="dispsection">${this.host._t('gs.light_group')}</label>
${this._renderColorRow('light_on', 'gs.light_on')}
${this._renderColorRow('light_off', 'gs.light_off')}
@@ -10660,7 +10673,7 @@ public _renderSettingsDialog(): TemplateResult {
</div>
<div class="row" slot="footer">
<button class="btn ghost" @click=${() =>
(this.host._settingsDialog = { ...this.host._settingsDialog!, colors: JSON.parse(JSON.stringify(DEFAULT_FILL_COLORS)), glowRadius: this.host._imperial ? 9.8 : 3, bgColor: null, northDeg: null, bgMode: 'daynight', sunRays: false })}>
(this.host._settingsDialog = { ...this.host._settingsDialog!, colors: JSON.parse(JSON.stringify(DEFAULT_FILL_COLORS)), glowRadius: this.host._imperial ? 9.8 : 3, bgColor: null, northDeg: null, bgMode: 'daynight', sunRays: false, showRoomTooltip: true })}>
${this.host._t('gs.reset')}
</button>
<span class="spacer"></span>
-1
View File
@@ -422,7 +422,6 @@
"marker.sub_entity": "Entität",
"title.general_settings": "Allgemeine Einstellungen",
"gs.title": "Allgemeine Einstellungen",
"gs.hint": "Füllfarben gelten für jeden Bereich; jede Farbe hat ihre eigene Deckkraft. Der Füllmodus wird in den Einstellungen des jeweiligen Bereichs festgelegt.",
"gs.light_group": "Füllung: Licht",
"gs.light_on": "Lichter an",
"gs.light_off": "Alle Lichter aus",
-1
View File
@@ -422,7 +422,6 @@
"marker.sub_entity": "entity",
"title.general_settings": "General settings",
"gs.title": "General settings",
"gs.hint": "Fill colors apply to every space; each color has its own opacity. Which fill mode a space uses is set in that space's dialog.",
"gs.light_group": "Fill: lights",
"gs.light_on": "Lights on",
"gs.light_off": "All lights off",
-1
View File
@@ -422,7 +422,6 @@
"marker.sub_entity": "entité",
"title.general_settings": "Paramètres généraux",
"gs.title": "Paramètres généraux",
"gs.hint": "Les couleurs de remplissage s’appliquent à tous les espaces ; chaque couleur possède sa propre opacité. Le mode de remplissage utilisé par un espace se règle dans la boîte de dialogue de cet espace.",
"gs.light_group": "Remplissage : lumières",
"gs.light_on": "Lumières allumées",
"gs.light_off": "Toutes les lumières éteintes",
-1
View File
@@ -422,7 +422,6 @@
"marker.sub_entity": "сущность",
"title.general_settings": "Общие настройки",
"gs.title": "Общие настройки",
"gs.hint": "Цвета заливок действуют на все пространства; у каждого цвета своя прозрачность. Какой режим заливки использует пространство — задаётся в его диалоге.",
"gs.light_group": "Заливка: освещение",
"gs.light_on": "Свет включён",
"gs.light_off": "Весь свет выключен",
+2 -2
View File
@@ -1,4 +1,4 @@
/** Lazy Help & feedback dictionaries owned by the editor runtime (#423). */
/** Lazy editor-only dictionaries owned by the editor runtime (#423). */
import { subst } from '../logic';
import type { Lang } from './registry';
import de from './support/de.json' with { type: 'json' };
@@ -15,7 +15,7 @@ const SUPPORT_DICTIONARIES: Record<Lang, Record<SupportI18nKey, string>> = {
fr,
};
/** Translate form-only support copy with the same synchronous English fallback. */
/** Translate editor-only copy with the same synchronous English fallback. */
export function supportT(
lang: Lang,
key: SupportI18nKey,
+2
View File
@@ -1,4 +1,6 @@
{
"gs.hint": "Füllfarben gelten für jeden Bereich; jede Farbe hat ihre eigene Deckkraft. Der Füllmodus wird in den Einstellungen des jeweiligen Bereichs festgelegt.",
"gs.show_room_tooltip": "Rauminformationen beim Darüberfahren anzeigen",
"support.about_group": "Über die Karte",
"support.guide_group": "Dokumentation",
"support.guide": "Benutzerhandbuch",
+2
View File
@@ -1,4 +1,6 @@
{
"gs.hint": "Fill colors apply to every space; each color has its own opacity. Which fill mode a space uses is set in that space's dialog.",
"gs.show_room_tooltip": "Show the room information window on hover",
"support.about_group": "About the card",
"support.guide_group": "Documentation",
"support.guide": "User guide",
+2
View File
@@ -1,4 +1,6 @@
{
"gs.hint": "Les couleurs de remplissage s’appliquent à tous les espaces ; chaque couleur possède sa propre opacité. Le mode de remplissage utilisé par un espace se règle dans la boîte de dialogue de cet espace.",
"gs.show_room_tooltip": "Afficher les informations de la pièce au survol",
"support.about_group": "À propos de la carte",
"support.guide_group": "Documentation d’aide",
"support.guide": "Guide utilisateur",
+2
View File
@@ -1,4 +1,6 @@
{
"gs.hint": "Цвета заливок действуют на все пространства; у каждого цвета своя прозрачность. Какой режим заливки использует пространство — задаётся в его диалоге.",
"gs.show_room_tooltip": "Показывать окно с информацией при наведении на комнату",
"support.about_group": "О карточке",
"support.guide_group": "Документация",
"support.guide": "Руководство пользователя",
+7
View File
@@ -1351,6 +1351,13 @@ export function stageBgOf(settings: any, disp: { bgColor: string | null }): stri
return safeStoredColor(g, '');
}
/** Global room-hover information preference. Legacy and malformed values keep
* the historical default; only an explicit boolean false disables the window. */
export function showRoomTooltipOf(settings: unknown): boolean {
return (settings as { show_room_tooltip?: unknown } | null | undefined)
?.show_room_tooltip !== false;
}
// ---------------- global fill colors ----------------
export interface FillColorEntry {
+2
View File
@@ -247,6 +247,8 @@ export interface ServerConfig {
/** Internal registry-Area provenance; not a user-facing setting. */
marker_area_snapshot?: MarkerAreaSnapshot;
icon_rules?: { pattern: string; icon: string }[];
/** Only an explicit false hides the room hover information window. */
show_room_tooltip?: boolean;
};
}
+2 -2
View File
@@ -126,11 +126,11 @@ test('i18n: every registered dictionary carries the English key set', () => {
}
});
test('i18n: lazy support dictionaries carry matching keys and placeholders', () => {
test('i18n: lazy editor dictionaries carry matching keys and placeholders', () => {
const supportEn = supportDictionaries.get('en');
const keys = Object.keys(supportEn).sort();
const placeholders = (value) => (String(value).match(/\{\w+\}/gu) || []).sort();
assert.equal(keys.length, 42);
assert.equal(keys.length, 44);
assert.equal(Object.keys(en).filter((key) => key.startsWith('support.')).join(','), 'support.title');
for (const { code } of LANGUAGE_REGISTRY) {
const dictionary = supportDictionaries.get(code);
+12
View File
@@ -26,6 +26,7 @@ import {
hassValue, valueWithUnit, decorTextScale, decorTextLines,
LIVE_TEXT_DASH, LIVE_TEXT_VALUE_MAX, DECOR_TEXT_SCALE_MIN, DECOR_TEXT_SCALE_MAX,
quantizeOpeningLightAmount,
showRoomTooltipOf,
} from '../test-build/logic.js';
import {
iconFor, compileIconRules, isValidPattern, iconFromDeviceClasses,
@@ -35,6 +36,17 @@ test('tap action editor choices keep the canonical order', () => {
assert.deepEqual([...TAP_ACTIONS], ['info', 'more-info', 'toggle', 'run', 'none']);
});
test('room tooltip preference is default-on and only exact false disables it', () => {
for (const value of [undefined, null, true, 0, 1, 'false', [], {}]) {
assert.equal(showRoomTooltipOf(value), true);
}
assert.equal(showRoomTooltipOf({ show_room_tooltip: undefined }), true);
assert.equal(showRoomTooltipOf({ show_room_tooltip: null }), true);
assert.equal(showRoomTooltipOf({ show_room_tooltip: true }), true);
assert.equal(showRoomTooltipOf({ show_room_tooltip: 'false' }), true);
assert.equal(showRoomTooltipOf({ show_room_tooltip: false }), false);
});
test('display normalization and alarm-capable metadata share one contract', () => {
assert.equal(normalizeDeviceDisplay(undefined), 'badge');
assert.equal(normalizeDeviceDisplay('ripple'), 'icon_ripple');
+3
View File
@@ -200,6 +200,7 @@ def test_rich_plan_projection_preserves_safe_structure_and_drops_unknown_values(
"model_version": 9,
"settings": {
"north_deg": 30,
"show_room_tooltip": False,
"fill_colors": {"warm": {"c": "#ffaa00", "a": 0.5, "secret": "drop"}},
"decor_default_style": {
"color": "#123456", "width_cm": 2, "secret": "drop",
@@ -290,6 +291,7 @@ def test_rich_plan_projection_preserves_safe_structure_and_drops_unknown_values(
assert package["versions"]["card"] == "unknown"
assert plan["settings"]["fill_colors"] == {"warm": {"a": 0.5, "c": "#ffaa00"}}
assert plan["settings"]["show_room_tooltip"] is False
assert plan["settings"]["decor_default_style"] == {"color": "#123456", "width_cm": 2}
assert space["rooms"][0]["poly"] == [[0, 0], [2, 0]]
assert space["rooms"][0]["settings"]["temp_source_kind"] == "entity"
@@ -334,6 +336,7 @@ def test_projection_helpers_fail_closed_on_malformed_shapes():
assert support_package._points(None) == []
assert support_package._custom_fill(None) is None
assert support_package._global_settings(None) == {}
assert support_package._global_settings({"show_room_tooltip": "false"}) == {}
assert support_package._room_settings(ids, None) == {}
assert support_package._project_layout(ids, None) == {}
assert support_package._summary(None, None)["spaces"] == 0
+13
View File
@@ -479,6 +479,19 @@ def test_sun_settings_global():
v.CONFIG_SCHEMA({"spaces": [], "settings": {"weather_entity": {"e": 1}}})
def test_room_tooltip_global_setting_is_strict_boolean_and_round_trips():
for value in (True, False):
out = v.CONFIG_SCHEMA(
{"spaces": [], "settings": {"show_room_tooltip": value}}
)
assert out["settings"]["show_room_tooltip"] is value
for bad in (None, 0, 1, "false", [], {}):
with pytest.raises(vol.Invalid):
v.CONFIG_SCHEMA(
{"spaces": [], "settings": {"show_room_tooltip": bad}}
)
def test_marker_area_snapshot_is_strict_and_bounded():
"""#126: Area provenance accepts only exact direct bindings and non-empty Areas."""
snapshot = {