From 40b73cc5999be6fc8f06eb5cac63f74e6c41c4f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 13:02:51 +0300 Subject: [PATCH] fix(space-card): nothing in the static schematic is hit-testable (#664) The stage is pointer-events:none, but that is not an inherited ban: the shared marker styles opt the 44 px floor (.dev::before) and the painted capsule (.device-shell-frame) back in for the interactive plan (#564). The static card imports them whole, so since v1.76.0-beta.2 it showed a hand cursor over every marker and swallowed clicks that do nothing. The card now closes every descendant opt-in (.hp-static-stage *, ::before, ::after). smoke_space_card asserts the browser hit test itself (elementFromPoint over the marker and a 9x9 grid across the stage) and the cursor, not the stage computed style that stayed green through the regression. Mutant static-card-descendants-hit-testable. Screenshots: 11 frames pixel-identical, fingerprint only (npm run docs:accept -- --identical). Issue: #664 User-Visible: yes --- demo/smoke_space_card.mjs | 39 ++++++++++++++++++++++++++++++++++- docs/ARCHITECTURE.md | 9 +++++++- docs/CHANGELOG.md | 7 +++++++ docs/CHANGELOG.ru.md | 7 +++++++ docs/images/screenshots.json | 24 ++++++++++----------- scripts/mutation-registry.mjs | 11 ++++++++++ src/space-card.ts | 12 +++++++++++ 7 files changed, 95 insertions(+), 14 deletions(-) diff --git a/demo/smoke_space_card.mjs b/demo/smoke_space_card.mjs index 90530e79..5365bfc7 100644 --- a/demo/smoke_space_card.mjs +++ b/demo/smoke_space_card.mjs @@ -138,6 +138,31 @@ const res = await page.evaluate(async () => { .map((value) => value.trim().toLowerCase()).filter(Boolean); const btn = card.renderRoot.querySelector('.hp-static-btn'); + // #664: the stage is pointer-events:none, but that is not an inherited ban — + // a descendant that opts back in (the shared 44 px floor and painted capsule + // of #564) becomes a hit target again, shows a pointer cursor and swallows + // clicks that do nothing. Ask the browser's hit test itself, not the stage's + // computed style: nothing inside the schematic may be under the pointer. + litMarker?.scrollIntoView({ block: 'center', inline: 'center' }); + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); + const hitAt = (x, y) => card.renderRoot.elementFromPoint(x, y); + const insideStage = (el) => !!el && !!stage && (el === stage || stage.contains(el)); + const markerBox = litMarker?.getBoundingClientRect(); + const markerHit = markerBox + ? hitAt(markerBox.left + markerBox.width / 2, markerBox.top + markerBox.height / 2) : null; + const stageBox = stage?.getBoundingClientRect(); + const gridHits = []; + if (stageBox) { + for (let i = 1; i < 10; i += 1) { + for (let j = 1; j < 10; j += 1) { + const x = stageBox.left + (stageBox.width * i) / 10; + const y = stageBox.top + (stageBox.height * j) / 10; + if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight) continue; + gridHits.push(hitAt(x, y)); + } + } + } + // deep-link: clicking the button pushes #space= let pushed = null; const orig = history.pushState; @@ -152,6 +177,14 @@ const res = await page.evaluate(async () => { return { stagePointerEvents: pe, + markerProbe: markerHit ? { + insideStage: insideStage(markerHit), + element: `${markerHit.tagName.toLowerCase()}.${[...markerHit.classList].join('.')}`, + cursor: getComputedStyle(markerHit).cursor, + } : null, + stageGridProbes: gridHits.length, + stageGridHitsInside: gridHits.filter(insideStage).length, + stageGridPointerCursors: gridHits.filter((el) => el && getComputedStyle(el).cursor === 'pointer').length, markers, litMarkerOn: !!litMarker?.classList.contains('on'), sharedFacePresent: !!litMarker?.querySelector('ha-icon'), @@ -221,6 +254,10 @@ const res = await page.evaluate(async () => { await browser.close(); const ok = res.stagePointerEvents === 'none' && + // #664 AC1/AC2: the browser's hit test finds nothing inside the schematic — + // not the marker, not the paper — and no probe shows a pointer cursor. + res.markerProbe && !res.markerProbe.insideStage && res.markerProbe.cursor !== 'pointer' && + res.stageGridProbes >= 40 && res.stageGridHitsInside === 0 && res.stageGridPointerCursors === 0 && res.markers > 0 && res.litMarkerOn && res.sharedFacePresent && @@ -263,4 +300,4 @@ console.log(JSON.stringify(res)); // именно остановить: иначе строка успеха печатается после «FAILED». if (await reportPageErrors()) process.exit(1); if (!ok) { console.error('FAIL space-card smoke'); process.exit(1); } -console.log('OK space-card: live shared marker face, pointer-events:none, deep-link button, error card'); +console.log('OK space-card: live shared marker face, pointer-events:none, nothing hit-testable in the schematic (#664), deep-link button, error card'); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index df7accdf..4f7efa3a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1665,7 +1665,14 @@ Shared, framework-light modules keep the two views from diverging: clearing the visible static snapshot. **Static contract:** the schematic layer (`.hp-static-stage`) is `pointer-events:none`; the -footer button lives outside it and stays clickable. +footer button lives outside it and stays clickable. `pointer-events` is not an +inherited ban, so the card also closes every descendant opt-in +(`.hp-static-stage *, *::before, *::after`): the shared marker styles re-enable +the 44 px floor and painted capsule for the interactive plan (#564), and +without that rule the schematic showed a pointer cursor over markers and +swallowed clicks (#664). `demo/smoke_space_card.mjs` asserts it with the +browser's own hit test (`elementFromPoint` over the marker and a grid across +the stage), not with the stage's computed style. **Static frame contract:** `fit` is normalised to `content | house`, with every missing, empty or unknown value resolving to `content`. The default calls the diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1f9f5fde..a26781b1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +- The static space card (`houseplan-space-card`) no longer shows a hand cursor + over device markers and no longer swallows clicks there: the whole schematic + is inert again, as documented, with an ordinary cursor everywhere and the + footer button as the only clickable control. The regression came with the + overlapping-marker fix in v1.76.0-beta.2 + ([#664](https://github.com/Matysh/houseplan-card/issues/664)). + ## v1.78.0-beta.4 — 2026-09-26 - The main toolbar is steadier and more compact: its editor close **×** now diff --git a/docs/CHANGELOG.ru.md b/docs/CHANGELOG.ru.md index 160b3bc4..fbbf39e7 100755 --- a/docs/CHANGELOG.ru.md +++ b/docs/CHANGELOG.ru.md @@ -8,6 +8,13 @@ ## Не выпущено +- Статическая карточка пространства (`houseplan-space-card`) больше не + показывает курсор-«руку» над значками устройств и не перехватывает там + клики: вся схема снова инертна, как и описано, — обычный курсор везде, а + единственный кликабельный элемент — кнопка в подвале. Регрессия пришла с + исправлением перекрытий маркеров в v1.76.0-beta.2 + ([#664](https://github.com/Matysh/houseplan-card/issues/664)). + ## v1.78.0-beta.4 — 2026-09-26 - Основная панель стала компактнее и перестала дёргаться: крестик **×** diff --git a/docs/images/screenshots.json b/docs/images/screenshots.json index 17a4b36e..585e24e6 100644 --- a/docs/images/screenshots.json +++ b/docs/images/screenshots.json @@ -3,7 +3,7 @@ "fixture": "synthetic-only", "chromium": "151.0.7922.34", "oxipng": null, - "sourceFingerprint": "490a776a02e7150ba38ebca89beb5046917eaaf908a3ed9e81c9006fdb1305cf", + "sourceFingerprint": "121a34d5ed5269ca95a59af2f9b9f4b797a1c324dc830d20ced0140dead40b4c", "captureScriptSha256": "e2d14d89de793011615662af7c83c433768d4761f6cec7f0b3860c797494299e", "command": "npm run build && node demo/docs/capture.mjs", "scenarios": { @@ -15,7 +15,7 @@ }, "theme": "dark", "language": "en", - "sourceSha256": "490a776a02e7150ba38ebca89beb5046917eaaf908a3ed9e81c9006fdb1305cf", + "sourceSha256": "121a34d5ed5269ca95a59af2f9b9f4b797a1c324dc830d20ced0140dead40b4c", "imageSha256": "1bde1bff8d4ab5f197320b54ef57186b3c07cf2121d5414ada63ce357826bfa8" }, "view-touch": { @@ -26,7 +26,7 @@ }, "theme": "dark", "language": "en", - "sourceSha256": "490a776a02e7150ba38ebca89beb5046917eaaf908a3ed9e81c9006fdb1305cf", + "sourceSha256": "121a34d5ed5269ca95a59af2f9b9f4b797a1c324dc830d20ced0140dead40b4c", "imageSha256": "f9dd5b54f86b814c00bc84149af84485f052b103cdcc9263b5c50bf5d32c69d6" }, "space-create": { @@ -37,7 +37,7 @@ }, "theme": "dark", "language": "en", - "sourceSha256": "490a776a02e7150ba38ebca89beb5046917eaaf908a3ed9e81c9006fdb1305cf", + "sourceSha256": "121a34d5ed5269ca95a59af2f9b9f4b797a1c324dc830d20ced0140dead40b4c", "imageSha256": "aab4811bba3afa6cc849cb9d3989d126b8c327b69a2f8f853e55fc077e0e9a1b" }, "room-contour-close": { @@ -48,7 +48,7 @@ }, "theme": "dark", "language": "en", - "sourceSha256": "490a776a02e7150ba38ebca89beb5046917eaaf908a3ed9e81c9006fdb1305cf", + "sourceSha256": "121a34d5ed5269ca95a59af2f9b9f4b797a1c324dc830d20ced0140dead40b4c", "imageSha256": "e106ff9db6c96adb08beba12cc299a88ac5fa47c079be316f185e5657bc656f0" }, "plan-context-tray": { @@ -59,7 +59,7 @@ }, "theme": "dark", "language": "en", - "sourceSha256": "490a776a02e7150ba38ebca89beb5046917eaaf908a3ed9e81c9006fdb1305cf", + "sourceSha256": "121a34d5ed5269ca95a59af2f9b9f4b797a1c324dc830d20ced0140dead40b4c", "imageSha256": "9b0db923fa7c8aff783b5c7050ee5b410b1b04df81a9e48bbc8111a7d0d47e76" }, "device-editor": { @@ -70,7 +70,7 @@ }, "theme": "dark", "language": "en", - "sourceSha256": "490a776a02e7150ba38ebca89beb5046917eaaf908a3ed9e81c9006fdb1305cf", + "sourceSha256": "121a34d5ed5269ca95a59af2f9b9f4b797a1c324dc830d20ced0140dead40b4c", "imageSha256": "198c084c0a45aa3afdf79181b80d8bb0347c95f3248b4fac9e05d69eb8a35f8f" }, "device-display-preview": { @@ -81,7 +81,7 @@ }, "theme": "dark", "language": "en", - "sourceSha256": "490a776a02e7150ba38ebca89beb5046917eaaf908a3ed9e81c9006fdb1305cf", + "sourceSha256": "121a34d5ed5269ca95a59af2f9b9f4b797a1c324dc830d20ced0140dead40b4c", "imageSha256": "e9b88d65dca74133646aa849d47bc09ad85eb8b16a168c4df6930e8f5856b5f5" }, "background-editor": { @@ -92,7 +92,7 @@ }, "theme": "dark", "language": "en", - "sourceSha256": "490a776a02e7150ba38ebca89beb5046917eaaf908a3ed9e81c9006fdb1305cf", + "sourceSha256": "121a34d5ed5269ca95a59af2f9b9f4b797a1c324dc830d20ced0140dead40b4c", "imageSha256": "44beadd61365ac1730c1efde846e528bfe92d8935fd167f1b6bf3bbad6af8f33" }, "room-card": { @@ -103,7 +103,7 @@ }, "theme": "dark", "language": "en", - "sourceSha256": "490a776a02e7150ba38ebca89beb5046917eaaf908a3ed9e81c9006fdb1305cf", + "sourceSha256": "121a34d5ed5269ca95a59af2f9b9f4b797a1c324dc830d20ced0140dead40b4c", "imageSha256": "d47988b03e6a078508b27b5612b6eafe8ecc33c891b589ddda1cc7c71f43b7c8" }, "device-info": { @@ -114,7 +114,7 @@ }, "theme": "dark", "language": "en", - "sourceSha256": "490a776a02e7150ba38ebca89beb5046917eaaf908a3ed9e81c9006fdb1305cf", + "sourceSha256": "121a34d5ed5269ca95a59af2f9b9f4b797a1c324dc830d20ced0140dead40b4c", "imageSha256": "4ec8c6df671742b90ca55222917076c47c5035a6df0587282e60873c0ce5f64e" }, "pdf-export": { @@ -125,7 +125,7 @@ }, "theme": "light", "language": "en", - "sourceSha256": "490a776a02e7150ba38ebca89beb5046917eaaf908a3ed9e81c9006fdb1305cf", + "sourceSha256": "121a34d5ed5269ca95a59af2f9b9f4b797a1c324dc830d20ced0140dead40b4c", "imageSha256": "ad3d735135b180ce015c9cda07fdcec08d7c8707abe0665734ef7b450a8a7093" } }, diff --git a/scripts/mutation-registry.mjs b/scripts/mutation-registry.mjs index 3be6a553..b2d1bbb9 100644 --- a/scripts/mutation-registry.mjs +++ b/scripts/mutation-registry.mjs @@ -3391,6 +3391,17 @@ const MUTANT_DEFINITIONS = [ replace: "", }], }, + { + id: 'static-card-descendants-hit-testable', + guard: 'node demo/smoke_space_card.mjs', + because: '#664: pointer-events:none on the stage does not stop a descendant that opts back in; ' + + 'without the subtree rule the shared #564 capsule shows a pointer cursor and swallows clicks', + patches: [{ + file: 'src/space-card.ts', + find: " .hp-static-stage *,\n .hp-static-stage *::before,\n .hp-static-stage *::after {\n pointer-events: none;\n }\n", + replace: "", + }], + }, { id: 'fit-house-hidden-walls-vote', guard: 'node demo/smoke_space_card.mjs', diff --git a/src/space-card.ts b/src/space-card.ts index 5941d1f5..b775885d 100644 --- a/src/space-card.ts +++ b/src/space-card.ts @@ -949,6 +949,18 @@ class HouseplanSpaceCard extends LitElement { pointer-events: none; /* kill ALL interaction on the schematic (§4) */ background: var(--ha-card-background, var(--card-background-color, #111)); } + /* #664: pointer-events is not an inherited ban — a descendant with its + own 'pointer-events: auto' becomes a hit target again. The shared + marker styles opt the 44 px floor (.dev::before) and the painted + capsule (.device-shell-frame) back in for the interactive plan + (#564), and this card imports them whole: the schematic then showed a + pointer cursor over every marker and swallowed clicks that do nothing. + Close every opt-in for the whole subtree, pseudo-elements included. */ + .hp-static-stage *, + .hp-static-stage *::before, + .hp-static-stage *::after { + pointer-events: none; + } .hp-static-stage > svg { position: absolute; inset: 0;