Merge inert-openings: View-mode cursors and inert openings v1.28.1

This commit is contained in:
Matysh
2026-07-22 10:44:23 +03:00
11 changed files with 153 additions and 73 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
FILES_URL = "/houseplan_files/files"
FILES_DIR = "houseplan/files"
CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.28.0"
VERSION = "1.28.1"
DEFAULT_CONFIG: dict = {
"spaces": [],
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -16,5 +16,5 @@
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [],
"single_config_entry": true,
"version": "1.28.0"
"version": "1.28.1"
}
+47
View File
@@ -0,0 +1,47 @@
import { launch } from './serve.mjs';
const { page, browser } = await launch();
const res = await page.evaluate(async () => {
const out = {};
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
// добавить открывание на f1
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== 'f1' ? s : ({
...s, openings: [{ id: 'op1', type: 'door', rx: 550, ry: 200, angle: 90, len_cm: 90 }] })) };
c.requestUpdate(); await c.updateComplete;
const hit = sr().querySelector('.op-hit');
out.hasOpening = !!hit;
const cs = hit ? getComputedStyle(hit) : null;
out.viewInert = cs ? cs.pointerEvents === 'none' && cs.cursor === 'default' : null;
out.lockBadgeInert = (() => { const l = sr().querySelector('.oplock'); return l ? getComputedStyle(l).pointerEvents === 'none' : 'no-badge'; })();
// 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 };
c._opClick({ stopPropagation(){} }, op);
out.planClickEdits = !!c._openingDialog;
c._openingDialog = null; c._setMode('view');
return out;
});
console.log(JSON.stringify(res, null, 1));
await browser.close();
File diff suppressed because one or more lines are too long
+20 -15
View File
File diff suppressed because one or more lines are too long
+11
View File
@@ -1,5 +1,16 @@
# Changelog
## 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.**
The marker dialog's room list includes area-less rooms (marked "no area,
+10 -2
View File
@@ -40,8 +40,16 @@ 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
- [ ] Opening: single click in View shows the info card; in Plan a click edits it;
double-click no longer does anything special [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]
## Onboarding ★
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "houseplan-card",
"version": "1.28.0",
"version": "1.28.1",
"description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT",
"type": "module",
+4 -10
View File
@@ -32,7 +32,7 @@ import './space-card';
import { cardStyles } from './styles';
import { langOf, t, type I18nKey } from './i18n';
const CARD_VERSION = '1.28.0';
const CARD_VERSION = '1.28.1';
const LS_KEY = 'houseplan_card_layout_v1';
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
const LS_ZOOM = 'houseplan_card_zoom_v1';
@@ -1261,15 +1261,9 @@ class HouseplanCard extends LitElement {
private _opClick(ev: MouseEvent, o: OpeningCfg & { rx: number; ry: number; rlen: number }): void {
ev.stopPropagation();
if (this._opDrag?.moved) return; // that click was the tail of a drag
if (this._mode === 'view') {
// view: an opening is a status object — show the door/lock info card
this._openingInfo = o;
return;
}
if (this._mode === 'plan' && this._tool !== 'opening') {
// plan: any click on an opening edits it (the Opening tool also does)
this._editOpening(o);
}
// openings are inert outside Plan mode (owner's decision: View must not
// interact with them at all); in Plan any click on an opening edits it
if (this._mode === 'plan') this._editOpening(o);
}
private _saveOpening(): void {
+18 -13
View File
@@ -242,22 +242,26 @@ export const cardStyles = css`
transition: opacity 0.15s;
pointer-events: none;
}
g.opening:hover .op-outline {
.stage.markup g.opening:hover .op-outline {
opacity: 0.9;
}
/* openings are pure status graphics outside Plan mode: no cursor, no hover,
no hit target View must not interact with them at all */
.op-hit {
fill: transparent;
cursor: grab;
pointer-events: auto;
touch-action: none; /* drags, not scrolls, on touch */
}
.op-hit:active {
cursor: grabbing;
pointer-events: none;
cursor: default;
}
.stage.markup .op-hit {
pointer-events: none; /* markup clicks go to the stage tools */
pointer-events: auto;
cursor: grab;
touch-action: none; /* drags, not scrolls, on touch */
}
.stage.markup .op-hit:active {
cursor: grabbing;
}
.oplock {
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);
@@ -268,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);
@@ -540,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);
@@ -553,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);