mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
feat v1.25.0: three interaction modes — View / Plan / Devices (docs/UX-MODES.md)
- View (always the default): display + device interaction only; no dragging of icons/labels/openings; panning can start on an icon; clean header - Plan: markup tools, openings editing (click-to-edit, drag along walls), label drag, space dialogs, fill palette; orange frame; icons hidden but labels visible - Devices: icon drag only here, click opens the marker editor directly; add/ show-all/reset/rules buttons; accent frame - segmented mode control (admins only); removed: markup toggle button, view-mode opening drag/dblclick (v1.23.1), 'drag anywhere' (v1.9 reversed) - README en+ru updated; TESTING.md Modes section; smoke_modes.mjs
This commit is contained in:
@@ -119,7 +119,7 @@ Later you can add as many spaces as you like (floors, yard, garage) with the **
|
||||
|
||||
### Step 2. Outline the rooms
|
||||
|
||||
After the first space is added, the card switches to markup mode by itself. Click grid points, connecting them with lines, and close the room outline by clicking the first point.
|
||||
After the first space is added, the card switches to the **Plan** tab by itself. The card has three mode tabs in the header — **View** (default: display and device control only, nothing can be moved or edited), **Plan** (rooms, openings, labels, space settings) and **Devices** (placing and configuring markers); the edit tabs are shown to administrators. In Plan, click grid points, connecting them with lines, and close the room outline by clicking the first point.
|
||||
|
||||
As soon as the outline is closed, the room-save dialog appears. Here you need to **bind the room to a Home Assistant area** — this is exactly what enables the automation. For utility rooms with no devices (hall, sauna) there is a **"No area"** button.
|
||||
|
||||
@@ -166,7 +166,7 @@ The mouse wheel or the **- / ⊹ / +** buttons zoom the plan in and out; on
|
||||
|
||||
### Step 5. Put the icons in their places
|
||||
|
||||
Device icons can be **dragged with the mouse at any time** — no separate "edit mode" needs to be enabled. Positions are saved on the server and are identical in all browsers and devices. The **↺** button in the header restores the automatic layout.
|
||||
Switch to the **Devices** tab to arrange icons: drag them with the mouse, click one to open its editor. In **View** mode nothing can be moved — panning the map never displaces a sensor (a top user request). Positions are saved on the server and are identical in all browsers and devices. The **↺** button restores the automatic layout.
|
||||
|
||||

|
||||
|
||||
|
||||
+1
-1
@@ -168,7 +168,7 @@ title: План дома
|
||||
|
||||
### Шаг 5. Расставьте значки по местам
|
||||
|
||||
Значки устройств можно **перетаскивать мышью в любой момент** — отдельный «режим правки» включать не нужно. Позиции сохраняются на сервере и одинаковы во всех браузерах и устройствах. Кнопка **↺** в шапке возвращает автоматическую раскладку.
|
||||
Расставлять значки нужно на вкладке **«Устройства»**: там они перетаскиваются мышью, а клик открывает редактор. В режиме **«Просмотр»** ничего сдвинуть нельзя — панорамирование карты больше не сдвигает датчики (главная просьба пользователей). Позиции сохраняются на сервере и одинаковы во всех браузерах и устройствах. Кнопка **↺** возвращает автоматическую раскладку.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
|
||||
FILES_URL = "/houseplan_files/files"
|
||||
FILES_DIR = "houseplan/files"
|
||||
CONF_ADMIN_ONLY = "admin_only"
|
||||
VERSION = "1.24.2"
|
||||
VERSION = "1.25.0"
|
||||
|
||||
DEFAULT_CONFIG: dict = {
|
||||
"spaces": [],
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,5 +16,5 @@
|
||||
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||
"requirements": [],
|
||||
"single_config_entry": true,
|
||||
"version": "1.24.2"
|
||||
"version": "1.25.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// UX modes shell (v1.25.0): view is display-only; plan/devices gate the tools.
|
||||
import { launch } from './serve.mjs';
|
||||
const { page, browser } = await launch();
|
||||
const out = {};
|
||||
const q = (sel) => page.evaluate((s) => (window.__card.shadowRoot || window.__card.renderRoot).querySelectorAll(s).length, sel);
|
||||
const st = () => page.evaluate(() => {
|
||||
const c = window.__card;
|
||||
const sr = c.shadowRoot || c.renderRoot;
|
||||
return {
|
||||
mode: c._mode,
|
||||
modeTabs: sr.querySelectorAll('.modetab').length,
|
||||
active: sr.querySelector('.modetab.active')?.textContent.trim(),
|
||||
editBtns: sr.querySelectorAll('.head .btn:not(.zb)').length,
|
||||
gears: sr.querySelectorAll('.tabedit').length,
|
||||
markupBar: !!sr.querySelector('.editbar'),
|
||||
stageClass: sr.querySelector('.stage').className,
|
||||
};
|
||||
});
|
||||
// 1) старт: view, чистая шапка
|
||||
out.start = await st();
|
||||
// 2) drag в view ничего не двигает и не мешает пану
|
||||
await page.evaluate(() => {
|
||||
const c = window.__card;
|
||||
const d = c._devices.find((x) => x.space === 'f1');
|
||||
const before = { ...c._pos(d) };
|
||||
c._pointerDown({ preventDefault(){}, clientX: 10, clientY: 10, target: { setPointerCapture(){} }, pointerId: 1 }, d);
|
||||
c._pointerMove({ clientX: 90, clientY: 60 }, d);
|
||||
c._pointerUp({}, d);
|
||||
const after = { ...c._pos(d) };
|
||||
window.__viewDragMoved = Math.abs(after.x - before.x) + Math.abs(after.y - before.y) > 0.5;
|
||||
clearTimeout(c._holdTimer);
|
||||
});
|
||||
out.viewDragMoved = await page.evaluate(() => window.__viewDragMoved);
|
||||
// long-press в view открывает инфо
|
||||
await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const d = c._devices.find((x) => x.space === 'f1');
|
||||
c._pointerDown({ preventDefault(){}, clientX: 10, clientY: 10, target: { setPointerCapture(){} }, pointerId: 2 }, d);
|
||||
await new Promise((r) => setTimeout(r, 700));
|
||||
});
|
||||
out.viewHoldInfo = await page.evaluate(() => { const r = !!window.__card._infoCard; window.__card._infoCard = null; window.__card._holdFired = false; return r; });
|
||||
// 3) режим План
|
||||
await page.evaluate(() => window.__card._setMode('plan'));
|
||||
await page.waitForTimeout(200);
|
||||
out.plan = await st();
|
||||
out.planIconsHidden = await page.evaluate(() => {
|
||||
const sr = window.__card.shadowRoot || window.__card.renderRoot;
|
||||
const dev = sr.querySelector('.dev');
|
||||
return dev ? getComputedStyle(dev).display === 'none' : 'no-dev';
|
||||
});
|
||||
// 4) режим Устройства: drag работает, клик открывает редактор
|
||||
await page.evaluate(() => window.__card._setMode('devices'));
|
||||
await page.waitForTimeout(200);
|
||||
out.devices = await st();
|
||||
out.devDragWorks = await page.evaluate(() => {
|
||||
const c = window.__card;
|
||||
const d = c._devices.find((x) => x.space === 'f1');
|
||||
const before = { ...c._pos(d) };
|
||||
c._pointerDown({ preventDefault(){}, clientX: 10, clientY: 10, target: { setPointerCapture(){} }, pointerId: 3 }, d);
|
||||
c._pointerMove({ clientX: 100, clientY: 70 }, d);
|
||||
c._pointerUp({}, d);
|
||||
const after = { ...c._pos(d) };
|
||||
return Math.abs(after.x - before.x) + Math.abs(after.y - before.y) > 0.5;
|
||||
});
|
||||
out.devClickOpensEditor = await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
c._drag = null;
|
||||
const d = c._devices.find((x) => x.space === 'f1');
|
||||
c._clickDevice({ stopPropagation(){} }, d);
|
||||
await c.updateComplete;
|
||||
const r = !!c._markerDialog;
|
||||
c._markerDialog = null;
|
||||
return r;
|
||||
});
|
||||
// 5) назад в view
|
||||
await page.evaluate(() => window.__card._setMode('view'));
|
||||
out.backToView = (await st()).mode;
|
||||
console.log(JSON.stringify(out, null, 1));
|
||||
await browser.close();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+62
-21
File diff suppressed because one or more lines are too long
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## v1.25.0 — 2026-07-21 (three interaction modes: View / Plan / Devices)
|
||||
The approved UX redesign (docs/UX-MODES.md), confirmed by user feedback (#3):
|
||||
|
||||
- **View** (default, and the only mode after every load): display and device
|
||||
interaction only — tap/long-press/tooltips/pan/zoom. Nothing can be dragged or
|
||||
edited; panning may start on top of an icon and never displaces it. The header
|
||||
carries only space tabs, the counter, zoom and the mode switcher.
|
||||
- **Plan**: everything about geometry and appearance — room outline/delete/merge/
|
||||
split tools, openings (placement, drag along walls, click-to-edit), room-label
|
||||
dragging, per-space gear dialog, add space, the ⚙ fill palette. Orange stage frame.
|
||||
- **Devices**: marker work — icon dragging lives ONLY here, a click opens the
|
||||
editor directly; add device, show-all curation, reset layout, icon rules. Accent frame.
|
||||
- The mode switcher is a segmented control shown to administrators; the standalone
|
||||
markup toggle button is gone, "drag anywhere" (v1.9) is consciously reversed,
|
||||
and the v1.23.1 view-mode opening drag/double-click moved into Plan.
|
||||
|
||||
## v1.24.2 — 2026-07-16 (lights fill: a color for rooms with no light sources)
|
||||
- The "Fill: lights" group in General settings gained a third color — **"No light
|
||||
sources"**. Its default opacity is 0, so rooms without any lights stay unfilled
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
|
||||
| Item | State |
|
||||
|---|---|
|
||||
| Version | **v1.24.2** everywhere (manifest, const.py, package.json, CARD_VERSION) |
|
||||
| Version | **v1.25.0** everywhere (manifest, const.py, package.json, CARD_VERSION) |
|
||||
| GitHub | https://github.com/Matysh/houseplan-card — branch `main`, releases v1.9.3…**v1.23.1** (latest published 2026-07-17, bundle auto-attached by release.yml) |
|
||||
| CI | `.github/workflows/validate.yml` (hacs + hassfest + frontend + backend) — **fully green** since v1.11.1; `release.yml` auto-attaches the card bundle (needs `permissions: contents: write`, fixed) |
|
||||
| HACS | Works as custom repository (id 1290210112 on the home instance). **Inclusion PR: https://github.com/hacs/default/pull/9004** (queue ≈2 months as of 2026-07). Lesson: #8995 was auto-closed by hacs-bot — the PR body MUST be their exact template with every checkbox ticked and all 3 links (release, HACS action run, hassfest run); a custom body gets closed without discussion |
|
||||
|
||||
@@ -29,6 +29,20 @@ Run the *core flows* (marked ★ below) in each environment at least once per mi
|
||||
- [ ] Removal: delete entry → Lovelace resource entry disappears; `.storage/houseplan.*` survives; reinstall picks the old config up
|
||||
- [ ] Diagnostics download works; personal fields (name/link/description/pdfs) are `**REDACTED**` [auto]
|
||||
|
||||
## Modes (v1.25.0) ★
|
||||
|
||||
- [ ] The card always loads in **View**; edit modes are never restored [auto]
|
||||
- [ ] View: pan/zoom/space-switch/tap/long-press/tooltips only — dragging an icon,
|
||||
label or opening does nothing; panning may start on top of an icon [auto]
|
||||
- [ ] View header: space tabs + count + zoom + mode tabs, ZERO edit buttons [auto]
|
||||
- [ ] Plan: markup toolbar, space gears, +space, ⚙ palette; device icons hidden,
|
||||
labels/openings draggable; orange stage frame [auto]
|
||||
- [ ] Devices: icon drag works, click opens the marker editor directly; +/👁/↺/⬡
|
||||
buttons; accent stage frame [auto]
|
||||
- [ ] Mode tabs hidden for non-admin users; segmented control highlights the active mode
|
||||
- [ ] Opening: single click in View shows the info card; in Plan a click edits it;
|
||||
double-click no longer does anything special [auto]
|
||||
|
||||
## Onboarding ★
|
||||
|
||||
- [ ] Empty config, HA has floors → floors-import wizard offers them sorted by level [auto]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "houseplan-card",
|
||||
"version": "1.24.2",
|
||||
"version": "1.25.0",
|
||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
+77
-57
@@ -31,7 +31,7 @@ import './space-card';
|
||||
import { cardStyles } from './styles';
|
||||
import { langOf, t, type I18nKey } from './i18n';
|
||||
|
||||
const CARD_VERSION = '1.24.2';
|
||||
const CARD_VERSION = '1.25.0';
|
||||
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';
|
||||
@@ -81,7 +81,20 @@ class HouseplanCard extends LitElement {
|
||||
private _toastTimer?: number;
|
||||
|
||||
// --- room markup editor ---
|
||||
private _markup = false;
|
||||
/** Interaction mode (docs/UX-MODES.md): view = display only, plan = geometry
|
||||
* editing, devices = marker placement/config. Never persisted — every load
|
||||
* starts in view. */
|
||||
private _mode: 'view' | 'plan' | 'devices' = 'view';
|
||||
|
||||
/** Edit tabs are offered to admins only (hass.user missing → assume admin). */
|
||||
private get _canEdit(): boolean {
|
||||
return this._norm && this.hass?.user?.is_admin !== false;
|
||||
}
|
||||
|
||||
/** Legacy alias: markup machinery is active exactly in plan mode. */
|
||||
private get _markup(): boolean {
|
||||
return this._mode === 'plan';
|
||||
}
|
||||
private _tool: MarkupTool = 'draw';
|
||||
private _path: number[][] = []; // current outline (render units, vertices snapped to the grid)
|
||||
private _cursorPt: number[] | null = null;
|
||||
@@ -99,7 +112,6 @@ class HouseplanCard extends LitElement {
|
||||
} | null = null;
|
||||
private _openingInfo: OpeningCfg | null = null;
|
||||
private _opDrag: { id: string; moved: boolean } | null = null;
|
||||
private _opClickTimer: number | null = null; // first room picked for a merge
|
||||
private _mergeDialog: { aId: string; bId: string; poly: number[][]; pick: 'a' | 'b' } | null = null;
|
||||
private _splitSel: { roomId: string; a: number[] | null } | null = null; // room being cut + first wall point
|
||||
// a split is applied only when the new room's dialog is confirmed — cancel leaves the room intact
|
||||
@@ -196,7 +208,7 @@ class HouseplanCard extends LitElement {
|
||||
_selId: { state: true },
|
||||
_toast: { state: true },
|
||||
_serverCfg: { state: true },
|
||||
_markup: { state: true },
|
||||
_mode: { state: true },
|
||||
_tool: { state: true },
|
||||
_path: { state: true },
|
||||
_cursorPt: { state: true },
|
||||
@@ -682,7 +694,11 @@ class HouseplanCard extends LitElement {
|
||||
private _clickDevice(ev: MouseEvent, d: DevItem): void {
|
||||
ev.stopPropagation();
|
||||
if (this._drag?.moved || this._suppressClick || this._holdFired) return;
|
||||
if (this._markup) return;
|
||||
if (this._mode === 'plan') return;
|
||||
if (this._mode === 'devices') {
|
||||
this._openMarkerDialog(d);
|
||||
return;
|
||||
}
|
||||
const domain = d.primary ? d.primary.split('.')[0] : null;
|
||||
const action = resolveTapAction(d.tapAction, this._config?.tap_action, domain);
|
||||
if (action === 'toggle' && d.primary) {
|
||||
@@ -825,7 +841,7 @@ class HouseplanCard extends LitElement {
|
||||
private _stagePointerDown(ev: PointerEvent): void {
|
||||
// do not interfere with icon dragging and markup drawing
|
||||
if (this._drag || this._markup) return;
|
||||
if ((ev.target as HTMLElement).closest('.dev')) return;
|
||||
if (this._mode === 'devices' && (ev.target as HTMLElement).closest('.dev')) return;
|
||||
this._pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
|
||||
const v = this._viewOr(this._spaceModel().vb);
|
||||
if (this._pointers.size === 1) {
|
||||
@@ -858,7 +874,10 @@ class HouseplanCard extends LitElement {
|
||||
} else if (this._panStart) {
|
||||
const ddx = ev.clientX - this._panStart.sx;
|
||||
const ddy = ev.clientY - this._panStart.sy;
|
||||
if (Math.abs(ddx) + Math.abs(ddy) > 4) this._suppressClick = true;
|
||||
if (Math.abs(ddx) + Math.abs(ddy) > 4) {
|
||||
this._suppressClick = true;
|
||||
clearTimeout(this._holdTimer);
|
||||
}
|
||||
if (this._zoom > 1 && this._view) {
|
||||
const stage = this._stageEl!;
|
||||
const v = this._view;
|
||||
@@ -892,23 +911,23 @@ class HouseplanCard extends LitElement {
|
||||
}
|
||||
|
||||
private _pointerDown(ev: PointerEvent, d: DevItem): void {
|
||||
if (this._markup) return; // in markup mode icons are not dragged
|
||||
if (this._mode === 'plan') return; // icons are hidden in plan mode anyway
|
||||
if (this._mode === 'view') {
|
||||
// view: no drag, no capture — panning may start on an icon; only the
|
||||
// long-press timer runs (cancelled by stage movement)
|
||||
this._holdFired = false;
|
||||
clearTimeout(this._holdTimer);
|
||||
this._holdTimer = window.setTimeout(() => {
|
||||
this._holdFired = true;
|
||||
this._infoCard = d;
|
||||
}, 600);
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
const p = this._pos(d);
|
||||
this._drag = { id: d.id, sx: ev.clientX, sy: ev.clientY, ox: p.x, oy: p.y, moved: false };
|
||||
(ev.target as HTMLElement).setPointerCapture(ev.pointerId);
|
||||
this._tip = null;
|
||||
// long-press always opens the info card — keeps metadata reachable
|
||||
// even when the tap action is set to toggle
|
||||
this._holdFired = false;
|
||||
clearTimeout(this._holdTimer);
|
||||
this._holdTimer = window.setTimeout(() => {
|
||||
if (this._drag && this._drag.id === d.id && !this._drag.moved) {
|
||||
this._holdFired = true;
|
||||
this._drag = null;
|
||||
this._infoCard = d;
|
||||
}
|
||||
}, 600);
|
||||
}
|
||||
|
||||
private _pointerMove(ev: PointerEvent, d: DevItem): void {
|
||||
@@ -998,12 +1017,13 @@ class HouseplanCard extends LitElement {
|
||||
return roomEdges(sp?.rooms || []).map((s) => [s[0] * NORM_W, s[1] * H, s[2] * NORM_W, s[3] * H]);
|
||||
}
|
||||
|
||||
private _toggleMarkup(): void {
|
||||
if (!this._norm) {
|
||||
private _setMode(mode: 'view' | 'plan' | 'devices'): void {
|
||||
if (this._mode === mode) return;
|
||||
if (mode === 'plan' && !this._norm) {
|
||||
this._showToast(this._t('toast.markup_needs_server'));
|
||||
return;
|
||||
}
|
||||
this._markup = !this._markup;
|
||||
this._mode = mode;
|
||||
this._path = [];
|
||||
this._cursorPt = null;
|
||||
this._tool = 'draw';
|
||||
@@ -1011,8 +1031,11 @@ class HouseplanCard extends LitElement {
|
||||
this._mergeDialog = null;
|
||||
this._splitSel = null;
|
||||
this._pendingSplit = null;
|
||||
this._selId = null;
|
||||
this._tip = null;
|
||||
}
|
||||
|
||||
|
||||
private _svgPoint(ev: MouseEvent): number[] {
|
||||
const stage = this.renderRoot.querySelector('.stage') as HTMLElement;
|
||||
const r = stage.getBoundingClientRect();
|
||||
@@ -1198,7 +1221,7 @@ class HouseplanCard extends LitElement {
|
||||
|
||||
/** Drag an opening along the walls (view mode): it re-snaps continuously. */
|
||||
private _opPointerDown(ev: PointerEvent, o: OpeningCfg): void {
|
||||
if (this._markup) return;
|
||||
if (this._mode !== 'plan') return;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
try {
|
||||
@@ -1236,26 +1259,17 @@ class HouseplanCard extends LitElement {
|
||||
/** Click: the status card (delayed so a double click can cancel it). */
|
||||
private _opClick(ev: MouseEvent, o: OpeningCfg & { rx: number; ry: number; rlen: number }): void {
|
||||
ev.stopPropagation();
|
||||
if (this._markup) return;
|
||||
if (this._opDrag?.moved) return; // that click was the tail of a drag
|
||||
if (this._opClickTimer) window.clearTimeout(this._opClickTimer);
|
||||
this._opClickTimer = window.setTimeout(() => {
|
||||
this._opClickTimer = null;
|
||||
if (this._mode === 'view') {
|
||||
// view: an opening is a status object — show the door/lock info card
|
||||
this._openingInfo = o;
|
||||
}, 250);
|
||||
return;
|
||||
}
|
||||
|
||||
/** Double click: the properties dialog. */
|
||||
private _opDblClick(ev: MouseEvent, o: OpeningCfg & { rx: number; ry: number; rlen: number }): void {
|
||||
ev.stopPropagation();
|
||||
if (this._markup) return;
|
||||
if (this._opClickTimer) {
|
||||
window.clearTimeout(this._opClickTimer);
|
||||
this._opClickTimer = null;
|
||||
}
|
||||
this._openingInfo = null;
|
||||
if (this._mode === 'plan' && this._tool !== 'opening') {
|
||||
// plan: any click on an opening edits it (the Opening tool also does)
|
||||
this._editOpening(o);
|
||||
}
|
||||
}
|
||||
|
||||
private _saveOpening(): void {
|
||||
const d = this._openingDialog;
|
||||
@@ -1972,7 +1986,7 @@ class HouseplanCard extends LitElement {
|
||||
// guide the user onward: straight into room markup mode
|
||||
this._importTotal = 0;
|
||||
this._space = this._serverCfg!.spaces[0]?.id || this._space;
|
||||
this._markup = true;
|
||||
this._mode = 'plan';
|
||||
this._tool = 'draw';
|
||||
this._path = [];
|
||||
this._cursorPt = null;
|
||||
@@ -2062,7 +2076,7 @@ class HouseplanCard extends LitElement {
|
||||
else if (this._importTotal > 0 && this._model.length) {
|
||||
this._importTotal = 0;
|
||||
this._space = this._serverCfg!.spaces[0]?.id || this._space;
|
||||
this._markup = true;
|
||||
this._mode = 'plan';
|
||||
this._showToast(this._t('import.done'));
|
||||
}
|
||||
}
|
||||
@@ -2337,7 +2351,7 @@ class HouseplanCard extends LitElement {
|
||||
this._restoreZoom();
|
||||
}}
|
||||
>
|
||||
${s.title}${this._norm
|
||||
${s.title}${this._norm && this._mode === 'plan'
|
||||
? html`<ha-icon class="tabedit" icon="mdi:cog-outline"
|
||||
title=${this._t('title.configure_space')}
|
||||
@click=${(e: Event) => {
|
||||
@@ -2347,13 +2361,24 @@ class HouseplanCard extends LitElement {
|
||||
: nothing}
|
||||
</button>`,
|
||||
)}
|
||||
${this._norm
|
||||
${this._norm && this._mode === 'plan'
|
||||
? html`<button class="tab tabadd" title=${this._t('title.add_space')}
|
||||
@click=${() => this._openSpaceDialog('create')}>
|
||||
<ha-icon icon="mdi:plus"></ha-icon>
|
||||
</button>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._canEdit
|
||||
? html`<div class="modes">
|
||||
${([['view', 'mdi:eye-outline'], ['plan', 'mdi:floor-plan'], ['devices', 'mdi:tune-variant']] as const).map(
|
||||
([m, ic]) => html`<button class="modetab ${this._mode === m ? 'active' : ''}"
|
||||
title=${this._t(('mode.' + m) as any)}
|
||||
@click=${() => this._setMode(m)}>
|
||||
<ha-icon icon=${ic}></ha-icon><span class="ml">${this._t(('mode.' + m) as any)}</span>
|
||||
</button>`,
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
<span class="count">${this._t('count.devices', { n: devs.length })}</span>
|
||||
<span class="spacer"></span>
|
||||
<div class="zoomctl">
|
||||
@@ -2362,14 +2387,12 @@ class HouseplanCard extends LitElement {
|
||||
title=${this._t('title.zoom_reset')}><ha-icon icon="mdi:fit-to-page-outline"></ha-icon></button>
|
||||
<button class="btn zb" @click=${() => this._stepZoom(1)} title=${this._t('title.zoom_in')}><ha-icon icon="mdi:plus"></ha-icon></button>
|
||||
</div>
|
||||
${this._norm
|
||||
${this._norm && this._mode === 'devices'
|
||||
? html`<button class="btn" @click=${() => this._openMarkerDialog()}
|
||||
title=${this._t('title.add_device')}>
|
||||
<ha-icon icon="mdi:plus-box-outline"></ha-icon>
|
||||
</button>`
|
||||
: nothing}
|
||||
${this._norm
|
||||
? html`<button class="btn ${this._showAll ? 'on' : ''}" @click=${this._toggleShowAll}
|
||||
</button>
|
||||
<button class="btn ${this._showAll ? 'on' : ''}" @click=${this._toggleShowAll}
|
||||
title=${this._t('title.show_all')}>
|
||||
<ha-icon icon="${this._showAll ? 'mdi:eye' : 'mdi:eye-off-outline'}"></ha-icon>
|
||||
</button>
|
||||
@@ -2378,20 +2401,18 @@ class HouseplanCard extends LitElement {
|
||||
</button>
|
||||
<button class="btn" @click=${this._openRulesDialog} title=${this._t('title.icon_rules')}>
|
||||
<ha-icon icon="mdi:shape-plus-outline"></ha-icon>
|
||||
</button>
|
||||
<button class="btn" @click=${this._openSettingsDialog} title=${this._t('title.general_settings')}>
|
||||
</button>`
|
||||
: nothing}
|
||||
${this._norm && this._mode === 'plan'
|
||||
? html`<button class="btn" @click=${this._openSettingsDialog} title=${this._t('title.general_settings')}>
|
||||
<ha-icon icon="mdi:cog-outline"></ha-icon>
|
||||
</button>`
|
||||
: nothing}
|
||||
<button class="btn ${this._markup ? 'on' : ''}" @click=${this._toggleMarkup}
|
||||
title=${this._t('title.markup')}>
|
||||
<ha-icon icon="mdi:vector-square-edit"></ha-icon>
|
||||
</button>
|
||||
</div>
|
||||
${this._markup ? this._renderMarkupBar() : nothing}
|
||||
</div>
|
||||
|
||||
<div class="stage ${this._markup ? 'markup' : ''} ${space.bg ? '' : 'noplan'}"
|
||||
<div class="stage ${this._markup ? 'markup' : ''} ${space.bg ? '' : 'noplan'} mode-${this._mode}"
|
||||
style="height:calc(100dvh - 118px)"
|
||||
@click=${(e: MouseEvent) => this._markupClick(e)}
|
||||
@wheel=${(e: WheelEvent) => this._onWheel(e)}
|
||||
@@ -2553,7 +2574,7 @@ class HouseplanCard extends LitElement {
|
||||
|
||||
/** Room-name labels are dragged exactly like device icons (same layout store). */
|
||||
private _labelDown(ev: PointerEvent, r: RoomCfg, spaceId: string): void {
|
||||
if (this._markup) return;
|
||||
if (this._mode !== 'plan') return;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const p = this._labelPos(r, spaceId);
|
||||
@@ -2693,7 +2714,6 @@ class HouseplanCard extends LitElement {
|
||||
<rect class="op-outline" x="${-half - 10}" y="-16" width="${o.rlen + 20}" height="32" rx="6"></rect>
|
||||
<rect class="op-hit" x="${-half - 12}" y="-20" width="${o.rlen + 24}" height="40"
|
||||
@click=${(e: MouseEvent) => this._opClick(e, o)}
|
||||
@dblclick=${(e: MouseEvent) => this._opDblClick(e, o)}
|
||||
@pointerdown=${(e: PointerEvent) => this._opPointerDown(e, o)}
|
||||
@pointermove=${(e: PointerEvent) => this._opPointerMove(e, o)}
|
||||
@pointerup=${(e: PointerEvent) => this._opPointerUp(e, o)}
|
||||
@@ -2719,7 +2739,7 @@ class HouseplanCard extends LitElement {
|
||||
const top = ((py - view.y) / view.h) * 100;
|
||||
return html`<div class="oplock ${locked ? 'locked' : known ? 'unlocked' : 'unknown'}"
|
||||
style="left:${left}%;top:${top}%"
|
||||
@click=${(e: MouseEvent) => { e.stopPropagation(); if (!this._markup) this._openingInfo = o; }}>
|
||||
@click=${(e: MouseEvent) => { e.stopPropagation(); if (this._mode === 'view') this._openingInfo = o; }}>
|
||||
<ha-icon icon="${locked ? 'mdi:lock' : known ? 'mdi:lock-open-variant' : 'mdi:lock-question'}"></ha-icon>
|
||||
</div>`;
|
||||
})}`;
|
||||
|
||||
+4
-1
@@ -239,5 +239,8 @@
|
||||
"gs.reset": "Reset to defaults",
|
||||
"gs.saved": "General settings saved",
|
||||
"space.show_lqi": "Show zigbee signal (LQI) next to devices",
|
||||
"gs.light_none": "No light sources"
|
||||
"gs.light_none": "No light sources",
|
||||
"mode.view": "View",
|
||||
"mode.plan": "Plan",
|
||||
"mode.devices": "Devices"
|
||||
}
|
||||
|
||||
+4
-1
@@ -239,5 +239,8 @@
|
||||
"gs.reset": "Сбросить к умолчаниям",
|
||||
"gs.saved": "Общие настройки сохранены",
|
||||
"space.show_lqi": "Показывать зигби-сигнал (LQI) у устройств",
|
||||
"gs.light_none": "Нет источников света"
|
||||
"gs.light_none": "Нет источников света",
|
||||
"mode.view": "Просмотр",
|
||||
"mode.plan": "План",
|
||||
"mode.devices": "Устройства"
|
||||
}
|
||||
|
||||
+42
-3
@@ -341,6 +341,7 @@ export const cardStyles = css`
|
||||
.ripple.active i:nth-child(n + 2) { display: none; }
|
||||
}
|
||||
.roomlabel {
|
||||
pointer-events: none; /* draggable only in plan mode (rule below) */
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: calc(var(--icon-size, 2.5cqw) * 0.5);
|
||||
@@ -348,10 +349,10 @@ export const cardStyles = css`
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
cursor: grab;
|
||||
pointer-events: auto;
|
||||
user-select: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.stage.markup .roomlabel { pointer-events: auto; }
|
||||
.roomlabel:active { cursor: grabbing; }
|
||||
.measurelayer {
|
||||
position: absolute;
|
||||
@@ -388,8 +389,46 @@ export const cardStyles = css`
|
||||
.stage.markup .room {
|
||||
pointer-events: none;
|
||||
}
|
||||
.stage.markup .devlayer {
|
||||
display: none; /* in markup mode icons must not get in the way */
|
||||
.stage.markup .devlayer .dev {
|
||||
display: none; /* in plan mode the icons do not get in the way; labels stay */
|
||||
}
|
||||
/* mode frames: the edit modes are visible at a glance */
|
||||
.stage.mode-plan {
|
||||
outline: 2px solid #ffc14d;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.stage.mode-devices {
|
||||
outline: 2px solid var(--hp-accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.modes {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
background: rgba(127, 127, 127, 0.12);
|
||||
border-radius: 10px;
|
||||
padding: 3px;
|
||||
}
|
||||
.modetab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hp-muted);
|
||||
padding: 5px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
.modetab ha-icon { --mdc-icon-size: 15px; }
|
||||
.modetab.active {
|
||||
background: var(--hp-accent);
|
||||
color: var(--text-primary-color, #fff);
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.modetab .ml { display: none; }
|
||||
}
|
||||
.room.picked {
|
||||
stroke: #ffc14d;
|
||||
|
||||
Reference in New Issue
Block a user