`.room { transition: 0.12s }` interpolates a room's colour and its
fill-opacity / stroke-opacity independently, and the visible opacity is their
product. The states without a fill kept their alpha in the colour with the
default opacity 1 (.overlay transparent, .yard rgba 0.14, .outlined rgba
0.06 / 0.55, .picked rgba 0.25), while .styled writes an opaque colour plus
fill-opacity: var(--room-fill-op). On a change between the two on the same
node one half rose while the other fell, and mid-way the room was darker than
at either end. Opening the space settings on a floor with no fill (the dialog
shows "no fill" as its own colour at alpha 0) flashed every room grey for
~0.1 s, 0 -> 0.241 -> 0; cancelling the dialog after a preview, entering and
leaving the plan editor briefly darkened the fill (0.18 -> 0.317 -> 0.06).
Every .room state now writes an opaque colour plus *-opacity, and
transparent only together with a zero opacity. The transition itself,
.styled and the --room-* variables are unchanged; the space card takes the
same styles. The resting paint is the same: the witness records each state's
colour and visible opacity as dev drew them, and screenshots of seven resting
states (View without fill, with fill and borders, plan editor, room picked for
a merge, yard with and without borders, yard in the plan editor) are
pixel-identical to dev outside the plan editor's tool hint, whose text shifts
by a sub-pixel between runs on dev too.
Witness: new demo/smoke_room_fill_transitions.mjs, deterministic. A
MutationObserver pauses the room's transitions at their first frame right
after Lit commits, and the smoke seeks them through 0..120 ms in 15 ms steps.
Red on dev: fill overshoot 0.241 / 0.125 / 0.137 / 0.134 on the four paths,
stroke 0.241 / 0.242 on the first two. A real colour change of the same room
still runs a fill transition (catches `transition: none`).
A card-mod rule that sets only `fill` on an unfilled room now meets
fill-opacity 0; CHANGELOG and STYLING-HOOKS say to set the opacity with the
colour (the values are generated, STYLING-HOOKS §3.3).
Issue: #746
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
A same-route remount that cannot edit yet - hass arrives after the
element is inserted (the demo's own order), or a non-admin waits for the
server's can_write - keeps the editor in _pendingNavMode and enters it
later through _resumePendingNavMode -> _setMode. The draft revival was
wired only into the immediate warm adoption (_requestMode(..., adopt)):
on the pending path the draft was lost, _warmRevivePending stayed up for
the instance's life, _warmSnapshot stopped writing dlg, and the next
remount brought back the predecessor's stale draft.
The adoption tail (draft revival once under a held refit, then the
settled stage as the refit baseline) is shared by both paths: the
sequencing lives in src/warm-mode-adoption.ts, the refit bookkeeping in
the card's _holdWarmRefit/_releaseWarmRefit. The pending mode still enters through _setMode, the transition
authority smoke_nav_persist holds it to; resumeWarmMode then settles the
revival. _setMode ends the passive boot grace and refits the camera to
a header measured before the editor chrome rendered, so a camera the
pending window left untouched is put back and held exactly as an
immediate adoption holds it; a camera that has already moved on (View
refit, the user's pan, another space) is left to the ordinary refit. A
mode that did not commit, an explicit mode command in the pending window
and a route departure settle the revival too: no outcome leaves
_warmRevivePending up. The core file gives back 4 lines.
smoke_warm_dialogs gains section H through the UI: the three late-write
orders keep mode, draft, dirty baseline and a frame-by-frame identical
viewport; the chain carries this instance's draft, not the predecessor's;
a space switch in the pending window eats the draft. 14 checks are red
on dev. The mutant warm-pending-mode-leaves-revive-waiting is guarded by
the smoke; docs/WARM-REMOUNT.md §2 describes the pending editor.
Issue: #756
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
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
The lazy-runtime contract (#353) says a non-terminal failure waits for
the next explicit intent and that there are no background retries. But
_renderBody calls ensure() on every repaint while a surface waits for
the editor or onboarding runtime, and to the loader that call was
indistinguishable from an intent. Surfaces the core opens without the
runtime - the kiosk size dialog after a 3 s hold, the floor import
wizard on an empty plan, a dialog a warm remount revives - therefore
turned one failure into a loop: the loader's own state change, the
toast and its expiry, every hass tick repainted, started a new cycle
and showed a new toast every ~3.5 s. A wall tablet whose old hashed
chunks answer 404 after an integration update sat in that loop forever.
EditorRuntimeLoader.ensure takes an intent: the render calls it as
'reconcile'. A reconcile starts the first cycle a surface needs, but
after a non-terminal failure it returns false without loading until an
explicit ensure() - a tab, an opener, _requestMode, "Add space" - has
started a new cycle. Explicit calls, the terminal fingerprint failure,
ready and an in-flight cycle behave as before, for every loader
instance. The card's render lines stay line-neutral.
smoke_lazy_editor_chunk gains the three surfaces offline through their
real paths (a 3 s touch hold on a kiosk card, an empty plan pushed by
the server, General settings revived by a remount): one cycle, one
notice and an idle loader over 8 s, then the Plan tab and "Add space"
heal. On dev: 4 requests / 3 notices, 6 / 2 and 3 / 2. The loader unit
test pins reconcile versus intent; the mutant
render-reconcile-restarts-editor-runtime-cycle is guarded by the smoke.
Issue: #757
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
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
#718 K7 takes the moon status once per opening of General settings,
outside the draft. A warm remount revives the open dialog on a new card
instance, but `_warmReviveDialog` restored only the draft: the new
instance had no opening of its own, so the "Now: ..." line never came
back.
A revive is an opening too. The `settings` branch now asks for the
status the way `_openSettingsDialog` does - through the lazy editor
runtime (`_openMoonStatus` -> `openMoonStatus`): at once when the
runtime is there (an editor revives after `_requestMode(..., adopt)`
has installed it), after it loads in View; once per revive and only
while that revived dialog is still open. The snapshot of now,
`hass.config` and `sun.sun` is the revive's own, nothing of the dead
instance's opening is carried over, the draft key and the dirty flag do
not change. The View graph gets no static moon-status import; other
dialog kinds never ask for the moon chunk.
demo/smoke_moon_status.mjs gains the revive scenarios - View, the plan
editor, a revive while the chunk is still loading, a space-dialog
revive that must not load the chunk; the first three are red on dev.
test/moon-settings.test.mjs executes the revive as a new opening; the
wiring itself is proven by the smoke, not by reading the monolith as
text (#624). docs/SUN.md and docs/WARM-REMOUNT.md say a revive is an
opening; scripts/smoke-links.mjs links the two new symbols to the smoke.
Issue: #731
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
The owner decided on 30.09 that the moon is not part of the "Follow the Sun"
environment but a switch of its own: with a static background (global or a
space's own) the card showed no moon even with the switch on, and the switch
said nothing about why the moon was missing right now.
With a static background there is no environment, so the moon stands in its
own layer, `.hp-moon-sky`: the first child of `.stage` / `.hp-static-stage`,
the whole scene, no z-index, filter or will-change, under the plan by DOM
order, fading with the #101 View weight. Inside is the very #661 element, so
place, size, art and fades are unchanged, and a background switch moves it to
its new parent in the same render without a flicker. The phase comes from the
same `resolveDayCycle`, computed only while the moon is on and on View; without
`sun.sun` both cards keep their 30 s clock ticker and re-render only when the
phase changes (the environment is still compared by its whole fingerprint).
General settings get a second caption line under the moon switch
(`data-moon-status`): one snapshot per opening, judged by the lazy chunk as if
the switch were on, first reason wins (no home, day, below 3°, under 3 %),
numbers rounded and clamped below the threshold they missed. `moonStatus`
decides "shown" with the same `moonShownAt` as the element. It lives in a
WeakMap beside the draft, so it never makes the dialog dirty; a closed
opening's result is dropped. The dialog loads the chunk through the gate's
loader (`withMoon`), now shared by every caller while a load is in flight, so
there is still one fingerprint check and one retry token.
Bundle (same build, against origin/dev): initial View 300 072 -> 300 248 B gzip
(+176 B, under the 500 B of the spec; budget and ceiling not raised); lazy
editor 238 558 -> 238 991 B (+433 B, the line and English strings); lazy moon
11 385 -> 11 712 B (+327 B, layer CSS and status). `src/moon.ts` stays out of
the initial and the editor graph; bundle-budget now refuses an editor/moon
overlap. Monolith metrics: hostRefs 4 885 -> 4 888 — the three `host.` reads of
`src/editors/moon-status.ts` (hass, `_settingsDialog`, requestUpdate) through
its own three-member interface, not the editor port; the other five metrics
are unchanged. houseplan-editor-runtime.ts grows by two lines (import, call).
Tests: AC9/AC10/AC15 and the sky layer in test/moon.test.mjs (the #661
"static -> nothing" check inverted), AC14 and the opening lifecycle in
test/moon-settings.test.mjs, smokes demo/smoke_moon_static.mjs (AC1-AC6; AC1
and AC3 were red on dev) and demo/smoke_moon_status.mjs (AC11/AC12), AC7 in
smoke_daycycle_layer_budget. Golden: two new scenes
(static-bg-moon-gibbous-white-light, static-bg-moon-crescent-south-dark,
matrix v70), the harness checks the moon's parent by background and waits for
the status line in the General settings frames. Four new mutants; the clock
ticker one is a browser guard (201 at the guideline of 200).
Issue: #718
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
- Vertical oblique projection: the floor matrix is the identity and a height
rises straight up by z·sin 20°, so the on-screen wall height is unchanged
and the cos 20° foreshortening of the plan, decor and anchors is gone.
- Device tiles and lock badges stand on the wall-top plane with one common
shift; the #651 placement search no longer runs in the live scene (its
removal is #714). Room names keep their Flat floor point.
- The 2.5D fit no longer reserves the 48 CSS px nudge budget.
- Switching projection keeps the camera when the previous projection was on
screen: saving the setting, entering an editor from 2.5D and adopting a warm
memo from the other projection re-read only the scalar zoom. A cold 2.5D
start still opens the 2.5D home.
- Opening faces are ordered along the oblique projector (s·y + z).
Witness: demo/smoke_iso_flat_parity.mjs (AC2–AC5, AC11) is red on the old code.
Issue: #713
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
At dawn, dusk and night the environment shows the moon in the top-left
corner of the scene, behind the plan: computed in the card from the home
coordinates and the browser clock (short Meeus series + topocentric
parallax, within 1.4° / 1.8 pp of JPL Horizons), one designer image under
a continuous phase mask with the lit side always on the left (owner
2026-09-29), a feathered terminator, 3°/3 % thresholds and the 2 s fade of
the window rays. Everything but a small gate lives in the lazy
moon-runtime chunk, with its own 30 s ticker. General settings: "Sun"
becomes "Sun and Moon" with one switch, on for new installations
(DEFAULT_CONFIG), off for existing ones.
The initial View graph sat 728 B under its budget: the gate is paid for by
moving fifteen dialog-only strings of General settings into the lazy
settings dictionary (#459) and by one build fingerprint literal instead of
three, so the graph ends 4 B above dev. Golden: two new moon scenes, and
the two General settings help frames show «Sun and Moon»; the WSL artifact
test fixture now models scenes whose first capture awaits acceptance.
Issue: #661
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Owner's decision in #694: icons must not change position with state.
Since #651 the rigid overlay layout sized a device by its value text and
badges, which change with HA state: a light toggling on changed its width
from 37.2 to 26.7 CSS px and re-laid out all 62 overlays of the dense
scene (~385 of 495 ms of stateUpdate; v1.77.0 had 208 ms).
- iso-scene-render.ts: the layout sees the state-free tile (icon at its
configured size, no value text, badge or supplemental metrics). An
HA-only change keeps the layout and refreshes only the visual extent
that scene bounds read, without a collision search.
- iso-overlays.ts: the rigid-group search skips candidates that already
lose to the fallback on room, then wall violations (lexicographic
bound). The result is unchanged — identical placement hash on the dense
scene — at about 35 % less work.
- docs/ISOMETRIC.md, changelogs; test #711; mutant
iso-device-layout-follows-state-again (written, not run — #709).
Local isometric-stage3-dense-v1, 3 samples: stateUpdate 522 → 89 ms
(v1.77.0: 208), modelReady 3665 → 3129, switchCycle 5544 → 4700.
Issue: #711
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
The Plan editor rule `.hp-stair.input-enabled .hp-stair-hit { cursor: move }`
also matched the View layer, which sets input-enabled only to receive
clicks. The hit area sits over the outline, so the link's pointer on the
group was never visible and every stair in View showed a drag it cannot do.
- src/stairs-view.ts: View stairs carry `hp-stair-view`.
- plan.styles.ts: `move` applies only without it; in View the hit area
keeps the group's cursor — pointer on a link, the stage's otherwise.
- demo/smoke_stairs.mjs: computed cursors in View (link, no target) and
in the Plan editor; the two View checks are red on the old code.
- test/stairs.test.mjs: the cascade without Chromium; mutant
view-stair-cursor-move-again.
- docs/STAIRS.md, changelogs.
Issue: #693
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
With the day/night background the plan went blurry after zooming from 100 %
(sharp when the page was opened at 800 %) and navigating a strongly zoomed
plan flashed the page white. Both came from #582's composition, not from the
wall hatch that #685 replaced:
- `.stage.daycycle.hp-safe-daycycle-outline .plan-svg` promoted the scene
with `will-change: transform`; Chromium freezes the raster scale of such a
layer, so the 100 % raster was shown stretched. The explicit layer #582
needs is now `will-change: opacity` (re-rasters at the current scale).
- The filtered outline had `overflow: visible` and a gesture exposed every
scene (#544) without bound, so the promoted layers grew with zoom squared
(CDP LayerTree, ~460 %: plan-svg 15.9x, outline 13.2x the stage; 39x after
navigating at 800 %). The full card clips its outline to its box and marks
it data-hp-live-overflow="clip" (never exposed); the live viewport bounds
every other exposure with an inline clip-path: inset(-25%) that leaves
with it, so idle DOM stays byte-identical (#531).
Owner-verified in Chrome 152 (built-in browser, DPR 2): sharp after 100 ->
800 %, no white flashes after reloading at 800 %.
Owner decision: #685's analytic gradient is reverted (13af1d5e), the single
<pattern> is back at every zoom; its close-up golden scenes stay and check
the pattern, its terminal-frame smoke checks the pattern.
Witnesses: demo/smoke_daycycle_zoom_layers.mjs (800 % x DPR 2: layers vs
stage, the hint, reload path, button/wheel/pinch); #582/#532 smokes now pin
the opacity hint; test/live-viewport.test.mjs (bounded exposure, clipped
scene); test/daycycle-layers.test.mjs (cascade). Four Node-guarded mutants.
Issue: #689
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
The Plan editor hid every device marker (display: none), so walls,
openings and stairs were placed against devices from memory. It now
shows them exactly as the Background editor does (#362): the same
markers as View, at the Background editor's effective opacity, fully
pointer-inert. The fade is filter: opacity(0.35) on each marker, not
on .devlayer, because room labels and room settings buttons share that
layer and stay opaque and interactive; filter multiplies the marker's
own opacity (.unavail -> 0.35 x 0.35), as the Background layer does.
Marker handlers already fail closed outside View/Devices.
demo/smoke_plan_device_landmarks.mjs proves AC1/AC2: same markers as
View, per-marker alpha equal to Background, core and capsule fall
through, a real click on a marker reaches the Walls tool and a room
settings button below, cursor and target unchanged, handlers inert, no
tabindex. Three mutants guard hiding, opacity override and the pointer
boundary. UX-MODES, DECOR-EDITOR, USER-GUIDE.ru mode table, changelogs.
Issue: #687
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Решение владельца 2026-09-28: «убрать рамку фокуса полностью». Навигационная
лестница View — фокусируемый `<g role="link" tabindex="0">` (#676), а своих
стилей фокуса у `.hp-stair` не было, поэтому браузер рисовал чёрно-белое
кольцо по прямоугольнику символа. Теперь `.hp-stair:focus` и
`:focus-visible` — `outline: none`: ни кольца браузера, ни замены. Лестница
остаётся в порядке Tab, Enter/Пробел по-прежнему переходят; hover и
выделение в редакторе не тронуты.
Свидетель — `demo/smoke_stairs.mjs`: снимок области лестницы с запасом на
кольцо без фокуса и с клавиатурным фокусом (`:focus-visible`) обязан совпасть
байт в байт; Enter на сфокусированной лестнице переводит на целевой этаж.
Без правила смок красный (`focusedStairPaintsNoFrame`). STAIRS.md и оба
changelog.
Issue: #686
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Лестница рисуется протяжкой, как прямоугольник или эллипс декора: длинная
ось протяжки — ось подъёма, стрелка смотрит от нажатия к отпусканию, клик
ставит размер по умолчанию. Выделенная лестница получает рамку декора в
верхнем оверлее карточки — выше тел стен, с узлами постоянного экранного
размера и курсорами по мировому направлению узла; винтовая — четыре узла на
касательных. Ресайз идёт от противоположной стороны, не зеркалит и не
поворачивает; магнит работает по сторонам: в пределах 6 клеток и 5° сторона
ложится на видимую грань стены, при перемещении лестница доворачивается не
больше чем на 5°, при ресайзе и черновике — никогда. Грани перегородок и
колонн получают внешнюю сторону по обходу многоугольника.
Клик, синтезированный после жеста, глушится: под «Лестницей» больше не
появляется копия, под «Выбором» не пропадает выделение. Диалог свойств
считает в сантиметрах лестницы (30…10000), а не в конвертере толщины стены;
нетронутое поле хранит число бит в бит, «Сохранить» без изменений ничего не
пишет. В Просмотре наведение на лестницу с корректной целью показывает
«Переход на этаж …» — ровно при том же условии, что делает её ссылкой.
Чистые функции слоя — в stairs-editor-model.ts с юнитами; смок лестниц
покрывает протяжку, курсоры, рамку над стенами, клик после жеста, диалог и
пять состояний подсказки; реестр мутантов получил шесть свидетелей.
Issue: #676
User-Visible: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
Прямые и винтовые лестницы получили отдельную модель, инструменты редактора, безопасную межэтажную навигацию, вычитание из чистой площади и плоское отображение в 2.5D.
Issue: #663
User-Visible: yes
The raised room label in 2.5D View sized its own box to the 44 px touch
minimum (min-height + justify-content: center). The name therefore sat
centred in 44 px while the metrics row, placed absolutely from the label
bottom, hung (44 - name height) / 2 below it: 18.3 px at a 9 px name and
6.4 px at a 39 px one on the demo stand, against a constant 0.1 name
height on the flat plan.
The 44 x 44 px floor moves into an invisible ::before (the door-lock
pattern), z-index -1 inside the label stacking context; the label box is
sized by its text again.
demo/smoke_iso_room_label_metrics.mjs measures gap / name height at two
cameras (font x4) in Flat and 2.5D, and the 44 px floor and area-link hit
targets on labels no raised device covers. Mutant
iso-room-label-44-box-centres-name.
Screenshots: 11 frames pixel-identical, fingerprint only.
Issue: #665
User-Visible: yes
The accent fill of the active Plan/Devices/Decor tab is now a pseudo-element
stretched over the 2 px group gap and the fixed 24 px X slot of #660, so
the X sits inside the highlighted zone without any box moving: the tab,
the slot, the X and the header keep their geometry. The X takes the tab
text colour, and the keyboard focus ring moves to the same pseudo-element
so it outlines the whole zone instead of cutting it at the tab edge.
.modes becomes its own stacking context so the z-index:-1 fill paints above
the group background. On a phone the mode tabs, and the fill, stay hidden.
smoke_toolbar_stable_width: painted-pixel probes for the gap, the slot edge
and top, and the first pixel after the slot, in every editor at every width
above 480 px; the X colour equals the tab colour. The existing geometry
and hit checks are unchanged and green. Mutant
toolbar-active-highlight-stops-at-tab.
Screenshots: 11 frames pixel-identical, fingerprint only.
Issue: #666
User-Visible: yes
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
Переход снова интерполирует общий бюджет сцены и панели инструментов вместо измерения скрытого исходного кадра. Документационные кадры попиксельно не изменились.
Issue: #660
User-Visible: yes
- settings.volumetric_view (General settings › Display, third switch) replaces
the alpha entry, the header projection toggle and the phone-menu item; one
rule for card, sidebar page and kiosk; editors stay Flat; backend accepts
only a boolean, support package copies it.
- Raised tiles in 2.5D View: no ring, rounded body min(0.275 D, 0.3 h), edge
0.1 D with the lab filters, ×1.12 size also in layout/collision, lift
0.075 D, one floor-shadow layer under all markers (theme × floor table),
frames hugging tile and edge (Alert > Focus > Selected > Hover), forced
colours without edge and shadow (src/iso-tiles.ts, styles/iso-tiles).
- Soft sun wash instead of projected Flat wedges (src/iso-sun.ts): same gates
and windows as sun_rays, length from elevation, parallelogram along the sun,
tone and streaks by floor lightness, sill line.
- Walls take the user's wall colour (top opaque, sides × .77/.68/.60); theme
rules for walls/openings/labels removed; furniture uses the Flat stroke rule.
- Smokes smoke_iso_tiles/iso_sun/iso_theme_walls/volumetric_setting, 16 new
mutants, acceptance scenes STAGE6_ACCEPTANCE_SCENARIOS (golden with their
Linux CI baselines), ACCEPTANCE.md side by side.
Issue: #649
User-Visible: yes
На окне не шире 480 px шапка карточки и страницы бокового меню занимает одну
строку ≤ 56 px: вкладки пространств (без шестерёнок и «+», прокрутка вбок,
текущая вкладка докручивается в видимую область), масштаб, одна шестерёнка
«Действия и настройки» и у админа слот крестика #647. Заголовок скрыт.
Меню шестерёнки (src/header-menu.ts): редакторы, настройки и добавление
пространства, общие настройки, PDF, помощь, сводная панель и объёмный вид
(alpha) — в фиксированном порядке и с теми же условиями и действиями, что у
прежних кнопок. Пункт закрывает меню; Escape возвращает фокус на шестерёнку;
касание вне меню поглощается прозрачной подложкой. На > 480 px шапка прежняя,
в киоске меню нет.
- три кнопки шапки вынесены в renderHeaderActions — шапка карточки не растёт;
- сводная панель отдаёт свои пункты методом menuItems();
- i18n: title.header_menu (en/ru/de/fr);
- потолок initial View 291 000 → 292 600 (замер 291 635), база bundleBytes;
- смок smoke_mobile_view_header, юнит header-menu, 10 мутантов; смоки
support_feedback, gear_tabs, modes, toolbar_stable_width переведены на
меню на ≤ 480 px; подпись вкладки в span.tabtitle без pointer-events;
- USER-GUIDE.ru, UX-MODES, TOUCH-SUPPORT, STYLING-HOOKS, data-hp-contract,
оба changelog.
Issue: #616
User-Visible: yes
Счётчик «N устр.» и ключ count.devices удалены вместе с правилом
`.head .count` (скрывало счётчик на ≤1100 px). Крестик закрытия редактора
вынесен из активной кнопки режима в отдельный слот `.editor-close-slot`
сразу после группы кнопок режимов: 24 × 24 px (--hp-editor-close-size),
одинаковый в редакторе и вне его; вне редактора слот пуст, aria-hidden,
без фокуса и pointer-events. Кликабельная зона × — весь слот (≥24×24, #195),
глиф 13 px; × виден на любой ширине, где видны кнопки режимов.
Смок smoke_toolbar_stable_width (1400/1000/768/390 px, en/ru): ширина
шапки и отступы кнопок стабильны во всех переходах, × виден, попадание
в край зоны закрывает редактор, нет нового overflow. smoke_editor_tabs,
smoke_edge_cases, smoke_hidden_flag переведены на слот/список устройств.
6 мутантов. docs: UX-MODES.md, STYLING-HOOKS.md, data-hp-contract.json.
Issue: #647
User-Visible: yes
Транзитная геометрия Resize теперь только вычисляет центр для текущего кадра и не удаляет сессионную позицию. Окончательная геометрия по-прежнему очищает точку, если она действительно вышла за комнату.
Issue: #645
User-Visible: yes
Кнопка сохраняет положение в пределах комнаты только на время сессии редактора,
корректно отменяется при pinch/cancel и учитывается подписями Resize.
Issue: #645
User-Visible: yes
General settings colour tiles (room fills and glow) paint their colour at
its opacity over the checkerboard again, so "No light sources" at 0 % no
longer looks like solid grey. The label ink is chosen from the visible
colour (hex mixed with the checkerboard by alpha). Colour plates
(flat-swatch without cover-swatch) stay solid — now witnessed by the
- hp-color-opacity: one `_solidSwatch` condition drives background and
opacity; cover-swatch keeps the checkerboard
- editors/color-tile-ink.ts: pure isLightHex/colorTileInk, unit-tested
- smoke_dialog_polish_605: tileAlpha, defaultsDiffer, noneInkDark,
liveAlpha, platesFlat, plateSolid; oneSurface drops the opaque check
- mutants M-615-tile, M-615-plate, M-615-label
- USER-GUIDE (ru/en): "Colour tile" row; both changelogs
Issue: #615
User-Visible: yes
На вкладках «На плане» и «Скрытые» — флажки строк, «Выбрать все (N)» по
всему отфильтрованному набору (включая строки за «Показать ещё»), панель
«Выбрано: K · Скрыть/Показать выбранные (K) · Снять выбор» в отдельном
контейнере .device-inbox-batch вне .device-inbox-filters.
- src/device-inbox.ts: чистые inboxVisibilityAllowed (один источник для
canHide/canShow и выбора), selectableInboxRows, effectiveInboxSelection,
applyInboxVisibility — свёртка одиночных действий; одиночное = пакет из
одной строки.
- src/device-inbox-batch.ts (новый, lazy editor): выбор, панель, строка-
флажок и writeInboxVisibility — одна запись houseplan/config/set с
expected_rev на пакет, busy='__batch__' (inert), откат маркеров при отказе,
выбор сохраняется и сужается. Одиночный _setInboxHidden рантайма — тонкая
обёртка над той же записью; рантайм стал короче базы на 14 строк.
- сброс выбора при смене вкладки (клик и стрелка), поиска и «Только новые».
- i18n en/ru/de/fr: 9 ключей по ТЗ §8.
- смок demo/smoke_device_inbox_batch.mjs (AC1–AC8, AC10), юниты
test/device-inbox.test.mjs (AC2, AC4), 4 мутанта в реестре.
- docs: USER-GUIDE ru/en, FILTERING.md, оба changelog.
- monolith-baseline: hostRefs 4948→4959, bundleBytes 2500387→2505569 —
новая фича (состояние выбора и запись идут через порт хоста).
- bundle-budget: LAZY_EDITOR_GZIP_CEILING 245 400 → 246 600 (замер 245 610,
+1 190 Б gzip от базы 244 420); initial View 289 449 → 289 765 (+316 Б,
строки en-словаря), в пределах потолка.
Golden device-inbox-* изменятся ожидаемо — пересъёмка предрелизным гейтом
на Linux CI (§11.4).
Issue: #618
User-Visible: yes