v1.55.3
Validate / smoke (push) Failing after 12m47s
Validate / hacs (push) Failing after 9s
Validate / hassfest (push) Failing after 8s
Validate / frontend (push) Successful in 2m24s
Validate / backend (push) Failing after 7m57s

This commit is contained in:
Matysh
2026-08-02 15:01:37 +03:00
14 changed files with 355 additions and 30 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ PLAN_ORPHAN_TTL_S = 3600
SCHEDULED_GRACE_S = 30 * 24 * 3600
FILES_DIR = "houseplan/files"
CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.55.2"
VERSION = "1.55.3"
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.55.2"
"version": "1.55.3"
}
+2 -1
View File
@@ -13,7 +13,8 @@ const st = () => page.evaluate(() => {
editBtns: sr.querySelectorAll('.head .btn:not(.zb)').length,
gears: sr.querySelectorAll('.tabedit').length,
markupBar: !!sr.querySelector('.editbar'),
stageClass: sr.querySelector('.stage').className,
// hpsettle is the transient post-boot grace (AUD-1552-02), not a mode class
stageClass: sr.querySelector('.stage').className.replace(/ ?\bhpsettle\b/, ''),
};
});
// 1) старт: view, чистая шапка
+180
View File
@@ -0,0 +1,180 @@
// AUD-1552-01 + AUD-1552-02: adversarial lifecycle regressions for the
// first-open boot veil (HP-1552). Both scenarios FAIL on v1.55.2.
//
// A. disconnect/reconnect while booting (Lovelace rebuilds its DOM, a view
// switch remounts the card). v1.55.2 cleared the boot timer on disconnect
// but kept its truthy id, so the reconnect never restarted the watcher and
// the plan stayed hidden FOREVER. Now the veil lifecycle restarts from
// connectedCallback: the veil must lift no later than the hard cap.
// B. layout shifts before, between and after the old poll ticks (150, 300,
// 450, 590 ms). v1.55.2 revealed the plan after two equal reads at
// 200/400 ms, so a panel landing at 450+ ms jumped on a VISIBLE plan. Now
// the veil holds for the full protective window with trailing quiescence:
// not a single frame may show the plan at a non-final stage height.
// C. a shift AFTER the reveal (slower than the cap) must glide, not snap:
// for a short grace the stage height transitions and the plan refits
// along with it (no transition at all on v1.55.2 — instant jump).
import { launch, check, finish } from './serve.mjs';
const { page, browser } = await launch({ width: 820, height: 760 });
// ---------- A: detach before the first tick, reattach, veil must lift ----------
const rec = await page.evaluate(async () => {
const out = {};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const c = document.createElement('houseplan-card');
c.setConfig({ type: 'custom:houseplan-card' });
c.hass = window.__card.hass;
const wrap = document.createElement('div');
wrap.style.cssText = 'position:fixed;left:0;top:0;width:800px;z-index:99;background:#000';
wrap.appendChild(c);
document.body.appendChild(wrap);
await sleep(50); // the first boot tick has not fired yet
out.bootingWhenDetached = c._booting === true;
c.remove(); // disconnect: timers die here
await sleep(30);
wrap.appendChild(c); // reconnect — the veil lifecycle must restart
const t0 = performance.now();
const sr = () => c.shadowRoot || c.renderRoot;
while (c._booting && performance.now() - t0 < 2000) await sleep(50);
out.revealMs = Math.round(performance.now() - t0);
out.bootSettledAfterReconnect = c._booting === false;
out.settledWithinCap = performance.now() - t0 < 1500; // hard cap 1200 + slack
await sleep(300); // let the fade finish
const stage = sr().querySelector('.stage');
const wrapEl = stage && stage.querySelector(':scope > .zoomwrap');
out.planVisibleAfterReconnect = !!wrapEl && getComputedStyle(wrapEl).visibility === 'visible';
out.noHpbootClass = !!stage && !stage.classList.contains('hpboot');
out.veilGone = !sr().querySelector('.bootveil');
// A2: a second detach DURING the fade must not leave a zombie veil either
const c2 = document.createElement('houseplan-card');
c2.setConfig({ type: 'custom:houseplan-card' });
c2.hass = window.__card.hass;
wrap.appendChild(c2);
const t1 = performance.now();
while (c2._booting && performance.now() - t1 < 2000) await sleep(50);
// _bootFading is true for ~220 ms after the reveal — detach right inside it
c2.remove();
await sleep(30);
wrap.appendChild(c2);
await sleep(400);
const sr2 = c2.shadowRoot || c2.renderRoot;
out.fadeVeilGoneAfterReattach = !sr2.querySelector('.bootveil') && c2._bootFading === false;
wrap.remove();
return out;
});
for (const [k, v] of Object.entries(rec)) check(k, v, k === 'revealMs' ? rec.revealMs : true);
// ---------- B: shifts at 150/300/450/590 ms — never a visible stale frame ----------
const shifts = [150, 300, 450, 590];
const outB = {};
for (const shiftMs of shifts) {
const res = await page.evaluate(async (shiftAt) => {
const out = {};
const spacer = document.createElement('div');
spacer.style.cssText = 'height:0px';
const c = document.createElement('houseplan-card');
c.setConfig({ type: 'custom:houseplan-card' });
c.hass = window.__card.hass;
const wrap = document.createElement('div');
wrap.style.cssText = 'position:fixed;left:0;top:0;width:800px;z-index:99;background:#000';
wrap.append(spacer, c);
document.body.appendChild(wrap);
const frames = [];
const sr = () => c.shadowRoot || c.renderRoot;
const t0 = performance.now();
let grown = false;
await new Promise((done) => {
const sample = () => {
const t = performance.now() - t0;
if (!grown && t >= shiftAt) {
grown = true; // the "HA panel" arrives
spacer.style.height = '56px';
window.dispatchEvent(new Event('resize'));
}
const stage = sr().querySelector('.stage');
if (stage) {
const veil = sr().querySelector('.bootveil');
const wrapEl = stage.querySelector(':scope > .zoomwrap');
const planHidden =
!wrapEl ||
getComputedStyle(wrapEl).visibility === 'hidden' ||
(veil && !veil.classList.contains('off') && getComputedStyle(veil).opacity !== '0');
frames.push({ t: Math.round(t), planVisible: !planHidden, stageH: stage.clientHeight });
}
if (t < 1900) requestAnimationFrame(sample);
else done();
};
requestAnimationFrame(sample);
});
const seen = frames;
const finalH = seen[seen.length - 1].stageH;
// THE assertion: no frame may show the plan at a stale (pre-panel) height
const bad = seen.filter((f) => f.planVisible && Math.abs(f.stageH - finalH) > 2);
out.noStaleFrame = bad.length === 0
? true
: `plan visible at h=${bad[0].stageH} (final ${finalH}) at t=${bad[0].t}ms`;
const shown = seen.filter((f) => f.planVisible);
out.planShown = shown.length >= 5 && Math.abs(shown[0].stageH - finalH) <= 2;
out.heightReacted = finalH !== seen[0].stageH;
wrap.remove();
return out;
}, shiftMs);
outB[`shift${shiftMs}_noStaleFrame`] = res.noStaleFrame;
outB[`shift${shiftMs}_planShown`] = res.planShown;
outB[`shift${shiftMs}_heightReacted`] = res.heightReacted;
check(`shift${shiftMs}_noStaleFrame`, res.noStaleFrame);
check(`shift${shiftMs}_planShown`, res.planShown);
check(`shift${shiftMs}_heightReacted`, res.heightReacted);
}
// ---------- C: shift AFTER the reveal glides instead of snapping ----------
const late = await page.evaluate(async () => {
const out = {};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const spacer = document.createElement('div');
spacer.style.cssText = 'height:0px';
const c = document.createElement('houseplan-card');
c.setConfig({ type: 'custom:houseplan-card' });
c.hass = window.__card.hass;
const wrap = document.createElement('div');
wrap.style.cssText = 'position:fixed;left:0;top:0;width:800px;z-index:99;background:#000';
wrap.append(spacer, c);
document.body.appendChild(wrap);
const sr = () => c.shadowRoot || c.renderRoot;
const t0 = performance.now();
while (c._booting && performance.now() - t0 < 2500) await sleep(25);
out.revealed = c._booting === false;
await sleep(120); // clearly after the reveal, still inside the soft grace
const stage = sr().querySelector('.stage');
const h0 = stage.clientHeight;
spacer.style.height = '56px'; // VERY late panel — slower than the cap
window.dispatchEvent(new Event('resize'));
const heights = [];
await new Promise((done) => {
const t1 = performance.now();
const sample = () => {
heights.push(stage.clientHeight);
if (performance.now() - t1 < 700) requestAnimationFrame(sample);
else done();
};
requestAnimationFrame(sample);
});
const hEnd = heights[heights.length - 1];
out.heightMoved = Math.abs(hEnd - h0) > 30;
// the glide: intermediate frames strictly between the endpoints
const mid = heights.filter((h) => Math.abs(h - h0) > 4 && Math.abs(h - hEnd) > 4);
out.lateShiftGlides = mid.length >= 3
? true
: `only ${mid.length} intermediate frame(s): ${h0} -> ${hEnd} snapped`;
out.settledAtFinal = Math.abs(hEnd - (h0 - 56)) <= 2;
wrap.remove();
return out;
});
check('late_revealed', late.revealed);
check('late_heightMoved', late.heightMoved);
check('late_lateShiftGlides', late.lateShiftGlides);
check('late_settledAtFinal', late.settledAtFinal);
await finish(browser, { ...rec, ...outB, ...late });
File diff suppressed because one or more lines are too long
+12 -3
View File
File diff suppressed because one or more lines are too long
+22
View File
@@ -1,5 +1,27 @@
# Changelog
## v1.55.3 — 2026-08-02
Patch release: two fixes for the v1.55.2 first-open veil, found by an
adversarial lifecycle audit (AUD-1552-01/02).
- **A dashboard rebuild during the first open can no longer leave the
plan hidden forever** (AUD-1552-01). Disconnecting the card while the
boot veil was up (Lovelace recreates its DOM, a view switch remounts
the card) killed the settle timers but kept their ids, so nothing ever
lifted the veil again. The veil lifecycle now restarts from every
reconnect — with a fresh clock and an unconditional hard cap — and the
fade-out also survives a mid-fade remount.
- **The veil no longer opens early, right before a late panel lands**
(AUD-1552-02). Two equal height reads at 200/400 ms used to reveal the
plan at ~400 ms, so Home Assistant chrome arriving at 450+ ms jumped
on a visible plan — exactly what the veil was meant to prevent. The
veil now holds for a full protective window with trailing quiescence
(height changes near the cap extend the wait), and for a short grace
after the reveal any later shift glides via a height transition
instead of snapping. Deliberate height changes (entering an editor)
still apply instantly.
## v1.55.2 — 2026-08-02
Patch release: a calmer first open — no zoom flash and no layout jumps
+23
View File
@@ -6,6 +6,29 @@
> **Правило проекта:** оба файла пополняются в одном коммите с самим
> изменением — как и остальная документация (см. docs/STATUS.md).
## v1.55.3 — 2026-08-02
Патч-релиз: два исправления вуали первого открытия из v1.55.2,
найденные adversarial-аудитом жизненного цикла (AUD-1552-01/02).
- **Пересборка дашборда во время первого открытия больше не оставляет
план скрытым навсегда** (AUD-1552-01). Отключение карточки при
поднятой вуали (Lovelace пересоздаёт DOM, переключение вью
перемонтирует карточку) убивало таймеры стабилизации, но сохраняло их
id — и вуаль уже никогда не снималась. Теперь жизненный цикл вуали
перезапускается при каждом повторном подключении — с новым отсчётом и
безусловным жёстким таймаутом; затухание тоже переживает размонтирование
посреди fade.
- **Вуаль больше не открывается рано, прямо перед поздней панелью**
(AUD-1552-02). Два одинаковых замера высоты на 200/400 мс открывали
план уже на ~400 мс, и хром Home Assistant, пришедший на 450+ мс,
прыгал на видимом плане — ровно то, от чего вуаль должна защищать.
Теперь вуаль держится полное защитное окно с trailing quiescence
(изменения высоты у края окна продлевают ожидание), а короткое время
после открытия поздний сдвиг применяется плавно (transition высоты),
а не скачком. Намеренные изменения высоты (вход в редактор)
по-прежнему применяются мгновенно.
## v1.55.2 — 2026-08-02
Патч-релиз: спокойное первое открытие — без вспышки зума и прыжков
+3 -3
View File
@@ -11,16 +11,16 @@
> (versions, publication, infrastructure), DEVELOPMENT.md for new gotchas,
> ARCHITECTURE.md for design changes, ROADMAP.md when plans move.
## Snapshot (2026-08-01)
## Snapshot (2026-08-02)
| Item | State |
|---|---|
| Version | **v1.55.2** everywhere (manifest, const.py, package.json, CARD_VERSION); deployed to the home instance |
| Version | **v1.55.3** everywhere (manifest, const.py, package.json, CARD_VERSION); deployed to the home instance. Closes the v1.55.2 audit findings AUD-1552-01/02 (boot-veil lifecycle: reconnect restart + full protective window with trailing quiescence + post-reveal soft grace; regressions in `demo/smoke_preloader_lifecycle.mjs`) |
| Workflow | Since 2026-07-22: minor changes go to branch **`dev`** (build + smokes → deploy home → commit → push, NO release); releases are batched on the owner's command (merge dev→main, one tag, one release with a summary changelog, CI checked on dev beforehand) |
| GitHub | https://github.com/Matysh/houseplan-card — **`main` carries every published release, the latest tag is the current version above**; `dev` is where work lands and is merged into `main` at release time (so `dev` is normally equal to or ahead of `main`, never behind). Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
| CI | validate.yml (hacs + hassfest + frontend + backend) green; release.yml attaches the bundle on release publish |
| HACS | Custom repository works. **Inclusion PR: hacs/default#9004** — open, valid, labeled, mergeable clean, never drafted. Queue: 1212 open, 835 older than ours. Merge rate COLLAPSED: 75 in July but almost all in the first decade, 0 in the last week (checked 2026-07-29) — maintainers process in rare bursts; ETA unknowable, months at best. Nothing actionable on our side |
| Home instance | ha.jbstudio.pro (SSH port **22222**, key `ha_jb`; HA config root is `/mnt/data/supervisor/homeassistant` — `/config` does NOT exist in this SSH environment), deployed **v1.55.2** via direct copy (HACS custom repo also installed) |
| Home instance | ha.jbstudio.pro (SSH port **22222**, key `ha_jb`; HA config root is `/mnt/data/supervisor/homeassistant` — `/config` does NOT exist in this SSH environment), deployed **v1.55.3** via direct copy (HACS custom repo also installed) |
| Localization | UI en/ru (src/i18n/*.json), everything user-visible localized incl. kiosk popover |
| Tests | Four layers: frontend unit (`npm test`, node:test over `test-build/`), pure backend (`pytest tests_backend`, runs anywhere), HA-harness backend (same folder, CI only — needs py3.13 + pytest-homeassistant-custom-component), and browser smokes (`demo/smoke_*.mjs`, headless chromium). **Counts are not written down here** — they went stale within two releases while the version line beside them was kept current, which reads as less coverage than exists (review R5-2). Run `npm run inventory` for the current numbers, or read them off the last CI run |
| Vacuums | Live robot vacuums shipped (docs/VACUUM.md): puck, server-side trails with display modes, fit-panel calibration. Verified on a live Dreame X50 Master |
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "houseplan-card",
"version": "1.55.2",
"version": "1.55.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "houseplan-card",
"version": "1.55.2",
"version": "1.55.3",
"license": "MIT",
"dependencies": {
"lit": "^3.1.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "houseplan-card",
"version": "1.55.2",
"version": "1.55.3",
"description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT",
"type": "module",
+75 -12
View File
@@ -47,7 +47,20 @@ import { cardStyles } from './styles';
import { fitInSquare, contentBounds, spaceModels } from './space-geometry';
import { langOf, t, type I18nKey } from './i18n';
const CARD_VERSION = '1.55.2';
const CARD_VERSION = '1.55.3';
/** HP-1552 boot-veil timing (AUD-1552-02). The veil holds for at least
* BOOT_MIN_MS; every stage-height change restarts a BOOT_QUIET_MS
* trailing-quiescence requirement (chrome still settling near the cap
* extends the wait); BOOT_MAX_MS lifts the veil unconditionally.
* BOOT_MIN_MS exceeds the old 600 ms window by a frame-latency margin: a
* panel applied right at the window's edge (~590 ms) only materializes in
* the stage height a couple of rAF/render frames later. */
const BOOT_MIN_MS = 700;
const BOOT_QUIET_MS = 250;
const BOOT_MAX_MS = 1200;
/** AUD-1552-02: post-reveal grace during which late chrome shifts glide
* (CSS height transition on the stage) instead of snapping. */
const BOOT_SOFT_MS = 1500;
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';
@@ -293,14 +306,21 @@ class HouseplanCard extends LitElement {
/** HP-1552: first-open boot veil. In normal (non-kiosk) mode the stage is
* calc(100dvh - _hdrH), and _hdrH is measured from HA's chrome — which
* finishes loading AFTER the card's first paint, so every late panel
* nudged the height and the plan visibly jumped. Until the height reads
* the same twice in a row (or ~600 ms pass) the plan hides behind a
* pulsing-house veil. First open only; kiosk is 100dvh and never jumps. */
* nudged the height and the plan visibly jumped. The plan hides behind a
* pulsing-house veil for a full protective window (AUD-1552-02: the old
* early reveal on "two equal reads" let a panel landing at 400-600 ms
* jump on a VISIBLE plan); the whole lifecycle restarts from
* connectedCallback (AUD-1552-01: timers die on disconnect — a Lovelace
* DOM rebuild mid-boot used to leave the plan hidden forever).
* First open only; kiosk is 100dvh and never jumps. */
private _booting = true;
private _bootFading = false; // veil kept one beat for the opacity-out
private _bootTimer?: number;
private _bootLastH = -1;
private _bootStart = 0;
private _bootLastChange = 0; // when the stage height last moved (quiescence clock)
private _bootSoft = false; // post-reveal grace: late chrome shifts glide, not jump
private _bootSoftTimer?: number;
/** The accidental-tap guard: pending confirmation for a toggle/run tap. */
private _tapConfirm: { text: string; exec: () => void } | null = null;
private _onboardingShown = false; // the auto space dialog is shown once per session
@@ -454,6 +474,7 @@ class HouseplanCard extends LitElement {
_hdrH: { state: true },
_booting: { state: true },
_bootFading: { state: true },
_bootSoft: { state: true },
_tapConfirm: { state: true },
hass: { attribute: false },
_config: { state: true },
@@ -515,6 +536,21 @@ class HouseplanCard extends LitElement {
this._cycleTimer = window.setInterval(() => this._cycleTick(), Number(this._config.cycle) * 1000);
}
window.addEventListener('hashchange', this._onHashChange);
// AUD-1552-01: the boot-veil timers die in disconnectedCallback, so a
// disconnect/reconnect while booting (Lovelace rebuilds its DOM, a view
// switch remounts the card) used to strand _booting=true with no watcher
// — the plan stayed hidden forever. Restart the veil lifecycle from every
// connect: a fresh watch (fresh clock, so the hard cap counts from the
// reconnect) while booting, or the tail timers if we detached mid-fade.
if (this._booting) this._bootWatch();
else if (this._bootFading) {
clearTimeout(this._bootTimer);
this._bootTimer = window.setTimeout(() => { this._bootFading = false; }, 220);
}
if (this._bootSoft) {
clearTimeout(this._bootSoftTimer);
this._bootSoftTimer = window.setTimeout(() => { this._bootSoft = false; }, BOOT_SOFT_MS);
}
}
public disconnectedCallback(): void {
@@ -530,6 +566,8 @@ class HouseplanCard extends LitElement {
clearTimeout(this._toastTimer);
clearTimeout(this._slideTimer);
clearTimeout(this._bootTimer);
this._bootTimer = undefined; // AUD-1552-01: a cleared id must not block the reconnect watcher
clearTimeout(this._bootSoftTimer);
this._saveConfigDebounced.flush(); // never leave an edit unsent on teardown
window.removeEventListener('hashchange', this._onHashChange);
clearTimeout(this._holdTimer);
@@ -1601,24 +1639,33 @@ class HouseplanCard extends LitElement {
}
/**
* HP-1552: poll the stage height until it settles — two equal reads in a
* row, or a hard cap of ~600 ms (HA's panels normally land well inside it).
* Only then is the plan revealed, already refit to the final geometry.
* HP-1552: hold the boot veil until the stage height settles. AUD-1552-02:
* the old "two equal reads at a 200 ms cadence" revealed the plan at
* ~400 ms, so an HA panel landing right after (450+ ms is realistic on a
* slow device) still jumped on a VISIBLE plan. The veil now holds for the
* full protective window (BOOT_MIN_MS); the height is sampled every 100 ms
* and any change restarts a trailing-quiescence requirement
* (BOOT_QUIET_MS), so chrome still settling near the cap extends the wait.
* BOOT_MAX_MS lifts the veil unconditionally — it can never get stuck.
*/
private _bootWatch(): void {
clearTimeout(this._bootTimer); // never two concurrent watchers (connect + updated)
this._bootStart = Date.now();
this._bootLastH = -1; // the first read only arms the comparison
this._bootLastChange = this._bootStart;
const tick = () => {
if (!this._booting) return;
const now = Date.now();
const h = this._stageEl ? this._stageEl.clientHeight : 0;
if ((h > 0 && h === this._bootLastH) || Date.now() - this._bootStart >= 600) {
if (h !== this._bootLastH) { this._bootLastH = h; this._bootLastChange = now; }
const elapsed = now - this._bootStart;
if (elapsed >= BOOT_MAX_MS || (elapsed >= BOOT_MIN_MS && h > 0 && now - this._bootLastChange >= BOOT_QUIET_MS)) {
this._bootSettled();
return;
}
this._bootLastH = h;
this._bootTimer = window.setTimeout(tick, 200);
this._bootTimer = window.setTimeout(tick, 100);
};
this._bootTimer = window.setTimeout(tick, 200);
this._bootTimer = window.setTimeout(tick, 100);
}
private _bootSettled(): void {
@@ -1627,6 +1674,21 @@ class HouseplanCard extends LitElement {
this._booting = false;
this._bootFading = true; // one soft opacity-out, then out of the DOM
this._bootTimer = window.setTimeout(() => { this._bootFading = false; }, 220);
// AUD-1552-02: chrome that lands after the cap (device slower than
// BOOT_MAX_MS) must not snap — for a short grace the stage height
// transitions and the viewport ResizeObserver refits the plan each frame.
this._bootSoft = true;
clearTimeout(this._bootSoftTimer);
this._bootSoftTimer = window.setTimeout(() => { this._bootSoft = false; }, BOOT_SOFT_MS);
}
/** The soft grace only covers PASSIVE late chrome. A user action that
* changes the stage height (entering/leaving an editor) must apply
* instantly — the plan may not drift under the pointer mid-drag. */
private _bootSoftCancel(): void {
if (!this._bootSoft) return;
clearTimeout(this._bootSoftTimer);
this._bootSoft = false;
}
/** Recompute the view for a new scene size, preserving zoom and center. */
@@ -2027,6 +2089,7 @@ class HouseplanCard extends LitElement {
private _setMode(mode: 'view' | 'plan' | 'devices' | 'decor'): void {
if (this._kiosk && mode !== 'view') return; // wall devices never edit
if (this._mode === mode) return;
this._bootSoftCancel(); // editor bars change the stage height DELIBERATELY — snap, no glide
if ((mode === 'plan' || mode === 'decor') && !this._norm) {
this._showToast(this._t('toast.markup_needs_server'));
return;
@@ -4737,7 +4800,7 @@ class HouseplanCard extends LitElement {
${this._markup ? this._renderMarkupBar() : this._mode === 'devices' ? this._renderDevicesBar() : this._mode === 'decor' ? this._renderDecorBar() : nothing}
</div>
<div class="stage ${this._markup ? 'markup tool-' + this._tool + (this._tool === 'split' && !this._splitSel ? ' pickstage' : '') + (this._tool === 'openwall' && this._openWallHover ? ' wallhot' : '') : ''} ${this._mode === 'decor' ? 'dtool-' + this._decorTool : ''} ${space.bg ? '' : 'noplan'} mode-${this._mode}${this._booting ? ' hpboot' : ''}"
<div class="stage ${this._markup ? 'markup tool-' + this._tool + (this._tool === 'split' && !this._splitSel ? ' pickstage' : '') + (this._tool === 'openwall' && this._openWallHover ? ' wallhot' : '') : ''} ${this._mode === 'decor' ? 'dtool-' + this._decorTool : ''} ${space.bg ? '' : 'noplan'} mode-${this._mode}${this._booting ? ' hpboot' : ''}${this._bootSoft ? ' hpsettle' : ''}"
style="height:${this._kiosk ? '100dvh' : `calc(100dvh - ${this._hdrH}px)`}${stageBg ? `;background:${stageBg}` : ''}"
@click=${(e: MouseEvent) => this._markupClick(e)}
@wheel=${(e: WheelEvent) => this._onWheel(e)}
+9
View File
@@ -156,6 +156,12 @@ export const cardStyles = css`
.stage.hpboot .zoombadge {
visibility: hidden;
}
/* AUD-1552-02: post-veil grace — HA chrome landing after the cap moves
the stage height smoothly; the viewport ResizeObserver refits the plan
along the transition, so a late panel glides instead of jumping. */
.stage.hpsettle {
transition: height 0.25s ease;
}
.bootveil {
position: absolute;
inset: 0;
@@ -188,6 +194,9 @@ export const cardStyles = css`
animation: none;
opacity: 0.7;
}
.stage.hpsettle {
transition: none;
}
}
.zoomctl {
display: inline-flex;