Compare commits

...
Author SHA1 Message Date
claude[bot] 7d477564ab docs: review document for #476
Issue: #476
User-Visible: no
2026-09-06 12:56:10 +00:00
claude[bot] 29d72b3344 docs: review document for #476
Issue: #476
User-Visible: no
2026-09-06 12:37:50 +00:00
Sergey Matyuninandclaude[bot] 64bd17ebfe test: harden color picker confirmation guards
Issue: #476
User-Visible: no
2026-09-06 12:37:50 +00:00
claude[bot] e189867fa9 docs: review document for #476
Issue: #476
User-Visible: no
2026-09-06 12:37:50 +00:00
Sergey Matyuninandclaude[bot] b70b7d7c8d test: register color picker browser coverage
Issue: #476
User-Visible: no
2026-09-06 12:37:50 +00:00
Sergey Matyuninandclaude[bot] 0ae5b5177a docs: refresh screenshot fingerprint
Issue: #476
User-Visible: no
2026-09-06 12:37:50 +00:00
Sergey Matyuninandclaude[bot] 4dc0a538cc feat: add color picker confirmation
Issue: #476
User-Visible: yes
2026-09-06 12:37:50 +00:00
claude[bot] 557bc1fd85 docs: review document for #476
Issue: #476
User-Visible: no
2026-09-06 12:37:50 +00:00
Sergey Matyuninandclaude[bot] b306dedb92 docs: clarify color picker invalid confirmation
Issue: #476
User-Visible: no
2026-09-06 12:37:50 +00:00
claude[bot] 98ac71b902 docs: review document for #476
Issue: #476
User-Visible: no
2026-09-06 12:37:50 +00:00
Sergey Matyuninandclaude[bot] 45b2a9d394 docs: specify color picker confirmation
Issue: #476
User-Visible: no
2026-09-06 12:37:50 +00:00
58 changed files with 1843 additions and 288 deletions
@@ -1,215 +1,215 @@
{
"schema": 1,
"fingerprint": "ab077e4fb68320e583ac75418056196bdb941d9751fbe28c49e91f8310519e28",
"fingerprint": "884182570484410d7ab1a7212b07de9f2b4e5683c0d859f4e9d3feaae9318c87",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-BotpFSfJ.js",
"houseplan-assets/houseplan-card-CODdCIWj.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 299568,
"initialViewGzipBytes": 299595,
"lazyFiles": [
"houseplan-assets/backdrop-pick-DtelLDb5.js",
"houseplan-assets/de-BJ59SqHd.js",
"houseplan-assets/editor-DCvR7dOu.js",
"houseplan-assets/fr-DNZtgC-A.js",
"houseplan-assets/guard-DDFB6K1x.js",
"houseplan-assets/houseplan-editor-runtime-00uay_9V.js",
"houseplan-assets/houseplan-onboarding-runtime-Ca87v6jq.js",
"houseplan-assets/hp-zigbee-topology-overlay-DTCmHov1.js",
"houseplan-assets/iso-scene-render-JRXPmIjZ.js",
"houseplan-assets/live-interaction-runtime-C10l_L0Y.js",
"houseplan-assets/zigbee-topology-B9AIr-UU.js",
"houseplan-assets/zigbee-topology-runtime-ZZkqg-Gu.js"
"houseplan-assets/backdrop-pick-BnsHKWaS.js",
"houseplan-assets/de-3MG52yOu.js",
"houseplan-assets/editor-BWu14hMI.js",
"houseplan-assets/fr-w8JLXMgW.js",
"houseplan-assets/guard-BR4mAQsM.js",
"houseplan-assets/houseplan-editor-runtime-Ak2xnYks.js",
"houseplan-assets/houseplan-onboarding-runtime-BjgK4V8E.js",
"houseplan-assets/hp-zigbee-topology-overlay-CKKJLQ66.js",
"houseplan-assets/iso-scene-render-DXu89PMK.js",
"houseplan-assets/live-interaction-runtime-D5KHv4hp.js",
"houseplan-assets/zigbee-topology-BUFPOnoy.js",
"houseplan-assets/zigbee-topology-runtime-Jy73PIeo.js"
],
"lazyGzipBytes": 264740,
"lazyGzipBytes": 264984,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-DtelLDb5.js",
"houseplan-assets/editor-DCvR7dOu.js",
"houseplan-assets/guard-DDFB6K1x.js",
"houseplan-assets/houseplan-editor-runtime-00uay_9V.js",
"houseplan-assets/zigbee-topology-B9AIr-UU.js"
"houseplan-assets/backdrop-pick-BnsHKWaS.js",
"houseplan-assets/editor-BWu14hMI.js",
"houseplan-assets/guard-BR4mAQsM.js",
"houseplan-assets/houseplan-editor-runtime-Ak2xnYks.js",
"houseplan-assets/zigbee-topology-BUFPOnoy.js"
],
"lazyEditorGzipBytes": 186891,
"lazyEditorGzipBytes": 187115,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-DtelLDb5.js",
"houseplan-assets/houseplan-onboarding-runtime-Ca87v6jq.js"
"houseplan-assets/backdrop-pick-BnsHKWaS.js",
"houseplan-assets/houseplan-onboarding-runtime-BjgK4V8E.js"
],
"lazyOnboardingGzipBytes": 14327,
"lazyOnboardingGzipBytes": 14328,
"lazyLocaleFiles": [
"houseplan-assets/de-BJ59SqHd.js",
"houseplan-assets/fr-DNZtgC-A.js"
"houseplan-assets/de-3MG52yOu.js",
"houseplan-assets/fr-w8JLXMgW.js"
],
"lazyLocaleGzipBytes": 50160,
"lazyLocaleGzipBytes": 50178,
"lazyIsometricFiles": [
"houseplan-assets/guard-DDFB6K1x.js",
"houseplan-assets/iso-scene-render-JRXPmIjZ.js"
"houseplan-assets/guard-BR4mAQsM.js",
"houseplan-assets/iso-scene-render-DXu89PMK.js"
],
"lazyIsometricGzipBytes": 13468,
"lazyIsometricGzipBytes": 13467,
"files": [
{
"path": "houseplan-assets/backdrop-pick-DtelLDb5.js",
"sha256": "43c706bd470dcc62916e9c5fafaed4d5cbef5328be3c98c20187a0a16227a060",
"path": "houseplan-assets/backdrop-pick-BnsHKWaS.js",
"sha256": "a1a1925a3567418f9f3e3b6541987ecf947ab98d672476a7ecd24468180ed8c7",
"rawBytes": 21592,
"gzipBytes": 7360,
"gzipBytes": 7359,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-BJ59SqHd.js",
"sha256": "31e73b1364053770ba079e518648a42a9e751e47ebef2e1c6907f1cd4cb8abf8",
"rawBytes": 84232,
"gzipBytes": 25357,
"path": "houseplan-assets/de-3MG52yOu.js",
"sha256": "d85a610248c36842c44cc0b851c62f8aeb8e9431cd71c12b9900d8372b17f8dd",
"rawBytes": 84260,
"gzipBytes": 25368,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-DCvR7dOu.js",
"sha256": "7e3fb571635b5e8e85b02158a98ca9dc826122a3df5daaa290720ad19468558b",
"path": "houseplan-assets/editor-BWu14hMI.js",
"sha256": "e793d2607239b96cbf314e36e4dd1c03017b5c9c7ae47b9d710dd7f47423011d",
"rawBytes": 3837,
"gzipBytes": 1588,
"gzipBytes": 1589,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-DNZtgC-A.js",
"sha256": "cdd9839ec611a780733d997c5605d8d1a0740d89c9e626cd1c5da29a9d4baa34",
"rawBytes": 86273,
"gzipBytes": 24803,
"path": "houseplan-assets/fr-w8JLXMgW.js",
"sha256": "78da22924a9d38e19e0787ce4c171bfba7df6c18f5460ccd4a2726bc1c415d8e",
"rawBytes": 86301,
"gzipBytes": 24810,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/guard-DDFB6K1x.js",
"sha256": "5aaf1212a318c9d9f7491e46444d7cb9650a57836ac336226e8ca99bec359bc1",
"path": "houseplan-assets/guard-BR4mAQsM.js",
"sha256": "31e7c009b3f724ca776293fef20b8ccc3d9f58d6afecb7d1d20be169e827d0a0",
"rawBytes": 518,
"gzipBytes": 370,
"gzipBytes": 371,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-BotpFSfJ.js",
"sha256": "f58ee9b09efdd7b905b824767aeb1a06f6056d2d481e6948715cb2e7ea84a906",
"rawBytes": 1072024,
"gzipBytes": 298771,
"path": "houseplan-assets/houseplan-card-CODdCIWj.js",
"sha256": "6011cb913b94cd0bb918c28928886a514af39a2162214423e467faf4b8ac31fe",
"rawBytes": 1072122,
"gzipBytes": 298799,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-BJ59SqHd.js",
"houseplan-assets/editor-DCvR7dOu.js",
"houseplan-assets/fr-DNZtgC-A.js",
"houseplan-assets/houseplan-editor-runtime-00uay_9V.js",
"houseplan-assets/houseplan-onboarding-runtime-Ca87v6jq.js",
"houseplan-assets/hp-zigbee-topology-overlay-DTCmHov1.js",
"houseplan-assets/iso-scene-render-JRXPmIjZ.js",
"houseplan-assets/live-interaction-runtime-C10l_L0Y.js"
"houseplan-assets/de-3MG52yOu.js",
"houseplan-assets/editor-BWu14hMI.js",
"houseplan-assets/fr-w8JLXMgW.js",
"houseplan-assets/houseplan-editor-runtime-Ak2xnYks.js",
"houseplan-assets/houseplan-onboarding-runtime-BjgK4V8E.js",
"houseplan-assets/hp-zigbee-topology-overlay-CKKJLQ66.js",
"houseplan-assets/iso-scene-render-DXu89PMK.js",
"houseplan-assets/live-interaction-runtime-D5KHv4hp.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-00uay_9V.js",
"sha256": "1148dc0e63fc468409d20189f01ec55ddb84fdc4ce370de1f4229cb585be4064",
"rawBytes": 639230,
"gzipBytes": 170143,
"path": "houseplan-assets/houseplan-editor-runtime-Ak2xnYks.js",
"sha256": "f124de9f93faf2aac6f6dcc8d7dfc5d40e7f4d887274843446f6aadb19dae52a",
"rawBytes": 640495,
"gzipBytes": 170365,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-DtelLDb5.js",
"houseplan-assets/guard-DDFB6K1x.js",
"houseplan-assets/houseplan-card-BotpFSfJ.js",
"houseplan-assets/zigbee-topology-B9AIr-UU.js"
"houseplan-assets/backdrop-pick-BnsHKWaS.js",
"houseplan-assets/guard-BR4mAQsM.js",
"houseplan-assets/houseplan-card-CODdCIWj.js",
"houseplan-assets/zigbee-topology-BUFPOnoy.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-ZZkqg-Gu.js"
"houseplan-assets/zigbee-topology-runtime-Jy73PIeo.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-Ca87v6jq.js",
"sha256": "767c9f0db979b669d28476f10408576426fb5c8c2f3b82b99a2c4df240760e6a",
"path": "houseplan-assets/houseplan-onboarding-runtime-BjgK4V8E.js",
"sha256": "7c7a9304466c21ec27ec027f5b3947e0c307538a016f20f4a407e9a43a81a18c",
"rawBytes": 28071,
"gzipBytes": 6967,
"gzipBytes": 6969,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-DtelLDb5.js",
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/backdrop-pick-BnsHKWaS.js",
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-DTCmHov1.js",
"sha256": "6531f4ca200c1a8f6c29735ce02aad05fc80b93ff1540a524fee0a28c7ba3c51",
"path": "houseplan-assets/hp-zigbee-topology-overlay-CKKJLQ66.js",
"sha256": "e6be2493d605f81e321c5ccd18cfba70dd86ed1816ea54deefa77be48c5e40dd",
"rawBytes": 11034,
"gzipBytes": 3732,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js",
"houseplan-assets/zigbee-topology-B9AIr-UU.js",
"houseplan-assets/zigbee-topology-runtime-ZZkqg-Gu.js"
"houseplan-assets/houseplan-card-CODdCIWj.js",
"houseplan-assets/zigbee-topology-BUFPOnoy.js",
"houseplan-assets/zigbee-topology-runtime-Jy73PIeo.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/iso-scene-render-JRXPmIjZ.js",
"sha256": "703309837539c51a0fd5a7be9038ce007c7a1a4ebadb123edc45a30fce70e95e",
"path": "houseplan-assets/iso-scene-render-DXu89PMK.js",
"sha256": "96f20eb2404fd8f6c116de5bfff6a2b109bcd0a250e918eecdf0968df3c6ed9c",
"rawBytes": 38593,
"gzipBytes": 13098,
"gzipBytes": 13096,
"isEntry": false,
"imports": [
"houseplan-assets/guard-DDFB6K1x.js",
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/guard-BR4mAQsM.js",
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-C10l_L0Y.js",
"sha256": "0005d5b3c867a686cb0c46306b6b5e6d287e55aeaa2156bf907c4c3f2eea5310",
"path": "houseplan-assets/live-interaction-runtime-D5KHv4hp.js",
"sha256": "da0e12640b26ae3ebe05677db6238847813d5c8a7ec0dc7c7149922bc7deac66",
"rawBytes": 5196,
"gzipBytes": 2128,
"gzipBytes": 2129,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-B9AIr-UU.js",
"sha256": "3d30830f5cbf87605340e98f2cd0773625418b2f79dccf2ea4a603bff91dbf11",
"path": "houseplan-assets/zigbee-topology-BUFPOnoy.js",
"sha256": "b497afdec2059b962c9f1a329e7f417650bfa5b28e684544c1ae415340fd208f",
"rawBytes": 18961,
"gzipBytes": 7430,
"gzipBytes": 7431,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-ZZkqg-Gu.js",
"sha256": "9c67889471882d864ffffb65d60bf4ce519896f1e6be6acbb4ddb13cb15f86a4",
"path": "houseplan-assets/zigbee-topology-runtime-Jy73PIeo.js",
"sha256": "ba8f56e776f1d47bea1c211a262154c31c4e13483857e3397ad9f951a8d7f8ce",
"rawBytes": 3941,
"gzipBytes": 1764,
"gzipBytes": 1765,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js",
"houseplan-assets/zigbee-topology-B9AIr-UU.js"
"houseplan-assets/houseplan-card-CODdCIWj.js",
"houseplan-assets/zigbee-topology-BUFPOnoy.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "2ea16018a3996968eee180625db1e039a119144fb01de931336bbf5d2fbfbeb6",
"sha256": "6e4f5e3ebbe429b4d330f3aff542aa4867fd87f0f249a5185c7429a00670b6d5",
"rawBytes": 1183,
"gzipBytes": 797,
"gzipBytes": 796,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/houseplan-card-CODdCIWj.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,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab077e4fb68320e583ac75418056196bdb941d9751fbe28c49e91f8310519e28";import{t as e,l as o,x as t,dK as s,A as a,dL as i,dM as l,dN as n,dJ as r,b as c}from"./houseplan-card-BotpFSfJ.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
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="884182570484410d7ab1a7212b07de9f2b4e5683c0d859f4e9d3feaae9318c87";import{t as e,l as o,x as t,dK as s,A as a,dL as i,dM as l,dN as n,dJ as r,b as c}from"./houseplan-card-CODdCIWj.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}
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__="ab077e4fb68320e583ac75418056196bdb941d9751fbe28c49e91f8310519e28";import{dH as r,dI as t,dJ as e}from"./houseplan-card-BotpFSfJ.js";const s={},a=r(class extends t{constructor(){super(...arguments),this.ot=s}render(r,t){return t()}update(r,[t,s]){if(Array.isArray(t)){if(Array.isArray(this.ot)&&this.ot.length===t.length&&t.every((r,t)=>r===this.ot[t]))return e}else if(this.ot===t)return e;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,s)}});export{a as i};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="884182570484410d7ab1a7212b07de9f2b4e5683c0d859f4e9d3feaae9318c87";import{dH as r,dI as t,dJ as e}from"./houseplan-card-CODdCIWj.js";const s={},a=r(class extends t{constructor(){super(...arguments),this.ot=s}render(r,t){return t()}update(r,[t,s]){if(Array.isArray(t)){if(Array.isArray(this.ot)&&this.ot.length===t.length&&t.every((r,t)=>r===this.ot[t]))return e}else if(this.ot===t)return e;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,s)}});export{a as i};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab077e4fb68320e583ac75418056196bdb941d9751fbe28c49e91f8310519e28";import{n as t,b as n}from"./zigbee-topology-B9AIr-UU.js";import{F as e}from"./houseplan-card-BotpFSfJ.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function m(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function g(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,g=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await m(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await m(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{f as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="884182570484410d7ab1a7212b07de9f2b4e5683c0d859f4e9d3feaae9318c87";import{n as t,b as n}from"./zigbee-topology-BUFPOnoy.js";import{F as e}from"./houseplan-card-CODdCIWj.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function m(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function g(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,g=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await m(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await m(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{f as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab077e4fb68320e583ac75418056196bdb941d9751fbe28c49e91f8310519e28";try{await import("./houseplan-assets/houseplan-card-BotpFSfJ.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__="884182570484410d7ab1a7212b07de9f2b4e5683c0d859f4e9d3feaae9318c87";try{await import("./houseplan-assets/houseplan-card-CODdCIWj.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)}
+63 -5
View File
@@ -41,7 +41,8 @@ const result = await page.evaluate(async () => {
&& !!surface()?.querySelector('input[type="number"]');
out.englishLabels = surface()?.getAttribute('aria-label') === 'Color picker'
&& ranges().map((input) => input.getAttribute('aria-label')).join('|')
=== 'Hue|Saturation|Brightness|Opacity';
=== 'Hue|Saturation|Brightness|Opacity'
&& surface()?.querySelector('.confirm')?.textContent.trim() === 'OK';
const rect = surface()?.getBoundingClientRect();
out.narrowViewportFits = !!rect && rect.left >= 0 && rect.right <= innerWidth
&& rect.top >= 0 && rect.bottom <= innerHeight
@@ -87,15 +88,70 @@ const result = await page.evaluate(async () => {
out.shortHexNormalizesAndEmits = picker.color === '#00aaff'
&& events.at(-1)?.color === '#00aaff';
let parentClicks = 0;
card.addEventListener('click', () => { parentClicks += 1; });
let confirm = surface().querySelector('.confirm');
const confirmRect = confirm?.getBoundingClientRect();
const surfaceRect = surface()?.getBoundingClientRect();
out.confirmIsFullWidthTouchTarget = !!confirmRect && !!surfaceRect
&& confirmRect.height >= 40
&& Math.abs(confirmRect.width - (surface().clientWidth - 24)) <= 1
&& surface()?.lastElementChild === confirm;
const eventsBeforeConfirm = events.length;
const parentClicksBeforeConfirm = parentClicks;
confirm?.click();
await picker.updateComplete;
await frame();
out.confirmClosesWithoutDuplicateOrClickThrough = trigger?.getAttribute('aria-expanded') === 'false'
&& picker.shadowRoot.activeElement === trigger
&& events.length === eventsBeforeConfirm
&& parentClicks === parentClicksBeforeConfirm;
trigger?.click();
await picker.updateComplete;
await frame();
const eventCount = events.length;
hex = surface().querySelector('input[type="text"]');
hex.value = '#12';
hex.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
await picker.updateComplete;
hex.dispatchEvent(new FocusEvent('blur'));
await picker.updateComplete;
hex = surface().querySelector('input[type="text"]');
out.invalidHexNeverEmits = events.length === eventCount
&& hex.value === '#00aaff' && hex.getAttribute('aria-invalid') === 'true';
out.invalidHexNeverEmits = events.length === eventCount;
out.invalidHexNormalizesDraft = hex.value === '#00aaff';
out.invalidHexKeepsVisibleError = hex.getAttribute('aria-invalid') === 'true';
confirm = surface().querySelector('.confirm');
confirm?.click();
await picker.updateComplete;
await frame();
confirm = surface().querySelector('.confirm');
confirm?.click();
await picker.updateComplete;
await frame();
hex = surface().querySelector('input[type="text"]');
out.repeatedConfirmCannotBypassInvalidHex = trigger?.getAttribute('aria-expanded') === 'true'
&& events.length === eventCount
&& hex?.getAttribute('aria-invalid') === 'true'
&& picker.shadowRoot.activeElement === hex;
hex.value = '#123456';
hex.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
await picker.updateComplete;
const eventsAfterCorrection = events.length;
surface().querySelector('.confirm')?.click();
await picker.updateComplete;
await frame();
out.validCorrectionAllowsConfirm = trigger?.getAttribute('aria-expanded') === 'false'
&& picker.shadowRoot.activeElement === trigger
&& picker.color === '#123456'
&& events.length === eventsAfterCorrection;
trigger?.click();
await picker.updateComplete;
await frame();
const field = surface().querySelector('.sv-field');
const fieldRect = field.getBoundingClientRect();
@@ -119,7 +175,8 @@ const result = await page.evaluate(async () => {
picker.showOpacity = false;
await picker.updateComplete;
out.colorOnlyKeepsColorControls = ranges().length === 3
&& !surface().querySelector('input[type="number"]');
&& !surface().querySelector('input[type="number"]')
&& surface()?.lastElementChild?.classList.contains('confirm');
picker.showOpacity = true;
await picker.updateComplete;
@@ -128,7 +185,8 @@ const result = await page.evaluate(async () => {
await card.updateComplete;
await picker.updateComplete;
out.cardLanguageOwnsCopy = surface()?.getAttribute('aria-label') === 'Выбор цвета'
&& surface()?.querySelector('input[type="text"]')?.getAttribute('aria-label') === 'Цвет HEX';
&& surface()?.querySelector('input[type="text"]')?.getAttribute('aria-label') === 'Цвет HEX'
&& surface()?.querySelector('.confirm')?.textContent.trim() === 'ОК';
const escapeControl = surface()?.querySelector('input[type="range"]');
escapeControl?.focus();
+17
View File
@@ -46,6 +46,23 @@ const result = await page.evaluate(async () => {
tempCold.renderRoot.querySelector('.trigger').click();
await tempCold.updateComplete;
lightOn.renderRoot.querySelector('.trigger').click();
await lightOn.updateComplete;
await frame();
const pickerSurface = lightOn._surface();
const confirmButton = pickerSurface?.querySelector('.confirm');
let pickerSurfaceClicks = 0;
pickerSurface?.addEventListener('click', () => { pickerSurfaceClicks += 1; });
confirmButton?.dispatchEvent(new MouseEvent('click', {
bubbles: true, composed: true, cancelable: true,
}));
await lightOn.updateComplete;
await frame();
out.generalConfirmDoesNotClickThrough = !!pickerSurface && !!confirmButton
&& lightOn.renderRoot.querySelector('.trigger')
?.getAttribute('aria-expanded') === 'false'
&& pickerSurfaceClicks === 0;
const originalGlobalBg = card._settingsDialog.bgColor;
globalBg.renderRoot.querySelector('.trigger').click();
await globalBg.updateComplete;
+8 -1
View File
@@ -224,9 +224,16 @@ const res = await page.evaluate(async () => {
await frame();
const pickerFallback = dialog?.shadowRoot?.querySelector('[data-hp-overlay="color-opacity"]');
const pickerControl = pickerFallback?.shadowRoot?.querySelector('input');
const pickerConfirm = pickerFallback?.shadowRoot?.querySelector('.confirm');
out.fallbackPickerFocusable = !!pickerControl
&& dialog?._focusableElements?.().includes(pickerControl);
pickerButton?.click();
out.fallbackPickerHasConfirm = pickerConfirm?.textContent.trim() === 'OK'
&& pickerFallback?.shadowRoot?.querySelector('.picker')?.lastElementChild === pickerConfirm;
pickerConfirm?.click();
await frame();
out.fallbackPickerConfirmCloses = pickerButton?.getAttribute('aria-expanded') === 'false'
&& picker?.shadowRoot?.activeElement === pickerButton;
if (pickerButton?.getAttribute('aria-expanded') === 'true') pickerButton.click();
if (show) Object.defineProperty(HTMLElement.prototype, 'showPopover', show);
else delete HTMLElement.prototype.showPopover;
if (hide) Object.defineProperty(HTMLElement.prototype, 'hidePopover', hide);
+103 -103
View File
@@ -1,215 +1,215 @@
{
"schema": 1,
"fingerprint": "ab077e4fb68320e583ac75418056196bdb941d9751fbe28c49e91f8310519e28",
"fingerprint": "884182570484410d7ab1a7212b07de9f2b4e5683c0d859f4e9d3feaae9318c87",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-BotpFSfJ.js",
"houseplan-assets/houseplan-card-CODdCIWj.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 299568,
"initialViewGzipBytes": 299595,
"lazyFiles": [
"houseplan-assets/backdrop-pick-DtelLDb5.js",
"houseplan-assets/de-BJ59SqHd.js",
"houseplan-assets/editor-DCvR7dOu.js",
"houseplan-assets/fr-DNZtgC-A.js",
"houseplan-assets/guard-DDFB6K1x.js",
"houseplan-assets/houseplan-editor-runtime-00uay_9V.js",
"houseplan-assets/houseplan-onboarding-runtime-Ca87v6jq.js",
"houseplan-assets/hp-zigbee-topology-overlay-DTCmHov1.js",
"houseplan-assets/iso-scene-render-JRXPmIjZ.js",
"houseplan-assets/live-interaction-runtime-C10l_L0Y.js",
"houseplan-assets/zigbee-topology-B9AIr-UU.js",
"houseplan-assets/zigbee-topology-runtime-ZZkqg-Gu.js"
"houseplan-assets/backdrop-pick-BnsHKWaS.js",
"houseplan-assets/de-3MG52yOu.js",
"houseplan-assets/editor-BWu14hMI.js",
"houseplan-assets/fr-w8JLXMgW.js",
"houseplan-assets/guard-BR4mAQsM.js",
"houseplan-assets/houseplan-editor-runtime-Ak2xnYks.js",
"houseplan-assets/houseplan-onboarding-runtime-BjgK4V8E.js",
"houseplan-assets/hp-zigbee-topology-overlay-CKKJLQ66.js",
"houseplan-assets/iso-scene-render-DXu89PMK.js",
"houseplan-assets/live-interaction-runtime-D5KHv4hp.js",
"houseplan-assets/zigbee-topology-BUFPOnoy.js",
"houseplan-assets/zigbee-topology-runtime-Jy73PIeo.js"
],
"lazyGzipBytes": 264740,
"lazyGzipBytes": 264984,
"lazyEditorFiles": [
"houseplan-assets/backdrop-pick-DtelLDb5.js",
"houseplan-assets/editor-DCvR7dOu.js",
"houseplan-assets/guard-DDFB6K1x.js",
"houseplan-assets/houseplan-editor-runtime-00uay_9V.js",
"houseplan-assets/zigbee-topology-B9AIr-UU.js"
"houseplan-assets/backdrop-pick-BnsHKWaS.js",
"houseplan-assets/editor-BWu14hMI.js",
"houseplan-assets/guard-BR4mAQsM.js",
"houseplan-assets/houseplan-editor-runtime-Ak2xnYks.js",
"houseplan-assets/zigbee-topology-BUFPOnoy.js"
],
"lazyEditorGzipBytes": 186891,
"lazyEditorGzipBytes": 187115,
"lazyOnboardingFiles": [
"houseplan-assets/backdrop-pick-DtelLDb5.js",
"houseplan-assets/houseplan-onboarding-runtime-Ca87v6jq.js"
"houseplan-assets/backdrop-pick-BnsHKWaS.js",
"houseplan-assets/houseplan-onboarding-runtime-BjgK4V8E.js"
],
"lazyOnboardingGzipBytes": 14327,
"lazyOnboardingGzipBytes": 14328,
"lazyLocaleFiles": [
"houseplan-assets/de-BJ59SqHd.js",
"houseplan-assets/fr-DNZtgC-A.js"
"houseplan-assets/de-3MG52yOu.js",
"houseplan-assets/fr-w8JLXMgW.js"
],
"lazyLocaleGzipBytes": 50160,
"lazyLocaleGzipBytes": 50178,
"lazyIsometricFiles": [
"houseplan-assets/guard-DDFB6K1x.js",
"houseplan-assets/iso-scene-render-JRXPmIjZ.js"
"houseplan-assets/guard-BR4mAQsM.js",
"houseplan-assets/iso-scene-render-DXu89PMK.js"
],
"lazyIsometricGzipBytes": 13468,
"lazyIsometricGzipBytes": 13467,
"files": [
{
"path": "houseplan-assets/backdrop-pick-DtelLDb5.js",
"sha256": "43c706bd470dcc62916e9c5fafaed4d5cbef5328be3c98c20187a0a16227a060",
"path": "houseplan-assets/backdrop-pick-BnsHKWaS.js",
"sha256": "a1a1925a3567418f9f3e3b6541987ecf947ab98d672476a7ecd24468180ed8c7",
"rawBytes": 21592,
"gzipBytes": 7360,
"gzipBytes": 7359,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/de-BJ59SqHd.js",
"sha256": "31e73b1364053770ba079e518648a42a9e751e47ebef2e1c6907f1cd4cb8abf8",
"rawBytes": 84232,
"gzipBytes": 25357,
"path": "houseplan-assets/de-3MG52yOu.js",
"sha256": "d85a610248c36842c44cc0b851c62f8aeb8e9431cd71c12b9900d8372b17f8dd",
"rawBytes": 84260,
"gzipBytes": 25368,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/editor-DCvR7dOu.js",
"sha256": "7e3fb571635b5e8e85b02158a98ca9dc826122a3df5daaa290720ad19468558b",
"path": "houseplan-assets/editor-BWu14hMI.js",
"sha256": "e793d2607239b96cbf314e36e4dd1c03017b5c9c7ae47b9d710dd7f47423011d",
"rawBytes": 3837,
"gzipBytes": 1588,
"gzipBytes": 1589,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/fr-DNZtgC-A.js",
"sha256": "cdd9839ec611a780733d997c5605d8d1a0740d89c9e626cd1c5da29a9d4baa34",
"rawBytes": 86273,
"gzipBytes": 24803,
"path": "houseplan-assets/fr-w8JLXMgW.js",
"sha256": "78da22924a9d38e19e0787ce4c171bfba7df6c18f5460ccd4a2726bc1c415d8e",
"rawBytes": 86301,
"gzipBytes": 24810,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-assets/guard-DDFB6K1x.js",
"sha256": "5aaf1212a318c9d9f7491e46444d7cb9650a57836ac336226e8ca99bec359bc1",
"path": "houseplan-assets/guard-BR4mAQsM.js",
"sha256": "31e7c009b3f724ca776293fef20b8ccc3d9f58d6afecb7d1d20be169e827d0a0",
"rawBytes": 518,
"gzipBytes": 370,
"gzipBytes": 371,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-BotpFSfJ.js",
"sha256": "f58ee9b09efdd7b905b824767aeb1a06f6056d2d481e6948715cb2e7ea84a906",
"rawBytes": 1072024,
"gzipBytes": 298771,
"path": "houseplan-assets/houseplan-card-CODdCIWj.js",
"sha256": "6011cb913b94cd0bb918c28928886a514af39a2162214423e467faf4b8ac31fe",
"rawBytes": 1072122,
"gzipBytes": 298799,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/de-BJ59SqHd.js",
"houseplan-assets/editor-DCvR7dOu.js",
"houseplan-assets/fr-DNZtgC-A.js",
"houseplan-assets/houseplan-editor-runtime-00uay_9V.js",
"houseplan-assets/houseplan-onboarding-runtime-Ca87v6jq.js",
"houseplan-assets/hp-zigbee-topology-overlay-DTCmHov1.js",
"houseplan-assets/iso-scene-render-JRXPmIjZ.js",
"houseplan-assets/live-interaction-runtime-C10l_L0Y.js"
"houseplan-assets/de-3MG52yOu.js",
"houseplan-assets/editor-BWu14hMI.js",
"houseplan-assets/fr-w8JLXMgW.js",
"houseplan-assets/houseplan-editor-runtime-Ak2xnYks.js",
"houseplan-assets/houseplan-onboarding-runtime-BjgK4V8E.js",
"houseplan-assets/hp-zigbee-topology-overlay-CKKJLQ66.js",
"houseplan-assets/iso-scene-render-DXu89PMK.js",
"houseplan-assets/live-interaction-runtime-D5KHv4hp.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-00uay_9V.js",
"sha256": "1148dc0e63fc468409d20189f01ec55ddb84fdc4ce370de1f4229cb585be4064",
"rawBytes": 639230,
"gzipBytes": 170143,
"path": "houseplan-assets/houseplan-editor-runtime-Ak2xnYks.js",
"sha256": "f124de9f93faf2aac6f6dcc8d7dfc5d40e7f4d887274843446f6aadb19dae52a",
"rawBytes": 640495,
"gzipBytes": 170365,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-DtelLDb5.js",
"houseplan-assets/guard-DDFB6K1x.js",
"houseplan-assets/houseplan-card-BotpFSfJ.js",
"houseplan-assets/zigbee-topology-B9AIr-UU.js"
"houseplan-assets/backdrop-pick-BnsHKWaS.js",
"houseplan-assets/guard-BR4mAQsM.js",
"houseplan-assets/houseplan-card-CODdCIWj.js",
"houseplan-assets/zigbee-topology-BUFPOnoy.js"
],
"dynamicImports": [
"houseplan-assets/zigbee-topology-runtime-ZZkqg-Gu.js"
"houseplan-assets/zigbee-topology-runtime-Jy73PIeo.js"
]
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-Ca87v6jq.js",
"sha256": "767c9f0db979b669d28476f10408576426fb5c8c2f3b82b99a2c4df240760e6a",
"path": "houseplan-assets/houseplan-onboarding-runtime-BjgK4V8E.js",
"sha256": "7c7a9304466c21ec27ec027f5b3947e0c307538a016f20f4a407e9a43a81a18c",
"rawBytes": 28071,
"gzipBytes": 6967,
"gzipBytes": 6969,
"isEntry": false,
"imports": [
"houseplan-assets/backdrop-pick-DtelLDb5.js",
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/backdrop-pick-BnsHKWaS.js",
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/hp-zigbee-topology-overlay-DTCmHov1.js",
"sha256": "6531f4ca200c1a8f6c29735ce02aad05fc80b93ff1540a524fee0a28c7ba3c51",
"path": "houseplan-assets/hp-zigbee-topology-overlay-CKKJLQ66.js",
"sha256": "e6be2493d605f81e321c5ccd18cfba70dd86ed1816ea54deefa77be48c5e40dd",
"rawBytes": 11034,
"gzipBytes": 3732,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js",
"houseplan-assets/zigbee-topology-B9AIr-UU.js",
"houseplan-assets/zigbee-topology-runtime-ZZkqg-Gu.js"
"houseplan-assets/houseplan-card-CODdCIWj.js",
"houseplan-assets/zigbee-topology-BUFPOnoy.js",
"houseplan-assets/zigbee-topology-runtime-Jy73PIeo.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/iso-scene-render-JRXPmIjZ.js",
"sha256": "703309837539c51a0fd5a7be9038ce007c7a1a4ebadb123edc45a30fce70e95e",
"path": "houseplan-assets/iso-scene-render-DXu89PMK.js",
"sha256": "96f20eb2404fd8f6c116de5bfff6a2b109bcd0a250e918eecdf0968df3c6ed9c",
"rawBytes": 38593,
"gzipBytes": 13098,
"gzipBytes": 13096,
"isEntry": false,
"imports": [
"houseplan-assets/guard-DDFB6K1x.js",
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/guard-BR4mAQsM.js",
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/live-interaction-runtime-C10l_L0Y.js",
"sha256": "0005d5b3c867a686cb0c46306b6b5e6d287e55aeaa2156bf907c4c3f2eea5310",
"path": "houseplan-assets/live-interaction-runtime-D5KHv4hp.js",
"sha256": "da0e12640b26ae3ebe05677db6238847813d5c8a7ec0dc7c7149922bc7deac66",
"rawBytes": 5196,
"gzipBytes": 2128,
"gzipBytes": 2129,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-B9AIr-UU.js",
"sha256": "3d30830f5cbf87605340e98f2cd0773625418b2f79dccf2ea4a603bff91dbf11",
"path": "houseplan-assets/zigbee-topology-BUFPOnoy.js",
"sha256": "b497afdec2059b962c9f1a329e7f417650bfa5b28e684544c1ae415340fd208f",
"rawBytes": 18961,
"gzipBytes": 7430,
"gzipBytes": 7431,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/houseplan-card-CODdCIWj.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/zigbee-topology-runtime-ZZkqg-Gu.js",
"sha256": "9c67889471882d864ffffb65d60bf4ce519896f1e6be6acbb4ddb13cb15f86a4",
"path": "houseplan-assets/zigbee-topology-runtime-Jy73PIeo.js",
"sha256": "ba8f56e776f1d47bea1c211a262154c31c4e13483857e3397ad9f951a8d7f8ce",
"rawBytes": 3941,
"gzipBytes": 1764,
"gzipBytes": 1765,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js",
"houseplan-assets/zigbee-topology-B9AIr-UU.js"
"houseplan-assets/houseplan-card-CODdCIWj.js",
"houseplan-assets/zigbee-topology-BUFPOnoy.js"
],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "2ea16018a3996968eee180625db1e039a119144fb01de931336bbf5d2fbfbeb6",
"sha256": "6e4f5e3ebbe429b4d330f3aff542aa4867fd87f0f249a5185c7429a00670b6d5",
"rawBytes": 1183,
"gzipBytes": 797,
"gzipBytes": 796,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-BotpFSfJ.js"
"houseplan-assets/houseplan-card-CODdCIWj.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,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab077e4fb68320e583ac75418056196bdb941d9751fbe28c49e91f8310519e28";import{t as e,l as o,x as t,dK as s,A as a,dL as i,dM as l,dN as n,dJ as r,b as c}from"./houseplan-card-BotpFSfJ.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
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="884182570484410d7ab1a7212b07de9f2b4e5683c0d859f4e9d3feaae9318c87";import{t as e,l as o,x as t,dK as s,A as a,dL as i,dM as l,dN as n,dJ as r,b as c}from"./houseplan-card-CODdCIWj.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}
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__="ab077e4fb68320e583ac75418056196bdb941d9751fbe28c49e91f8310519e28";import{dH as r,dI as t,dJ as e}from"./houseplan-card-BotpFSfJ.js";const s={},a=r(class extends t{constructor(){super(...arguments),this.ot=s}render(r,t){return t()}update(r,[t,s]){if(Array.isArray(t)){if(Array.isArray(this.ot)&&this.ot.length===t.length&&t.every((r,t)=>r===this.ot[t]))return e}else if(this.ot===t)return e;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,s)}});export{a as i};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="884182570484410d7ab1a7212b07de9f2b4e5683c0d859f4e9d3feaae9318c87";import{dH as r,dI as t,dJ as e}from"./houseplan-card-CODdCIWj.js";const s={},a=r(class extends t{constructor(){super(...arguments),this.ot=s}render(r,t){return t()}update(r,[t,s]){if(Array.isArray(t)){if(Array.isArray(this.ot)&&this.ot.length===t.length&&t.every((r,t)=>r===this.ot[t]))return e}else if(this.ot===t)return e;return this.ot=Array.isArray(t)?Array.from(t):t,this.render(t,s)}});export{a as i};
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 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab077e4fb68320e583ac75418056196bdb941d9751fbe28c49e91f8310519e28";import{n as t,b as n}from"./zigbee-topology-B9AIr-UU.js";import{F as e}from"./houseplan-card-BotpFSfJ.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function m(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function g(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,g=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await m(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await m(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{f as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="884182570484410d7ab1a7212b07de9f2b4e5683c0d859f4e9d3feaae9318c87";import{n as t,b as n}from"./zigbee-topology-BUFPOnoy.js";import{F as e}from"./houseplan-card-CODdCIWj.js";const o=new WeakMap;function i(t){const n=function(t){const n=t?.connection||t;return!n||"object"!=typeof n&&"function"!=typeof n?null:n}(t);if(!n)return null;let e=o.get(n);return e||(e={revision:0,topologies:[],states:{},listeners:new Set,inflight:new Map},o.set(n,e)),e}function r(t,n,e){t.states={...t.states,[n]:e},function(t){t.revision++;for(const n of t.listeners)n()}(t)}function s(t){const n=t?.code;if("permission"===n||"unsupported"===n||"timeout"===n||"invalid_topic"===n||"invalid_payload"===n)return n;const e=String(t?.message||"").toLowerCase();return e.includes("unauthor")||e.includes("permission")?"permission":e.includes("unknown_command")||e.includes("not found")?"unsupported":"provider"}function a(t){return Object.assign(new Error(t),{code:t})}function c(t,n,e){const o=t.inflight.get(n);if(o)return o;r(t,n,{phase:"loading"});const i=e().then(n=>{if(!n.nodes.length&&n.warnings.some(t=>"invalid_payload"===t.code))throw a("invalid_payload");!function(t,n){t.topologies=[...t.topologies.filter(t=>!(t.provider===n.provider&&t.instanceId===n.instanceId)),n],r(t,"zha"===n.provider?"zha":`z2m:${n.instanceId}`,{phase:"ready",obtainedAt:n.obtainedAt,partial:n.warnings.length>0})}(t,n)}).catch(e=>{r(t,n,{phase:"error",error:s(e)})}).finally(()=>t.inflight.delete(n));return t.inflight.set(n,i),i}function l(t){const n=i(t);return n?{revision:n.revision,topologies:n.topologies,states:n.states}:{revision:0,topologies:[],states:{}}}function u(t,n){const e=i(t);return e?(e.listeners.add(n),()=>e.listeners.delete(n)):()=>{}}function p(t){if(!0!==t?.user?.is_admin)throw a("permission")}function f(n){const e=i(n);return e?c(e,"zha",async()=>{if(p(n),"function"!=typeof n?.callWS)throw a("unsupported");return t(await n.callWS({type:"zha/devices"}))}):Promise.resolve()}function d(t){return null!==t&&"object"==typeof t?t:null}function h(t){const n=d(t),e=n?.payload??t;if("string"!=typeof e)return e;try{return JSON.parse(e)}catch{return null}}async function m(t,n){let e;try{return await Promise.race([t,new Promise((t,o)=>{e=globalThis.setTimeout(()=>o(a("timeout")),Math.max(1,n))})])}finally{void 0!==e&&globalThis.clearTimeout(e)}}function g(t,o,r=15e4){const s=i(t);if(!s)return Promise.resolve();const l=e(o);return c(s,`z2m:${l||String(o)}`,async()=>{if(p(t),!l)throw a("invalid_topic");const e=t.connection,o=e?.subscribeMessage;if("function"!=typeof o||"function"!=typeof t?.callService)throw a("unsupported");const i=function(){const t=globalThis.crypto;return"function"==typeof t?.randomUUID?`houseplan-${t.randomUUID()}`:`houseplan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}(),s=Date.now()+Math.max(1,r);let c=!1,u=null,f=null,g=null;const y=new Promise(t=>{u=t}),w=new Promise((t,n)=>{f=t,g=n}),b=[];try{const r=await o.call(e,t=>{!0===d(t)?.retain&&h(t)&&u?.()},{type:"mqtt/subscribe",topic:`${l}/bridge/info`});"function"==typeof r&&b.push(r);const p=await o.call(e,t=>{if(!0===d(t)?.retain)return;const n=h(t);null!==n?n&&function(t){const n=d(t),e=n?.transaction??d(n?.data)?.transaction;return"string"==typeof e||"number"==typeof e?String(e):null}(n)===i&&f?.(n):c&&g?.(a("invalid_payload"))},{type:"mqtt/subscribe",topic:`${l}/bridge/response/networkmap`});"function"==typeof p&&b.push(p),await m(y,Math.min(4e3,Math.max(1,s-Date.now()))),c=!0,await t.callService("mqtt","publish",{topic:`${l}/bridge/request/networkmap`,payload:JSON.stringify({type:"raw",routes:!1,transaction:i}),qos:0,retain:!1});const v=await m(w,s-Date.now()),_=d(v)?.status;if(_&&"ok"!==_)throw a("provider");return n(v,l)}finally{for(const t of b)try{t()}catch{}}})}export{f as readZhaTopology,g as refreshZ2mTopology,u as subscribeZigbeeTopology,l as zigbeeTopologyRuntimeSnapshot};
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="ab077e4fb68320e583ac75418056196bdb941d9751fbe28c49e91f8310519e28";try{await import("./houseplan-assets/houseplan-card-BotpFSfJ.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__="884182570484410d7ab1a7212b07de9f2b4e5683c0d859f4e9d3feaae9318c87";try{await import("./houseplan-assets/houseplan-card-CODdCIWj.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)}
+5
View File
@@ -2,6 +2,11 @@
## Unreleased
- Every color picker now has a clear full-width **OK** button. Colors and
opacity still apply live, while invalid HEX input stays open for correction
instead of being accepted accidentally
([#476](https://github.com/Matysh/houseplan-card/issues/476)).
## v1.73.0-beta.1 — 2026-09-06
- Active Zigbee topology links and route bubbles now stay readable above room
+5
View File
@@ -8,6 +8,11 @@
## Не выпущено
- Во всех окнах выбора цвета появилась понятная полноширинная кнопка **«ОК»**.
Цвет и прозрачность по-прежнему применяются сразу, а при некорректном HEX
окно остаётся открытым, чтобы значение можно было исправить
([#476](https://github.com/Matysh/houseplan-card/issues/476)).
## v1.73.0-beta.1 — 2026-09-06
- Активные линии и подписи Zigbee-топологии теперь читаются поверх названий
+9
View File
@@ -446,6 +446,15 @@
labels through the unchanged color/opacity event contract, and the shared
component contains no native `input[type=color]`
[unit: `color-picker.test.mjs`].
- [ ] The localized full-width OK button is the final picker control, remains at
least 40 CSS px high in native and fallback surfaces, and closes without a
duplicate value event or click-through to the parent card
[unit: `color-picker.test.mjs`, auto: `smoke_color_picker.mjs`,
`smoke_help_affordance.mjs`].
- [ ] Invalid HEX keeps the picker open and focused on its error even after a
repeated OK; only new valid HEX input unlocks confirmation, while live
color/opacity updates and the existing outside/trigger/Escape close paths
retain their values [auto: `smoke_color_picker.mjs`].
- [ ] At 390 px the one surface exposes hue, saturation, brightness, HEX and
opacity without horizontal overflow; keyboard Shift+Arrow, touch pointer
cancellation, invalid HEX recovery, Escape focus return and disabled mode
+12 -12
View File
@@ -3,7 +3,7 @@
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"oxipng": "oxipng 10.2.0",
"sourceFingerprint": "13b317f3b91e5c6e34f62eca94446d9f3e90deca29f5b7f4623031b781a25163",
"sourceFingerprint": "8e63cfc0f990da55e9b9a9f6691503693d3b552dad77ee95a720df3aa16fc297",
"captureScriptSha256": "cadb8e1bcab9f1dcdd7d75b3b90ddcbaaeb2b8c2a098f575a21f39ff70f5c59c",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -15,7 +15,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "13b317f3b91e5c6e34f62eca94446d9f3e90deca29f5b7f4623031b781a25163",
"sourceSha256": "8e63cfc0f990da55e9b9a9f6691503693d3b552dad77ee95a720df3aa16fc297",
"imageSha256": "80a70361dc18dd0461568df332062e6482c633af5d280954f8b675701418a76d"
},
"view-touch": {
@@ -26,7 +26,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "13b317f3b91e5c6e34f62eca94446d9f3e90deca29f5b7f4623031b781a25163",
"sourceSha256": "8e63cfc0f990da55e9b9a9f6691503693d3b552dad77ee95a720df3aa16fc297",
"imageSha256": "4106cc28847047505f46921ff95765d8abdf5b382d9d17d4c5e4ad129dd8f6be"
},
"space-create": {
@@ -37,7 +37,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "13b317f3b91e5c6e34f62eca94446d9f3e90deca29f5b7f4623031b781a25163",
"sourceSha256": "8e63cfc0f990da55e9b9a9f6691503693d3b552dad77ee95a720df3aa16fc297",
"imageSha256": "9f0970de09772674c3156b9fc5b2d49de0ceb22be11ecf8b136047d37d100a84"
},
"room-contour-close": {
@@ -48,7 +48,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "13b317f3b91e5c6e34f62eca94446d9f3e90deca29f5b7f4623031b781a25163",
"sourceSha256": "8e63cfc0f990da55e9b9a9f6691503693d3b552dad77ee95a720df3aa16fc297",
"imageSha256": "1dab6cc3b9d1bf7d8c40f0e5137f8c688683c9b7eabc5167da99d41ecfdd5b79"
},
"plan-context-tray": {
@@ -59,7 +59,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "13b317f3b91e5c6e34f62eca94446d9f3e90deca29f5b7f4623031b781a25163",
"sourceSha256": "8e63cfc0f990da55e9b9a9f6691503693d3b552dad77ee95a720df3aa16fc297",
"imageSha256": "94ef50762753c0ac6ddc84d2521c4232a3f9ecd89843810d2df61c517592bed7"
},
"device-editor": {
@@ -70,7 +70,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "13b317f3b91e5c6e34f62eca94446d9f3e90deca29f5b7f4623031b781a25163",
"sourceSha256": "8e63cfc0f990da55e9b9a9f6691503693d3b552dad77ee95a720df3aa16fc297",
"imageSha256": "53bcd3dd041c8ddef7c2d5a9defc7ebfc10b296849dad0c2115603009c715f6b"
},
"device-display-preview": {
@@ -81,7 +81,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "13b317f3b91e5c6e34f62eca94446d9f3e90deca29f5b7f4623031b781a25163",
"sourceSha256": "8e63cfc0f990da55e9b9a9f6691503693d3b552dad77ee95a720df3aa16fc297",
"imageSha256": "c797362e523e7ea42cab19e9e3dca374bce9e786cb84601993d235264b040d1e"
},
"background-editor": {
@@ -92,7 +92,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "13b317f3b91e5c6e34f62eca94446d9f3e90deca29f5b7f4623031b781a25163",
"sourceSha256": "8e63cfc0f990da55e9b9a9f6691503693d3b552dad77ee95a720df3aa16fc297",
"imageSha256": "054170fd9ef45762b602b4d5c9c3b9ea9724858be61af137970c246f485c13bb"
},
"room-card": {
@@ -103,7 +103,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "13b317f3b91e5c6e34f62eca94446d9f3e90deca29f5b7f4623031b781a25163",
"sourceSha256": "8e63cfc0f990da55e9b9a9f6691503693d3b552dad77ee95a720df3aa16fc297",
"imageSha256": "2ae4a58853d98e10d12456b2078ec2f6a0b597722c8310d7b016abd2bc42561e"
},
"device-info": {
@@ -114,11 +114,11 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "13b317f3b91e5c6e34f62eca94446d9f3e90deca29f5b7f4623031b781a25163",
"sourceSha256": "8e63cfc0f990da55e9b9a9f6691503693d3b552dad77ee95a720df3aa16fc297",
"imageSha256": "aeeadb860404c3cbd130e858db6dd1d78b7001a6855405034a86959a9bc35669"
}
},
"acceptedOn": "win32",
"acceptedOn": "linux",
"acceptance": {
"declared": [
"space-create",
+204
View File
@@ -0,0 +1,204 @@
# CODE-REVIEW-476-r1
- **Issue:** #476 — Явное завершение выбора цвета кнопкой «ОК»
- **Этап:** код-ревью (PROCESS.md §2.7), заход r1, блокирующих циклов израсходовано 0 из 4
- **Ветка:** `issue/476-color-picker-ok`, HEAD `b5a1001e` (приведена к `dev` конвейером,
поверх легло 4 коммита `dev`: `34a6b276 → b5a1001e`). Разбор — полный, не по дельте
(ребейз на ушедший вперёд `dev`, §7.2/§2.10).
- **Диапазон:** `git log --oneline origin/dev..HEAD` — 7 коммитов (2 ТЗ, 2 документа
ревью ТЗ, 3 реализации); материал — `git diff origin/dev...HEAD`, 53 файла.
- **ТЗ:** `docs/specs/476-color-picker-ok.md`, лёгкий трек не применялся (полный трек по
собственному решению аналитики — новый UX-контракт), ревью ТЗ зелёное на r2
(`docs/reviews/SPEC-REVIEW-476-r2.md`).
## Скоуп диффа
Общий `hp-color-opacity`: полноширинная кнопка «ОК» (последний DOM-control),
CSS-контракт (100% width, ≥40px, forced-colors), новый признак `_hexNeedsValidInput`
(защита от снятия ошибки повторным «ОК» без нового ввода), `color_picker.confirm` в
4 словарях, per-card labels в `houseplan-card.ts`, обновлённые unit/i18n-тесты,
расширенный `demo/smoke_color_picker.mjs` и правки `demo/smoke_help_affordance.mjs`
(fallback-путь), запись в `scripts/smoke-links.mjs`, оба changelog, `docs/TESTING.md`,
пересобранные `dist/**`/`custom_components/houseplan/frontend/**`, обновлённый
`docs/images/screenshots.json` (только fingerprint, PNG не менялись). Backend, модель
геометрии, config/storage не затронуты.
## Как проверялось
Зелёного Validate на `b5a1001e` не найдено — прогнал дешёвые и часть тяжёлых гейтов сам.
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | чисто, без вывода |
| Unit-тесты | `npm test` | `2077 tests, pass 2076, fail 0, skipped 1` — совпадает с заявленным в хендоффе |
| Build + sync | `npm run build && npm run bundle:sync` | `dist` собран; `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` — идентичны; `git status --short` после `bundle:sync` пуст (demo/srv/assets, custom_components и dist совпадают с закоммиченным, включая несобственную копию стенда) |
| Docs-гейт | `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 12 external links)` — обязателен, diff трогает `src/**` |
| Новый `any` | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | «Новых any нет» (59 добавленных строк в 2 файлах) |
| Выбор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | Зарегистрированная связь (2): `smoke_color_picker.mjs`, `smoke_help_affordance.mjs`. Слабая связь (15, общее имя `stopPropagation`) — просмотрел список: `smoke_cover_*`, `smoke_decor`, `smoke_edit_walk`, `smoke_editor_gestures`, `smoke_furniture`, `smoke_hide_layers`, `smoke_inert_openings`, `smoke_modes`, `smoke_room_cards`, `smoke_space_settings`, `smoke_tap_ctx`, `smoke_tap_run`, `smoke_toggle_confirmation`, `smoke_value_face_source` — все про другие компоненты, ни один не адресует `hp-color-opacity`; не гонял |
| Целевой смок 1 | `node demo/smoke_color_picker.mjs` | `OK`, все 21 поле `true` |
| Целевой смок 2 | `node demo/smoke_help_affordance.mjs` | `OK`, все поля `true`, включая новые `fallbackPickerHasConfirm`/`fallbackPickerConfirmCloses` |
| Bundle budget | `npm run bundle:budget` | initial View `299683 B` gzip, потолок `300500±2000` — в бюджете; `::warning::` про низкий запас (1383 Б) — долг #367, не новый и не вызван этим диффом |
| Golden (advisory) | `npm run golden:verify` | 5 названных ТЗ сцен (`decor-color-popover-mobile-ru`, `decor-color-popover-desktop-en`, `general-color-popover-desktop-en`, `device-ripple-color-popover-mobile-ru`, `space-room-color-popover-desktop-ru`) — `different`, ожидаемо (новая кнопка); остальные ~100 сцен — `passed`. Просмотрел `actual/`+`diff/` для обеих цветовых сцен глазами — разница только в кнопке и вертикальном сдвиге содержимого попапа, цвет/геометрия/тема не поехали. Принятие эталонов — предрелизный гейт (`golden:accept -- --reviewed` на Linux CI), не гейт код-ревью; автор в хендоффе прямо назвал это «НЕ сделано» |
| Docs screenshots | ссылка автора: run `34029831929` | `gh run view` → `completed/success`; SHA `ce07db74` — предок HEAD; `check-docs.mjs` на HEAD зелёный, то есть текущий фингерпринт уже совпадает с деревом |
| Process-gate | `node scripts/process-gate.mjs` | «гейт пройден, предупреждений 0» |
| Трейлеры | `git log origin/dev..HEAD` (7 коммитов) | у всех `Issue: #476`; `11cc5386` (реализация) — `User-Visible: yes`, оба changelog в том же коммите; остальные 6 — `User-Visible: no` |
**Мутации, прогнанные лично** (§2.7 «чем краснеет», защитные AC):
| # | Мутация | Файл | Прогон | Результат |
|---|---|---|---|---|
| 1 | Снял ветку `if (this._hexNeedsValidInput) { this._hexInvalid = true; return; }` в `_commitHex` — повторное «ОК» без нового ввода закрывало бы picker | `src/hp-color-opacity.ts` | `npm run bundle:sync && node demo/smoke_color_picker.mjs` | `repeatedConfirmCannotBypassInvalidHex: expected true, got false` — тест красится, как обязан (см. M1) |
| 2 | Убрал `if (normalized !== this._lastValidColor)` — `_commitHex` эмитит всегда | `src/hp-color-opacity.ts` | то же | `confirmClosesWithoutDuplicateOrClickThrough: false`, `validCorrectionAllowsConfirm: false` — AC2 доказан исполнением |
| 3 | Убрал `event.stopPropagation()` из `_confirm` | `src/hp-color-opacity.ts` | то же + отдельный probe-скрипт с листенерами на каждом уровне DOM | `confirmClosesWithoutDuplicateOrClickThrough: true` — **тест НЕ покраснел** (см. M2); probe показал, что событие гасится на `.editor-secondary`, на один уровень выше `hp-color-opacity`, независимо от этой мутации |
После каждой мутации источник восстановлен из `git show HEAD:src/hp-color-opacity.ts`,
`npm run bundle:sync` прогнан повторно, `git status --short` — пусто, `demo/smoke_color_picker.mjs`
снова `OK` (проверено).
**Не гонял и почему:**
- `python -m pytest tests_backend` — diff не трогает `custom_components/**/*.py`.
- `npm run invariants` — diff не меняет рёбра комнат, записи толщины, `layout`,
`marker.space`, `open_spans`; геометрия не затронута.
- Performance-профили — не названы в AC; §15 ТЗ обоснованно утверждает отсутствие
влияния (один статический `button`, один `click`-handler в уже открытом lazy-picker).
- Полная матрица `demo/smoke_*.mjs` и полный `scripts/mutation-gate.mjs` — предрелизные
гейты (PROCESS §8), не гейт код-ревью; по дельте прогнаны только зарегистрированные
смоки плюс личные точечные мутации.
- `npm run golden:accept` — не моя роль на этом этапе; смотри выше.
## Находки
### Medium (в скоупе задачи, обе)
**M1 — AC4: защита от обхода невалидного HEX повторным «ОК» не имеет постоянного
свидетеля в `scripts/mutation-gate.mjs`.**
Признак `_hexNeedsValidInput` — это ровно то, что r1 ревью ТЗ потребовало добавить
(`SPEC-REVIEW-476-r1.md`), и ровно то, что доказывается только браузерным смоком
(`demo/smoke_color_picker.mjs`, дорогой гейт: пересборка бандла + Chromium). По
PROCESS.md §2.7: «Мутант в `scripts/mutation-gate.mjs` обязателен, когда защита живёт
в продуктовом коде и проверяется дорогим гейтом… там ревьюер не воспроизведёт
отрицательный прогон второй раз». Диф не добавляет ни одной записи в
`scripts/mutation-gate.mjs` (только запись-указатель в `scripts/smoke-links.mjs`, это
другой реестр — для выбора смоков, не для мутационного гейта).
Я лично применил ровно эту мутацию (см. таблицу выше, #1) и получил красный
`repeatedConfirmCannotBypassInvalidHex`, так что защита реальна и смок сегодня её ловит
— но без постоянной записи в реестре это разовое доказательство ревьюера, а не
воспроизводимый гейт: следующий рефакторинг того же кода останется незамеченным, если
не начитает эту главу отчёта.
**Чинится:** зарегистрировать мутант (по образцу #366/#384/#330 — `guard: node
demo/smoke_color_picker.mjs`, патч — снятие ветки `if (this._hexNeedsValidInput)` в
`_commitHex`), прогнать `node scripts/mutation-gate.mjs --check` и целевой `--id=`.
**M2 — AC3/§7.2.4: заявленная защита «клик не проваливается в план/toolbar под
поверхностью» не имеет проверки, способной упасть на снятой защите.**
Я убрал `event.stopPropagation()` из `_confirm` (единственный код защиты, добавленный
этой задачей) и прогнал `demo/smoke_color_picker.mjs` — `confirmClosesWithoutDuplicateOrClickThrough`
остался `true`. Добавил зонды-слушатели на каждом уровне DOM (кнопка → `.picker` →
shadowRoot picker → хост `hp-color-opacity` → `.editor-secondary` → shadowRoot `card`
→ `card`) и увидел, что событие в проверяемом сценарии (decor-tool color внутри
`.editor-secondary`) гасится ещё на `.editor-secondary`, на уровень выше самого
пикера, независимо от того, вызывает ли `_confirm` `stopPropagation()`.
Продуктового бага здесь нет — клик в план сегодня не проваливается, даже с двойным
запасом. Но это значит, что заявленная в АС3/§7.2.4 защита самого `hp-color-opacity`
не доказана ни одним тестом, который способен упасть: в единственном
протестированном потребителе есть посторонний внешний гаситель, а другие потребители
общего пикера (general settings, room/space, ripple color) не обязательно обёрнуты
таким же контейнером — для них `_confirm`'s `stopPropagation()` может быть
единственной защитой, и она никем не проверяется.
**Чинится** одним из двух путей: (а) перенести click-through-пробу в потребителя без
такого внешнего гасителя (например, `general-color` или `device-ripple-color`,
благо они уже в golden-матрице) так, чтобы снятие `stopPropagation()` красило смок;
либо (б) зарегистрировать мутант в `scripts/mutation-gate.mjs` с guard'ом на
`demo/smoke_color_picker.mjs`, но тогда сам смок сначала должен научиться падать по
пункту (а) — иначе мутант зарегистрирован, но бесполезен.
High: 0. Medium вне скоупа: 0.
### Low (сняты с записью, не блокируют)
- **L1** `src/houseplan-card.ts:5932` — `invalidHex: …, confirm: …` на одной строке
(`git diff` слепил две записи объекта). Чисто косметика, синтаксис и typecheck
корректны; не мешает чтению настолько, чтобы требовать отдельного цикла. Снято.
- **L2** AC3 «keyboard activation» (Enter/Space по кнопке) проверяется в смоке только
через `.click()`, не через реальный `KeyboardEvent`. Риск исчезающий: кнопка —
нативный `<button type="button">` без собственного `keydown`-перехватчика, поэтому
активация с клавиатуры и мышью в браузере производят один и тот же `click`. Снято.
- **L3** AC5 в ТЗ (§12) декларирует доказательство «smoke» для всех путей закрытия
(`outside pointer`, `trigger`, `Escape`), но ни `demo/smoke_color_picker.mjs`, ни
unit-тесты не проверяют `outside pointer` и повторный клик по `trigger` — ни до
этой задачи, ни после (проверил `git show origin/dev:demo/smoke_color_picker.mjs`
— тоже нет). Код `_outsidePointerDown` и ветка закрытия в `_toggle()` этим диффом
не тронуты (проверено чтением, не исполнением) — риска регрессии нет, `Escape`
же покрыт и прогнан зелёным. Пред-существующий пробел плана тестов, не этой
задачи; попутно не чиню (§ «скоуп не расширяется»), фиксирую, чтобы не читать
как «покрыто», когда это не так.
## AC — таблица доказательств (весь список, r1)
| AC | Чем доказан | Чем краснеет |
|---|---|---|
| AC1 видимая кнопка/геометрия | unit (`color-picker.test.mjs`: `width:100%`, `min-height:40px`, DOM-порядок) + `golden:verify` (5 сцен, диф — только кнопка, см. выше) | не защитный AC (расположение/CSS), правило §2.7 не требует столбца |
| AC2 live parity, без дублирующего события | smoke `confirmClosesWithoutDuplicateOrClickThrough`, `validCorrectionAllowsConfirm` | мутация #2 лично: `_commitHex` эмитит всегда → оба поля `false` |
| AC3 успешное завершение / без click-through | smoke `confirmClosesWithoutDuplicateOrClickThrough` (aria-expanded, focus, `parentClicks`) | click-through часть — **не красится** (M2); закрытие/фокус — красится (проверено логикой `_closePicker(true)`, не мутировал отдельно, риск низкий, код тривиален) |
| AC4 невалидный HEX / защита от обхода | unit (regex на `_hexNeedsValidInput`) + smoke `invalidHex*`, `repeatedConfirmCannotBypassInvalidHex` | мутация #1 лично: красное (M1 — нет постоянной регистрации) |
| AC5 прежние close paths | smoke `escapeClosesFirstAndRefocuses` (Escape); outside/trigger — не проверены автотестом, проверено чтением (L3) | Escape: не мутировал отдельно, код path нетронут диффом |
| AC6 i18n/a11y | unit (`test/i18n.test.mjs`, `color-picker.test.mjs` — EN/RU/DE/FR parity, DOM-порядок = порядок Tab) + smoke `englishLabels`/`cardLanguageOwnsCopy` | не защитный AC (текст/раскладка) |
| AC7 touch/fallback/lifecycle | smoke `confirmIsFullWidthTouchTarget`, `smoke_help_affordance.mjs` (`fallbackPickerHasConfirm`, `fallbackPickerConfirmCloses`) | не мутировал; оба пути (Popover/fallback) реально прогнаны через разные смоки, расхождения нет |
| AC8 совместимость/release/бюджеты | `no-new-any`, `check-docs`, `bundle:budget`, оба changelog в одном коммите (см. таблицу гейтов) | не защитный AC в терминах §2.7 |
## Что проверено и корректно
- Состояние `_hexNeedsValidInput` синхронно с §7.3 ТЗ: снимается только новым
`input`-событием с валидным HEX (`_hexInput`), не снимается нормализацией draft в
`_commitHex`, не снимается повторным «ОК», не снимается `blur` — прочитано построчно
и подтверждено мутацией #1.
- `_hexDraft`/`_lastValidColor` остаются синхронными после любого live-изменения (hue/
sat/val/opacity тоже проходят через `_emit`, который обновляет `_hexDraft`), поэтому
обычное «ОК» после слайдеров не создаёт дублирующий emit — подтверждено мутацией #2
от противного и штатным прогоном смока.
- Кнопка — последний DOM-control независимо от `showOpacity` (проверено и unit-, и
smoke-ассертами на разных инстансах, включая `showOpacity=false`).
- i18n: 4 языка добавлены синхронно; DE/FR allow-list для омографов с EN обновлён
обоснованно (`OK` там действительно совпадает с английским текстом — это ожидаемый
омограф, а не пропущенный перевод).
- Оба changelog в одном `User-Visible: yes` коммите (`11cc5386`), трейлеры на всех 7
коммитах корректны, ветка/issue совпадают.
- Backend, config, миграция, геометрия не затронуты — соответствует ТЗ §10 и §6
(не входит).
- Golden-diff глазами: разница ровно в кнопке и вертикальном сдвиге контента, ничего
постороннего не поехало ни в одной из 5 сцен.
## Чего не проверял
- `pytest tests_backend`, `npm run invariants`, performance-профили, полная матрица
`demo/smoke_*.mjs`, полный `scripts/mutation-gate.mjs`, `golden:accept` — см.
обоснование в таблице гейтов выше.
- 15 «слабых» смоков по `stopPropagation` из вывода `smoke-select.mjs` — просмотрел
список имён, все про другие поверхности, не про `hp-color-opacity`; не гонял.
## Вывод
High: 0. Medium в скоупе: 2 (M1, M2) — обе чинятся в этой же задаче, без отдельного
issue (#202). Verdict: жёлтый.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/476-color-picker-ok`, коммит `34a6b276473c` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `94485fdc0eca504692dd2776c7f3c86518a8dfb9`
```
git log --all --format='%H %T' | grep 94485fdc0eca
```
- ТЗ `docs/specs/476-color-picker-ok.md`, блоб `d21066d56e69c35fe7d0b40d9f965968dac9f803`
```
git log --all --find-object=d21066d56e69c35fe7d0b40d9f965968dac9f803 -- docs/specs/476-color-picker-ok.md
```
+151
View File
@@ -0,0 +1,151 @@
# CODE-REVIEW-476-r2
- **Issue:** #476 — Явное завершение выбора цвета кнопкой «ОК»
- **Этап:** код-ревью (PROCESS.md §2.7), заход r2, блокирующих циклов израсходовано 1 из 4
- **Ветка:** `issue/476-color-picker-ok`, HEAD `6e0e1f8c` (`origin/dev` = `fc5973c2`, ровно merge-base — ветка полностью приведена к `dev`, ребейз чист, конфликтов нет)
- **Диапазон:** `git log --oneline origin/dev..HEAD` — 9 коммитов; материал — `git diff origin/dev...HEAD`, 56 файлов
- **ТЗ:** `docs/specs/476-color-picker-ok.md`, полный трек, ревью ТЗ зелёное на r2 (`docs/reviews/SPEC-REVIEW-476-r2.md`)
## Почему разбор полный, а не по дельте
Между r1 код-ревью (жёлтый, `docs/reviews/CODE-REVIEW-476-r1.md`, доказано на `HEAD b5a1001e`) и этим заходом ветка дважды двигалась:
1. автор устранил M1 и M2 коммитом `test: harden color picker confirmation guards` — локальная правка тестов/реестров, ожидаемый предмет обычной дельты;
2. затем `origin/dev` ушёл вперёд ещё на 5 коммитов, review не запустился («Ревью не запускалось» — конфликт слияния, цикл не израсходован), и автор перебазировал ветку на `fc5973c2`, разрешив конфликт только в сгенерированных `dist/**`/`custom_components/houseplan/frontend/**` (пересобраны из объединённых исходников).
Пункт 2 — ребейз на ушедший вперёд `dev`, что PROCESS.md §2.10/§7.2 прямо называет «другим кодом», обязывающим к полному разбору. Я его сделал: ниже — полная таблица гейтов и AC, а не только диф вокруг M1/M2. Практически это означало не доверять чужому заявлению «конфликтовали только сгенерированные файлы», а проверить самому: `git log --oneline origin/dev..HEAD -- src/hp-color-opacity.ts` показывает **ровно один** коммит (`82f65acd`, реализация, предшествующая r1-ревью) — второй ребейз действительно не тронул продуктовую логику, только сгенерированные бандлы и `docs/images/screenshots.json` (fingerprint). Это подтверждено чтением, а не принято на слово.
## О «SHA не назван» (проверка §2.10, шаг 1)
Комментарий-вердикт r1 в issue (11:47:29Z) сам по себе не называет проверенный SHA — но материал по процессу объявляется не в комментарии, а в блоке «Материал раунда» документа (`CODE-REVIEW-476-r1.md`), и там SHA есть: в прозе документа — `HEAD b5a1001e` (после того как конвейер сам привёл ветку к `dev`, "34a6b276 → b5a1001e"), в машинном блоке — коммит `34a6b276473c` (тот же материал, тег до этого приведения) плюс дерево `94485fdc0eca...`. Отсутствие SHA в комментарии — не находка: PROCESS.md §2.10 явно возлагает эту обязанность на документ, а не на комментарий.
Оба анкера (`34a6b276473c`, `b5a1001e`) сейчас не резолвятся в этом чекауте (`git cat-file -t 94485fdc0eca... ` → `could not get object info`) — это следствие **второго** ребейза (пункт 2 выше), случившегося **после** публикации документа r1 (12:11 против 11:47). На момент публикации r1 материал был жив, что и требуется §2.10; это обычный случай осиротевшего SHA («98 из 804»), не находка. Проверить содержимое напрямую я не мог, поэтому опираюсь на: (а) единственный коммит, трогающий `src/hp-color-opacity.ts`, не менялся между r1 и сейчас (см. выше), (б) построчное сравнение текста файла ниже совпадает с тем, что описывает документ r1.
## Скоуп диффа (полный)
`src/hp-color-opacity.ts` (61 строка, коммит `82f65acd`, не менялся после r1) — полноширинная кнопка «ОК», `_hexNeedsValidInput` защита от обхода невалидного HEX повторным подтверждением, `_confirm()` со `stopPropagation()`. `src/houseplan-card.ts` (1 строка) — `confirm` label. `src/i18n/{en,ru,de,fr}.json` — `color_picker.confirm`. `test/color-picker.test.mjs`, `test/i18n.test.mjs` — юниты на DOM-порядок/CSS/label/i18n-parity. `demo/smoke_color_picker.mjs` (расширен), `demo/smoke_help_affordance.mjs` (fallback-путь), **новый** `demo/smoke_color_picker_consumers.mjs` (M2). `scripts/mutation-gate.mjs` (2 новых мутанта, M1+M2), `scripts/smoke-links.mjs` (реестр выбора смоков). Оба changelog, `docs/TESTING.md`, `docs/specs/README.md`, пересобранные `dist/**`/`custom_components/houseplan/frontend/**`, обновлённый `docs/images/screenshots.json` (только fingerprint). Backend, геометрия, config/storage не затронуты.
## Как проверялось
Зелёного Validate на `6e0e1f8c` нет (`gh run view 34032363066` — `completed/failure`, единственный красный job — `Golden-кадры против принятых эталонов`; `Фронтенд`, `Предполётные проверки`, оба перф/смок-джоба и все 3 шарда браузерных смоков — `success`). Прогнал сам:
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | чисто, без вывода |
| Unit-тесты | `npm test` | `2077 tests, pass 2076, fail 0, skipped 1` — совпадает с заявленным |
| Build + sync | `npm run build && npm run bundle:sync` | `dist` собран; `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` — идентичны; `git status --short` пуст после сборки |
| Docs-гейт | `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 12 external links)` — обязателен, diff трогает `src/**` |
| Новый `any` | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | «Новых any нет» (59 добавленных строк в 2 файлах — те же, что в r1: делта не добавила новых строк в `.ts`) |
| Process-gate | `node scripts/process-gate.mjs` | «гейт пройден, предупреждений 0» (9 коммитов в диапазоне) |
| Выбор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | Прямое совпадение (1): `smoke_color_picker_consumers.mjs` (← `_surface`, новый файл появился в диффе). Зарегистрированная связь (2): `smoke_color_picker.mjs`, `smoke_help_affordance.mjs`. Слабая связь (15, `stopPropagation`) — те же имена, что и в r1 (`smoke_cover_*`, `smoke_decor`, `smoke_edit_walk`, `smoke_editor_gestures`, `smoke_furniture`, `smoke_hide_layers`, `smoke_inert_openings`, `smoke_modes`, `smoke_room_cards`, `smoke_space_settings`, `smoke_tap_ctx`, `smoke_tap_run`, `smoke_toggle_confirmation`, `smoke_value_face_source`) — просмотрел список повторно, все про другие поверхности; не гонял |
| Целевой смок 1 | `node demo/smoke_color_picker.mjs` | `OK`, все 21 поле `true`, включая `repeatedConfirmCannotBypassInvalidHex: true` |
| Целевой смок 2 (новый) | `node demo/smoke_color_picker_consumers.mjs` | `OK`, все 13 полей `true`, включая `generalConfirmDoesNotClickThrough: true` |
| Целевой смок 3 | `node demo/smoke_help_affordance.mjs` | `OK`, все поля `true`, включая `fallbackPickerHasConfirm`/`fallbackPickerConfirmCloses` |
| Bundle budget | `npm run bundle:budget` | initial View `299595 B` gzip, потолок `300500±2000` — в бюджете; запас `1471 Б` — тот же пред-существующий долг #367, не новый |
| Golden (advisory) | `npm run golden:verify` | 15 `different` из 150 сцен: 5 названных в ТЗ color-popover сцен (ожидаемо, см. AC1) + **10 `isometric-*`**, не относящихся к этому диффу (см. отдельный раздел ниже); 135 `passed` |
| Мутация 1 (M1) | `node scripts/mutation-gate.mjs --id=color-picker-invalid-confirm-latch-removed` | патч снимает `if (this._hexNeedsValidInput) {...}` в `_commitHex`; guard — `smoke_color_picker.mjs` → «тест покраснел, как обязан», «поймано 1 из 1» |
| Мутация 2 (M2) | `node scripts/mutation-gate.mjs --id=color-picker-confirm-click-through` | патч снимает `event.stopPropagation()` из `_confirm`; guard — новый `smoke_color_picker_consumers.mjs` → «тест покраснел, как обязан», «поймано 1 из 1» |
| Применимость всех мутантов | `node scripts/mutation-gate.mjs --check` | все патчи ложатся на текущий код, включая оба новых |
**Не гонял и почему** (не изменилось с r1, диф их не задевает):
- `python -m pytest tests_backend` — diff не трогает `custom_components/**/*.py`.
- `npm run invariants` / `model-invariants.mjs` — diff не меняет рёбра комнат, толщину, `layout`, `marker.space`, `open_spans`.
- Performance-профили — не названы в AC; §15 ТЗ обоснованно утверждает отсутствие влияния (один статический `button`, один `click`-handler в уже открытом lazy-picker); perf-smoke job в CI зелёный.
- Полная матрица `demo/smoke_*.mjs` и полный `scripts/mutation-gate.mjs` (226 смоков, десятки мутантов) — предрелизные гейты (PROCESS §8), не гейт код-ревью на задаче такого объёма; прогнаны только целевые/прямые + два новых мутанта.
- `npm run golden:accept` — не моя роль на этом этапе.
- «Одно число — один источник»: диф не добавляет и не меняет пользовательскую числовую величину (кнопка не показывает значение), правило неприменимо; `test/single-source-numbers.test.mjs` зелёный в составе `npm test`.
## Golden: 10 `isometric-*` diff — не относится к #476
`golden:verify` на этой ветке даёт 15 `different`. Пять совпадают с ТЗ (§13.4: `decor-color-popover-mobile-ru`, `decor-color-popover-desktop-en`, `general-color-popover-desktop-en`, `device-ripple-color-popover-mobile-ru`, `space-room-color-popover-desktop-ru`) — просмотрел `actual/`+`diff/` глазами, разница ровно в кнопке и вертикальном сдвиге содержимого, цвет/геометрия/тема не поехали. Остальные десять — все сцены `isometric-*` (`isometric-geometry-view-{dark,light}`, `isometric-live-layers-dark`, `isometric-touch-kiosk-dark`, `isometric-large-warm-remount-dark`, `isometric-stage3-overlays-{light,dark}`, `isometric-stage3-openings-dark`, `isometric-stage3-forced-colors-dark`, `isometric-stage3-no-filter-dark`) — их не должно быть, диф этой задачи не трогает ни один `src/*isometric*` файл.
Проверил происхождение: поднял временный `git worktree add /tmp/dev-check origin/dev --detach` (чистый `fc5973c2`, без единого коммита #476), собрал бандл (`npm run bundle:sync`) и прогнал `golden:verify` там же — те же ровно 10 сцен `isometric-*` выходят `different`, 0 отношения к color-picker. Chromium совпадает с эталоном побайтово по версии (`151.0.7922.34` — `demo/golden/baselines/baselines-index.json` vs установленный движок), так что это не известный кейс расхождения рендеринга по окружению. Причина видна по истории `dev`: `be5eeb41 fix: remove isometric overlay plates` (issue #471) поменял рендер, но последний коммит, принимающий golden-эталоны, — `de215578 test: accept isometric stage 3 golden baselines`, **до** `be5eeb41`; после него в `dev` нет коммита с принятыми isometric-эталонами. Это преднесённый долг `dev` от #471, полностью независимый от этой ветки — красный CI job `Golden-кадры против принятых эталонов` на `6e0e1f8c` красится и по этой причине тоже, но её причина не в #476. Golden — предрелизный, не код-ревью гейт (PROCESS §8); не завожу отдельный issue: не моя находка по существу (баг чужой, уже смердженной задачи #471, чинится обычным циклом приёмки эталонов перед бетой, а не патчем из этой ветки), и я не уверен, что это дефект, а не намеренно отложенное до релизного гейта принятие — распознавать и чинить чужой merged-код здесь означало бы расширять скоуп этой задачи.
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
| **M1** — AC4-защита (`_hexNeedsValidInput`) доказана только смоком, без постоянного мутанта в `scripts/mutation-gate.mjs` | Зарегистрирован мутант `color-picker-invalid-confirm-latch-removed` (guard `demo/smoke_color_picker.mjs`) | `scripts/mutation-gate.mjs` (коммит `6e0e1f8c`); лично прогнал `--id=color-picker-invalid-confirm-latch-removed` → «тест покраснел, как обязан» |
| **M2** — AC3-защита (`stopPropagation()` в `_confirm`) не доказана тестом, способным упасть — единственный проверенный потребитель гасил клик выше по DOM независимо от мутации | Добавлен `demo/smoke_color_picker_consumers.mjs`, сценарий `generalConfirmDoesNotClickThrough` на «lightOn» (general settings, потребитель без внешнего гасителя); зарегистрирован мутант `color-picker-confirm-click-through` (guard — новый смок) | `demo/smoke_color_picker_consumers.mjs:49-63`, `scripts/mutation-gate.mjs` (коммит `6e0e1f8c`); лично прогнал `--id=color-picker-confirm-click-through` → «тест покраснел, как обязан» |
Оба закрытия проверены исполнением мутанта, не заявлением автора: патч применяется к `_commitHex`/`_confirm`, гоняется именно названный guard, тест красится, патч откатывается, `git status --short` пуст.
## Унаследовано из r1 (без повторной проверки)
Документ: `docs/reviews/CODE-REVIEW-476-r1.md`, материал — дерево `94485fdc0eca504692dd2776c7f3c86518a8dfb9` (SHA-анкеры мертвы после второго ребейза, см. раздел выше; сам r1-документ пережил ребейз бит-в-бит, коммит `baea0fcf`).
Основание наследования — не доверие, а факт: `src/hp-color-opacity.ts` не менялся ни одним коммитом между `82f65acd` (реализация, предшествующая r1) и `HEAD`; я лично перечитал этот файл целиком в текущем HEAD (строки 480–740) и построчно сверил с описанием r1 (`_hexNeedsValidInput` семантика, `_confirm`/`_commitHex`/`_hexInput` взаимодействие, DOM-порядок кнопки, CSS-контракт) — расхождений нет.
Наследуется без повторной проверки:
- **AC1** (видимая кнопка/геометрия) — unit + 5 golden-сцен, разница только в кнопке (я сам пересмотрел эти же 5 сцен в этом заходе, см. таблицу гейтов — согласуется с r1);
- **AC2** (live parity, без дублирующего события) — мутация r1 №2 (снятие условия дедупликации в `_commitHex`) остаётся в силе, код не менялся;
- **AC3** (успешное завершение, кроме click-through-защиты — она пересмотрена заново выше) — закрытие/фокус-часть, код не менялся;
- **AC5** (прежние close paths) — Escape/outside/trigger, код нетронут;
- **AC6** (i18n/a11y) — 4 языка, DOM-порядок = Tab-порядок;
- **AC7** (touch/fallback/lifecycle, кроме нового `generalConfirmDoesNotClickThrough`-сценария) — Popover/fallback паритет;
- **AC8** (совместимость/бюджеты) — нет новых config/storage/backend полей;
- Low L1 (стилевая склейка `houseplan-card.ts:5932`), L2 (keyboard activation только через `.click()`), L3 (outside pointer/повторный trigger не покрыты автотестом, пред-существующий пробел) — сняты r1 с записью, делта их не касается, повторно не пересматривал.
## Находки
Нет. Обе Medium-находки r1 закрыты и лично перепроверены исполнением (см. «Закрытие раунда r1»). Новых находок дельта (M1/M2-фикс + чистый пересбор `dist/**` при ребейзе) не внесла — единственный файл продуктовой логики (`src/hp-color-opacity.ts`) не менялся со времени r1.
High: 0. Medium в скоупе: 0. Medium вне скоупа: 0 (десять `isometric-*` golden-diff — не находка этой задачи, см. раздел выше; issue не завожу).
## AC — таблица доказательств (r2, полная)
| AC | Чем доказан | Чем краснеет | Статус |
|---|---|---|---|
| AC1 видимая кнопка/геометрия | unit + `golden:verify` (5 сцен, diff — только кнопка) | не защитный AC, §2.7 не требует столбца | унаследовано из r1, перепроверено визуально в этом заходе |
| AC2 live parity, без дублирующего события | smoke `confirmClosesWithoutDuplicateOrClickThrough`, `validCorrectionAllowsConfirm` | мутация r1 №2 (не перезапускал — код не менялся) | унаследовано из r1 |
| AC3 успешное завершение / без click-through | smoke `confirmClosesWithoutDuplicateOrClickThrough` + **новый** `generalConfirmDoesNotClickThrough` | мутация `color-picker-confirm-click-through` — лично прогнал в этом заходе, красное | **перепроверено в r2** (M2 закрыта) |
| AC4 невалидный HEX / защита от обхода | unit + smoke `invalidHex*`, `repeatedConfirmCannotBypassInvalidHex` | мутация `color-picker-invalid-confirm-latch-removed` — лично прогнал в этом заходе, красное | **перепроверено в r2** (M1 закрыта) |
| AC5 прежние close paths | smoke `escapeClosesFirstAndRefocuses`; outside/trigger — проверено чтением (L3, пред-существующий пробел) | код не тронут | унаследовано из r1 |
| AC6 i18n/a11y | unit (`i18n.test.mjs`, `color-picker.test.mjs`) + smoke `englishLabels`/`cardLanguageOwnsCopy` | не защитный AC | унаследовано из r1 |
| AC7 touch/fallback/lifecycle | smoke `confirmIsFullWidthTouchTarget`, `smoke_help_affordance.mjs` (`fallbackPickerHasConfirm`/`fallbackPickerConfirmCloses`) + новый consumer-смок | не мутировал отдельно (оба пути реально прогнаны) | унаследовано + расширено новым смоком |
| AC8 совместимость/release/бюджеты | `no-new-any`, `check-docs`, `bundle:budget`, оба changelog в `82f65acd` | не защитный AC | унаследовано из r1 |
## Что проверено и корректно
- `src/hp-color-opacity.ts` идентичен коду, разобранному в r1: единственный коммит диапазона, трогающий файл (`82f65acd`), лежит до r1-ревью; перечитал файл заново и подтвердил соответствие описанию.
- Обе Medium-находки r1 закрыты воспроизводимыми мутантами, лично прогнанными до и после патча: `color-picker-invalid-confirm-latch-removed` и `color-picker-confirm-click-through` оба «покраснели, как обязаны».
- `demo/smoke_color_picker_consumers.mjs` тестирует click-through защиту на потребителе (`general settings/lightOn`) без внешнего гасителя событий — устраняет ровно ту слабость, которую нашёл r1 (там decor-tool consumer гасил клик независимо от продуктовой защиты).
- Дельта после второго ребейза ограничена сгенерированными `dist/**`/`custom_components/houseplan/frontend/**` и `docs/images/screenshots.json` (fingerprint) — подтверждено: `git log --oneline origin/dev..HEAD -- src/hp-color-opacity.ts` даёт один коммит, предшествующий обоим ребейзам.
- `dist`/`custom_components/houseplan/frontend` синхронны с исходником (`cmp` идентичны, `git status --short` пуст после пересборки).
- Process-gate, трейлеры (`Issue: #476` на всех 9 коммитах, `User-Visible: yes` только на `82f65acd` с обоими changelog в этом же коммите) — корректны.
- 10 golden-diff `isometric-*` подтверждённо не относятся к этому диффу: воспроизведены на чистом `origin/dev` в отдельном worktree без единого коммита #476.
## Чего не проверял
- `pytest tests_backend`, `npm run invariants`, performance-профили, полная матрица `demo/smoke_*.mjs`, полный `scripts/mutation-gate.mjs` (кроме двух целевых), `golden:accept` — см. обоснование в таблице гейтов.
- 15 «слабых» смоков по `stopPropagation` — просмотрел список имён повторно (не изменился с r1), все про другие поверхности, не про `hp-color-opacity`.
- Не расследовал глубже происхождение golden-долга #471 сверх того, что нужно, чтобы отделить его от этой задачи (не моя роль — задача другого issue).
## Вывод
High: 0. Medium в скоупе: 0. Medium вне скоупа: 0. Verdict: **зелёный**.
---
## Материал раунда
- Ветка: `issue/476-color-picker-ok`, HEAD `6e0e1f8ca24b10a87569f9fec07810e08b7fa71c`.
- `origin/dev` = `fc5973c2239a2d25485b1acc955d0b0da620370c`, совпадает с `git merge-base origin/dev HEAD` — ветка полностью приведена, конфликтов нет.
- Диапазон: `git log --oneline origin/dev..HEAD` (9 коммитов) / `git diff origin/dev...HEAD` (56 файлов).
- Предыдущий документ: `docs/reviews/CODE-REVIEW-476-r1.md` (коммит `baea0fcfedfacd0393a750750279ce4f869dd5b9`), материал того захода — дерево `94485fdc0eca504692dd2776c7f3c86518a8dfb9` (SHA-анкеры `34a6b276473c`/`b5a1001e` мертвы после второго ребейза, объяснено выше).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/476-color-picker-ok`, коммит `6e0e1f8ca24b` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `2e8d8ce4b2320c3d28cc4e815b1c45da973a797e`
```
git log --all --format='%H %T' | grep 2e8d8ce4b232
```
- ТЗ `docs/specs/476-color-picker-ok.md`, блоб `d21066d56e69c35fe7d0b40d9f965968dac9f803`
```
git log --all --find-object=d21066d56e69c35fe7d0b40d9f965968dac9f803 -- docs/specs/476-color-picker-ok.md
```
+271
View File
@@ -0,0 +1,271 @@
# CODE-REVIEW-476-r3
- **Issue:** #476 — Явное завершение выбора цвета кнопкой «ОК»
- **Этап:** код-ревью (PROCESS.md §2.7), заход r3, блокирующих циклов израсходовано 1 из 4
- **Ветка:** `issue/476-color-picker-ok`, HEAD `29d72b33` (detached). `origin/dev` = `88e4cf50`,
совпадает с `git merge-base origin/dev HEAD` — ветка полностью приведена конвейером к `dev`,
ребейз чист, конфликтов нет. Поверх материала r2 (`fc5973c2`) легло ещё **14 коммитов** `dev`
(issue #472, #473) — это третий по счёту ребейз ветки на ушедший вперёд `dev`.
- **Диапазон:** `git log --oneline origin/dev..HEAD` — 10 коммитов; материал —
`git diff origin/dev...HEAD`, 57 файлов, +1572/-288.
- **ТЗ:** `docs/specs/476-color-picker-ok.md`, полный трек, ревью ТЗ зелёное на r2
(`docs/reviews/SPEC-REVIEW-476-r2.md`).
## Почему разбор полный, а не по дельте
Между CODE-REVIEW-476-r2 (зелёный, материал `fc5973c2`/`6e0e1f8c`) и этим заходом `origin/dev`
ушёл вперёд ещё на 14 коммитов (`fc5973c2..88e4cf50`): диф-зависимый перф-смок и мутационная
отчётность (#473), синхронизация `mutation-gate.yml` между `main`/`dev` (#472). PROCESS.md
§2.10/§7.2 прямо называет ребейз на ушедший вперёд `dev` «другим кодом», обязывающим к полному
разбору, а не к проверке только дельты. Сделал полный разбор: перепрогнал весь набор гейтов
заново на текущем дереве, а не поверил чужому заявлению «конфликтовали только сгенерированные
файлы».
Проверил сам, что продуктовая логика при этом не пострадала:
`git log --oneline origin/dev..HEAD -- src/hp-color-opacity.ts src/houseplan-card.ts` даёт
**ровно один** коммит — `4dc0a538 feat: add color picker confirmation`, тот же, что был
разобран в r1/r2. Все 14 новых коммитов `dev` не касаются ни одного из этих двух файлов.
## Скоуп диффа (полный)
`src/hp-color-opacity.ts` (61 строка) — полноширинная кнопка «ОК» как последний DOM-control,
CSS-контракт (100% width, ≥40px, forced-colors), признак `_hexNeedsValidInput` (защита от снятия
ошибки повторным «ОК» без нового ввода), `_confirm()` со `stopPropagation()`. `src/houseplan-card.ts`
(1 строка) — `confirm` label. `src/i18n/{en,ru,de,fr}.json` — `color_picker.confirm`.
`test/color-picker.test.mjs`, `test/i18n.test.mjs` — юниты на DOM-порядок/CSS/label/i18n-parity.
`demo/smoke_color_picker.mjs` (расширен), `demo/smoke_help_affordance.mjs` (fallback-путь),
`demo/smoke_color_picker_consumers.mjs` (новый, M2-фикс). `scripts/mutation-gate.mjs` (2 мутанта,
M1+M2), `scripts/smoke-links.mjs` (реестр выбора смоков). Оба changelog, `docs/TESTING.md`,
`docs/specs/README.md`, пересобранные `dist/**`/`custom_components/houseplan/frontend/**`,
обновлённый `docs/images/screenshots.json` (только fingerprint). Backend, геометрия,
config/storage не затронуты.
## Как проверялось
Зелёного Validate на `29d72b33` нет (in-progress на момент разбора, job `Golden` красный на
предыдущих коммитах диапазона). Прогнал сам:
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | чисто, без вывода |
| Unit-тесты | `npm test` | `2107 tests, pass 2106, fail 0, skipped 1` (рост со 2077 у r1/r2 — новые тесты #472/#473, не #476) |
| Build + sync | `npm run build && npm run bundle:sync` | `dist` собран; `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` — идентичны; `git status --short` пуст после сборки (все 3 копии бандла совпадают с закоммиченным) |
| Docs-гейт | `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 12 external links)` — обязателен, diff трогает `src/**` |
| Новый `any` | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | «Новых any нет» (59 добавленных строк в 2 файлах — те же, что в r1/r2: дельта не добавила новых строк в `.ts`) |
| Process-gate | `node scripts/process-gate.mjs` | «гейт пройден, предупреждений 0» (10 коммитов в диапазоне) |
| Выбор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | Прямое совпадение (1): `smoke_color_picker_consumers.mjs` (← `_surface`). Зарегистрированная связь (2): `smoke_color_picker.mjs`, `smoke_help_affordance.mjs`. Слабая связь (15, общее имя `stopPropagation`) — тот же список, что в r1/r2 (`smoke_cover_*`, `smoke_decor`, `smoke_edit_walk`, `smoke_editor_gestures`, `smoke_furniture`, `smoke_hide_layers`, `smoke_inert_openings`, `smoke_modes`, `smoke_room_cards`, `smoke_space_settings`, `smoke_tap_ctx`, `smoke_tap_run`, `smoke_toggle_confirmation`, `smoke_value_face_source`) — просмотрел повторно, все про другие поверхности, не про `hp-color-opacity`; не гонял |
| Целевой смок 1 | `node demo/smoke_color_picker.mjs` | `OK`, все 21 поле `true`, включая `repeatedConfirmCannotBypassInvalidHex: true` |
| Целевой смок 2 | `node demo/smoke_color_picker_consumers.mjs` | `OK`, все 13 полей `true`, включая `generalConfirmDoesNotClickThrough: true` |
| Целевой смок 3 | `node demo/smoke_help_affordance.mjs` | `OK`, все 41 поле `true`, включая `fallbackPickerHasConfirm`/`fallbackPickerConfirmCloses` |
| Bundle budget | `npm run bundle:budget` | initial View `299595 B` gzip, потолок `300500±2000` — в бюджете; запас `1471 Б` — тот же пред-существующий долг #367 (не новый, не вызван этим диффом) |
| Golden (advisory, полный прогон) | `npm run golden:verify` | 161 сцена: **15 `different`, 146 `passed`**. Ровно те же 5 color-popover сцен, что называет ТЗ §13.4 (см. ниже), плюс те же 10 `isometric-*`, что r2 уже отнёс к пред-существующему долгу #471 — состав не изменился с r2 несмотря на 14 новых коммитов `dev` между заходами |
| Мутация 1 (M1) | `node scripts/mutation-gate.mjs --id=color-picker-invalid-confirm-latch-removed` | патч снимает `if (this._hexNeedsValidInput) {...}` в `_commitHex`; чистый прогон `smoke_color_picker.mjs` — зелёный, с мутацией — «тест покраснел, как обязан», «поймано 1 из 1» |
| Мутация 2 (M2) | `node scripts/mutation-gate.mjs --id=color-picker-confirm-click-through` | патч снимает `event.stopPropagation()` из `_confirm`; чистый прогон `smoke_color_picker_consumers.mjs` — зелёный, с мутацией — «тест покраснел, как обязан», «поймано 1 из 1» |
| Применимость всех мутантов | `node scripts/mutation-gate.mjs --check` | все патчи (включая 2 наших) ложатся на текущий код ровно один раз — реестр не отстал |
**Не гонял и почему:**
- `python -m pytest tests_backend` — diff не трогает `custom_components/**/*.py`.
- `npm run invariants`/`model-invariants.mjs` — diff не меняет рёбра комнат, толщину, `layout`,
`marker.space`, `open_spans`; геометрия не затронута.
- Полная матрица `demo/smoke_*.mjs` (226 смоков) и полный `scripts/mutation-gate.mjs` (десятки
мутантов, кроме двух целевых) — предрелизные гейты (PROCESS §8), не гейт код-ревью на задаче
такого объёма.
- `npm run golden:accept` — не моя роль на этом этапе.
- «Одно число — один источник»: диф не добавляет и не меняет пользовательскую числовую величину
(кнопка не показывает значение), правило неприменимо; `test/single-source-numbers.test.mjs`
зелёный в составе `npm test`.
## Golden: 10 `isometric-*` diff — по-прежнему не относится к #476 (переподтверждено)
`golden:verify` на текущем HEAD (`29d72b33`) даёт ровно те же 15 `different`, что r2 нашёл на
`6e0e1f8c`, несмотря на 14 промежуточных коммита `dev`: 5 названных ТЗ §13.4 color-popover сцен
(`decor-color-popover-mobile-ru`, `decor-color-popover-desktop-en`, `general-color-popover-desktop-en`,
`device-ripple-color-popover-mobile-ru`, `space-room-color-popover-desktop-ru`) + 10 `isometric-*`
(`isometric-geometry-view-{dark,light}`, `isometric-live-layers-dark`, `isometric-touch-kiosk-dark`,
`isometric-large-warm-remount-dark`, `isometric-stage3-overlays-{light,dark}`,
`isometric-stage3-openings-dark`, `isometric-stage3-forced-colors-dark`,
`isometric-stage3-no-filter-dark`).
Просмотрел лично `artifacts/golden/actual/` и `diff/` для всех 5 color-popover сцен (2 приложены
как скриншоты в этом разборе): разница — ровно новая полноширинная кнопка «ОК»/«OK» и вертикальный
сдвиг содержимого попапа под ней; цвет, геометрия, тема не поехали ни в одной сцене.
Происхождение 10 `isometric-*` не расследовал заново с нуля — это уже сделал r2 (worktree на
чистом `origin/dev` без единого коммита #476, тот же результат) и проследил до пред-существующего
долга `dev`: `be5eeb41` (#471) изменил изометрический рендер, а последний коммит, принимающий
golden-эталоны, — `de215578`, раньше него. Проверил сам, что новые 14 коммитов между r2 и r3 не
меняют этот факт: `git diff fc5973c2..origin/dev --stat -- demo/golden/` — пусто (ни один
baseline не принимался), а diff #473 (`d95255ad`) правит только строку unit-теста
(`test/iso-scene-render.test.mjs`), не рендер и не сами эталоны. Долг тот же, не новый и не
дельта этой задачи; issue не завожу — по тем же основаниям, что и r2 (чужой смерженный код #471,
предрелизный гейт, не гейт код-ревью).
## Новое в r3: диф-зависимый перф-смок (#473) теперь целится в этот диф — проверил, шум окружения
Между r2 и r3 в `dev` прилетел диф-зависимый перф-смок (#473, `.github/workflows/validate.yml`):
правка `src/houseplan-card.ts` попадает под шаблон `perf_interaction` в
`scripts/classify-changes.mjs` (`^src\/(live-[^/]+|render-[^/]+|houseplan-render-lifecycle|houseplan-card)\.ts$`),
поэтому CI Validate на этом SHA обязан прогнать `large-house-interaction-v1` в дополнение к
обычным glow-профилям — гейта, которого не существовало на момент r1/r2. ТЗ §15 утверждает
отсутствие перфвлияния (один статический `button` в уже открытом lazy-picker), но раз появился
новый обязательный по диффу гейт, я его прогнал сам, а не доверился формулировке ТЗ:
```
npm run benchmark:large-house -- --profile=large-house-interaction-v1 --samples=3 --warmups=1
npm run benchmark:compare -- --absolute-only --budgets=demo/performance/budgets-interaction-smoke.json
```
Результат на `HEAD`: 2 из 51 проверок красные — `timing.interactionSeriesMs.median` (3230.9 vs
потолок 3000) и `longTask.editorSeries.maxSingleMs` (221 vs потолок 150).
Чтобы отличить регрессию от шума текущего окружения (тот же метод, что r2 применил к golden),
прогнал тот же профиль на чистом `origin/dev` (`88e4cf50`) в отдельном `git worktree`, без единого
коммита #476:
```
git worktree add /tmp/dev-clean origin/dev --detach
# (общий node_modules переиспользован symlink'ом — package-lock.json идентичен, diff пуст)
npm run bundle:sync
npm run benchmark:large-house -- --profile=large-house-interaction-v1 --samples=3 --warmups=1
```
Результат на чистом `dev` без #476: `timing.interactionSeriesMs.median` = 3122.6 — **тоже красный**
против того же потолка 3000. `longTask.editorSeries.maxSingleMs` на `dev` = 137 (проходит, но с
запасом 13 мс из 150 — тот же порядок величины, что просадка на HEAD). Оба замера — 3 сэмпла на
этой песочнице, разброс между веткой и чистым `dev` (3230.9 vs 3122.6, ~3.5%) меньше, чем запас
threshold'а до потолка (0%), то есть неотличим от шума общего/разделяемого раннера. Регрессия
не подтверждена: ветка с #476 и чистый `dev` без него одинаково красятся по основной метрике —
причина не в этом диффе. Не завожу отдельный issue: в отличие от golden (детерминированный
байтовый диф), это шумная временная метрика с 3 сэмплами на нештатной песочнице — у меня нет
основания утверждать, что это вообще дефект, а не особенность именно этого раннера. Owner/pipeline
стоит иметь в виду, что этот перф-гейт может красить `Validate` на всём диапазоне `dev`, а не
только на этой ветке — но это не находка задачи #476.
## Закрытие раунда r2
r2 не оставил находок (Вердикт: зелёный, High: 0, Medium: 0) — таблица «находка → чем закрыта»
пуста по построению. Единственное событие между r2 и r3 — второй по счёту ребейз ветки
(`fc5973c2 → 88e4cf50`, +14 коммитов `dev`), который сам по себе не находка (см. «Почему разбор
полный» выше), а обязательное основание для полного повторного разбора.
## Унаследовано из r2 (перепроверено, не просто принято на слово)
Документ: `docs/reviews/CODE-REVIEW-476-r2.md`, материал — `HEAD 6e0e1f8c` (SHA мёртв после
третьего ребейза — обычный случай осиротевшего SHA, §2.10, не находка; дерево материала r2 в
документе — `2e8d8ce4b232`).
Основание — не доверие, а факт, перепроверенный лично в этом заходе:
`git log --oneline origin/dev..HEAD -- src/hp-color-opacity.ts src/houseplan-card.ts` даёт один
и тот же единственный коммит `4dc0a538`, что и в r1/r2 (там он назывался `82f65acd`/`11cc5386` до
двух последующих ребейзов — тот же контент, другой хэш после перезаписи истории при ребейзе).
Перечитал оба файла целиком в текущем HEAD — расхождений с описанием r2 нет.
Наследуется (код не менялся, повторно не выводил заново логику AC, но перепрогнал все гейты,
которые их доказывают, см. таблицу выше — это отличает «унаследовано» здесь от классической
дельты):
- **AC1** (видимая кнопка/геометрия) — unit + 5 golden-сцен, разница только в кнопке (лично
пересмотрел все 5 в этом заходе, включая 2 приложенных скриншота);
- **AC2** (live parity, без дублирующего события) — код и мутационное покрытие не менялись;
- **AC3** (успешное завершение, включая click-through-защиту M2) — мутант
`color-picker-confirm-click-through` лично прогнан заново в этом заходе, красится;
- **AC4** (невалидный HEX, защита от обхода M1) — мутант
`color-picker-invalid-confirm-latch-removed` лично прогнан заново в этом заходе, красится;
- **AC5** (прежние close paths) — Escape/outside/trigger, код нетронут; outside/повторный
trigger по-прежнему не покрыты автотестом (L3 из r1, пред-существующий пробел, не этой задачи);
- **AC6** (i18n/a11y) — 4 языка, DOM-порядок = Tab-порядок, юниты зелёные в этом прогоне;
- **AC7** (touch/fallback/lifecycle) — Popover/fallback паритет, оба смока зелёные в этом прогоне;
- **AC8** (совместимость/бюджеты) — нет новых config/storage/backend полей, `bundle:budget` в
бюджете в этом прогоне;
- Low L1 (`houseplan-card.ts:5935` — стилевая склейка двух полей в одну строку), L2 (keyboard
activation только через `.click()`), L3 (outside/trigger не покрыты автотестом) — сняты r1 с
записью, делта их не касается; L1 проверил построчно — та же склейка на месте, ничего не
ухудшилось.
## AC — таблица доказательств (r3, полная, с «чем краснеет» для защитных AC)
| AC | Чем доказан | Чем краснеет | Статус в r3 |
|---|---|---|---|
| AC1 видимая кнопка/геометрия | unit + `golden:verify` (5 сцен) | не защитный AC (расположение/CSS) | переподтверждено, разница та же |
| AC2 live parity, без дублирующего события | smoke `confirmClosesWithoutDuplicateOrClickThrough`, `validCorrectionAllowsConfirm` | код не менялся с r2, мутационно не перепрогонял отдельно (не новый защитный механизм) | унаследовано |
| AC3 успешное завершение / без click-through | smoke `confirmClosesWithoutDuplicateOrClickThrough` + `generalConfirmDoesNotClickThrough` | мутация `color-picker-confirm-click-through` — лично прогнал в r3, красное | **перепроверено в r3** |
| AC4 невалидный HEX / защита от обхода | unit + smoke `invalidHex*`, `repeatedConfirmCannotBypassInvalidHex` | мутация `color-picker-invalid-confirm-latch-removed` — лично прогнал в r3, красное | **перепроверено в r3** |
| AC5 прежние close paths | smoke `escapeClosesFirstAndRefocuses`; outside/trigger — проверено чтением (L3, пред-существующий пробел) | код не тронут | унаследовано |
| AC6 i18n/a11y | unit (`i18n.test.mjs`, `color-picker.test.mjs`) + smoke `englishLabels`/`cardLanguageOwnsCopy` | не защитный AC | унаследовано, юниты зелёные в r3 |
| AC7 touch/fallback/lifecycle | smoke `confirmIsFullWidthTouchTarget`, `smoke_help_affordance.mjs` + `smoke_color_picker_consumers.mjs` | не мутировал отдельно, оба пути реально прогнаны | унаследовано, все смоки зелёные в r3 |
| AC8 совместимость/release/бюджеты | `no-new-any`, `check-docs`, `bundle:budget`, оба changelog в `4dc0a538` | не защитный AC | унаследовано, гейты зелёные в r3 |
## Находки
Нет. High: 0. Medium в скоупе: 0. Medium вне скоупа: 0.
- 10 `isometric-*` golden-diff — пред-существующий долг `dev` от #471, переподтверждено в r3
(см. раздел выше), не находка этой задачи, issue не завожу (та же позиция, что r2).
- Красный `large-house-interaction-v1` перф-профиль — воспроизводится и на чистом `origin/dev`
без #476, неотличим от шума общего раннера этой песочницы при 3 сэмплах; недостаточно
оснований признать это дефектом, issue не завожу.
## Что проверено и корректно
- `src/hp-color-opacity.ts`/`src/houseplan-card.ts` идентичны коду, разобранному в r1/r2:
единственный коммит диапазона, трогающий эти файлы (`4dc0a538`), не менялся третьим ребейзом.
- Обе Medium-находки r1 (M1/M2) остаются закрытыми воспроизводимыми мутантами, лично прогнанными
заново в этом заходе на текущем HEAD: оба «покраснели, как обязаны».
- `dist`/`custom_components/houseplan/frontend`/`demo/srv/assets` синхронны с исходником (`cmp`
идентичны, `git status --short` пуст после пересборки).
- Трейлеры: все 10 коммитов диапазона несут `Issue: #476`; ровно один (`4dc0a538`) —
`User-Visible: yes` с правками в обоих changelog в этом же коммите; `process-gate.mjs` — 0
предупреждений.
- 5 golden-diff color-popover сцен — визуально ровно кнопка и связанный со сдвиг контента, ничего
постороннего не поехало (лично пересмотрел все 5).
- 10 golden-diff `isometric-*` подтверждённо не относятся к этому диффу (переподтверждено фактом:
ни один коммит диапазона не трогает `src/*iso*`, состав diff не изменился за 14 коммитов dev).
- Новый диф-зависимый перф-гейт (#473) корректно классифицирует `houseplan-card.ts` как
`perf_interaction` — прогнал его сам и показал, что красный результат не связан с #476.
## Чего не проверял
- `pytest tests_backend`, `npm run invariants`/`model-invariants.mjs`, полная матрица
`demo/smoke_*.mjs`, полный `scripts/mutation-gate.mjs` (кроме 2 целевых), `golden:accept` —
см. обоснование в таблице гейтов.
- 15 «слабых» смоков по `stopPropagation` — просмотрел список имён (не изменился с r1/r2), все
про другие поверхности, не про `hp-color-opacity`; не гонял.
- `large-house-isometric-v1` перф-профиль — диф не трогает `src/iso-*`, `perf_iso` в
`scripts/classify-changes.mjs` не срабатывает на этом диффе, гейт не обязателен.
- Не расследовал глубже происхождение golden-долга #471 сверх подтверждения, что 14 новых
коммитов `dev` его не меняли (не моя роль — задача другого issue).
- Не выяснял, воспроизводится ли красный `large-house-interaction-v1` на дедиковинном раннере
GitHub Actions — не мой доступ; зафиксировал только то, что он не зависит от #476 в этой
песочнице.
## Вывод
High: 0. Medium в скоупе: 0. Medium вне скоупа: 0. Verdict: **зелёный**.
---
## Материал раунда
- Ветка: `issue/476-color-picker-ok`, HEAD `29d72b334446fb9cfe91009a9a945d1e5a14be00`.
- `origin/dev` = `88e4cf50e338c0f53f008a30acca5b46a8611289`, совпадает с
`git merge-base origin/dev HEAD` — ветка полностью приведена, конфликтов нет.
- Диапазон: `git log --oneline origin/dev..HEAD` (10 коммитов) /
`git diff origin/dev...HEAD` (57 файлов, +1572/-288).
- Предыдущий документ: `docs/reviews/CODE-REVIEW-476-r2.md`, материал того захода — `HEAD 6e0e1f8c`
(мёртв после этого, третьего ребейза; дерево материала r2 — `2e8d8ce4b232`, см. документ r2).
- ТЗ: `docs/specs/476-color-picker-ok.md`, ревью ТЗ зелёное — `docs/reviews/SPEC-REVIEW-476-r2.md`.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/476-color-picker-ok`, коммит `e49c48a74021` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `6b9ca28c3f24d21dc3913e4a18f9bf1bc3b47a26`
```
git log --all --format='%H %T' | grep 6b9ca28c3f24
```
- ТЗ `docs/specs/476-color-picker-ok.md`, блоб `d21066d56e69c35fe7d0b40d9f965968dac9f803`
```
git log --all --find-object=d21066d56e69c35fe7d0b40d9f965968dac9f803 -- docs/specs/476-color-picker-ok.md
```
+187
View File
@@ -0,0 +1,187 @@
# SPEC-REVIEW-476-r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/476
- **Этап:** ТЗ на ревью (PROCESS.md §2.4), заход r1, блокирующих циклов израсходовано 0/4
- **Артефакт ТЗ:** `docs/specs/476-color-picker-ok.md` (ветка `issue/476-color-picker-ok`, HEAD `acd28558`)
- **Трек:** полный (обоснование в аналитике: новый наблюдаемый UX-контракт завершения)
- **Ревьюер:** Claude (роль «ревьюер ТЗ», отдельная от автора — Codex)
## Скоуп проверки
Читал в порядке из системного промпта: `docs/SCOPE.md`, `PROCESS.md` (§1–§10.4),
`AGENTS.md`, тело issue #476 и все 5 комментариев (аналитика, продуктовые
вопросы, решение владельца, занятие, хендофф автора), `docs/USER-GUIDE.ru.md`
(раздел про образец цвета/палитру), и сам ТЗ `docs/specs/476-color-picker-ok.md`
целиком. Канонического документа отдельно под color-picker нет — ближайший
контракт живёт в `USER-GUIDE.ru.md` и в самом компоненте.
Задача — только документация (класс C), продуктовый код не менялся: рабочее
дерево уже содержит эту ветку, что позволило свести claims ТЗ с реальным
кодом `src/hp-color-opacity.ts`, `src/houseplan-card.ts`, `src/i18n/*.json`,
`demo/golden/matrix.mjs`, `demo/golden/baselines/baselines-index.json` вместо
того, чтобы верить пересказу автора.
## Как проверялось
Это ревью ТЗ, а не код-ревью: гейты `typecheck`/`test`/`build` к диффу класса C
не относятся и не гонялись (диф — `docs/specs/476-color-picker-ok.md` +
`docs/specs/README.md`, оба класса C). Вместо этого каждое фактическое
утверждение ТЗ о текущем поведении/API сверено с реальным исходником:
| Утверждение ТЗ | Проверено по | Результат |
|---|---|---|
| `hp-color-opacity` эмитит `hp-color-opacity-change` с `{color, opacity}` сразу при live-изменении | `src/hp-color-opacity.ts:682-698` (`_emit`) | подтверждено |
| Закрытие через trigger/outside/`Escape`/disconnect уже существует и не откатывает live-значение | `_toggle`, `_outsidePointerDown`, `_keyDown`, `disconnectedCallback` | подтверждено |
| `_closePicker(refocus, reason)` — единый lifecycle-путь для всех close reasons | `_closePicker` (`hp-color-opacity.ts:436-456`) | подтверждено, сигнатура `(refocus=false, reason='exclusive')` |
| `ColorPickerLabels` сейчас без поля `confirm`, единый источник — `_colorPickerLabels` в `houseplan-card.ts` | `hp-color-opacity.ts:8-15`, `houseplan-card.ts:5927-5936` | подтверждено; все потребители (`decor-image-editor.ts`, `houseplan-editor-runtime.ts`, `houseplan-onboarding-runtime.ts`) читают именно этот геттер — правка одной точки закрывает всех |
| `showOpacity=false` потребители существуют (Glow, ripple) | `grep showOpacity=\${false}` → 5 мест, включая ripple/Glow | подтверждено |
| Ключи i18n уже используют плоский `color_picker.*` формат в en/ru/de/fr | `src/i18n/{en,ru,de,fr}.json:18-23` | подтверждено, добавление `color_picker.confirm` последовательно с существующим стилем |
| Пять названных golden-сцен существуют и покрывают заявленные комбинации (RU/EN, light/dark, desktop/touch, opacity/color-only) | `demo/golden/matrix.mjs:940-984`, `baselines-index.json:152-164` | подтверждено; `device-ripple-color-popover-mobile-ru` — реальный `showOpacity=false` кейс (ripple) |
| `docs/USER-GUIDE.ru.md` уже документирует, что изменения в палитре — «черновик до сохранения родительского диалога» | `USER-GUIDE.ru.md:295-296` | подтверждено и **согласуется** с Q1-решением владельца (ТЗ не создаёт новую семантику поверх задокументированной) |
| `docs/specs/README.md` получил строку на #476 | `git diff origin/dev..HEAD -- docs/specs/README.md` | подтверждено |
Ни одно из проверенных утверждений не оказалось догадкой, выданной за факт —
все грамматически привязаны к реальному коду или реальному документу.
## Находки
### Medium (в скоупе, чинится в текущем ТЗ) — §18 противоречит §7.3/AC4
**Файл:** `docs/specs/476-color-picker-ok.md`, §18 «Принятые предположения» vs
§7.3 «Невалидный HEX» и AC4.
**Суть.** §18 предполагает: «confirm вызывает существующий HEX commit helper
и использует результат валидации». Существующий `_commitHex()`
(`hp-color-opacity.ts:663-673`) на невалидном значении делает:
```ts
if (!normalized) {
this._hexDraft = this._lastValidColor; // возвращает валидную строку в поле
this._hexInvalid = true;
return;
}
```
Это уже возвращает в `_hexDraft` валидную строку (последнее применённое
значение). Если «ОК» реализовать буквально как «вызвать `_commitHex()` и
посмотреть на результат», то **повторное нажатие «ОК» без нового ввода**
запустит `_commitHex()` второй раз над уже-валидным `_hexDraft` →
`normalizeHexColor` пройдёт → `_hexInvalid = false` → picker закроется.
Ровно это прямо запрещено ТЗ: «повторное «ОК» без нового валидного
пользовательского ввода не имеет права снять состояние ошибки только потому,
что commit уже вернул в поле последнее валидное значение» (§7.3, последний
абзац), и это же явно закреплено как обязательный шаг теста в собственном
плане автора: «повторным «ОК» доказать отсутствие обхода» (§13, п.2) и как
защитный AC4: «Повторное «ОК» без нового валидного input также не закрывает
поверхность».
**Воспроизведение (по коду, а не по исполнению — компонента не собран для
класса C диффа):** invalid HEX → blur/commit (error виден, `_hexDraft` =
last valid) → фокус НЕ трогая hex-поле → клик «ОК», реализованный по букве
§18 → `_commitHex()` над валидным `_hexDraft` → `_hexInvalid=false` → close.
AC4-мутант «доказать отсутствие обхода» покраснеет при такой реализации.
**Почему это не мелочь.** Это не тонкость реализации, свободно решаемая
разработчиком: §18 прямо называет конкретный существующий helper и описывает
механизм, который математически не может выполнить соседний обязательный
AC. Технический автор (Codex), доверившись §18 буквально, произведёт код,
проваливающий собственный тест из плана автора — ровно тот класс дефекта,
который спек-ревью обязано ловить до кода, а не после.
**Требуемая правка.** §18 должен явно называть механизм различения «поле
не менялось со времени ошибки» vs «новое валидное значение введено» —
например, отдельный флаг, сбрасываемый только по `input`-событию хекс-поля
(а не по `_commitHex()`), который «ОК» обязан проверить перед закрытием.
Формулировку «вызывает существующий HEX commit helper» нужно уточнить или
заменить, поскольку буквальное её прочтение ломает AC4. Это в скоупе задачи
(правится в тексте того же ТЗ), не в скоупе другого issue.
### Low — §7.2 нумерованный список читается как безусловный
**Файл:** `docs/specs/476-color-picker-ok.md`, §7.2, шаг 3: «возвращает
keyboard focus на swatch trigger» стоит в общем списке 1–4 без явного «при
валидном draft», хотя относится к тому же условию, что и шаг 2. §7.3 отдельно
и явно указывает, что при невалидном HEX фокус должен идти в HEX-поле, а не на
trigger — так что конфликта по существу нет, но при беглом чтении только §7.2
шаг 3 можно принять за безусловное действие. Снимаю без правки (Low, решение
ревьюера с записью): §7.3 формулирует более специфичное и явное правило и
имеет приоритет по построению документа; смысловой неоднозначности,
влияющей на AC, нет.
## Что проверено и корректно
- Оба продуктовых вопроса (§7.1 «сценарий» и «что человек увидит») отвечены
предметно, персона и поверхность совпадают с `docs/SCOPE.md` (J4/J6), без
терминов реализации в разделе «после».
- Открытых продуктовых вопросов нет: все 4 вопроса Q1–Q4 закрыты явным
решением владельца в комментарии, ТЗ переносит эти решения в §4 без
расхождений и без добавления новых недоговорённостей.
- Полный трек обоснован названным критерием (§5 PROCESS.md: новый UX-контракт
завершения) — не «обычный трек» без причины.
- Скоуп/не-скоуп (§5–§6) разделены чётко, граница «расширение публичного
контракта → возврат в S3-spec» присутствует.
- AC1–AC8 пронумерованы, у каждого указан способ доказательства
(unit/smoke/golden/docs gate), формулировки допускают ровно одну трактовку
результата (числа событий, DOM-порядок, `aria-invalid`, focus target).
Не нашёл ни одного AC, не проверяемого автотестом или явным ревью.
- Модель данных/миграция/compatibility (§10): корректно «нет изменений» —
проверено, что `ColorPickerLabels` — compile-time тип House Plan, а не
persisted-конфигурация, добавление поля не требует миграции; downgrade-путь
описан и правдоподобен (просто пропадает кнопка, event contract не трогается).
- i18n (§9): 4 языка перечислены, ключ следует уже используемому плоскому
формату, defensive fallback `OK` для стороннего consumer описан и не
противоречит текущему `_labels()` fallback-паттерну (`hp-color-opacity.ts:512-522`,
который уже делает `labels.x || DEFAULT_LABELS.x` — новое поле встраивается
в тот же паттерн естественно).
- Touch/a11y (§8): требования (40px, native `button`, forced-colors, no
click-through) конкретны и проверяемы; не противоречат
`docs/TOUCH-SUPPORT.md` (touch — best effort, но не деградация уже
доступной поверхности).
- Риски (§16) перечислены по одному на каждый нетривиальный контракт, у
каждого назван AC, который его снимает — не общие слова.
- Откат (§17) реалистичен: удаление кнопки/label/i18n-ключа не требует
миграции данных.
- Release-артефакты (§14): changelog RU+EN, golden-обновление пяти реальных
(не выдуманных) сцен, явное «нет perf/security артефактов» с обоснованием.
- Автор не рецензирует своё же ТЗ — сессия ревью отдельная, роли соблюдены
(PROCESS.md §6).
## Чего не проверял
- Не гонял `typecheck`/`test`/`build`/`golden:verify` — диф класса C
(документация), продуктовый код не менялся; эти гейты относятся к
код-ревью следующего этапа этой же задачи.
- Не проверял `docs/CONFIG-COMPATIBILITY.md` построчно — ТЗ заявляет «нет
миграции/новых полей», и это утверждение проверено против кода
(`ColorPickerLabels` — не персистентный тип), этого достаточно для вывода
«нет compatibility-влияния» без полного чтения документа.
- Не оценивал будущую точность CSS-реализации (padding/scroll-behaviour на
очень маленьких viewport) — это подробность код-ревью и golden-приёмки, а
не то, что можно верифицировать по тексту ТЗ.
- Не связывался с владельцем: новых продуктовых вопросов не возникло, оба
найденных пункта — технические (реализация валидации/фокуса), решаются
автором и ревьюером без владельца по PROCESS.md §7.1.
## Вывод
Один Medium-находка **в скоупе** задачи (несогласованность §18 и §7.3/AC4 по
механизму анти-обхода невалидного HEX) без High — вердикт жёлтый. Автор
правит текст ТЗ (уточняет §18, не код), правка проходит повторный цикл
ревью. Low-находка снята с записью, чинить не нужно.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/476-color-picker-ok`, коммит `acd28558b394` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `f54408b100e7cd2929096cf49134046c02f5bb65`
```
git log --all --format='%H %T' | grep f54408b100e7
```
- ТЗ `docs/specs/476-color-picker-ok.md`, блоб `ef9e07a28df7b6590c9bc0e0cfc241f0a85f256a`
```
git log --all --find-object=ef9e07a28df7b6590c9bc0e0cfc241f0a85f256a -- docs/specs/476-color-picker-ok.md
```
+123
View File
@@ -0,0 +1,123 @@
# SPEC-REVIEW-476-r2
- **Issue:** https://github.com/Matysh/houseplan-card/issues/476
- **Этап:** ТЗ на ревью (PROCESS.md §2.4), заход r2, блокирующих циклов израсходовано 1/4
- **Артефакт ТЗ:** `docs/specs/476-color-picker-ok.md` (ветка `issue/476-color-picker-ok`, HEAD `a29c854f`)
- **Предыдущий раунд:** SPEC-REVIEW-476-r1.md, вердикт жёлтый, ревью получено на `acd28558` (SHA восстановлен из
`Материал раунда` того же документа — в тексте вердикта issue-комментария SHA не назван, это отдельная находка
процесса, см. ниже)
- **Трек:** полный (не пересматривался — установлен в r1, дельта его не касается)
- **Ревьюер:** Claude (роль «ревьюер ТЗ», отдельная от автора — Codex)
## Дельта r1 → r2
```
git diff acd28558..a29c854f -- docs/specs/476-color-picker-ok.md
```
Один коммит автора: `a29c854f docs: clarify color picker invalid confirmation` (13 строк, только
`docs/specs/476-color-picker-ok.md`, +11/-2). Никакой другой файл дельтой не тронут — продуктовый код,
i18n, тесты, changelog не менялись (и не должны были: ТЗ ещё не перешло в код).
Правка — только §7.3 «Невалидный HEX» и §18 «Принятые предположения». Изменение локальное и не является ни
ребейзом на ушедший вперёд `dev`, ни сменой контракта поведения, ни новой подсистемой: это точечное уточнение
механизма против ровно одной r1-находки. Объём разбора этого раунда сведён к дельте и её последствиям (AC4,
§7.3, §18, §13 п.2), остальное наследуется из r1.
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| Medium: §18 предполагал, что confirm вызывает существующий `_commitHex()` и использует его результат — буквальная реализация ломает AC4, потому что повторный `_commitHex()` над уже нормализованным (сброшенным к последнему валидному) `_hexDraft` пройдёт валидацию и снимет `_hexInvalid`, закрыв picker без нового пользовательского ввода | §18 переформулирован: confirm может переиспользовать commit helper, но решение о закрытии выводится не только из нормализованного `_hexDraft`, а из отдельного признака «был ли новый `input` с валидным HEX после неуспешного commit»; признак не сбрасывается нормализацией draft, повторным confirm, blur или переводом фокуса — только новым валидным `input` | `docs/specs/476-color-picker-ok.md` §18, абзац 2 (diff `a29c854f`, +5 строк); идентичная формулировка продублирована в §7.3, последний абзац (+4 строки того же коммита) — обе секции синхронны, не разошлись |
| Low: §7.2 шаг 3 («возвращает фокус на trigger») читается как безусловный вне контекста §7.3 | Не правилась — r1 сам снял находку с записью («§7.3 имеет приоритет по построению документа, смысловой неоднозначности, влияющей на AC, нет»), автор не обязан был её чинить | `docs/reviews/SPEC-REVIEW-476-r1.md`, раздел «Low» |
Обе строки таблицы содержат конкретную строку текста, а не заявление автора «исправлено» — я прочитал диф и
сверил формулировку со сценарием, который сломал бы AC4.
## Проверка дельты по существу
Проверил, что новая формулировка §18/§7.3 действительно устраняет математическое противоречие с AC4, а не
маскирует его другой недосказанностью:
1. **Логика непротиворечива.** Новый признак — отдельная переменная состояния, не производная от
`_hexDraft`/`_hexInvalid`. Она устанавливается неуспешным commit'ом и снимается только `input`-событием с
валидным HEX. Blur, повторный confirm и внутренняя нормализация draft внутри commit helper явно перечислены
как НЕ снимающие признак — то есть сценарий из r1 («второй confirm над уже-нормализованным draft закрывает
picker») больше не проходит: второй confirm видит признак всё ещё установленным и обязан отказать в закрытии.
2. **Технически осуществимо, не только текст.** Сверил с реальным кодом `src/hp-color-opacity.ts`: `_hexInput`
(строка 649, обработчик `@input`) и `_commitHex` (строка 663, вызывается по `@blur`/`Enter`/по новому confirm)
— уже два разных обработчика, один привязан именно к `input`-событию поля. Новый признак естественно вешается
в `_hexInput`, не требуя изобретать несуществующий hook. §18 прямо помечен как «предположения, свободно
изменяемые ревьюером» — я не требую от автора большей технической детализации, чем нужно, чтобы AC4 не
ломался буквальным прочтением; этого условия текст теперь достигает.
3. **AC4 и план теста (§13 п.2, защитный мутант в §13 п.5 «закрыть surface при invalid HEX») не менялись
дельтой и не нуждаются в правке** — они уже требовали ровно то поведение, которое §18 теперь корректно
описывает. До правки была нестыковка между «что доказывает тест» и «что предполагает механизм»; после
правки оба говорят одно и то же.
4. Не нашёл новой находки, которую внесла бы сама правка (флип в другую сторону, новая недосказанность,
рассинхрон между §7.3 и §18) — тексты двух секций идентичны по формулировке, не только по смыслу.
Отдельная процессная находка (не по задаче, а по r1-вердикту): SHA, на котором был получен вердикт r1, не
назван в тексте issue-комментария с вердиктом («Вердикт: жёлтый · заход r1 …»); я восстановил его из раздела
«Материал раунда» документа `SPEC-REVIEW-476-r1.md` (`HEAD acd28558`) и перепроверил по времени коммитов
(`acd28558` 13:59:20+03:00 — до `972ef72b` 14:05:47+03:00 публикации r1-документа — до `a29c854f` 14:06:31+03:00
фикса). Это не блокирует данный раунд — документ r1 SHA всё-таки содержал, только не сам комментарий-вердикт.
## Унаследовано из r1 (без повторной проверки)
Все пункты ниже не пересматривались: дельта их не касается (правка ограничена §7.3/§18/AC4-механизмом), а
источник вывода — `docs/reviews/SPEC-REVIEW-476-r1.md`, получен на `acd28558`:
- Сверка фактических утверждений ТЗ о текущем коде/API с исходником (`_emit`, `_closePicker`, `_toggle`,
`_outsidePointerDown`, `_keyDown`, disconnect-путь, `ColorPickerLabels` без поля `confirm`, 5 потребителей
`showOpacity=false`, плоский формат i18n-ключей в 4 языках, существование и состав пяти golden-сцен,
`docs/specs/README.md`) — ни одна не оказалась догадкой, выданной за факт.
- Продуктовые вопросы Q1–Q4 закрыты решением владельца без новых открытых пунктов.
- Полный трек обоснован (новый наблюдаемый UX-контракт завершения).
- §5/§6 скоуп и не-скоуп разделены чётко, граница «расширение контракта → возврат в S3-spec» присутствует.
- AC1–AC3, AC5–AC8 сформулированы однозначно, у каждого назван способ доказательства (unit/smoke/golden/docs
gate) — дельта их текст не меняла и доказательная база (существующий код/тесты) не менялась тоже.
- §9 i18n, §10 модель данных/совместимость/downgrade, §8 touch/a11y, §14 release-артефакты, §16 риски, §17
откат — приняты без повторной проверки, содержимое этих секций дельтой не тронуто.
- Роли соблюдены: автор ТЗ (Codex) не рецензирует своё же ТЗ.
## Что проверено и корректно (специфично для r2)
- §7.3 и §18 после правки говорят об одном и том же механизме одними словами — не разошлись при редактировании
двух мест сразу.
- Новый признак технически привязываем к существующему разделению `_hexInput`/`_commitHex` в коде — предположение
не голословно.
- AC4 и защитный мутант §13 п.5 остаются достижимыми буквальным прочтением исправленного §18 (в r1 буквальное
прочтение делало их недостижимыми — сейчас нет).
## Чего не проверял
- Не гонял `typecheck`/`test`/`build`/`golden:verify` и `check-docs.mjs` — диф r2 (13 строк одного .md) класса C,
продуктовый код не менялся; `check-docs.mjs` для этого коммита уже гонялся автором и указан «passed» в
хендофф-комментарии, повторный прогон не добавляет информации к тексту-ревью.
- Не пересматривал ничего вне §7.3/§18/AC4 — см. раздел «Унаследовано из r1» с обоснованием, что дельта туда
не дотягивается.
- Не связывался с владельцем — новых продуктовых вопросов дельта не создала, различие «поле не менялось» vs
«новое валидное значение» техническое, не продуктовое.
## Вывод
Единственная Medium-находка r1 закрыта точной, синхронной правкой двух секций, логически устраняющей
противоречие с AC4, и технически реализуемой на существующей структуре кода. Новых находок дельта не внесла.
Вердикт зелёный; бюджет блокирующих циклов не расходуется (израсходовано 1/4, без изменений).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/476-color-picker-ok`, коммит `a29c854f9904` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `fa031b74f2bfc5cd4b1eb39cf8e77b6a76386284`
```
git log --all --format='%H %T' | grep fa031b74f2bf
```
- ТЗ `docs/specs/476-color-picker-ok.md`, блоб `d21066d56e69c35fe7d0b40d9f965968dac9f803`
```
git log --all --find-object=d21066d56e69c35fe7d0b40d9f965968dac9f803 -- docs/specs/476-color-picker-ok.md
```
+348
View File
@@ -0,0 +1,348 @@
# #476 — Явное завершение выбора цвета кнопкой «ОК»
- **Issue:** https://github.com/Matysh/houseplan-card/issues/476
- **Тип / приоритет:** feature + polish / P2
- **Трек:** полный; появляется новый наблюдаемый UX-контракт завершения общей
поверхности выбора цвета, включая desktop, touch и fallback без Popover API
- **Оценка:** пользовательская ценность 7/10; ценность для разработки 6/10;
сложность 3/10; риск 3/10
- **Связано:** #57, #180; `docs/SCOPE.md`, `docs/TOUCH-SUPPORT.md`
## 1. Сценарий
Персона — Home admin из `docs/SCOPE.md`. Поверхность — любой редакторский или
настроечный диалог House Plan, в котором используется стандартный выбор цвета.
Момент — администратор уже настроил цвет и, если доступно, прозрачность, но не
понимает, каким действием завершить работу с открытой поверхностью.
Desktop с мышью и клавиатурой остаётся эталонной средой редакторов. Та же кнопка
доступна на touch, поскольку `hp-color-opacity` уже является общей touch-capable
поверхностью и явное завершение не требует нового жеста.
## 2. Что человек увидит до и после
**До:** после настройки цвета в picker нет явного действия завершения, поэтому
пользователь вынужден догадываться, что поверхность закроется нажатием снаружи,
повторным нажатием на образец или `Escape`.
**После:** внизу picker находится заметная полноширинная кнопка «ОК»; нажатие
сохраняет уже показанный результат и закрывает поверхность, а прежние способы
закрытия продолжают работать.
## 3. Проблема
Единый `hp-color-opacity`, созданный в #57 и распространённый на все места
выбора цвета в #180, применяет корректные изменения сразу через событие
`hp-color-opacity-change`. Он умеет закрываться через trigger, outside pointer,
`Escape`, смену transient overlay и lifecycle родительского диалога, но не
показывает ни одного явного действия внутри самой поверхности.
Технически выбор уже состоялся, но визуально интерфейс выглядит незавершённым.
Это повторяется во всех потребителях общего компонента: decor, room/space,
общих цветах, Glow и ripple.
## 4. Решения владельца
1. Сохраняется нынешнее live-применение. «ОК» не создаёт отдельную транзакцию и
не откладывает изменение родительского draft.
2. Outside click/tap, повторное нажатие swatch и `Escape` сохраняются; каждый
путь оставляет последнее валидное live-применённое значение.
3. При незавершённом невалидном HEX «ОК» не закрывает picker. Последнее валидное
применённое значение сохраняется, а ошибка у HEX-поля остаётся видимой до
исправления.
4. Кнопка — полноширинная primary-кнопка внизу picker.
## 5. Скоуп
- одна кнопка подтверждения внутри общей поверхности `hp-color-opacity`;
- одинаковый interaction contract в native Popover API и portal/fallback;
- одинаковое поведение для `showOpacity=true` и color-only потребителей;
- локализованная подпись кнопки во всех поддерживаемых языках;
- keyboard focus, screen-reader name, forced-colors и touch hit target;
- сохранение действующих live events и всех прежних путей закрытия;
- unit, browser smoke и reviewed golden для светлой/тёмной тем, desktop/touch;
- пользовательская запись в обоих changelog.
## 6. Не входит
- кнопка «Отмена», Reset, история, presets, eyedropper или новый palette UI;
- транзакционный черновик цвета внутри компонента либо откат live-изменений;
- изменение состава HSV/HEX/opacity controls и их порядка;
- изменение формата `#rrggbb`, opacity `[0, 1]` или
`hp-color-opacity-change`;
- изменение Save/Cancel родительских диалогов и серверной конфигурации;
- отдельные правила для decor, room, space, general settings, Glow или ripple;
- новый глобальный keyboard shortcut: `Enter` в HEX-поле сохраняет свой
текущий смысл, а `Enter`/`Space` на самой кнопке работает нативно;
- переработка floating placement, overlay stacking либо touch parity остальных
редакторских операций.
Если реализация потребует изменить момент применения значения, detail события,
родительские drafts или закрытие других transient overlays, задача возвращается
в `S3-spec` как расширение публичного контракта.
## 7. Контракт поведения
### 7.1 Live-применение
Hue, saturation, brightness, валидный HEX и opacity продолжают немедленно
обновлять локальный preview и отправлять существующее bubbling/composed событие
`hp-color-opacity-change` с detail `{ color, opacity }`. Родительский consumer
продолжает обновлять свой draft или сохранённый editor style так же, как до
#476.
Открытие picker и нажатие «ОК» без нового значения не отправляют событие.
Нажатие «ОК» после уже live-применённого изменения также не отправляет
дублирующее событие: кнопка завершает взаимодействие, а не повторно применяет
состояние.
### 7.2 Кнопка «ОК»
Кнопка является последним focusable control в DOM-порядке picker и находится
после opacity-row либо после HEX-control у `showOpacity=false`. Она занимает всю
доступную ширину внутренней области, имеет высоту не менее 40 CSS px и следует
theme tokens House Plan для primary action. В `forced-colors` используются
системные цвета/граница, а не невидимый theme-only fill.
Один click/tap либо нативная активация `Enter`/`Space`:
1. валидирует текущий HEX draft по существующему правилу commit;
2. при валидном draft закрывает picker тем же единым lifecycle-путём, что
остальные close reasons;
3. возвращает keyboard focus на swatch trigger;
4. не передаёт click/tap нижележащему plan, toolbar или родительскому dialog.
Кнопка существует и работает одинаково во всех экземплярах общего компонента;
отдельного opt-in property у consumers нет.
### 7.3 Невалидный HEX
Если на момент нажатия «ОК» HEX draft не нормализуется как 3- или 6-значный
цвет либо поле всё ещё несёт результат предыдущей неуспешной валидации:
- picker остаётся открытым;
- `aria-invalid=true` и существующее локализованное сообщение ошибки остаются
видимыми;
- последнее валидное значение color/opacity и родительский draft не меняются;
- новое `hp-color-opacity-change` не отправляется;
- focus остаётся/переводится в HEX input, чтобы значение можно было исправить.
После ввода валидного HEX повторное «ОК» закрывает picker по §7.2. Точное
существующее правило отображения невалидного draft (нормализация поля к
последнему валидному значению при commit) не меняется этой задачей. При этом
повторное «ОК» без нового валидного пользовательского ввода не имеет права
снять состояние ошибки только потому, что commit уже вернул в поле последнее
валидное значение: поверхность остаётся открытой до реального исправления.
Компонент обязан отдельно запоминать, что после неуспешного commit не было
нового `input`-события HEX-поля. Этот признак не сбрасывается нормализацией
draft внутри commit helper, повторным нажатием «ОК», blur или переводом фокуса;
он снимается только новым пользовательским `input` с валидным HEX.
### 7.4 Остальные способы закрытия
Повторное нажатие trigger, pointer down вне surface, `Escape`, открытие другой
exclusive transient surface, закрытие родительского диалога, mode change,
disconnect и потеря валидного anchor продолжают закрывать picker по нынешним
правилам. Они не откатывают уже live-применённое значение и не требуют
предварительно нажать «ОК».
`Escape` из focus внутри picker закрывает сначала picker и возвращает focus на
trigger; второй `Escape` принадлежит родительскому dialog. Outside pointer и
trigger-close сохраняют свой текущий focus contract.
## 8. UX, touch и доступность
- `hp-color-opacity` сохраняет `role="dialog"`, доступное название и одну
floating surface; кнопка — нативный `button type="button"` с видимым текстом.
- Полноширинная цель имеет минимум 40 px высоты. На узком viewport она остаётся
в обычном scroll flow поверхности, не перекрывает HSV/HEX/opacity controls и
не увеличивает ширину за viewport.
- Tap по кнопке закрывает только picker и не превращается в editor action.
Pointer capture HSV-поля и `pointercancel` остаются прежними.
- Light/dark theme, browser zoom, visual viewport, flip/shift placement и
portal fallback используют существующий `FloatingSurfaceController`.
- Новых анимаций нет; `prefers-reduced-motion` не меняется.
Touch editor остаётся best effort по `docs/TOUCH-SUPPORT.md`, но доступная уже
поверхность выбора цвета получает ту же явную кнопку без намеренной деградации.
View, kiosk и device actions не затрагиваются.
## 9. i18n
`ColorPickerLabels` получает обязательное поле `confirm`; его per-card значение
передаётся всем существующим picker instances через уже общий
`_colorPickerLabels`.
Добавить `color_picker.confirm` в синхронные словари `src/i18n/{en,ru,de,fr}.json`:
- RU: `ОК`;
- EN: `OK`;
- DE: `OK`;
- FR: `OK`.
Fallback внутри presentation-only компонента — `OK`. Отдельный aria-only ключ
не нужен: видимый текст является доступным именем.
## 10. Модель данных, миграция и совместимость
Server config, editor drafts, storage/model version и backend schema не
меняются. Новых persisted полей, compatibility aliases и миграции нет.
Публичный consumer API сохраняет свойства `color`, `opacity`, `disabled`,
`showOpacity`, событие и его detail. Дополнение `ColorPickerLabels.confirm`
является внутренним compile-time контрактом House Plan: все production
consumers уже получают единый объект labels. Для defensive runtime fallback
отсутствующее/пустое `confirm` отображается как `OK`, поэтому стороннее ручное
создание custom element со старым объектом labels не ломается.
Downgrade возвращает прежний picker без кнопки и продолжает читать тот же
config. Данные не теряются и обратная миграция не нужна.
## 11. Затронутые файлы и модули
- `src/hp-color-opacity.ts` — label, кнопка, validation/close path и стили;
- `src/houseplan-card.ts` — per-card localized `ColorPickerLabels`;
- `src/i18n/{en,ru,de,fr}.json` — `color_picker.confirm`;
- `test/color-picker.test.mjs` и i18n/source contract tests;
- `demo/smoke_color_picker.mjs` — click/tap, invalid HEX, focus и события;
- при необходимости существующий fallback smoke, без создания второго
component implementation;
- `demo/golden/matrix.mjs`/`harness.mjs` только если существующим color-picker
сценам нужна подготовка для видимой кнопки; принимаемые PNG и golden index;
- `docs/TESTING.md`, `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`;
- generated `dist/**` и `custom_components/houseplan/frontend/**` по
manifest-driven bundle contract.
User Guide и config compatibility docs не меняются: новая кнопка
самообъясняющаяся, а конфигурационный контракт отсутствует. Если в реализации
понадобится объяснять кнопку отдельно в руководстве, это считается сигналом,
что affordance не решил исходную проблему.
## 12. Критерии приёмки
- **AC1 — видимая кнопка и геометрия (unit + golden).** Каждый открытый
`hp-color-opacity`, включая `showOpacity=false`, показывает последним control
полноширинную primary-кнопку с высотой не менее 40 CSS px; она помещается в
light/dark desktop/touch кадрах без clipping и перекрытия controls.
- **AC2 — live parity (unit + smoke).** Hue/SV/HEX/opacity по-прежнему немедленно
отправляют ровно существующий `{ color, opacity }` и обновляют parent draft;
открытие и «ОК» не добавляют event, если значение уже применено.
- **AC3 — успешное завершение (smoke).** Реальный click/tap и keyboard activation
по «ОК» закрывают native popover и fallback, ставят `aria-expanded=false`,
возвращают focus на trigger и не вызывают действие plan/dialog под surface.
- **AC4 — невалидный HEX (unit + smoke).** При невалидном draft «ОК» оставляет
picker открытым, error/`aria-invalid` видимыми, focus в HEX input, последнее
валидное parent value неизменным и не отправляет событие; после исправления
та же кнопка закрывает picker. Повторное «ОК» без нового валидного input также
не закрывает поверхность.
- **AC5 — прежние close paths (smoke).** Outside pointer, trigger и `Escape`
продолжают закрывать surface и сохраняют последнее валидное live-значение;
`Escape` не закрывает родительский dialog тем же нажатием.
- **AC6 — i18n и a11y (unit + smoke).** EN/RU/DE/FR имеют parity, production
instances получают per-card `confirm`, fallback даёт `OK`, видимый текст
является accessible name, Tab достигает кнопки после последнего field.
- **AC7 — touch/fallback/lifecycle (smoke + golden).** На narrow touch кнопка
имеет достаточную цель, tap потребляется surface; Popover API и forced
fallback дают один и тот же результат, disconnect не оставляет portal и
pending emit.
- **AC8 — совместимость, release и бюджеты (unit + docs gate + commands).** Нет
новых config/storage/backend полей и изменений event API; оба changelog
содержат пользовательскую запись, целевые docs/tests актуальны, bundle
проходит действующие initial/editor gzip budgets.
## 13. План автотестов
1. Расширить `test/color-picker.test.mjs`: обязательный `confirm` label,
defensive `OK`, native `type="button"`, последний DOM-control,
полноширинный/40 px CSS contract и неизменный event detail.
2. Расширить `demo/smoke_color_picker.mjs` реальными действиями:
- live изменить hue и opacity, запомнить число событий, нажать «ОК» и
доказать close/focus без дополнительного события;
- открыть снова, ввести невалидный HEX, нажать «ОК», проверить open/error/
focus/no-event, повторным «ОК» доказать отсутствие обхода, исправить и
закрыть;
- повторить color-only consumer;
- доказать сохранение outside/trigger/`Escape`.
3. Forced-fallback path проверить существующим smoke affordance либо узким
расширением color-picker smoke: portal удалён, нижележащий click не вызван.
4. Обновить существующие golden-сцены, уже покрывающие общую surface:
`decor-color-popover-mobile-ru`, `decor-color-popover-desktop-en`,
`general-color-popover-desktop-en`, `device-ripple-color-popover-mobile-ru`,
`space-room-color-popover-desktop-ru`. Вместе они доказывают RU/EN,
light/dark, desktop/touch, opacity/color-only. Не создавать дублирующую сцену
только ради той же кнопки.
5. Защитные мутанты: удалить click handler, повторно emit на confirm, закрыть
surface при invalid HEX, убрать stopPropagation, не передать новый label в
один из языков и сделать кнопку auto-width — соответствующие AC обязаны
покраснеть.
## 14. Release-артефакты
- Пользовательские записи со ссылкой на #476 в `docs/CHANGELOG.md` и
`docs/CHANGELOG.ru.md` в product-коммите.
- `docs/TESTING.md` актуализирует общий color-picker contract.
- Пять существующих color-picker golden обновляются из канонического Linux CI,
явно просматриваются и принимаются с `Baseline-Reviewed` по процессу.
- Документационные screenshots переснимаются/принимаются только если их
visual fingerprint объявлен устаревшим обычным gate; отдельного нового
пользовательского screenshot не требуется.
- Performance/security artifacts не добавляются: кнопка не входит в render
loop, не читает сеть и не меняет данные. Действующие bundle budget и
prerelease performance gates остаются обязательными.
## 15. Производительность и безопасность
Один статический button и один click handler добавляются только в уже открытый
lazy editor picker. Закрытая карточка, View, camera render, geometry и backend
не получают новой работы. Runtime dependency и сетевой запрос не добавляются.
Кнопка не выполняет HA action, не пишет config и не обходит parent Save/Cancel.
Потребление click/tap защищает от случайного действия под закрывающейся
поверхностью. Существующие CSP, permissions и privacy contract не меняются.
## 16. Риски
- **Случайно превратить live picker в transaction.** Снимается AC2 и прямой
проверкой parent draft до «ОК».
- **Повторно отправить последнее значение.** Снимается счётчиком событий до и
после confirm в AC2.
- **Закрыть surface с невалидным HEX.** Снимается единым validation result и
AC4, без проверки только CSS-класса.
- **Разойтись между Popover API и fallback.** Снимается одним `_pickerTemplate`
и реальными smoke обоих путей.
- **Провалить tap в план после удаления portal.** Снимается потреблением события
и sentinel-action в AC3/AC7.
- **Обрезать нижнюю кнопку на touch/zoom.** Снимается существующим viewport
placement, scroll flow и reviewed narrow golden.
- **Оставить один язык без кнопки.** Снимается type/i18n parity и AC6.
## 17. Откат
Feature flag не нужен: изменение локально для общей surface и не меняет данные.
Для отката удаляются button/handler, поле `confirm` и новый i18n key, а прежние
close paths и live event contract остаются рабочими. Родительские consumers,
config и backend откатывать не требуется.
Если после выпуска понадобится другая семантика подтверждения — например,
транзакционный draft с Cancel, — это отдельная продуктовая задача и миграция
interaction contract, а не скрытая правка обработчика #476.
## 18. Принятые предположения
Следующие технические решения предположительны и могут быть свободно изменены
ревьюером без нового продуктового решения владельца:
- рабочее имя нового поля — `ColorPickerLabels.confirm`, ключ —
`color_picker.confirm`;
- confirm может переиспользовать существующий HEX commit helper, но решение о
закрытии не выводится только из нормализованного `_hexDraft`: компонент хранит
отдельный признак неуспешного commit без последующего валидного
пользовательского `input`. Невалидный commit устанавливает признак, а снять
его может только новое `input`-событие HEX-поля с валидным значением; blur,
нормализация draft и повторный confirm его не снимают. `_closePicker(true,
...)` вызывается только когда текущий draft валиден и этот признак снят;
- отдельный custom event `confirm` не нужен: родители уже получили live value;
- кнопка стилизуется внутри shadow DOM `hp-color-opacity` theme tokens без
зависимости от HA-private web components;
- тест fallback может быть частью существующего smoke, если это сохраняет
реальное browser proof и не дублирует весь сценарий.
+1
View File
@@ -187,6 +187,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#461](https://github.com/Matysh/houseplan-card/issues/461) Быстрый commit промежуточной точки цепочки стен | [461-wall-draw-click-performance.md](461-wall-draw-click-performance.md) |
| [#464](https://github.com/Matysh/houseplan-card/issues/464) Верхний контекстный слой Zigbee-топологии | [464-zigbee-topology-layer-order.md](464-zigbee-topology-layer-order.md) |
| [#473](https://github.com/Matysh/houseplan-card/issues/473) Свидетели перф-дельты #160 и диффозависимый перф-смок | [473-iso-perf-witnesses-and-smoke.md](473-iso-perf-witnesses-and-smoke.md) |
| [#476](https://github.com/Matysh/houseplan-card/issues/476) Явное завершение выбора цвета кнопкой «ОК» | [476-color-picker-ok.md](476-color-picker-ok.md) |
## P3
+33
View File
@@ -6713,6 +6713,39 @@ const MUTANT_DEFINITIONS = [
replace: "params.getAll('hp_alpha_stage3')",
}],
},
{
id: 'color-picker-invalid-confirm-latch-removed',
guard: 'node demo/smoke_color_picker.mjs',
because: 'after an invalid HEX draft is normalized for display, repeated confirmation must '
+ 'still wait for a new valid input event; only the real picker lifecycle proves that the '
+ 'normalized fallback cannot bypass the #476 validation latch',
patches: [{
file: 'src/hp-color-opacity.ts',
find: ' if (this._hexNeedsValidInput) {\n'
+ ' this._hexInvalid = true;\n'
+ ' return;\n'
+ ' }',
replace: ' if (false) {\n'
+ ' this._hexInvalid = true;\n'
+ ' return;\n'
+ ' }',
}],
},
{
id: 'color-picker-confirm-click-through',
guard: 'node demo/smoke_color_picker_consumers.mjs',
because: 'the full-width confirmation inside a general-settings dialog must consume its own '
+ 'click before it reaches the picker surface; observing that direct ancestor keeps outer '
+ 'toolbar and modal stoppers from hiding removal of the #476 picker-level defense',
patches: [{
file: 'src/hp-color-opacity.ts',
find: ' private _confirm(event: Event): void {\n'
+ ' event.preventDefault();\n'
+ ' event.stopPropagation();',
replace: ' private _confirm(event: Event): void {\n'
+ ' event.preventDefault();',
}],
},
];
const mutationCardSource = readFileSync(join(repoRoot, 'src/houseplan-card.ts'), 'utf8');
+13
View File
@@ -27,6 +27,19 @@
/** @type {SmokeLink[]} */
export const SMOKE_LINKS = [
{
symbols: [
'DEFAULT_LABELS', '_closePicker', '_commitHex', '_confirm',
'_hexInvalid', '_hexNeedsValidInput', '_surface',
],
smokes: [
'smoke_color_picker.mjs', 'smoke_color_picker_consumers.mjs', 'smoke_help_affordance.mjs',
],
because: 'the #476 production-bundle scenarios prove the full-width localized confirmation, '
+ 'live-event count, invalid-HEX latch, focus and click containment in both the native '
+ 'popover and the picker surface inside a general-settings dialog; those browser '
+ 'lifecycle effects are not visible to source or pure color conversion tests',
},
{
symbols: ['HpDialog', '_ensureNativeModal', '_useHaDialog', '_usesHaDialog'],
smokes: ['smoke_dialog_modal_recovery.mjs'],
+1 -1
View File
@@ -5932,7 +5932,7 @@ export class HouseplanCard extends LitElement {
saturation: this._t('color_picker.saturation'),
value: this._t('color_picker.value'),
hex: this._t('color_picker.hex'),
invalidHex: this._t('color_picker.invalid_hex'),
invalidHex: this._t('color_picker.invalid_hex'), confirm: this._t('color_picker.confirm'),
};
}
+58 -3
View File
@@ -12,6 +12,7 @@ export type ColorPickerLabels = {
value: string;
hex: string;
invalidHex: string;
confirm: string;
};
const DEFAULT_LABELS: ColorPickerLabels = {
@@ -21,6 +22,7 @@ const DEFAULT_LABELS: ColorPickerLabels = {
value: 'Brightness',
hex: 'Hex color',
invalidHex: 'Enter a 3- or 6-digit hex color',
confirm: 'OK',
};
/**
@@ -49,6 +51,10 @@ export class HpColorOpacity extends LitElement {
private _value = 0;
private _hexDraft = '#607d8b';
private _hexInvalid = false;
/** An invalid commit may normalize the visible draft back to a valid value.
* Keep the error latched until the user actually types a new valid HEX so a
* repeated confirmation cannot mistake that normalization for a correction. */
private _hexNeedsValidInput = false;
private _lastValidColor = '#607d8b';
private _activePointerId: number | null = null;
@@ -303,6 +309,23 @@ export class HpColorOpacity extends LitElement {
color: var(--secondary-text-color, #9aa4ad);
font-size: 12px;
}
.confirm {
width: 100%;
min-height: 40px;
box-sizing: border-box;
border: 1px solid var(--primary-color, #03a9f4);
border-radius: 6px;
padding: 8px 12px;
color: var(--text-primary-color, #fff);
background: var(--primary-color, #03a9f4);
cursor: pointer;
font: inherit;
font-weight: 600;
}
.confirm:focus-visible {
outline: 2px solid var(--primary-text-color, #fff);
outline-offset: -4px;
}
:host([disabled]) {
opacity: .5;
pointer-events: none;
@@ -325,6 +348,12 @@ export class HpColorOpacity extends LitElement {
.hue-range::-moz-range-thumb {
box-shadow: none;
}
.confirm {
border-color: ButtonText;
color: HighlightText;
background: Highlight;
forced-color-adjust: none;
}
}
`;
@@ -415,6 +444,7 @@ export class HpColorOpacity extends LitElement {
this._forceFallback = false;
this._syncFromColor(this.color, false);
this._hexInvalid = false;
this._hexNeedsValidInput = false;
this._activePointerId = null;
this._open = true;
await this.updateComplete;
@@ -518,6 +548,7 @@ export class HpColorOpacity extends LitElement {
value: labels.value || DEFAULT_LABELS.value,
hex: labels.hex || DEFAULT_LABELS.hex,
invalidHex: labels.invalidHex || DEFAULT_LABELS.invalidHex,
confirm: labels.confirm || DEFAULT_LABELS.confirm,
};
}
@@ -531,6 +562,7 @@ export class HpColorOpacity extends LitElement {
this._value = hsv.v;
this._hexDraft = normalized;
this._hexInvalid = false;
this._hexNeedsValidInput = false;
this._lastValidColor = normalized;
}
@@ -649,10 +681,14 @@ export class HpColorOpacity extends LitElement {
private _hexInput(event: Event): void {
const value = (event.target as HTMLInputElement).value;
this._hexDraft = value;
this._hexInvalid = false;
const normalized = normalizeHexColor(value);
const rgb = normalized ? hexToRgb(normalized) : null;
if (!normalized || !rgb) return;
if (!normalized || !rgb) {
this._hexInvalid = this._hexNeedsValidInput;
return;
}
this._hexNeedsValidInput = false;
this._hexInvalid = false;
const hsv = rgbToHsv(rgb);
if (hsv.s > 0.0001) this._hue = hsv.h;
this._saturation = hsv.s;
@@ -665,13 +701,31 @@ export class HpColorOpacity extends LitElement {
if (!normalized) {
this._hexDraft = this._lastValidColor;
this._hexInvalid = true;
this._hexNeedsValidInput = true;
return;
}
this._hexDraft = normalized;
if (this._hexNeedsValidInput) {
this._hexInvalid = true;
return;
}
this._hexInvalid = false;
if (normalized !== this._lastValidColor) this._emit(normalized, this.opacity);
}
private _confirm(event: Event): void {
event.preventDefault();
event.stopPropagation();
this._commitHex();
if (this._hexInvalid || this._hexNeedsValidInput) {
this.updateComplete.then(() => {
this._surface()?.querySelector<HTMLInputElement>('input[type="text"]')?.focus();
});
return;
}
this._closePicker(true);
}
private _hexKeyDown(event: KeyboardEvent): void {
if (event.key !== 'Enter') return;
event.preventDefault();
@@ -689,7 +743,7 @@ export class HpColorOpacity extends LitElement {
this.opacity = clamped;
this._lastValidColor = normalized;
if (!preserveHexDraft) this._hexDraft = normalized;
this._hexInvalid = false;
this._hexInvalid = this._hexNeedsValidInput;
this.dispatchEvent(new CustomEvent('hp-color-opacity-change', {
detail: { color: normalized, opacity: clamped },
bubbles: true,
@@ -763,6 +817,7 @@ export class HpColorOpacity extends LitElement {
@change=${(e: Event) => this._setOpacity(Number((e.target as HTMLInputElement).value) / 100)} />
<span class="pct">%</span>
</div>` : nothing}
<button class="confirm" type="button" @click=${(e: Event) => this._confirm(e)}>${labels.confirm}</button>
</div>`;
}
+1
View File
@@ -21,6 +21,7 @@
"color_picker.value": "Helligkeit",
"color_picker.hex": "Hex-Farbe",
"color_picker.invalid_hex": "Geben Sie eine 3- oder 6-stellige Hex-Farbe ein",
"color_picker.confirm": "OK",
"btn.properties": "Eigenschaften",
"btn.keep_as_walls": "Als Wände behalten",
"btn.delete_room_keep_walls": "Raum löschen, Wände behalten",
+1
View File
@@ -21,6 +21,7 @@
"color_picker.value": "Brightness",
"color_picker.hex": "Hex color",
"color_picker.invalid_hex": "Enter a 3- or 6-digit hex color",
"color_picker.confirm": "OK",
"btn.properties": "Properties",
"btn.keep_as_walls": "Keep as walls",
"btn.delete_room_keep_walls": "Delete room, keep walls",
+1
View File
@@ -21,6 +21,7 @@
"color_picker.value": "Luminosité",
"color_picker.hex": "Couleur hexadécimale",
"color_picker.invalid_hex": "Saisissez une couleur hexadécimale à 3 ou 6 chiffres",
"color_picker.confirm": "OK",
"btn.properties": "Propriétés",
"btn.keep_as_walls": "Conserver comme murs",
"btn.delete_room_keep_walls": "Supprimer la pièce, conserver les murs",
+1
View File
@@ -21,6 +21,7 @@
"color_picker.value": "Яркость",
"color_picker.hex": "Цвет HEX",
"color_picker.invalid_hex": "Введите цвет HEX из 3 или 6 цифр",
"color_picker.confirm": "ОК",
"btn.properties": "Свойства",
"btn.keep_as_walls": "Оставить стенами",
"btn.delete_room_keep_walls": "Удалить комнату, оставить стены",
+14
View File
@@ -53,7 +53,16 @@ test('the shared component keeps its API and contains no nested native color pic
for (const token of [
'public color', 'public opacity', 'public disabled', 'public showOpacity',
'hp-color-opacity-change', 'detail: { color: normalized, opacity: clamped }',
'confirm: string', "confirm: 'OK'",
]) assert.match(component, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
assert.match(component, /<button class="confirm" type="button"[^>]*@click=/);
assert.match(component, /\.confirm\s*\{[\s\S]*?width:\s*100%;[\s\S]*?min-height:\s*40px;/);
assert.match(component, /@media \(forced-colors:\s*active\)[\s\S]*?\.confirm\s*\{[\s\S]*?background:\s*Highlight;/);
assert.ok(component.indexOf('<button class="confirm"') > component.indexOf('this.showOpacity ? html`<div class="row">'),
'the confirmation remains the final picker control after the optional opacity row');
assert.match(component, /private _hexNeedsValidInput = false/);
assert.match(component, /this\._hexInvalid = this\._hexNeedsValidInput/);
assert.match(component, /if \(this\._hexInvalid \|\| this\._hexNeedsValidInput\)/);
const pickerCount = (card.match(/<hp-color-opacity/g) || []).length;
assert.ok(pickerCount > 0);
assert.equal((card.match(/\.pickerLabels=\$\{this\._colorPickerLabels\}/g) || []).length, pickerCount,
@@ -70,6 +79,11 @@ test('the shared component keeps its API and contains no nested native color pic
'Glow, ripple and both background pickers stay color-only');
assert.equal((card.match(/\.showOpacity=\$\{true\}/g) || []).length, 2,
'general fill rows and room color explicitly keep their existing opacity');
assert.match(card, /confirm:\s*this\._t\('color_picker\.confirm'\)/);
for (const locale of ['en', 'ru', 'de', 'fr']) {
const messages = JSON.parse(readFileSync(new URL(`../src/i18n/${locale}.json`, import.meta.url), 'utf8'));
assert.equal(messages['color_picker.confirm'], locale === 'ru' ? 'ОК' : 'OK');
}
});
test('the hue range exposes one cyclic spectrum without restyling other ranges', () => {
+2
View File
@@ -445,6 +445,7 @@ test('i18n: German catalog keeps the product glossary and has no translation sen
test('i18n: German values equal to English are explicitly reviewed', () => {
const allowed = new Set([
'color_picker.confirm',
'editor.palette',
'confirm.delete_partition_openings_item',
'marker.name_ph',
@@ -493,6 +494,7 @@ test('i18n: French values equal to English are explicitly reviewed (#371)', () =
// Every entry here is a legitimate French/English homograph or a unit,
// template or brand token — reviewed with the contributed dictionary.
const allowed = new Set([
'color_picker.confirm',
'color_picker.saturation',
'confirm.delete_partition_openings_item',
'decor.rect',