fix(space-card): key room shapes by space and id so a config change never repaints a stranger (#745)

The space card drew its rooms with a bare map(), so Lit reused room nodes by
position, and `.room { transition: 0.12s }` (planStyles is part of this card's
styles too) drew a node's fill and stroke in from whichever room held it
before. Two paths change the room set in the same DOM: a new `space` in
setConfig of the same element (the card editor's preview), and a config event
from any device that inserts, removes, reorders or re-zones a room of the
shown space. Filled rooms faded out and back in for ~0.12 s, unfilled ones
briefly darkened in a filled room's place.

The list is now keyed(space.id, repeat(rooms, (r, i) => r.id || i, ...)),
the same shape as the full card after #742: the outer key handles the space
change, the inner one keeps a node bound to its room inside a space. An
id-less room keys by its numeric index, which never equals a string id. The
transition itself stays: it smooths a real fill change on the same room. The
#742 note in plan.styles.ts now names the space card as well.

Witness: a new section of smoke_space_card. The config is delivered by a
server push (__hpTest.setServerConfig), the event the card subscribes to.
Red on dev: node r1 reused for g1 with fill/fill-opacity transitions and a
first-frame fill of rgba(0, 0, 0, 0) / 0; a room inserted first shifts all
four nodes and replays fill transitions. A real custom_fill change still runs
a fill transition on the same node (catches `transition: none`). One mutant:
inner key replaced by map(), guarded by AC2 (checked by hand: red).

Issue: #745
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 12:15:40 +00:00
committed by claude[bot]
parent 707cb18f7e
commit 6cd29f9b2b
7 changed files with 187 additions and 7 deletions
+13
View File
@@ -9931,6 +9931,19 @@ const MUTANT_DEFINITIONS = [
replace: ' return svg`${shape}${outline}`;\n });\n })()}',
}],
},
{
id: 'space-card-rooms-rendered-without-keys',
guard: 'node demo/smoke_space_card.mjs',
because: 'a config event that inserts, removes or re-zones a room shifts the space card\'s '
+ 'room list inside one space: a positional map() hands each room node to its neighbour, '
+ 'so the node no longer belongs to its data-id and the 0.12s .room transition draws the '
+ 'fill in from the neighbour\'s value (#745 AC2)',
patches: [{
file: 'src/space-render.ts',
find: ' const roomShapes = keyed(space.id, repeat(shownRooms, (r, index) => r.id || index, (r) => {',
replace: ' const roomShapes = keyed(space.id, shownRooms.map((r) => {',
}],
},
{
id: 'live-editor-devices-drops-align-guides',
guard: 'node demo/smoke_align_guides.mjs',