fix: preserve editor behavior across lazy runtime

Issue: #337
User-Visible: no
This commit is contained in:
Matysh
2026-08-28 05:40:28 +00:00
committed by claude[bot]
parent 37a5d14569
commit 9b519b0d4c
28 changed files with 441 additions and 305 deletions
@@ -1,96 +1,96 @@
{
"schema": 1,
"fingerprint": "8e6599b6df97336743ce3679c5cb470f3fbdd7f9938031e18c183aafbc1bc99d",
"fingerprint": "44665795c1c6c8a71d238f632410abbffb53c5141b6f3a4bfc43c0e8fc0d643b",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-Dkqrn1Gf.js",
"houseplan-assets/houseplan-card-DBXjcSq1.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 255385,
"initialViewGzipBytes": 255778,
"lazyFiles": [
"houseplan-assets/editor-nxh2RavO.js",
"houseplan-assets/houseplan-editor-runtime-pmkXpWgM.js",
"houseplan-assets/houseplan-onboarding-runtime-D8Xw4TIp.js",
"houseplan-assets/space-deletion-CxhzkCZ-.js"
"houseplan-assets/editor-CYQ7VHTl.js",
"houseplan-assets/houseplan-editor-runtime-BO7QGYJM.js",
"houseplan-assets/houseplan-onboarding-runtime-z7Wii6dB.js",
"houseplan-assets/space-deletion-T2T2ygLX.js"
],
"lazyGzipBytes": 138411,
"lazyGzipBytes": 138433,
"lazyEditorFiles": [
"houseplan-assets/editor-nxh2RavO.js",
"houseplan-assets/houseplan-editor-runtime-pmkXpWgM.js",
"houseplan-assets/space-deletion-CxhzkCZ-.js"
"houseplan-assets/editor-CYQ7VHTl.js",
"houseplan-assets/houseplan-editor-runtime-BO7QGYJM.js",
"houseplan-assets/space-deletion-T2T2ygLX.js"
],
"lazyEditorGzipBytes": 131765,
"lazyEditorGzipBytes": 131785,
"lazyOnboardingFiles": [
"houseplan-assets/houseplan-onboarding-runtime-D8Xw4TIp.js",
"houseplan-assets/space-deletion-CxhzkCZ-.js"
"houseplan-assets/houseplan-onboarding-runtime-z7Wii6dB.js",
"houseplan-assets/space-deletion-T2T2ygLX.js"
],
"lazyOnboardingGzipBytes": 7199,
"lazyOnboardingGzipBytes": 7202,
"files": [
{
"path": "houseplan-assets/editor-nxh2RavO.js",
"sha256": "31d3b868b707870acac4b6a590392c659f1c8a105eaa9f93100dc8cbca412bcc",
"path": "houseplan-assets/editor-CYQ7VHTl.js",
"sha256": "8e84813025fedea3247c809af7f2f66fbd54cb9e4bf3a844ac8886b9a46febea",
"rawBytes": 3699,
"gzipBytes": 1529,
"gzipBytes": 1533,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dkqrn1Gf.js"
"houseplan-assets/houseplan-card-DBXjcSq1.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-Dkqrn1Gf.js",
"sha256": "5a952defec4dc81f58b4d521d7b96fd01320a40561c708eac4d68fa1435843c6",
"rawBytes": 908487,
"gzipBytes": 255204,
"path": "houseplan-assets/houseplan-card-DBXjcSq1.js",
"sha256": "3f412eaa85fb14826a7cb5a927fc8562dda0fb57437527fd29dc2cf6a4c96086",
"rawBytes": 909977,
"gzipBytes": 255594,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/editor-nxh2RavO.js",
"houseplan-assets/houseplan-editor-runtime-pmkXpWgM.js",
"houseplan-assets/houseplan-onboarding-runtime-D8Xw4TIp.js"
"houseplan-assets/editor-CYQ7VHTl.js",
"houseplan-assets/houseplan-editor-runtime-BO7QGYJM.js",
"houseplan-assets/houseplan-onboarding-runtime-z7Wii6dB.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-pmkXpWgM.js",
"sha256": "34a909cd7878ea28b70873c9c8d4fcd1ab35f448a50b83e6f383ac0fee9a0548",
"rawBytes": 504658,
"gzipBytes": 129683,
"path": "houseplan-assets/houseplan-editor-runtime-BO7QGYJM.js",
"sha256": "0eaa16f6e1b0d84c8f4c2473091b658a49e6b3a00750483e27aafb8e45c8bfb8",
"rawBytes": 504952,
"gzipBytes": 129698,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dkqrn1Gf.js",
"houseplan-assets/space-deletion-CxhzkCZ-.js"
"houseplan-assets/houseplan-card-DBXjcSq1.js",
"houseplan-assets/space-deletion-T2T2ygLX.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-D8Xw4TIp.js",
"sha256": "335283735608e582ed75c30afb0f74698ac18baf02c78c842248dfbec7a0a87b",
"path": "houseplan-assets/houseplan-onboarding-runtime-z7Wii6dB.js",
"sha256": "478cb408cf3152c3fba0e801ea6dabac0d350054ebbd4ea1e05295d7c2f8d314",
"rawBytes": 26356,
"gzipBytes": 6646,
"gzipBytes": 6648,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dkqrn1Gf.js",
"houseplan-assets/space-deletion-CxhzkCZ-.js"
"houseplan-assets/houseplan-card-DBXjcSq1.js",
"houseplan-assets/space-deletion-T2T2ygLX.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-deletion-CxhzkCZ-.js",
"sha256": "35845b98110f5542da56ec8e3a2a8a42afb5e84f6672f46a7fe5fbfe04d0e2f9",
"path": "houseplan-assets/space-deletion-T2T2ygLX.js",
"sha256": "680ef46b62ec73376c97100b26b220bc440e00d926c759d53df724d8e10bc781",
"rawBytes": 911,
"gzipBytes": 553,
"gzipBytes": 554,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "c1218a459eebfbcce7220c236e61be6f9519988ce9ed9e5c6355539b521e1a52",
"sha256": "579e3d211be1ba3a440d423b2d0efd27cb8f5061bcbcee6bf54e38b95a8e915b",
"rawBytes": 190,
"gzipBytes": 181,
"gzipBytes": 184,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-Dkqrn1Gf.js"
"houseplan-assets/houseplan-card-DBXjcSq1.js"
],
"dynamicImports": []
}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="8e6599b6df97336743ce3679c5cb470f3fbdd7f9938031e18c183aafbc1bc99d";import{c as e,l as o,t,d2 as s,A as a,b as l}from"./houseplan-card-Dkqrn1Gf.js";class i 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")}],l=this._floorToken;l&&a.push({value:l,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const i="string"==typeof this._config?.floor?this._config.floor:"";i&&!e.some(e=>e.value===i)&&a.push({value:i,label:i}),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;this._loadSpaces();const e=this._lang,o={title:t(e,"editor.title"),floor:t(e,"editor.floor"),default_floor:t(e,"editor.default_floor"),language:t(e,"editor.language"),icon_size:t(e,"editor.icon_size"),show_temperature:t(e,"editor.show_temperature"),live_states:t(e,"editor.live_states"),show_signal:t(e,"editor.show_signal"),kiosk:t(e,"editor.kiosk"),cycle:t(e,"editor.cycle")},s=this._schema,i=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),n=e=>l`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="44665795c1c6c8a71d238f632410abbffb53c5141b6f3a4bfc43c0e8fc0d643b";import{c as e,l as o,t,d2 as s,A as a,b as l}from"./houseplan-card-DBXjcSq1.js";class i 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")}],l=this._floorToken;l&&a.push({value:l,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const i="string"==typeof this._config?.floor?this._config.floor:"";i&&!e.some(e=>e.value===i)&&a.push({value:i,label:i}),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;this._loadSpaces();const e=this._lang,o={title:t(e,"editor.title"),floor:t(e,"editor.floor"),default_floor:t(e,"editor.default_floor"),language:t(e,"editor.language"),icon_size:t(e,"editor.icon_size"),show_temperature:t(e,"editor.show_temperature"),live_states:t(e,"editor.live_states"),show_signal:t(e,"editor.show_signal"),kiosk:t(e,"editor.kiosk"),cycle:t(e,"editor.cycle")},s=this._schema,i=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),n=e=>l`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
@@ -1 +1 @@
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="8e6599b6df97336743ce3679c5cb470f3fbdd7f9938031e18c183aafbc1bc99d";export{t as a,e as c,o as i,r as s,s as t};
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="44665795c1c6c8a71d238f632410abbffb53c5141b6f3a4bfc43c0e8fc0d643b";export{t as a,e as c,o as i,r as s,s as t};
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="8e6599b6df97336743ce3679c5cb470f3fbdd7f9938031e18c183aafbc1bc99d";export{d3 as HouseplanCard}from"./houseplan-assets/houseplan-card-Dkqrn1Gf.js";
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="44665795c1c6c8a71d238f632410abbffb53c5141b6f3a4bfc43c0e8fc0d643b";export{d3 as HouseplanCard}from"./houseplan-assets/houseplan-card-DBXjcSq1.js";
+1 -1
View File
@@ -72,7 +72,7 @@ const res = await page.evaluate(async () => {
out.legacyCardStillRestoresNav = ordinary._space === ids[1]
&& ordinary._fixedFloorState().kind === 'absent';
const editor = document.createElement('houseplan-card-editor');
const editor = await customElements.get('houseplan-card').getConfigElement();
editor.hass = base.hass;
editor.setConfig({ type: 'custom:houseplan-card', floor: 1, title: 'Fixed' });
document.body.appendChild(editor);
+6 -1
View File
@@ -58,7 +58,12 @@ const res = await page.evaluate(async () => {
// terser либо инлайнит строку (v1.56.0), либо оставляет переменную (v${xx}) —
// во втором случае доразрешаем её по присваиванию xx="1.56.0".
// Версия — SemVer, у пре-релиза есть суффикс (1.58.0-beta.1), он тоже часть строки.
const bundle = readFileSync(new URL('./srv/assets/houseplan-card.js', import.meta.url), 'utf8');
const assetsRoot = new URL('./srv/assets/', import.meta.url);
const assetManifest = JSON.parse(readFileSync(new URL('houseplan-assets.json', assetsRoot), 'utf8'));
const bundle = assetManifest.files
.filter((file) => file.path.endsWith('.js'))
.map((file) => readFileSync(new URL(file.path, assetsRoot), 'utf8'))
.join('\n');
const SEMVER = '\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?';
const m = bundle.match(new RegExp(`HOUSEPLAN-CARD %c v(?:(${SEMVER})|\\$\\{(\\w+)\\})`));
const BUNDLE_VERSION = m?.[1] ?? (m?.[2] && bundle.match(new RegExp(`[^\\w$]${m[2]}="(${SEMVER})"`))?.[1]);
+5 -2
View File
@@ -38,7 +38,9 @@ await page.evaluate(async () => {
d_temp: { s: 'f1', x: 0.72, y: 0.24 },
};
const sharedLayout = card._layout;
const originalGridLevels = card._gridLevels.bind(card);
await card._ensureEditorRuntime();
const runtime = card._editorRuntime;
const originalGridLevels = runtime._gridLevels.bind(runtime);
const scalePoint = (point, factor) => point.map((value) => value * factor);
const scaledFixture = (cellCm) => {
const factor = 5 / cellCm;
@@ -96,7 +98,8 @@ await page.evaluate(async () => {
card._decorTool = mode === 'decor' ? 'backdrop' : 'select';
// Grid density is the one allowed difference. Mask it in every editor
// raster pair and prove the precision ratio separately below.
card._gridLevels = mode === 'view' ? originalGridLevels : () => null;
runtime._gridLevels = mode === 'view' ? originalGridLevels : () => null;
card._gridLevels = () => runtime._gridLevels();
card._labs = { ...card._labs, active: projection === 'iso' ? ['iso'] : [] };
card._viewPreference = { ...card._viewPreference, f1: projection };
card._isoFallback.clear();
+5 -4
View File
@@ -83,8 +83,9 @@ const res = await page.evaluate(async () => {
// The production registry adapter is covered by unit/backend tests. Stub
// only the transactional projection here so the smoke owns no shared
// registry singleton state and remains deterministic.
const realPreview = c._markerPreviewDevice.bind(c);
c._markerPreviewDevice = (draft) => draft.devId === multi.id ? {
const runtime = c._editorRuntime;
const realPreview = runtime._markerPreviewDevice.bind(runtime);
runtime._markerPreviewDevice = (draft) => draft.devId === multi.id ? {
...multi,
entities: [multi.primary, extra],
marker: {
@@ -99,7 +100,7 @@ const res = await page.evaluate(async () => {
});
await c.updateComplete;
out.leadingDraftIsAlways = c._markerDialog.lightRole === 'always';
out.leadingPreviewHasExtra = c._markerPreviewDevice(c._markerDialog)?.entities?.includes(extra) === true;
out.leadingPreviewHasExtra = runtime._markerPreviewDevice(c._markerDialog)?.entities?.includes(extra) === true;
const leading = sr().querySelector('hp-dialog #marker-light-entity');
out.leadingSelectorForMultiple = !!leading && leading.options.length === 3;
out.savedLeadingEntitySelected = leading?.value === extra
@@ -114,7 +115,7 @@ const res = await page.evaluate(async () => {
out.leadingUpdatesPreview = false;
}
c._markerDialog = null; await c.updateComplete;
c._markerPreviewDevice = realPreview;
runtime._markerPreviewDevice = realPreview;
const single = c._devices.find((device) => device.id === 'd_lamp');
c._openMarkerDialog(single); await c.updateComplete;
+2 -1
View File
@@ -3,6 +3,7 @@ const { page, browser } = await launch();
const res = await page.evaluate(async () => {
const out = {};
const c = window.__card;
await c._ensureEditorRuntime();
// Permission-delayed warm navigation must use the complete transition
// authority instead of assigning the private mode field directly.
c._setMode('view', false); await c.updateComplete;
@@ -38,7 +39,7 @@ const res = await page.evaluate(async () => {
document.body.appendChild(legacy);
await new Promise((r) => setTimeout(r, 300));
out.legacySpaceOnly = legacy._space === 'garden' && legacy._mode === 'view';
legacy._setMode('devices'); await legacy.updateComplete;
await legacy._requestMode('devices'); await legacy.updateComplete;
const migrated = JSON.parse(localStorage.getItem('houseplan_card_nav_v1'));
out.legacyRewrittenOnNextNav = migrated.space === 'garden'
&& !Object.hasOwn(migrated, 'mode');
+1 -1
View File
@@ -188,7 +188,7 @@ const out = await page.evaluate(async () => {
card._spaceDialog = null;
await card.updateComplete;
const editor = document.createElement('houseplan-card-editor');
const editor = await customElements.get('houseplan-card').getConfigElement();
editor.hass = {
...card.hass,
callWS: async (message) => message.type === 'houseplan/config/get'
+5 -4
View File
@@ -44,7 +44,7 @@ const out = await page.evaluate(async () => {
card._planSnapGeometryCache = null;
card.requestUpdate();
};
card._setMode('plan');
await card._requestMode('plan');
card._activateOpeningPlacement('door');
await update();
@@ -71,16 +71,17 @@ const out = await page.evaluate(async () => {
&& Math.abs(endpointCandidate.host.t - (endpointX - 250) / 500) < 1e-9
&& !!endpointCandidate.measure.labels[0].text;
const originalSvgPoint = card._svgPoint;
const runtime = card._editorRuntime;
const originalSvgPoint = runtime._svgPoint;
card._opDrag = {
id: saved.id, moved: false, sx: 0, sy: 0, dirty: false,
before: card._geometrySnapshot(),
};
card._svgPoint = () => [250, 500];
runtime._svgPoint = () => [250, 500];
card._opPointerMove(new PointerEvent('pointermove', {
pointerId: 186, clientX: 20, clientY: 0, bubbles: true,
}), saved);
card._svgPoint = originalSvgPoint;
runtime._svgPoint = originalSvgPoint;
card._opPointerUp(new PointerEvent('pointerup', { pointerId: 186, bubbles: true }), saved);
await update();
result.directDragStopsAtSameJambBoundary = Math.abs(saved.host.t - (endpointX - 250) / 500) < 1e-9
+1
View File
@@ -51,6 +51,7 @@ const rec = await page.evaluate(async () => {
// A detach during resume/mode motion kills the one RAF owner. The transient
// state must die with it, otherwise the same element comes back inert or
// permanently pinned to an intermediate camera.
await c._ensureEditorRuntime();
c._resumeSettling = true;
c._slide = 'left';
const from = c._currentModeVisual('view');
+11 -2
View File
@@ -35,6 +35,11 @@ const res = await page.evaluate(async () => {
while (c._booting && performance.now() - t0 < 2500) await sleep(30);
await sleep(350);
};
const waitFor = async (predicate, timeout = 2500) => {
const started = performance.now();
while (!predicate() && performance.now() - started < timeout) await sleep(20);
return predicate();
};
const rect = (c) => (c._view ? [c._view.x, c._view.y, c._view.w, c._view.h] : null);
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
/** покадрово: ни один кадр после пересоздания не отличается от эталона */
@@ -61,6 +66,7 @@ const res = await page.evaluate(async () => {
c.requestUpdate(); await c.updateComplete; await sleep(100);
const zoomA = c._zoom, viewA = rect(c);
out.aPanned = viewA[0] > 1 || viewA[1] > 1; // sanity: вид действительно смещён
await c._ensureEditorRuntime();
c._openSpaceDialog('edit', c._space); await c.updateComplete;
c._spaceDialog = { ...c._spaceDialog, title: 'ЧЕРНОВИК-42' }; // недосохранённая правка
await c.updateComplete;
@@ -80,7 +86,8 @@ const res = await page.evaluate(async () => {
out.aNoZombieAfterClose = !c._spaceDialog;
// ================= B. редактор устройств: зум редактора + карточка =======
c._setMode('devices'); await c.updateComplete; await sleep(120);
await c._requestMode('devices'); await c.updateComplete;
await waitFor(() => !c._modeTransitionBusy);
c._applyView(3.4, 430, 380); c.requestUpdate(); await c.updateComplete; await sleep(100);
const zoomB = c._zoom, viewB = rect(c);
const dev = c._devices.find((d) => d.space === c._space);
@@ -91,6 +98,8 @@ const res = await page.evaluate(async () => {
c.remove(); await sleep(20);
c = mk();
await waitFor(() => c._mode === 'devices' && !!c._markerDialog);
await c.updateComplete;
out.bModeRestored = c._mode === 'devices';
out.bViewBitExact = await watchView(c, zoomB, viewB);
out.bDialogSurvived = !!c._markerDialog;
@@ -116,7 +125,7 @@ const res = await page.evaluate(async () => {
out.dAlignNotRevived = !c._alignDialog;
// ================= E. реальный уход с маршрута завершает редактор =======
c._setMode('devices'); await c.updateComplete; await sleep(60);
await c._requestMode('devices'); await c.updateComplete; await sleep(60);
const returnUrl = `${location.pathname}${location.search}${location.hash}`;
const returnSpace = c._space;
const routeDev = c._devices.find((d) => d.space === returnSpace);
+4 -1
View File
@@ -48,13 +48,14 @@ const res = await page.evaluate(async () => {
A._applyView(2.15, 300, 420);
A.requestUpdate(); await A.updateComplete; await sleep(60);
const zoomA = A._zoom;
await A._ensureEditorRuntime();
A._openSpaceDialog('edit', A._space); await A.updateComplete;
A._spaceDialog = { ...A._spaceDialog, title: 'DRAFT-FROM-A' };
await A.updateComplete; await sleep(30);
const B = mk(); // вторая карточка, тот же конфиг
await settle(B);
B._setMode('devices'); await B.updateComplete; await settle(B);
await B._requestMode('devices'); await B.updateComplete; await settle(B);
B._applyView(3.35, 200, 200);
B.requestUpdate(); await B.updateComplete; await sleep(60);
const zoomB = B._zoom;
@@ -77,6 +78,7 @@ const res = await page.evaluate(async () => {
HP._warmBootReset(400);
let c = mk();
await settle(c);
await c._ensureEditorRuntime();
c._openSpaceDialog('edit', c._space); await c.updateComplete;
c._spaceDialog = { ...c._spaceDialog, title: 'DRAFT-RAPID' };
await c.updateComplete; await sleep(30);
@@ -95,6 +97,7 @@ const res = await page.evaluate(async () => {
HP._warmBootReset(400);
c = mk();
await settle(c);
await c._ensureEditorRuntime();
c._openSpaceDialog('edit', c._space); await c.updateComplete;
c._spaceDialog = { ...c._spaceDialog, title: 'DRAFT-TTL', planFile: 'x'.repeat(4096) };
await c.updateComplete; await sleep(30);
+43 -43
View File
@@ -1,96 +1,96 @@
{
"schema": 1,
"fingerprint": "8e6599b6df97336743ce3679c5cb470f3fbdd7f9938031e18c183aafbc1bc99d",
"fingerprint": "44665795c1c6c8a71d238f632410abbffb53c5141b6f3a4bfc43c0e8fc0d643b",
"entry": "houseplan-card.js",
"initialViewFiles": [
"houseplan-assets/houseplan-card-Dkqrn1Gf.js",
"houseplan-assets/houseplan-card-DBXjcSq1.js",
"houseplan-card.js"
],
"initialViewGzipBytes": 255385,
"initialViewGzipBytes": 255778,
"lazyFiles": [
"houseplan-assets/editor-nxh2RavO.js",
"houseplan-assets/houseplan-editor-runtime-pmkXpWgM.js",
"houseplan-assets/houseplan-onboarding-runtime-D8Xw4TIp.js",
"houseplan-assets/space-deletion-CxhzkCZ-.js"
"houseplan-assets/editor-CYQ7VHTl.js",
"houseplan-assets/houseplan-editor-runtime-BO7QGYJM.js",
"houseplan-assets/houseplan-onboarding-runtime-z7Wii6dB.js",
"houseplan-assets/space-deletion-T2T2ygLX.js"
],
"lazyGzipBytes": 138411,
"lazyGzipBytes": 138433,
"lazyEditorFiles": [
"houseplan-assets/editor-nxh2RavO.js",
"houseplan-assets/houseplan-editor-runtime-pmkXpWgM.js",
"houseplan-assets/space-deletion-CxhzkCZ-.js"
"houseplan-assets/editor-CYQ7VHTl.js",
"houseplan-assets/houseplan-editor-runtime-BO7QGYJM.js",
"houseplan-assets/space-deletion-T2T2ygLX.js"
],
"lazyEditorGzipBytes": 131765,
"lazyEditorGzipBytes": 131785,
"lazyOnboardingFiles": [
"houseplan-assets/houseplan-onboarding-runtime-D8Xw4TIp.js",
"houseplan-assets/space-deletion-CxhzkCZ-.js"
"houseplan-assets/houseplan-onboarding-runtime-z7Wii6dB.js",
"houseplan-assets/space-deletion-T2T2ygLX.js"
],
"lazyOnboardingGzipBytes": 7199,
"lazyOnboardingGzipBytes": 7202,
"files": [
{
"path": "houseplan-assets/editor-nxh2RavO.js",
"sha256": "31d3b868b707870acac4b6a590392c659f1c8a105eaa9f93100dc8cbca412bcc",
"path": "houseplan-assets/editor-CYQ7VHTl.js",
"sha256": "8e84813025fedea3247c809af7f2f66fbd54cb9e4bf3a844ac8886b9a46febea",
"rawBytes": 3699,
"gzipBytes": 1529,
"gzipBytes": 1533,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dkqrn1Gf.js"
"houseplan-assets/houseplan-card-DBXjcSq1.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-card-Dkqrn1Gf.js",
"sha256": "5a952defec4dc81f58b4d521d7b96fd01320a40561c708eac4d68fa1435843c6",
"rawBytes": 908487,
"gzipBytes": 255204,
"path": "houseplan-assets/houseplan-card-DBXjcSq1.js",
"sha256": "3f412eaa85fb14826a7cb5a927fc8562dda0fb57437527fd29dc2cf6a4c96086",
"rawBytes": 909977,
"gzipBytes": 255594,
"isEntry": false,
"imports": [],
"dynamicImports": [
"houseplan-assets/editor-nxh2RavO.js",
"houseplan-assets/houseplan-editor-runtime-pmkXpWgM.js",
"houseplan-assets/houseplan-onboarding-runtime-D8Xw4TIp.js"
"houseplan-assets/editor-CYQ7VHTl.js",
"houseplan-assets/houseplan-editor-runtime-BO7QGYJM.js",
"houseplan-assets/houseplan-onboarding-runtime-z7Wii6dB.js"
]
},
{
"path": "houseplan-assets/houseplan-editor-runtime-pmkXpWgM.js",
"sha256": "34a909cd7878ea28b70873c9c8d4fcd1ab35f448a50b83e6f383ac0fee9a0548",
"rawBytes": 504658,
"gzipBytes": 129683,
"path": "houseplan-assets/houseplan-editor-runtime-BO7QGYJM.js",
"sha256": "0eaa16f6e1b0d84c8f4c2473091b658a49e6b3a00750483e27aafb8e45c8bfb8",
"rawBytes": 504952,
"gzipBytes": 129698,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dkqrn1Gf.js",
"houseplan-assets/space-deletion-CxhzkCZ-.js"
"houseplan-assets/houseplan-card-DBXjcSq1.js",
"houseplan-assets/space-deletion-T2T2ygLX.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/houseplan-onboarding-runtime-D8Xw4TIp.js",
"sha256": "335283735608e582ed75c30afb0f74698ac18baf02c78c842248dfbec7a0a87b",
"path": "houseplan-assets/houseplan-onboarding-runtime-z7Wii6dB.js",
"sha256": "478cb408cf3152c3fba0e801ea6dabac0d350054ebbd4ea1e05295d7c2f8d314",
"rawBytes": 26356,
"gzipBytes": 6646,
"gzipBytes": 6648,
"isEntry": false,
"imports": [
"houseplan-assets/houseplan-card-Dkqrn1Gf.js",
"houseplan-assets/space-deletion-CxhzkCZ-.js"
"houseplan-assets/houseplan-card-DBXjcSq1.js",
"houseplan-assets/space-deletion-T2T2ygLX.js"
],
"dynamicImports": []
},
{
"path": "houseplan-assets/space-deletion-CxhzkCZ-.js",
"sha256": "35845b98110f5542da56ec8e3a2a8a42afb5e84f6672f46a7fe5fbfe04d0e2f9",
"path": "houseplan-assets/space-deletion-T2T2ygLX.js",
"sha256": "680ef46b62ec73376c97100b26b220bc440e00d926c759d53df724d8e10bc781",
"rawBytes": 911,
"gzipBytes": 553,
"gzipBytes": 554,
"isEntry": false,
"imports": [],
"dynamicImports": []
},
{
"path": "houseplan-card.js",
"sha256": "c1218a459eebfbcce7220c236e61be6f9519988ce9ed9e5c6355539b521e1a52",
"sha256": "579e3d211be1ba3a440d423b2d0efd27cb8f5061bcbcee6bf54e38b95a8e915b",
"rawBytes": 190,
"gzipBytes": 181,
"gzipBytes": 184,
"isEntry": true,
"imports": [
"houseplan-assets/houseplan-card-Dkqrn1Gf.js"
"houseplan-assets/houseplan-card-DBXjcSq1.js"
],
"dynamicImports": []
}
@@ -1,4 +1,4 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="8e6599b6df97336743ce3679c5cb470f3fbdd7f9938031e18c183aafbc1bc99d";import{c as e,l as o,t,d2 as s,A as a,b as l}from"./houseplan-card-Dkqrn1Gf.js";class i 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")}],l=this._floorToken;l&&a.push({value:l,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const i="string"==typeof this._config?.floor?this._config.floor:"";i&&!e.some(e=>e.value===i)&&a.push({value:i,label:i}),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;this._loadSpaces();const e=this._lang,o={title:t(e,"editor.title"),floor:t(e,"editor.floor"),default_floor:t(e,"editor.default_floor"),language:t(e,"editor.language"),icon_size:t(e,"editor.icon_size"),show_temperature:t(e,"editor.show_temperature"),live_states:t(e,"editor.live_states"),show_signal:t(e,"editor.show_signal"),kiosk:t(e,"editor.kiosk"),cycle:t(e,"editor.cycle")},s=this._schema,i=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),n=e=>l`<ha-form
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="44665795c1c6c8a71d238f632410abbffb53c5141b6f3a4bfc43c0e8fc0d643b";import{c as e,l as o,t,d2 as s,A as a,b as l}from"./houseplan-card-DBXjcSq1.js";class i 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")}],l=this._floorToken;l&&a.push({value:l,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const i="string"==typeof this._config?.floor?this._config.floor:"";i&&!e.some(e=>e.value===i)&&a.push({value:i,label:i}),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;this._loadSpaces();const e=this._lang,o={title:t(e,"editor.title"),floor:t(e,"editor.floor"),default_floor:t(e,"editor.default_floor"),language:t(e,"editor.language"),icon_size:t(e,"editor.icon_size"),show_temperature:t(e,"editor.show_temperature"),live_states:t(e,"editor.live_states"),show_signal:t(e,"editor.show_signal"),kiosk:t(e,"editor.kiosk"),cycle:t(e,"editor.cycle")},s=this._schema,i=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),n=e=>l`<ha-form
.hass=${this.hass}
.data=${this._formData}
.schema=${e}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="8e6599b6df97336743ce3679c5cb470f3fbdd7f9938031e18c183aafbc1bc99d";export{t as a,e as c,o as i,r as s,s as t};
function e(e,o){return{id:e,title:o,plan_url:null,view_box:[0,0,1,1],rooms:[],wall_segments:[]}}function o(e="file"){const o="draw"===e;return{source:e,showBorders:o,showNames:o,displayTouched:!1}}function r(e,o){if(e.displayTouched)return{...e,source:o};const r="draw"===o;return{...e,source:o,showBorders:r,showNames:r}}function s(e,o,r){return{...e,[o]:r,displayTouched:!0}}function t(e,o,r){const s=(e?.spaces||[]).find(e=>e?.id===r),t=new Set((s?.rooms||[]).map(e=>String(e?.id||"")).filter(Boolean)),n=[...new Set((e?.markers||[]).filter(e=>!0!==e?.removed&&"string"==typeof e?.id).filter(e=>e.space===r||"string"==typeof e.room_id&&t.has(e.room_id)||o?.[e.id]?.s===r).map(e=>e.id))].sort((e,o)=>e.localeCompare(o));return{markerIds:n,count:n.length}}globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="44665795c1c6c8a71d238f632410abbffb53c5141b6f3a4bfc43c0e8fc0d643b";export{t as a,e as c,o as i,r as s,s as t};
+1 -1
View File
@@ -1 +1 @@
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="8e6599b6df97336743ce3679c5cb470f3fbdd7f9938031e18c183aafbc1bc99d";export{d3 as HouseplanCard}from"./houseplan-assets/houseplan-card-Dkqrn1Gf.js";
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="44665795c1c6c8a71d238f632410abbffb53c5141b6f3a4bfc43c0e8fc0d643b";export{d3 as HouseplanCard}from"./houseplan-assets/houseplan-card-DBXjcSq1.js";
+11 -11
View File
@@ -2,7 +2,7 @@
"version": 1,
"fixture": "synthetic-only",
"chromium": "151.0.7922.34",
"sourceFingerprint": "7bc16fbd70daeb6cb0a0f87f4def8c99f08af12a4246ebc1c0f95abc83db2b4c",
"sourceFingerprint": "d1ea5e9d0f2cab819d45b4cbff399cb6b0a2dc17857ff8434ab4379b9a87875a",
"captureScriptSha256": "23e17620d030b52ea1da5187d4f638f029478ad4f1c56bb64b07dd635397972a",
"command": "npm run build && node demo/docs/capture.mjs",
"scenarios": {
@@ -14,7 +14,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "7bc16fbd70daeb6cb0a0f87f4def8c99f08af12a4246ebc1c0f95abc83db2b4c",
"sourceSha256": "d1ea5e9d0f2cab819d45b4cbff399cb6b0a2dc17857ff8434ab4379b9a87875a",
"imageSha256": "9190bd86a07b0cb0502019d76d3286c0542181e1b4a4f862077d9cefa120613c"
},
"view-touch": {
@@ -25,7 +25,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "7bc16fbd70daeb6cb0a0f87f4def8c99f08af12a4246ebc1c0f95abc83db2b4c",
"sourceSha256": "d1ea5e9d0f2cab819d45b4cbff399cb6b0a2dc17857ff8434ab4379b9a87875a",
"imageSha256": "cfa2f77eb68df69ae9fc9b51ffe2f14df01f7b74444fd2b038cadc1e9dfee22b"
},
"space-create": {
@@ -36,7 +36,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "7bc16fbd70daeb6cb0a0f87f4def8c99f08af12a4246ebc1c0f95abc83db2b4c",
"sourceSha256": "d1ea5e9d0f2cab819d45b4cbff399cb6b0a2dc17857ff8434ab4379b9a87875a",
"imageSha256": "a86d4211af24923d5048e9094d0d35f9b28129394d00cd72582a8f8c2dd7ff75"
},
"room-contour-close": {
@@ -47,7 +47,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "7bc16fbd70daeb6cb0a0f87f4def8c99f08af12a4246ebc1c0f95abc83db2b4c",
"sourceSha256": "d1ea5e9d0f2cab819d45b4cbff399cb6b0a2dc17857ff8434ab4379b9a87875a",
"imageSha256": "c7effd4ccd19bcaff9138458583368aaab488dcb6fc7ea5ab766383cb5377ac4"
},
"plan-context-tray": {
@@ -58,7 +58,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "7bc16fbd70daeb6cb0a0f87f4def8c99f08af12a4246ebc1c0f95abc83db2b4c",
"sourceSha256": "d1ea5e9d0f2cab819d45b4cbff399cb6b0a2dc17857ff8434ab4379b9a87875a",
"imageSha256": "af1194f073f00b03af02b949a072cffb21bfd29995aa65a067eaaaec74ef8490"
},
"device-editor": {
@@ -69,7 +69,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "7bc16fbd70daeb6cb0a0f87f4def8c99f08af12a4246ebc1c0f95abc83db2b4c",
"sourceSha256": "d1ea5e9d0f2cab819d45b4cbff399cb6b0a2dc17857ff8434ab4379b9a87875a",
"imageSha256": "a01bad41635437f8580090b162b94a7f609d7758f2346deaf2b09669e13a8b4a"
},
"device-display-preview": {
@@ -80,7 +80,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "7bc16fbd70daeb6cb0a0f87f4def8c99f08af12a4246ebc1c0f95abc83db2b4c",
"sourceSha256": "d1ea5e9d0f2cab819d45b4cbff399cb6b0a2dc17857ff8434ab4379b9a87875a",
"imageSha256": "7151c96dc048381f7a7070ecb85a82a890b2f4c703628f14fa472eb8dacbf3dc"
},
"background-editor": {
@@ -91,7 +91,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "7bc16fbd70daeb6cb0a0f87f4def8c99f08af12a4246ebc1c0f95abc83db2b4c",
"sourceSha256": "d1ea5e9d0f2cab819d45b4cbff399cb6b0a2dc17857ff8434ab4379b9a87875a",
"imageSha256": "9f08f3f3711b100d68fb1a05af269e43e47d8f8d12c0cbe4f9ab557138722989"
},
"room-card": {
@@ -102,7 +102,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "7bc16fbd70daeb6cb0a0f87f4def8c99f08af12a4246ebc1c0f95abc83db2b4c",
"sourceSha256": "d1ea5e9d0f2cab819d45b4cbff399cb6b0a2dc17857ff8434ab4379b9a87875a",
"imageSha256": "66d8b0a509909822ff6b891a483bad9d2c72b20f08e287355883aa5205c6cee0"
},
"device-info": {
@@ -113,7 +113,7 @@
},
"theme": "dark",
"language": "en",
"sourceSha256": "7bc16fbd70daeb6cb0a0f87f4def8c99f08af12a4246ebc1c0f95abc83db2b4c",
"sourceSha256": "d1ea5e9d0f2cab819d45b4cbff399cb6b0a2dc17857ff8434ab4379b9a87875a",
"imageSha256": "f4d3bda00a2fce54f408b1f20e03b5c590ba4b168fd214a28aafb1692b1f3124"
}
}
+70 -9
View File
@@ -890,10 +890,21 @@ export class HouseplanCard extends LitElement {
public async _requestMode(
mode: 'view' | 'plan' | 'devices' | 'decor',
animate = true,
adopt = false,
): Promise<void> {
const request = ++this._editorModeRequest;
if (mode !== 'view' && !(await this._ensureEditorRuntime())) return;
if (request !== this._editorModeRequest) return;
if (adopt) {
this._adoptMode(mode);
if (this._warmRevivePending) {
clearTimeout(this._warmReviveTimer);
this._warmReviveTimer = undefined;
this._warmReviveDialog();
}
this.requestUpdate();
return;
}
this._setMode(mode, animate);
}
public hass?: any;
@@ -3176,8 +3187,13 @@ export class HouseplanCard extends LitElement {
}
this._commitSpace(vp.space, true);
this._navApplied = true;
// the editor comes back only where an editor is allowed at all
this._adoptMode(vp.mode !== 'view' && this._canEdit && !config.kiosk ? vp.mode : 'view');
// A remounted card owns a fresh lazy-runtime instance. Keep View committed
// until that runtime has installed, then adopt the remembered editor
// atomically without rebuilding its camera through a View -> editor
// transition. A newer user command invalidates this request through the
// shared request counter.
const restoredMode = vp.mode !== 'view' && this._canEdit && !config.kiosk ? vp.mode : 'view';
this._adoptMode('view');
// AUD-159B6-04: the memo is the NEWER and MORE SPECIFIC record than the
// global LS nav — a neighbour card writing `mode=devices` into localStorage
// must not be replayed over this owner's viewport once can_write answers
@@ -3203,6 +3219,7 @@ export class HouseplanCard extends LitElement {
this._resize?.restoreSelection(vp.rszSel);
this._decorSel = vp.decorSel;
this._warmVpArmed = true; // _loadFromServer must not re-centre this
if (restoredMode !== 'view') void this._requestMode(restoredMode, false, true);
}
/** Merge a patch into this card's warm entry. `create` is false everywhere
@@ -3338,12 +3355,24 @@ export class HouseplanCard extends LitElement {
* detached (`freed`) and the snapshot is ours to consume — exactly once.
*/
private _warmReviveDialog(): void {
this._warmRevivePending = false;
const e = this._warmSlot; // AUD-159B1-01: OUR slot, never a neighbour's
this._warmReviveTimer = undefined;
if (!e || !e.dlg) return;
if (!e || !e.dlg) {
this._warmRevivePending = false;
return;
}
const d = e.dlg;
const freed = e.freed;
if (d.mode !== this._mode && d.mode !== 'view' && this._warmVp?.mode === d.mode
&& !this._editorRuntime) {
// The dialog belongs to the editor viewport that is currently waiting
// for its lazy runtime. Do not consume it against the temporary View;
// `_requestMode(..., adopt=true)` revives it immediately after the mode
// can be committed safely.
this._warmRevivePending = true;
return;
}
this._warmRevivePending = false;
e.dlg = null; e.freed = 0; // consume-once: no zombie on the third mount
clearTimeout(e.evict); e.evict = 0;
if (!freed || Date.now() - freed > WARM_REVIVE_MS) return; // owner alive, or gone long ago
@@ -9941,7 +9970,7 @@ export class HouseplanCard extends LitElement {
}
private _checkOptimizeGeometry(config: ServerConfig): OptimizeGeometryPreflightResult {
return this._editorRuntimeOrThrow()._checkOptimizeGeometry(config);
return this._editorRuntimeOrThrow()._checkOptimizeGeometryImpl(config);
}
/** One strict production source shared by editor commits and stale rechecks. */
@@ -9952,7 +9981,7 @@ export class HouseplanCard extends LitElement {
geometry: ReturnType<typeof wallBodiesGeometry>,
) => void,
) {
return this._editorRuntimeOrThrow()._checkSpacePhysicalGeometry(config, spaceId, captureWallGeometry);
return this._editorRuntimeOrThrow()._checkSpacePhysicalGeometryImpl(config, spaceId, captureWallGeometry);
}
private _optimizeReferenceContext(
@@ -10597,12 +10626,44 @@ export class HouseplanCard extends LitElement {
}
private _saveKioskScale(patch: Partial<{ icon: number; font: number }>): void {
return this._editorRuntimeOrThrow()._saveKioskScale(patch);
this._kioskScale = { ...this._kioskScale, ...patch };
try {
localStorage.setItem(LS_KIOSK, JSON.stringify(this._kioskScale));
} catch {
/* ignore */
}
this.requestUpdate();
}
/** Per-SCREEN size settings (wall tablets differ) — stored locally. */
private _renderKioskDialog(): TemplateResult {
return this._editorRuntimeOrThrow()._renderKioskDialog();
const k = this._kioskScale;
const row = (key: 'icon' | 'font', label: string) => {
const value = Math.round(k[key] * 100);
const input = (event: Event) => {
const next = Number((event.target as HTMLInputElement).value);
if (Number.isFinite(next)) this._saveKioskScale({ [key]: next / 100 });
};
return html`<label>${label}</label>
<div class="colorrow">
<input type="range" min="50" max="300" step="5" .value=${String(value)}
@input=${input} aria-label=${label} />
<span class="opv">${value}%</span>
</div>`;
};
return html`<hp-dialog .hass=${this.hass} .title=${this._t('kiosk.title')} icon="mdi:tablet"
dismiss-on-scrim @hp-close=${() => (this._kioskDialog = false)}>
<div class="body">
<div class="rhint">${this._t('kiosk.hint')}</div>
${row('icon', this._t('kiosk.icon_scale'))}
${row('font', this._t('kiosk.font_scale'))}
</div>
<div class="row" slot="footer">
<button class="btn ghost" @click=${() => this._saveKioskScale({ icon: 1, font: 1 })}>${this._t('gs.reset')}</button>
<span class="spacer"></span>
<button class="btn on" @click=${() => (this._kioskDialog = false)}>${this._t('btn.close')}</button>
</div>
</hp-dialog>`;
}
// ================= render =================
@@ -11286,7 +11347,7 @@ export class HouseplanCard extends LitElement {
${this._model.map((m) => html`<span class="kdot ${m.id === this._space ? 'on' : ''}"></span>`)}
</div>`
: nothing}
${this._kioskDialog ? this._editorRuntime ? this._renderKioskDialog() : nothing : nothing}
${this._kioskDialog ? this._renderKioskDialog() : nothing}
${this._vacFit ? html`<div class="vaccalbar">
<span>${this._t('vac.fit_hint')}</span>
<button class="btn ghostbtn" @click=${() => this._vacFitTurn({ rot: ((this._vacFit!.p.rot + 90) % 360) as any })}>${this._t('vac.fit_rotate')}</button>
+58 -52
View File
@@ -1114,6 +1114,12 @@ export interface HouseplanEditorHostPort {
_sendConfigCandidate: (candidate: ServerConfig) => Promise<void>;
_writeConfig: () => Promise<void>;
_requestMode: (mode: 'view' | 'plan' | 'devices' | 'decor', animate?: boolean) => Promise<void>;
_checkOptimizeGeometry: (config: ServerConfig) => OptimizeGeometryPreflightResult;
_checkSpacePhysicalGeometry: (
config: ServerConfig,
spaceId: string,
captureWallGeometry?: (geometry: ReturnType<typeof wallBodiesGeometry>) => void,
) => ReturnType<typeof checkSpacePhysicalGeometry>;
_primeDrawWallField: () => void;
_reloadRejectedPhysicalWrite: () => Promise<void>;
_zoom: number;
@@ -1752,7 +1758,7 @@ public _writeConfig(): Promise<void> {
const exactFingerprint = spacePhysicalGeometryFingerprint(candidateSpace);
if (exactFingerprint === accepted.fingerprint) continue;
let safe = false;
try { safe = this._checkSpacePhysicalGeometry(candidate, spaceId).ok; } catch { safe = false; }
try { safe = this.host._checkSpacePhysicalGeometry(candidate, spaceId).ok; } catch { safe = false; }
if (!safe) {
this._restoreGeometryStateLocal(accepted.before);
this.host._pendingPhysicalWrites.delete(spaceId);
@@ -2076,7 +2082,7 @@ public _commitPhysicalGeometry(
// repair/atomise a degraded legacy projection, but it must never be used to
// make an otherwise rejected user edit look safe.
let legacySafe = false;
try { legacySafe = this._checkSpacePhysicalGeometry(liveCandidate, before.spaceId).ok; }
try { legacySafe = this.host._checkSpacePhysicalGeometry(liveCandidate, before.spaceId).ok; }
catch { legacySafe = false; }
if (!legacySafe) {
this._clearGeometryGesture();
@@ -2147,7 +2153,7 @@ public _commitPhysicalGeometry(
authoredPoints.push(...additionalAuthoredPoints.map((point) => [point[0], point[1]]));
safe = wallModelOffGridValueCount(afterSpace)
<= wallModelOffGridValueCount(historyBefore, authoredPoints)
&& this._checkSpacePhysicalGeometry(committedCandidate, before.spaceId).ok;
&& this.host._checkSpacePhysicalGeometry(committedCandidate, before.spaceId).ok;
} catch {
safe = false;
}
@@ -2256,7 +2262,7 @@ public _applyGeometryState(
let safe = false;
try {
const check = restoredCandidate
? this._checkSpacePhysicalGeometry(restoredCandidate, state.spaceId)
? this.host._checkSpacePhysicalGeometry(restoredCandidate, state.spaceId)
: null;
safe = !!check?.ok || !!(allowHistoryBoundaryRepair
&& check?.reason === 'wall-degraded-extra');
@@ -2279,7 +2285,7 @@ public _applyGeometryState(
let safe = false;
try {
const check = committedCandidate
? this._checkSpacePhysicalGeometry(committedCandidate, state.spaceId)
? this.host._checkSpacePhysicalGeometry(committedCandidate, state.spaceId)
: null;
// A history snapshot can predate the write-time wall degradation that
// canonicalized its command. Restore that one repairable baseline so
@@ -3600,7 +3606,7 @@ public _rszSpaceCandidateGeometry(spaceId: string, sp: any): {
} as ServerConfig;
let wallGeometry: ReturnType<typeof wallBodiesGeometry> | null = null;
try {
const check = this._checkSpacePhysicalGeometry(
const check = this.host._checkSpacePhysicalGeometry(
candidate, spaceId, (geometry) => { wallGeometry = geometry; },
);
return { ok: check.ok, wallGeometry };
@@ -3617,7 +3623,7 @@ public _rszSpaceCandidateRenderable(spaceId: string, sp: any): boolean {
spaces: this.host._serverCfg.spaces.map((space) =>
space.id === spaceId ? sp : space),
} as ServerConfig;
return this._checkSpacePhysicalGeometry(candidate, spaceId).ok;
return this.host._checkSpacePhysicalGeometry(candidate, spaceId).ok;
} catch {
return false;
}
@@ -5204,11 +5210,11 @@ public _renderDecorBar(): TemplateResult {
</button>`,
)}
${this.host._editorToolbarGroups.map((group) => this._renderEditorGroupLauncher(group))}
<button class="btn ghost" @click=${this._undoGeometry} ?disabled=${!undoName}
<button class="btn ghost" @click=${() => this._undoGeometry()} ?disabled=${!undoName}
title=${undoName ? this.host._t('history.undo_named', { name: undoName }) : this.host._t('history.undo_empty')}>
<ha-icon icon="mdi:undo-variant"></ha-icon>${this.host._t('history.undo')}
</button>
<button class="btn ghost" @click=${this._redoGeometry} ?disabled=${!redoName}
<button class="btn ghost" @click=${() => this._redoGeometry()} ?disabled=${!redoName}
title=${redoName ? this.host._t('history.redo_named', { name: redoName }) : this.host._t('history.redo_empty')}>
<ha-icon icon="mdi:redo-variant"></ha-icon>${this.host._t('history.redo')}
</button>
@@ -5234,7 +5240,7 @@ public _renderDecorEraseConfirm(): TemplateResult {
<button class="btn ghost" @click=${() => (this.host._decorEraseConfirm = null)}>
${this.host._t('btn.cancel')}
</button>
<button class="btn danger" @click=${this._confirmDecorErase}>
<button class="btn danger" @click=${() => this._confirmDecorErase()}>
<ha-icon icon="mdi:eraser"></ha-icon>${this.host._t('decor.erase')}
</button>
</div>
@@ -7395,7 +7401,7 @@ public _findInboxDevice(row: DeviceInboxRow): void {
requestAnimationFrame(() => requestAnimationFrame(focus));
}
public _deviceInboxTabKey(event: KeyboardEvent): void {
public _deviceInboxTabKey = (event: KeyboardEvent): void => {
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
const dialog = this.host._deviceInbox;
if (!dialog) return;
@@ -7404,7 +7410,7 @@ public _deviceInboxTabKey(event: KeyboardEvent): void {
const index = (tabs.indexOf(dialog.tab) + offset + tabs.length) % tabs.length;
this.host._deviceInbox = { ...dialog, tab: tabs[index], limit: 100, onlyNew: false };
event.preventDefault();
}
};
public _openMarkerDialog(d?: DevItem): void {
// A global-camera snapshot belongs to one explicit expansion only; never
@@ -8582,7 +8588,7 @@ public async _copyPreflightDiagnostics(): Promise<void> {
}
}
public _checkOptimizeGeometry(config: ServerConfig): OptimizeGeometryPreflightResult {
public _checkOptimizeGeometryImpl(config: ServerConfig): OptimizeGeometryPreflightResult {
return checkOptimizeGeometry(config, {
fallbackSpaceName: (index) => this.host._t('gs.align_preflight_space', {
n: String(index),
@@ -8590,7 +8596,7 @@ public _checkOptimizeGeometry(config: ServerConfig): OptimizeGeometryPreflightRe
});
}
public _checkSpacePhysicalGeometry(
public _checkSpacePhysicalGeometryImpl(
config: ServerConfig,
spaceId: string,
captureWallGeometry?: (
@@ -8679,7 +8685,7 @@ public _previewAlignDialog(removeLiveMissingPositions: boolean): void {
this._showWallModelMigrationBlocked(error);
return;
}
const preflight = r.changed ? this._checkOptimizeGeometry(r.config) : null;
const preflight = r.changed ? this.host._checkOptimizeGeometry(r.config) : null;
if (preflight) this._reportPreflightFailure(preflight, r.config);
// The maximum geometry shift is an UPPER BOUND, not a sample. The run
// measured every element in the centimetres of ITS OWN space — converting
@@ -8712,7 +8718,7 @@ public async _runAlignToGrid(): Promise<void> {
if (!d || d.busy || !this.host._serverCfg || !d.changed || !d.preflight?.ok) return;
const fingerprint = contentFingerprint(d.config);
if (d.preflight.fingerprint !== fingerprint) {
const preflight = this._checkOptimizeGeometry(d.config);
const preflight = this.host._checkOptimizeGeometry(d.config);
this._reportPreflightFailure(preflight, d.config);
d = { ...d, preflight };
this.host._alignDialog = d;
@@ -9040,7 +9046,7 @@ public _renderBackupExportDialog(): TemplateResult {
<button class="btn ghost" autofocus @click=${() => (this.host._backupExportDialog = null)}>${this.host._t('btn.cancel')}</button>
<span class="spacer"></span>
<button class="btn on" ?disabled=${d.busy || (d.kind === 'space' && !currentSpace)}
@click=${this._runBackupExport}>
@click=${() => this._runBackupExport()}>
<ha-icon icon="mdi:download"></ha-icon>${d.busy ? '…' : this.host._t('backup.download')}
</button>
</div>
@@ -9163,7 +9169,7 @@ public _renderBackupImportDialog(): TemplateResult {
<button class="btn ghost" autofocus @click=${() => (this.host._backupImportDialog = null)}>${this.host._t('btn.cancel')}</button>
<span class="spacer"></span>
${p ? html`<button class="btn ${p.kind === 'full' ? 'danger' : 'on'}"
?disabled=${d.busy || (p.confirmation_required && !d.confirmMissing)} @click=${this._applyBackupImport}>
?disabled=${d.busy || (p.confirmation_required && !d.confirmMissing)} @click=${() => this._applyBackupImport()}>
<ha-icon icon=${p.kind === 'full' ? 'mdi:database-import' : 'mdi:plus'}></ha-icon>
${d.busy ? '…' : this.host._t(p.kind === 'full' ? 'backup.replace' : 'backup.add')}
</button>` : nothing}
@@ -9443,7 +9449,7 @@ public _renderAlignDialog(): TemplateResult {
})}</p>
<button class="btn ghost optimize-cleanup" type="button"
aria-pressed=${d.removeLiveMissingPositions ? 'true' : 'false'}
@click=${this._toggleOptimizeLivePositions} ?disabled=${d.busy}>
@click=${() => this._toggleOptimizeLivePositions()} ?disabled=${d.busy}>
<ha-icon icon=${d.removeLiveMissingPositions ? 'mdi:undo' : 'mdi:map-marker-remove-outline'}></ha-icon>
${this.host._t(d.removeLiveMissingPositions
? 'gs.optimize_live_keep' : 'gs.optimize_live_remove')}
@@ -9489,7 +9495,7 @@ public _renderAlignDialog(): TemplateResult {
<span class="spacer"></span>
<button class="btn ghost" @click=${() => { this.host._alignDialog = null; this.host._preflightClipboardFallback = null; }}>${this.host._t('btn.cancel')}</button>
${!d.changed || !d.preflight?.ok ? nothing : html`
<button class="btn on" @click=${this._runAlignToGrid} ?disabled=${d.busy}>
<button class="btn on" @click=${() => this._runAlignToGrid()} ?disabled=${d.busy}>
<ha-icon icon="mdi:check"></ha-icon>${d.busy ? '…' : this.host._t('gs.align_run')}
</button>`}
</div>
@@ -9592,7 +9598,7 @@ public _renderSettingsDialog(): TemplateResult {
<label class="dispsection">${this.host._t('gs.backup_group')}</label>
<div class="rhint">${this.host._t('gs.backup_hint')}</div>
<div class="backupactions">
<button class="btn ghost" @click=${this._openBackupExport}>
<button class="btn ghost" @click=${() => this._openBackupExport()}>
<ha-icon icon="mdi:download"></ha-icon>${this.host._t('backup.export_open')}
</button>
<span class="backupupload">
@@ -9600,10 +9606,10 @@ public _renderSettingsDialog(): TemplateResult {
((e.currentTarget as HTMLElement).nextElementSibling as HTMLInputElement | null)?.click()}>
<ha-icon icon="mdi:upload"></ha-icon>${this.host._t('backup.import_open')}
</button>
<input type="file" accept="application/json,.json" @change=${this._pickBackupImport} />
<input type="file" accept="application/json,.json" @change=${(event: Event) => this._pickBackupImport(event)} />
</span>
${this.host._canOptimizeUndo && this.host._undoKind === 'import' ? html`
<button class="btn ghost" @click=${this._undoPlanOptimization}
<button class="btn ghost" @click=${() => this._undoPlanOptimization()}
?disabled=${this.host._optimizeUndoBusy}>
<ha-icon icon="mdi:undo-variant"></ha-icon>${this.host._t('backup.undo_import')}
</button>` : nothing}
@@ -9611,12 +9617,12 @@ public _renderSettingsDialog(): TemplateResult {
<label class="dispsection">${this.host._t('gs.grid_group')}</label>
<div class="rhint">${this.host._t('gs.grid_hint')}</div>
<div class="colorrow gsrow">
<button class="btn ghost alignall" @click=${this._openAlignDialog}>
<button class="btn ghost alignall" @click=${() => this._openAlignDialog()}>
<ha-icon icon="mdi:broom"></ha-icon>${this.host._t('gs.align_all')}
</button>
</div>
${this.host._canOptimizeUndo && this.host._undoKind !== 'import' ? html`<div class="colorrow gsrow">
<button class="btn ghost alignall" @click=${this._undoPlanOptimization}
<button class="btn ghost alignall" @click=${() => this._undoPlanOptimization()}
?disabled=${this.host._optimizeUndoBusy}>
<ha-icon icon="mdi:undo-variant"></ha-icon>${this.host._t('gs.optimize_undo')}
</button>
@@ -9635,7 +9641,7 @@ public _renderSettingsDialog(): TemplateResult {
</button>
<span class="spacer"></span>
<button class="btn ghost" @click=${() => (this.host._settingsDialog = null)}>${this.host._t('btn.cancel')}</button>
<button class="btn on" @click=${this._saveSettingsDialog} ?disabled=${this.host._settingsDialog!.busy}>
<button class="btn on" @click=${() => this._saveSettingsDialog()} ?disabled=${this.host._settingsDialog!.busy}>
<ha-icon icon="mdi:check"></ha-icon>${this.host._settingsDialog!.busy ? '…' : this.host._t('btn.save')}
</button>
</div>
@@ -9739,7 +9745,7 @@ public _renderRulesDialog(): TemplateResult {
</button>
<span class="spacer"></span>
<button class="btn ghost" @click=${() => (this.host._rulesDialog = null)}>${this.host._t('btn.cancel')}</button>
<button class="btn on" @click=${this._saveRules} ?disabled=${d.busy}>
<button class="btn on" @click=${() => this._saveRules()} ?disabled=${d.busy}>
<ha-icon icon="mdi:check"></ha-icon>${d.busy ? '…' : this.host._t('btn.save')}
</button>
</div>
@@ -10702,19 +10708,19 @@ public _renderOpeningDialog(): TemplateResult {
<div class="row dialog-action-footer" slot="footer">
${d.id
? html`<div class="dialog-action-group dialog-action-danger">
<button class="btn danger" @click=${this._deleteOpening}>
<button class="btn danger" @click=${() => this._deleteOpening()}>
<ha-icon icon="mdi:delete-outline"></ha-icon>${this.host._t('btn.delete')}
</button>
</div>`
: nothing}
${orphan
? html`<button class="btn ghost" @click=${this._rebindPartitionOpening}>
? html`<button class="btn ghost" @click=${() => this._rebindPartitionOpening()}>
${this.host._t('opening.rebind_partition')}
</button>`
: nothing}
<div class="dialog-action-group dialog-action-commit">
<button class="btn ghost" @click=${() => (this.host._openingDialog = null)}>${this.host._t('btn.cancel')}</button>
<button class="btn on" @click=${this._saveOpening}>
<button class="btn on" @click=${() => this._saveOpening()}>
<ha-icon icon="mdi:check"></ha-icon>${this.host._t('btn.save')}
</button>
</div>
@@ -11149,7 +11155,7 @@ public _renderPartitionDeleteDialog(): TemplateResult {
${this.host._t('btn.cancel')}
</button>
<span class="spacer"></span>
<button class="btn danger" @click=${this._confirmPartitionDelete}>
<button class="btn danger" @click=${() => this._confirmPartitionDelete()}>
<ha-icon icon="mdi:delete-outline"></ha-icon>${this.host._t('btn.delete')}
</button>
</div>
@@ -11219,19 +11225,19 @@ public _renderPhysicalDialog(): TemplateResult {
<div class="row dialog-action-footer physicalfooter" slot="footer">
<div class="dialog-action-group dialog-action-danger">
${d.kind === 'draft' ? html`
<button class="btn danger" @click=${this._deleteDraftSegment}>
<button class="btn danger" @click=${() => this._deleteDraftSegment()}>
<ha-icon icon="mdi:vector-line"></ha-icon>${this.host._t('physical.delete_segment')}
</button>
<button class="btn danger" @click=${this._deleteDraftWhole}>
<button class="btn danger" @click=${() => this._deleteDraftWhole()}>
<ha-icon icon="mdi:delete-outline"></ha-icon>${this.host._t('physical.delete_draft')}
</button>` : html`
<button class="btn danger" @click=${this._deletePhysicalSelection}>
<button class="btn danger" @click=${() => this._deletePhysicalSelection()}>
<ha-icon icon="mdi:delete-outline"></ha-icon>${this.host._t('btn.delete')}
</button>`}
</div>
<div class="dialog-action-group dialog-action-commit">
<button class="btn ghost" @click=${() => (this.host._physicalDialog = null)}>${this.host._t('btn.cancel')}</button>
<button class="btn on" @click=${this._savePhysicalDialog}>
<button class="btn on" @click=${() => this._savePhysicalDialog()}>
<ha-icon icon="mdi:check"></ha-icon>${this.host._t('btn.save')}
</button>
</div>
@@ -11295,12 +11301,12 @@ public _renderMarkupBar(): TemplateResult {
title=${this.host._t('title.markup_delroom')}>
<ha-icon icon="mdi:delete-outline"></ha-icon>${this.host._t('markup.delete_room')}
</button>
<button class="btn ghost" @click=${this._undoGeometry}
<button class="btn ghost" @click=${() => this._undoGeometry()}
?disabled=${!undoName}
title=${undoTitle} aria-label=${undoTitle}>
<ha-icon icon="mdi:undo-variant" aria-hidden="true"></ha-icon>
</button>
<button class="btn ghost" @click=${this._redoGeometry}
<button class="btn ghost" @click=${() => this._redoGeometry()}
?disabled=${!redoName}
title=${redoTitle} aria-label=${redoTitle}>
<ha-icon icon="mdi:redo-variant" aria-hidden="true"></ha-icon>
@@ -11320,11 +11326,11 @@ public _renderDevicesBar(): TemplateResult {
return html`<div class="editbar devbar">
<div class="editbar-tools" tabindex="-1" ?inert=${this.host._modeTransitionBusy}>
<ha-icon icon="mdi:tune-variant" class="warn"></ha-icon>
<button class="btn ${this.host._showAll ? 'on' : ''}" @click=${this._openDeviceInbox}
<button class="btn ${this.host._showAll ? 'on' : ''}" @click=${() => this._openDeviceInbox()}
title=${this.host._t('device_inbox.title' as any)}>
<ha-icon icon="mdi:devices"></ha-icon>${this.host._t('device_inbox.button' as any)}
</button>
<button class="btn" @click=${this._openRulesDialog} title=${this.host._t('title.icon_rules')}>
<button class="btn" @click=${() => this._openRulesDialog()} title=${this.host._t('title.icon_rules')}>
<ha-icon icon="mdi:shape-plus-outline"></ha-icon>${this.host._t('devbar.rules')}
</button>
${this.host._editorToolbarGroups.map((group) => this._renderEditorGroupLauncher(group))}
@@ -11371,7 +11377,7 @@ public _renderDeviceInbox(): TemplateResult {
${this.host._t('device_inbox.add_virtual' as any)}
</button>
</div>
<div class="device-inbox-tabs" role="tablist" @keydown=${this._deviceInboxTabKey}>
<div class="device-inbox-tabs" role="tablist" @keydown=${(event: KeyboardEvent) => this._deviceInboxTabKey(event)}>
${(['on_plan', 'available', 'hidden', 'readd'] as DeviceInboxCategory[]).map((tab) => html`
<button type="button" role="tab" aria-selected=${dialog.tab === tab ? 'true' : 'false'}
class=${dialog.tab === tab ? 'on' : ''}
@@ -11984,7 +11990,7 @@ public _renderMarkerDialog(): TemplateResult {
})();
return html`<hp-dialog .hass=${this.host.hass}
.title=${d.devId ? this.host._t('info.device_header') : this.host._t('marker.new_device')}
icon="mdi:shape-plus" wide @hp-close=${this._closeMarkerDialog}>
icon="mdi:shape-plus" wide @hp-close=${() => this._closeMarkerDialog()}>
<div class="body">
${bindingStatus?.kind === 'ha_disabled'
? html`<div class="habindingbanner" role="status">
@@ -12533,15 +12539,15 @@ public _renderMarkerDialog(): TemplateResult {
: nothing}
${d.devId
? html`<button class="btn danger" type="button" ?disabled=${d.busy}
title=${this.host._t('marker.delete_tip')} @click=${this._deleteMarker}>
title=${this.host._t('marker.delete_tip')} @click=${() => this._deleteMarker()}>
<ha-icon icon="mdi:delete-outline"></ha-icon>${this.host._t('btn.delete')}
</button>`
: nothing}
</div>
<div class="markersaveactions">
<button class="btn ghost" ?disabled=${d.busy}
@click=${this._closeMarkerDialog}>${this.host._t('btn.cancel')}</button>
<button class="btn on" @click=${this._saveMarker}
@click=${() => this._closeMarkerDialog()}>${this.host._t('btn.cancel')}</button>
<button class="btn on" @click=${() => this._saveMarker()}
?disabled=${d.busy || (d.bindingMode === 'ha' && (!d.binding || d.binding === 'virtual'
|| (!d.devId && bindingStatus?.kind !== 'active')))}
title=${d.bindingMode === 'ha' && (!d.binding || d.binding === 'virtual') ? this.host._t('marker.pick_ph') : ''}>
@@ -12594,7 +12600,7 @@ public _renderSpaceDialog(): TemplateResult {
<input type="file" hidden accept=".svg,.png,.jpg,.jpeg,.webp,image/svg+xml,image/png,image/jpeg,image/webp"
@change=${(e: Event) => this._pickPlanFile(e)} />
</span>
<button class="btn ghost" @click=${this._toggleServerPlans}
<button class="btn ghost" @click=${() => this._toggleServerPlans()}
title=${this.host._t('space.pick_saved_hint')}>
<ha-icon icon="mdi:folder-image"></ha-icon>${this.host._t('space.pick_saved')}
</button>
@@ -12807,7 +12813,7 @@ public _renderSpaceDialog(): TemplateResult {
<div class="row dialog-action-footer" slot="footer">
${d.mode === 'edit'
? html`<div class="dialog-action-group dialog-action-danger">
<button class="btn danger" @click=${this._deleteSpace} ?disabled=${d.busy}>
<button class="btn danger" @click=${() => this._deleteSpace()} ?disabled=${d.busy}>
<ha-icon icon="mdi:delete-outline"></ha-icon>${this.host._t('btn.delete')}
</button>
</div>`
@@ -12817,7 +12823,7 @@ public _renderSpaceDialog(): TemplateResult {
? html`<button class="btn ghost" @click=${() => this._skipImport()}>${this.host._t('btn.skip')}</button>`
: nothing}
<button class="btn ghost" @click=${close}>${this.host._t('btn.cancel')}</button>
<button class="btn on" @click=${this._saveSpaceDialog}
<button class="btn on" @click=${() => this._saveSpaceDialog()}
?disabled=${!d.title.trim() || (d.source === 'file' && !(d.planFile || d.planUrl)) || d.busy}
title=${d.source === 'file' && !(d.planFile || d.planUrl) ? this.host._t('title.need_plan') : ''}>
<ha-icon icon="mdi:check"></ha-icon>${d.busy ? '…' : this.host._t('btn.save')}
@@ -12850,7 +12856,7 @@ public _renderMergeDialog(): TemplateResult {
<div class="row" slot="footer">
<span class="spacer"></span>
<button class="btn ghost" @click=${() => (this.host._mergeDialog = null)}>${this.host._t('btn.cancel')}</button>
<button class="btn on" @click=${this._commitMerge}>
<button class="btn on" @click=${() => this._commitMerge()}>
<ha-icon icon="mdi:check"></ha-icon>${this.host._t('btn.save')}
</button>
</div>
@@ -12924,7 +12930,7 @@ public _renderRoomDialog(): TemplateResult {
return html`<hp-dialog class="roomdialog" .hass=${this.host.hass} wide
.title=${edit ? this.host._t('room.settings_title')
: batchProgress || this.host._t('room.new')}
icon=${edit ? 'mdi:cog-outline' : 'mdi:floor-plan'} @hp-close=${this._roomDialogCancel}>
icon=${edit ? 'mdi:cog-outline' : 'mdi:floor-plan'} @hp-close=${() => this._roomDialogCancel()}>
<div class="body">
${batchProgress ? html`<p class="muted" role="status" aria-live="polite">
${batchProgress}
@@ -12998,16 +13004,16 @@ public _renderRoomDialog(): TemplateResult {
)}
</div>
<div class="row roomfooter" slot="footer">
<button class="btn ghost" @click=${this._roomDialogCancel}>${this.host._t('btn.cancel')}</button>
<button class="btn ghost" @click=${() => this._roomDialogCancel()}>${this.host._t('btn.cancel')}</button>
<span class="spacer"></span>
${edit
? html`<button class="btn on" @click=${() => this._saveRoomEdit()} ?disabled=${!this.host._nameSel.trim()}>
<ha-icon icon="mdi:check"></ha-icon>${this.host._t('btn.save')}
</button>`
: html`${!this.host._pendingSplit ? html`<button class="btn ghost" @click=${this._keepClosedAsPartitions}>
: html`${!this.host._pendingSplit ? html`<button class="btn ghost" @click=${() => this._keepClosedAsPartitions()}>
<ha-icon icon="mdi:wall"></ha-icon>${this.host._t('btn.keep_as_walls')}
</button>` : nothing}
<button class="btn on room-save" @click=${this._saveRoom} ?disabled=${!canSaveNew}>
<button class="btn on room-save" @click=${() => this._saveRoom()} ?disabled=${!canSaveNew}>
<ha-icon icon="mdi:check"></ha-icon>${this.host._t('btn.save')}
</button>`}
</div>
+11
View File
@@ -121,6 +121,17 @@ test('source type references stay portable across build hosts', () => {
);
});
test('lazy runtime event handlers keep the runtime receiver', () => {
const runtime = readFileSync(join(repoRoot, 'src', 'houseplan-editor-runtime.ts'), 'utf8');
const bareHandlers = [...runtime.matchAll(/@[a-zA-Z-]+=\$\{this\.(_[A-Za-z0-9]+)\}/g)]
.map((match) => match[1]);
assert.deepEqual(
bareHandlers,
[],
`Lit binds bare listeners to the host element, not the lazy runtime: ${bareHandlers.join(', ')}`,
);
});
test('documentation capture materializes the complete bundle tree', () => {
const capture = readFileSync(join(repoRoot, 'demo', 'docs', 'capture.mjs'), 'utf8');
assert.match(capture, /import ['"]\.\.\/\.\.\/scripts\/bundle-sync\.mjs['"]/);