feat v1.38.2: restore last space and editor mode across reloads

- LS_NAV stores {space, mode}; restored in setConfig from the cached
  config, with a retry after the live config when the cache was stale;
  deep-link hash wins; edit modes restored for admins only
- UX-MODES updated (owner reversed the 'always start in View' rule)
- smoke_nav_persist.mjs; TESTING/CHANGELOG same-commit
This commit is contained in:
Matysh
2026-07-23 15:19:09 +03:00
parent 8895354c4e
commit 3811a1ca73
11 changed files with 114 additions and 33 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
FILES_URL = "/houseplan_files/files" FILES_URL = "/houseplan_files/files"
FILES_DIR = "houseplan/files" FILES_DIR = "houseplan/files"
CONF_ADMIN_ONLY = "admin_only" CONF_ADMIN_ONLY = "admin_only"
VERSION = "1.38.1" VERSION = "1.38.2"
DEFAULT_CONFIG: dict = { DEFAULT_CONFIG: dict = {
"spaces": [], "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", "issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
"requirements": [], "requirements": [],
"single_config_entry": true, "single_config_entry": true,
"version": "1.38.1" "version": "1.38.2"
} }
+35
View File
@@ -0,0 +1,35 @@
import { launch } from './serve.mjs';
const { page, browser } = await launch();
const res = await page.evaluate(async () => {
const out = {};
const c = window.__card;
// выбрать f2 и редактор устройств
c._space = 'garden'; c._setMode('devices'); await c.updateComplete;
const nav = JSON.parse(localStorage.getItem('houseplan_card_nav_v1'));
out.saved = nav.space === 'garden' && nav.mode === 'devices';
// пересоздать карточку (эмуляция закрытия вкладки; кэш конфига в LS уже есть)
const c2 = document.createElement('houseplan-card');
c2.setConfig({ type: 'custom:houseplan-card' });
c2.hass = c.hass;
document.body.appendChild(c2);
await new Promise((r) => setTimeout(r, 300));
c2.hass = { ...c.hass };
await c2.updateComplete;
out.spaceRestored = c2._space === 'garden';
out.modeRestored = c2._mode === 'devices';
// хэш приоритетнее сохранённого
location.hash = '#space=f1';
const c3 = document.createElement('houseplan-card');
c3.setConfig({ type: 'custom:houseplan-card' });
c3.hass = c.hass;
document.body.appendChild(c3);
await new Promise((r) => setTimeout(r, 300));
out.hashWins = c3._space === 'f1';
location.hash = '';
c2.remove(); c3.remove();
// вернуть исходное
c._setMode('view'); c._space = 'f1'; c._saveNav(); await c.updateComplete;
return out;
});
console.log(JSON.stringify(res, null, 1));
await browser.close();
File diff suppressed because one or more lines are too long
+9 -9
View File
File diff suppressed because one or more lines are too long
+7
View File
@@ -1,5 +1,12 @@
# Changelog # Changelog
## v1.38.2 — 2026-07-23
- The card now **remembers where you were**: the selected space and the active
editor survive navigation and closing the tab (localStorage; edit modes are
restored for admins only). A `#space=` deep link still wins over the saved
space. This reverses the earlier "always start in View" rule — the owner's
call.
## v1.38.1 — 2026-07-23 (tap action cleanup, right-click more-info) ## v1.38.1 — 2026-07-23 (tap action cleanup, right-click more-info)
- The per-device action is now one of three: **Device card** (renamed from - The per-device action is now one of three: **Device card** (renamed from
"Info card", the default), HA more-info, Toggle. The confusing "As the card "Info card", the default), HA more-info, Toggle. The confusing "As the card
+4
View File
@@ -140,6 +140,10 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
(explicit ripple color still wins); off/white lights unchanged [auto] (explicit ripple color still wins); off/white lights unchanged [auto]
- [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any - [ ] Alarm pulse (v1.27.0): leak/smoke/gas/CO/siren in 'on' pulse a red ring over any
display mode; clears on 'off'; unavailable never alarms [auto]; reduced-motion static display mode; clears on 'off'; unavailable never alarms [auto]; reduced-motion static
- [ ] Nav persistence (v1.38.2): closing/reopening the tab restores the last
space AND editor mode (admins; localStorage); a #space= deep link beats
the saved space; a stale cache without the saved space retries after the
live config loads [auto]
- [ ] Tap action cleanup + right click (v1.38.1): the per-device action list - [ ] Tap action cleanup + right click (v1.38.1): the per-device action list
has three options (Device card / HA more-info / Toggle), no "card has three options (Device card / HA more-info / Toggle), no "card
default" — the card editor's global tap option is gone and ignored; default" — the card editor's global tap option is gone and ignored;
+5 -2
View File
@@ -17,8 +17,11 @@ NO tab (since v1.30.2). The Background editor (v1.33.0) manages a purely visual
decor layer (lines/rects/ovals/text in `space.decor`, drawn under the rooms, decor layer (lines/rects/ovals/text in `space.decor`, drawn under the rooms,
inert everywhere outside its editor). inert everywhere outside its editor).
- **View** is the implicit default state: no editor tab is active. It is the only - **View** is the implicit default state: no editor tab is active. Since
state after every load (edit modes are never restored across reloads). v1.38.2 the last space AND editor mode are restored across reloads
(localStorage, admins only for edit modes) — closing and reopening the tab
lands you where you were (owner's decision, reversing the earlier
"never restore" rule).
- Activating an editor tab highlights it and opens that editor's bottom toolbar - Activating an editor tab highlights it and opens that editor's bottom toolbar
(both editors have one since v1.30.2). The toolbar and the active tab each (both editors have one since v1.30.2). The toolbar and the active tab each
carry an **X** that closes the editor back to View; re-clicking the active tab carry an **X** that closes the editor back to View; re-clicking the active tab
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "houseplan-card", "name": "houseplan-card",
"version": "1.38.1", "version": "1.38.2",
"description": "Interactive house plan Lovelace card for Home Assistant", "description": "Interactive house plan Lovelace card for Home Assistant",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
+33 -1
View File
@@ -32,10 +32,11 @@ import './space-card';
import { cardStyles } from './styles'; import { cardStyles } from './styles';
import { langOf, t, type I18nKey } from './i18n'; import { langOf, t, type I18nKey } from './i18n';
const CARD_VERSION = '1.38.1'; const CARD_VERSION = '1.38.2';
const LS_KEY = 'houseplan_card_layout_v1'; 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_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
const LS_ZOOM = 'houseplan_card_zoom_v1'; const LS_ZOOM = 'houseplan_card_zoom_v1';
const LS_NAV = 'houseplan_card_nav_v1'; // last space + editor mode (owner: restore where you were)
const NORM_W = 1000; // width of the render space for normalized configs const NORM_W = 1000; // width of the render space for normalized configs
const GRID_N = 240; // grid points across the plan width (half the previous step; old nodes are a subset of the new ones, positions are preserved) const GRID_N = 240; // grid points across the plan width (half the previous step; old nodes are a subset of the new ones, positions are preserved)
@@ -204,6 +205,7 @@ class HouseplanCard extends LitElement {
} | null = null; } | null = null;
private _keyHandler = (e: KeyboardEvent) => this._onKey(e); private _keyHandler = (e: KeyboardEvent) => this._onKey(e);
private _hashApplied = false; private _hashApplied = false;
private _navApplied = false; // the saved space was restored (or the user navigated)
/** Deep-link: read `#space=<id>` from the URL (used by embedded houseplan-space-card). */ /** Deep-link: read `#space=<id>` from the URL (used by embedded houseplan-space-card). */
private _hashSpace(): string { private _hashSpace(): string {
const m = /(?:^|[#&])space=([^&]+)/.exec(window.location.hash || ''); const m = /(?:^|[#&])space=([^&]+)/.exec(window.location.hash || '');
@@ -386,9 +388,13 @@ class HouseplanCard extends LitElement {
this._layout = c.layout || {}; this._layout = c.layout || {};
this._serverStorage = true; this._serverStorage = true;
const hs = this._hashSpace(); const hs = this._hashSpace();
const nav = this._savedNav();
if (hs && this._model.find((sp) => sp.id === hs)) { this._space = hs; this._hashApplied = true; } if (hs && this._model.find((sp) => sp.id === hs)) { this._space = hs; this._hashApplied = true; }
else if (nav?.space && this._model.find((sp) => sp.id === nav.space)) { this._space = nav.space; this._navApplied = true; }
else if (config.default_floor) this._space = config.default_floor; else if (config.default_floor) this._space = config.default_floor;
else if (!this._model.find((sp) => sp.id === this._space)) this._space = this._model[0]?.id || this._space; else if (!this._model.find((sp) => sp.id === this._space)) this._space = this._model[0]?.id || this._space;
// reopenning the tab lands you in the same editor you left (admins only)
if (nav?.mode && nav.mode !== 'view' && this._canEdit) this._mode = nav.mode;
} }
} catch { } catch {
/* ignore */ /* ignore */
@@ -550,9 +556,16 @@ class HouseplanCard extends LitElement {
}, 'houseplan_config_updated'); }, 'houseplan_config_updated');
} }
const hs = this._hashSpace(); const hs = this._hashSpace();
const nav = this._savedNav();
if (!this._hashApplied && hs && this._model.find((s) => s.id === hs)) { if (!this._hashApplied && hs && this._model.find((s) => s.id === hs)) {
this._space = hs; this._space = hs;
this._hashApplied = true; this._hashApplied = true;
} else if (nav?.space && !this._navApplied && !this._hashApplied
&& this._model.find((s) => s.id === nav.space)) {
// the cached config might have been stale (no such space) — retry once
// the live config is in
this._space = nav.space;
this._navApplied = true;
} else if (this._norm && !this._model.find((s) => s.id === this._space)) { } else if (this._norm && !this._model.find((s) => s.id === this._space)) {
this._space = this._model[0]?.id || this._space; this._space = this._model[0]?.id || this._space;
} }
@@ -1187,6 +1200,22 @@ class HouseplanCard extends LitElement {
return roomEdges(sp?.rooms || []).map((s) => [s[0] * NORM_W, s[1] * H, s[2] * NORM_W, s[3] * H]); return roomEdges(sp?.rooms || []).map((s) => [s[0] * NORM_W, s[1] * H, s[2] * NORM_W, s[3] * H]);
} }
private _savedNav(): { space?: string; mode?: 'view' | 'plan' | 'devices' | 'decor' } | null {
try {
return JSON.parse(localStorage.getItem(LS_NAV) || 'null');
} catch {
return null;
}
}
private _saveNav(): void {
try {
localStorage.setItem(LS_NAV, JSON.stringify({ space: this._space, mode: this._mode }));
} catch {
/* private mode etc. */
}
}
private _setMode(mode: 'view' | 'plan' | 'devices' | 'decor'): void { private _setMode(mode: 'view' | 'plan' | 'devices' | 'decor'): void {
if (this._mode === mode) return; if (this._mode === mode) return;
if ((mode === 'plan' || mode === 'decor') && !this._norm) { if ((mode === 'plan' || mode === 'decor') && !this._norm) {
@@ -1206,6 +1235,7 @@ class HouseplanCard extends LitElement {
this._decorDraft = null; this._decorDraft = null;
this._decorSel = null; this._decorSel = null;
this._decorTool = 'select'; this._decorTool = 'select';
this._saveNav();
} }
@@ -3062,7 +3092,9 @@ class HouseplanCard extends LitElement {
@click=${() => { @click=${() => {
this._space = s.id; this._space = s.id;
this._selId = null; this._selId = null;
this._navApplied = true;
this._restoreZoom(); this._restoreZoom();
this._saveNav();
}} }}
> >
${s.title}${this._norm && this._canEdit ${s.title}${this._norm && this._canEdit