fix(plan): key room shapes by space and id so a floor switch never repaints a stranger (#742)

The flat room list was a bare map(), so Lit reused room nodes by position.
On a floor switch the previous floor's room node became the new floor's room
and `.room { transition: 0.12s }` drew its fill and stroke in from the old
computed values: one paper-white frame, then two or three frames darker than
the final fill (alpha rises while fill-opacity falls), then the fill. Between
two filled floors the fill bled in from the other floor's colour.

The list is now keyed(space.id, repeat(rooms, (r, i) => r.id || i, ...)), the
shape #534 settled on for openings and markers. The outer key handles the
floor switch, including room ids repeated on two floors (ids are unique only
within a space); the inner key keeps a node bound to its room inside a space,
where inserts, merges, splits and the editor filter shift positions. An
id-less room keys by its numeric index, which never equals a string id. The
transition itself stays: it smooths hover and a real fill change on the same
floor.

Witness: a new section of smoke_space_switch_transitions, before physicalize
(after it the first room changes template branch and the node is recreated
anyway). Red on dev: five transitions on g1, node r1 reused for g1, computed
fill rgba(0, 0, 0, 0) / 1; room nodes swapped by an insert; same-id case
reuses r1. A real custom_fill change still runs a fill transition (catches
`transition: none`). Two mutants: inner key removed, outer key removed.

Issue: #742
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-10-01 10:13:50 +00:00
committed by claude[bot]
parent 88985d2e4c
commit a0f72280ee
7 changed files with 185 additions and 9 deletions
@@ -16,8 +16,8 @@ to prove that the Node witness actually kills it.
| Paint, cascade and layer composition | 26 | The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels produced by Chromium. |
| Pointer geometry and trusted interaction | 45 | The invariant depends on hit testing, pointer capture, touch/keyboard dispatch or live DOM geometry. |
| Responsive DOM layout | 36 | The invariant depends on measured element boxes, responsive breakpoints, native/HA dialog shells or focusable target size. |
| Custom-element and HA browser lifecycle | 86 | The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition. |
| **Total** | **201 / 200** | Growth above the cap fails `mutation-gate --check`. |
| Custom-element and HA browser lifecycle | 88 | The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition. |
| **Total** | **203 / 200** | Growth above the cap fails `mutation-gate --check`. |
## Measured effect
@@ -261,6 +261,8 @@ The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy
- `room-gear-second-touch-keeps-drag`
- `room-settings-click-does-not-open`
- `room-tooltip-off-skips-pointer-modality`
- `rooms-rendered-without-keys`
- `rooms-rendered-without-space-key`
- `same-space-room-change-recenters`
- `space-create-hidden-display-override`
- `stairs-view-pan-opens-target-floor`