fix v1.28.1 (revised per owner spec): View-mode cursors and inert openings

- device icons: pointer cursor in View (grab only in Devices mode); click
  behavior untouched
- openings in View: pure drawings always — no cursor/hover/hit/click
- lock badge: the one exception — visible and clickable in View (info card),
  inert in Plan; removed a duplicate pointer-events in .oplock
- smoke covers all cursor/badge cases
This commit is contained in:
Matysh
2026-07-22 10:42:12 +03:00
parent 961d6afe67
commit 0a7b9a627f
8 changed files with 67 additions and 39 deletions
@@ -264,7 +264,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
cursor: grabbing;
}
.oplock {
pointer-events: none; /* a status badge, not a control */
pointer-events: none; /* inert while editing; clickable in View (rule below) */
position: absolute;
transform: translate(-50%, -50%);
width: calc(var(--icon-size, 2.5cqw) * 0.62);
@@ -275,9 +275,11 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
justify-content: center;
background: var(--hp-bg);
border: 1px solid var(--hp-line);
z-index: 1;
}
.stage.mode-view .oplock {
pointer-events: auto;
cursor: pointer;
z-index: 1;
}
.oplock ha-icon {
--mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.4);
@@ -547,7 +549,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
align-items: center;
justify-content: center;
color: var(--hp-txt);
cursor: grab;
cursor: pointer;
pointer-events: auto;
transition: background 0.15s, border-color 0.15s, opacity 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
@@ -560,9 +562,8 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
justify-content: center;
line-height: 0;
}
.dev:active {
cursor: grabbing;
}
.stage.mode-devices .dev { cursor: grab; }
.stage.mode-devices .dev:active { cursor: grabbing; }
.dev:hover {
background: var(--hp-accent);
color: var(--text-primary-color, #fff);
@@ -1463,7 +1464,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
@pointercancel=${e=>this._opPointerUp(e,t)}></rect>
</g>`})}`}_renderOpeningLocks(t){const e=this._openingsR.filter(t=>"door"===t.type&&t.lock);return e.length?H`${e.map(e=>{const i=this.hass.states[e.lock]?.state,s="locked"===i,o=s||["unlocked","open","opening","unlocking","locking"].includes(String(i)),n=(e.angle+90)*Math.PI/180,r=16*(e.flip_v?-1:1),a=e.rx+Math.cos(n)*r,l=e.ry+Math.sin(n)*r,c=(a-t.x)/t.w*100,h=(l-t.y)/t.h*100;return H`<div class="oplock ${s?"locked":o?"unlocked":"unknown"}"
style="left:${c}%;top:${h}%"
>
@click=${t=>{t.stopPropagation(),"view"===this._mode&&(this._openingInfo=e)}}>
<ha-icon icon="${s?"mdi:lock":o?"mdi:lock-open-variant":"mdi:lock-question"}"></ha-icon>
</div>`})}`:H``}_renderOpeningInfoCard(){const t=this._openingInfo,e=t.contact?this.hass.states[t.contact]?.state:null,i=this._openingAmt(t),s=t.lock?this.hass.states[t.lock]?.state:null,o=(t,e,i,s="")=>H`<div class="oprow ${s}"><ha-icon icon=${t}></ha-icon><span>${e}</span><b>${i}</b></div>`;return H`<div class="menuwrap dialogwrap" @click=${()=>this._openingInfo=null}>
<div class="dialog" @click=${t=>t.stopPropagation()}>
+17
View File
@@ -16,8 +16,25 @@ const res = await page.evaluate(async () => {
// View: клик по открыванию ничего не открывает
c._opClick({ stopPropagation(){} }, { id: 'op1', rx: 550, ry: 200, rlen: 90 });
out.viewClickNoop = !c._openingInfo && !c._openingDialog;
// курсор устройств в Просмотре — pointer, в Устройствах — grab
const dev = sr().querySelector('.dev');
out.viewDevCursor = getComputedStyle(dev).cursor;
// бейдж замка: замок привязан → бейдж кликабелен и открывает инфо
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s2) => s2.id !== 'f1' ? s2 : ({
...s2, openings: [{ id: 'op1', type: 'door', rx: 550, ry: 200, angle: 90, len_cm: 90, lock: 'lock.front_door' }] })) };
c.requestUpdate(); await c.updateComplete;
const badge = sr().querySelector('.oplock');
out.badgeShown = !!badge;
const bcs = badge ? getComputedStyle(badge) : null;
out.badgeClickableInView = bcs ? bcs.pointerEvents === 'auto' && bcs.cursor === 'pointer' : null;
badge?.click(); await c.updateComplete;
out.badgeOpensInfo = !!c._openingInfo;
c._openingInfo = null;
// Plan: op-hit интерактивен, клик редактирует
c._setMode('devices'); await c.updateComplete;
out.devModeCursor = getComputedStyle(sr().querySelector('.dev')).cursor;
c._setMode('plan'); await c.updateComplete;
out.badgeInertInPlan = (() => { const b = sr().querySelector('.oplock'); return b ? getComputedStyle(b).pointerEvents === 'none' : 'no-badge'; })();
const cs2 = getComputedStyle(sr().querySelector('.op-hit'));
out.planInteractive = cs2.pointerEvents === 'auto' && cs2.cursor === 'grab';
const op = c._spaceModel().openings?.[0] || { id: 'op1', rx: 550, ry: 200, rlen: 90 };
+8 -7
View File
@@ -264,7 +264,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
cursor: grabbing;
}
.oplock {
pointer-events: none; /* a status badge, not a control */
pointer-events: none; /* inert while editing; clickable in View (rule below) */
position: absolute;
transform: translate(-50%, -50%);
width: calc(var(--icon-size, 2.5cqw) * 0.62);
@@ -275,9 +275,11 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
justify-content: center;
background: var(--hp-bg);
border: 1px solid var(--hp-line);
z-index: 1;
}
.stage.mode-view .oplock {
pointer-events: auto;
cursor: pointer;
z-index: 1;
}
.oplock ha-icon {
--mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.4);
@@ -547,7 +549,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
align-items: center;
justify-content: center;
color: var(--hp-txt);
cursor: grab;
cursor: pointer;
pointer-events: auto;
transition: background 0.15s, border-color 0.15s, opacity 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
@@ -560,9 +562,8 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
justify-content: center;
line-height: 0;
}
.dev:active {
cursor: grabbing;
}
.stage.mode-devices .dev { cursor: grab; }
.stage.mode-devices .dev:active { cursor: grabbing; }
.dev:hover {
background: var(--hp-accent);
color: var(--text-primary-color, #fff);
@@ -1463,7 +1464,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
@pointercancel=${e=>this._opPointerUp(e,t)}></rect>
</g>`})}`}_renderOpeningLocks(t){const e=this._openingsR.filter(t=>"door"===t.type&&t.lock);return e.length?H`${e.map(e=>{const i=this.hass.states[e.lock]?.state,s="locked"===i,o=s||["unlocked","open","opening","unlocking","locking"].includes(String(i)),n=(e.angle+90)*Math.PI/180,r=16*(e.flip_v?-1:1),a=e.rx+Math.cos(n)*r,l=e.ry+Math.sin(n)*r,c=(a-t.x)/t.w*100,h=(l-t.y)/t.h*100;return H`<div class="oplock ${s?"locked":o?"unlocked":"unknown"}"
style="left:${c}%;top:${h}%"
>
@click=${t=>{t.stopPropagation(),"view"===this._mode&&(this._openingInfo=e)}}>
<ha-icon icon="${s?"mdi:lock":o?"mdi:lock-open-variant":"mdi:lock-question"}"></ha-icon>
</div>`})}`:H``}_renderOpeningInfoCard(){const t=this._openingInfo,e=t.contact?this.hass.states[t.contact]?.state:null,i=this._openingAmt(t),s=t.lock?this.hass.states[t.lock]?.state:null,o=(t,e,i,s="")=>H`<div class="oprow ${s}"><ha-icon icon=${t}></ha-icon><span>${e}</span><b>${i}</b></div>`;return H`<div class="menuwrap dialogwrap" @click=${()=>this._openingInfo=null}>
<div class="dialog" @click=${t=>t.stopPropagation()}>
+8 -7
View File
@@ -264,7 +264,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
cursor: grabbing;
}
.oplock {
pointer-events: none; /* a status badge, not a control */
pointer-events: none; /* inert while editing; clickable in View (rule below) */
position: absolute;
transform: translate(-50%, -50%);
width: calc(var(--icon-size, 2.5cqw) * 0.62);
@@ -275,9 +275,11 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
justify-content: center;
background: var(--hp-bg);
border: 1px solid var(--hp-line);
z-index: 1;
}
.stage.mode-view .oplock {
pointer-events: auto;
cursor: pointer;
z-index: 1;
}
.oplock ha-icon {
--mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.4);
@@ -547,7 +549,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
align-items: center;
justify-content: center;
color: var(--hp-txt);
cursor: grab;
cursor: pointer;
pointer-events: auto;
transition: background 0.15s, border-color 0.15s, opacity 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
@@ -560,9 +562,8 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
justify-content: center;
line-height: 0;
}
.dev:active {
cursor: grabbing;
}
.stage.mode-devices .dev { cursor: grab; }
.stage.mode-devices .dev:active { cursor: grabbing; }
.dev:hover {
background: var(--hp-accent);
color: var(--text-primary-color, #fff);
@@ -1463,7 +1464,7 @@ const t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow
@pointercancel=${e=>this._opPointerUp(e,t)}></rect>
</g>`})}`}_renderOpeningLocks(t){const e=this._openingsR.filter(t=>"door"===t.type&&t.lock);return e.length?H`${e.map(e=>{const i=this.hass.states[e.lock]?.state,s="locked"===i,o=s||["unlocked","open","opening","unlocking","locking"].includes(String(i)),n=(e.angle+90)*Math.PI/180,r=16*(e.flip_v?-1:1),a=e.rx+Math.cos(n)*r,l=e.ry+Math.sin(n)*r,c=(a-t.x)/t.w*100,h=(l-t.y)/t.h*100;return H`<div class="oplock ${s?"locked":o?"unlocked":"unknown"}"
style="left:${c}%;top:${h}%"
>
@click=${t=>{t.stopPropagation(),"view"===this._mode&&(this._openingInfo=e)}}>
<ha-icon icon="${s?"mdi:lock":o?"mdi:lock-open-variant":"mdi:lock-question"}"></ha-icon>
</div>`})}`:H``}_renderOpeningInfoCard(){const t=this._openingInfo,e=t.contact?this.hass.states[t.contact]?.state:null,i=this._openingAmt(t),s=t.lock?this.hass.states[t.lock]?.state:null,o=(t,e,i,s="")=>H`<div class="oprow ${s}"><ha-icon icon=${t}></ha-icon><span>${e}</span><b>${i}</b></div>`;return H`<div class="menuwrap dialogwrap" @click=${()=>this._openingInfo=null}>
<div class="dialog" @click=${t=>t.stopPropagation()}>
+10 -8
View File
@@ -1,13 +1,15 @@
# Changelog
## v1.28.1 — 2026-07-21 (openings are fully inert in View)
- In View mode doors and windows are now **pure status graphics**: no grab
cursor, no hover outline, no hit target, no click — nothing interacts (the
v1.25.0 pass left the hit target and the info-card click behind; the lock
badge also reacted). The lock badge is a pure indicator now.
- In Plan an opening is fully interactive: grab cursor + hover outline are back
there, dragging along walls works, and a click with ANY tool opens its
properties dialog.
## v1.28.1 — 2026-07-22 (View-mode polish: cursors and inert openings)
- **Device icons in View no longer show the grab cursor** (drag lives in the
Devices mode); they show a pointer — clicking still works exactly as before.
- **Doors and windows in View are pure drawings**: no grab cursor, no hover
outline, no hit target, no click — regardless of what is bound to them.
- **The lock badge is the one exception**: when a lock is bound, the badge is
shown and clickable in View (pointer cursor, click opens the door/lock info
card). It stays inert in Plan so it does not fight editing.
- In Plan an opening remains fully interactive: grab cursor + hover outline,
dragging along walls, and a click with ANY tool opens its properties dialog.
## v1.28.0 — 2026-07-21 (sub-area rooms: manual placement without an HA area; issue #3)
- **Devices can now be placed into rooms that have no Home Assistant area.**
+8 -3
View File
@@ -40,9 +40,14 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
- [ ] Devices: icon drag works, click opens the marker editor directly; +/👁/↺/⬡
buttons; accent stage frame [auto]
- [ ] Mode tabs hidden for non-admin users; segmented control highlights the active mode
- [ ] Openings are FULLY inert in View (v1.28.1): no cursor change, no hover outline,
no hit target, no click — pure status graphics; the lock badge is a pure
indicator too [auto]
- [ ] Openings in View (v1.28.1): the door/window itself is a pure drawing — no
cursor change, no hover outline, no hit target, no click, regardless of
bindings [auto]
- [ ] The LOCK BADGE is the one exception: when a lock is bound it is shown and
clickable in View (pointer cursor, click → door/lock info card); inert in
Plan so it does not fight editing [auto]
- [ ] Device icons in View show a pointer cursor (no grab); grab only in the
Devices mode [auto]
- [ ] In Plan an opening is interactive: grab cursor, hover outline, drag along
walls, click (any tool) opens its properties [auto]
+1 -1
View File
@@ -2772,7 +2772,7 @@ class HouseplanCard extends LitElement {
const top = ((py - view.y) / view.h) * 100;
return html`<div class="oplock ${locked ? 'locked' : known ? 'unlocked' : 'unknown'}"
style="left:${left}%;top:${top}%"
>
@click=${(e: MouseEvent) => { e.stopPropagation(); if (this._mode === 'view') this._openingInfo = o; }}>
<ha-icon icon="${locked ? 'mdi:lock' : known ? 'mdi:lock-open-variant' : 'mdi:lock-question'}"></ha-icon>
</div>`;
})}`;
+7 -6
View File
@@ -261,7 +261,7 @@ export const cardStyles = css`
cursor: grabbing;
}
.oplock {
pointer-events: none; /* a status badge, not a control */
pointer-events: none; /* inert while editing; clickable in View (rule below) */
position: absolute;
transform: translate(-50%, -50%);
width: calc(var(--icon-size, 2.5cqw) * 0.62);
@@ -272,9 +272,11 @@ export const cardStyles = css`
justify-content: center;
background: var(--hp-bg);
border: 1px solid var(--hp-line);
z-index: 1;
}
.stage.mode-view .oplock {
pointer-events: auto;
cursor: pointer;
z-index: 1;
}
.oplock ha-icon {
--mdc-icon-size: calc(var(--icon-size, 2.5cqw) * 0.4);
@@ -544,7 +546,7 @@ export const cardStyles = css`
align-items: center;
justify-content: center;
color: var(--hp-txt);
cursor: grab;
cursor: pointer;
pointer-events: auto;
transition: background 0.15s, border-color 0.15s, opacity 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
@@ -557,9 +559,8 @@ export const cardStyles = css`
justify-content: center;
line-height: 0;
}
.dev:active {
cursor: grabbing;
}
.stage.mode-devices .dev { cursor: grab; }
.stage.mode-devices .dev:active { cursor: grabbing; }
.dev:hover {
background: var(--hp-accent);
color: var(--text-primary-color, #fff);