mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
ux v1.43.3: room gear discoverability, bigger metrics, touch tooltips take two
- the room gear became a fixed-size pill button (was 0.9em/60% opacity inside the label — invisible in practice, field report); shown on unnamed rooms too, which is where you name them - metrics line 0.62em -> 0.75em (unreadable on tablets) - tooltips: latch on the first touch/pen pointer event instead of trusting (hover: none) alone; any touch drops an open tip - smoke_feedback_v2.mjs; docs same-commit
This commit is contained in:
@@ -13,7 +13,7 @@ FILES_URL = "/houseplan_files/files"
|
|||||||
CONTENT_URL = "/api/houseplan/content"
|
CONTENT_URL = "/api/houseplan/content"
|
||||||
FILES_DIR = "houseplan/files"
|
FILES_DIR = "houseplan/files"
|
||||||
CONF_ADMIN_ONLY = "admin_only"
|
CONF_ADMIN_ONLY = "admin_only"
|
||||||
VERSION = "1.43.2"
|
VERSION = "1.43.3"
|
||||||
|
|
||||||
DEFAULT_CONFIG: dict = {
|
DEFAULT_CONFIG: dict = {
|
||||||
"spaces": [],
|
"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",
|
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"version": "1.43.2"
|
"version": "1.43.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { launch, check, checkAll, finish } from './serve.mjs';
|
||||||
|
const { page, browser } = await launch();
|
||||||
|
const res = await page.evaluate(async () => {
|
||||||
|
const out = {};
|
||||||
|
const c = window.__card;
|
||||||
|
const sr = () => c.shadowRoot || c.renderRoot;
|
||||||
|
// 1) кнопка настроек комнаты в редакторе плана: заметная, фиксированного размера
|
||||||
|
c._setMode('plan'); await c.updateComplete;
|
||||||
|
const btn = sr().querySelector('.rlgearbtn');
|
||||||
|
out.gearButtonShown = !!btn;
|
||||||
|
const cs = btn ? getComputedStyle(btn) : null;
|
||||||
|
out.gearReadable = cs ? parseFloat(cs.fontSize) >= 10 && cs.pointerEvents === 'auto' : null;
|
||||||
|
out.gearHasLabel = btn ? btn.textContent.trim().length > 0 : null;
|
||||||
|
const box = btn?.getBoundingClientRect();
|
||||||
|
out.gearTapTarget = box ? box.height >= 18 && box.width >= 40 : null;
|
||||||
|
btn.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
||||||
|
await c.updateComplete;
|
||||||
|
out.gearOpensDialog = c._roomDialog === true && !!c._roomEditId;
|
||||||
|
c._roomDialogCancel(); await c.updateComplete;
|
||||||
|
// 2) комната без имени тоже получает кнопку (её там и называют)
|
||||||
|
const room = c._curSpaceCfg.rooms[0];
|
||||||
|
const savedName = room.name;
|
||||||
|
room.name = '';
|
||||||
|
c._saveConfig(); c.requestUpdate(); await c.updateComplete;
|
||||||
|
out.unnamedStillHasGear = sr().querySelectorAll('.rlgearbtn').length >= 1;
|
||||||
|
room.name = savedName; c._saveConfig(); c.requestUpdate(); await c.updateComplete;
|
||||||
|
// 3) метрики стали крупнее: 0.75em вместо 0.62em
|
||||||
|
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== c._space ? s : ({
|
||||||
|
...s, settings: { ...(s.settings || {}), show_names: true, label_temp: true } })) };
|
||||||
|
c._setMode('view'); c._saveConfig(); c.requestUpdate(); await c.updateComplete;
|
||||||
|
await new Promise((r) => setTimeout(r, 150));
|
||||||
|
const lbl = [...sr().querySelectorAll('.roomlabel')].find((l) => l.querySelector('.rlmetrics'));
|
||||||
|
if (lbl) {
|
||||||
|
const nameSz = parseFloat(getComputedStyle(lbl.querySelector('.rlname')).fontSize);
|
||||||
|
const metaSz = parseFloat(getComputedStyle(lbl.querySelector('.rlmetrics')).fontSize);
|
||||||
|
out.metricsRatio = Math.round((metaSz / nameSz) * 100) / 100;
|
||||||
|
} else out.metricsRatio = 'no-metrics';
|
||||||
|
// 4) касание помечает сессию как тач и гасит тултип
|
||||||
|
c._tip = { x: 1, y: 1, title: 't', meta: 'm' };
|
||||||
|
c._notePointer(new PointerEvent('pointerdown', { pointerType: 'touch' }));
|
||||||
|
out.touchClearsTip = c._tip === null;
|
||||||
|
c._showTip(new MouseEvent('mousemove', { clientX: 5, clientY: 5 }), 'x', 'y');
|
||||||
|
out.noTipAfterTouch = !c._tip;
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
check('metricsRatio 0.75', res.metricsRatio, 0.75);
|
||||||
|
delete res.metricsRatio;
|
||||||
|
checkAll(res);
|
||||||
|
await finish(browser, res);
|
||||||
@@ -10,7 +10,7 @@ const res = await page.evaluate(async () => {
|
|||||||
...s, settings: { ...(s.settings || {}), show_names: true, fill_mode: 'temp', label_temp: true } })) };
|
...s, settings: { ...(s.settings || {}), show_names: true, fill_mode: 'temp', label_temp: true } })) };
|
||||||
c._setMode('plan'); c.requestUpdate(); await c.updateComplete;
|
c._setMode('plan'); c.requestUpdate(); await c.updateComplete;
|
||||||
// 1) шестерёнка на карточке комнаты в редакторе плана
|
// 1) шестерёнка на карточке комнаты в редакторе плана
|
||||||
const gear = sr().querySelector('.rlgear');
|
const gear = sr().querySelector('.rlgearbtn');
|
||||||
out.gearShown = !!gear;
|
out.gearShown = !!gear;
|
||||||
gear.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
gear.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
||||||
await c.updateComplete;
|
await c.updateComplete;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Vendored
+32
-9
File diff suppressed because one or more lines are too long
@@ -1,5 +1,21 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.43.3 — 2026-07-27 (user feedback: discoverability and touch)
|
||||||
|
|
||||||
|
- **Room settings were unfindable.** The gear added in v1.42.0 lived inside the
|
||||||
|
room label at 0.9em of its font and 60% opacity — a few pale pixels on a
|
||||||
|
normal plan. It is now a pill button "⚙ Room" of a fixed, readable size that
|
||||||
|
does not shrink with the card font, and it appears on **unnamed rooms too**
|
||||||
|
(that is where you name them). This also unblocks the font-size sliders,
|
||||||
|
which nobody could reach.
|
||||||
|
- **Metrics line enlarged** from 0.62 to 0.75 of the room name — the reporter
|
||||||
|
could scale the name but the sensor line stayed unreadable on a tablet. The
|
||||||
|
per-room and per-space multipliers still apply on top.
|
||||||
|
- **Touch tooltips, take two.** The `(hover: none)` guard was not enough: some
|
||||||
|
devices, skins, styluses and paired mice report `hover: hover`, so tips still
|
||||||
|
stuck under the finger. The card now also latches on the first touch/pen
|
||||||
|
pointer event and drops any open tooltip on touch.
|
||||||
|
|
||||||
## v1.43.2 — 2026-07-27 (external audit: the test layer)
|
## v1.43.2 — 2026-07-27 (external audit: the test layer)
|
||||||
|
|
||||||
- **The smoke suite can finally fail (T1).** All 48 headless-browser smokes used
|
- **The smoke suite can finally fail (T1).** All 48 headless-browser smokes used
|
||||||
|
|||||||
@@ -20,6 +20,16 @@
|
|||||||
bundle. Sanity ritual: break one invariant on purpose (e.g. remove the
|
bundle. Sanity ritual: break one invariant on purpose (e.g. remove the
|
||||||
kiosk editor guard) and confirm the matching smoke goes red [auto: CI job "smoke"]
|
kiosk editor guard) and confirm the matching smoke goes red [auto: CI job "smoke"]
|
||||||
|
|
||||||
|
- [ ] Room gear discoverability (v1.43.3, user feedback): in the Plan editor
|
||||||
|
every room card carries a pill button "⚙ Room" of a FIXED readable size
|
||||||
|
(independent of the card font) — including rooms without a name; it opens
|
||||||
|
Room settings [auto: smoke_feedback_v2]
|
||||||
|
- [ ] Metrics readability (v1.43.3): the metrics line is 0.75 of the room name
|
||||||
|
(was 0.62 — unreadable on tablets); per-room sliders still apply on top [auto: smoke_feedback_v2]
|
||||||
|
- [ ] Touch tooltips, take two (v1.43.3): a hover tooltip never appears after
|
||||||
|
ANY touch/pen pointer event, even if the browser claims `hover: hover`
|
||||||
|
(stylus, paired mouse, vendor skins) [auto: smoke_feedback_v2]
|
||||||
|
|
||||||
## Environments matrix
|
## Environments matrix
|
||||||
|
|
||||||
Run the *core flows* (marked ★ below) in each environment at least once per minor release:
|
Run the *core flows* (marked ★ below) in each environment at least once per minor release:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.43.2",
|
"version": "1.43.3",
|
||||||
"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",
|
||||||
|
|||||||
+36
-14
@@ -32,7 +32,7 @@ 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.43.2';
|
const CARD_VERSION = '1.43.3';
|
||||||
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';
|
||||||
@@ -1350,17 +1350,35 @@ class HouseplanCard extends LitElement {
|
|||||||
}, 3500);
|
}, 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True on touch-first devices (tablets/phones): no real hover there. */
|
/**
|
||||||
private static readonly _noHover =
|
* Touch-first surface: no hover tooltips.
|
||||||
|
*
|
||||||
|
* The media query alone was not enough (field report, 2026-07-27: tooltips
|
||||||
|
* still stuck on a OnePlus). Some devices/skins report `hover: hover`, and a
|
||||||
|
* stylus or a paired mouse flips it too. So this also latches on the FIRST
|
||||||
|
* touch pointer event and never unlatches for that session — a device that
|
||||||
|
* has been touched once is a touch device.
|
||||||
|
*/
|
||||||
|
private static _touchSeen = false;
|
||||||
|
private static readonly _noHoverMq =
|
||||||
typeof window !== 'undefined' &&
|
typeof window !== 'undefined' &&
|
||||||
typeof window.matchMedia === 'function' &&
|
typeof window.matchMedia === 'function' &&
|
||||||
window.matchMedia('(hover: none)').matches;
|
window.matchMedia('(hover: none)').matches;
|
||||||
|
|
||||||
|
private get _noHover(): boolean {
|
||||||
|
return HouseplanCard._noHoverMq || HouseplanCard._touchSeen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Any touch anywhere marks the session as touch-first and kills open tips. */
|
||||||
|
private _notePointer(ev: PointerEvent): void {
|
||||||
|
if (ev.pointerType === 'touch' || ev.pointerType === 'pen') {
|
||||||
|
HouseplanCard._touchSeen = true;
|
||||||
|
if (this._tip) this._tip = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _showTip(ev: MouseEvent, title: string, meta: string, lqi?: number | null, temp?: number | null): void {
|
private _showTip(ev: MouseEvent, title: string, meta: string, lqi?: number | null, temp?: number | null): void {
|
||||||
// Field feedback: on tablets every tap synthesized a mousemove and popped
|
if (this._noHover) return;
|
||||||
// the hover tooltip over the finger. Touch devices get NO hover tooltips —
|
|
||||||
// the same data lives in room cards and the long-press device card.
|
|
||||||
if (HouseplanCard._noHover) return;
|
|
||||||
if (this._drag) return;
|
if (this._drag) return;
|
||||||
this._tip = { x: ev.clientX, y: ev.clientY, title, meta, lqi, temp };
|
this._tip = { x: ev.clientX, y: ev.clientY, title, meta, lqi, temp };
|
||||||
}
|
}
|
||||||
@@ -3475,7 +3493,7 @@ class HouseplanCard extends LitElement {
|
|||||||
style="height:${this._kiosk ? '100dvh' : 'calc(100dvh - 118px)'}"
|
style="height:${this._kiosk ? '100dvh' : 'calc(100dvh - 118px)'}"
|
||||||
@click=${(e: MouseEvent) => this._markupClick(e)}
|
@click=${(e: MouseEvent) => this._markupClick(e)}
|
||||||
@wheel=${(e: WheelEvent) => this._onWheel(e)}
|
@wheel=${(e: WheelEvent) => this._onWheel(e)}
|
||||||
@pointerdown=${(e: PointerEvent) => this._stagePointerDown(e)}
|
@pointerdown=${(e: PointerEvent) => { this._notePointer(e); this._stagePointerDown(e); }}
|
||||||
@pointermove=${(e: PointerEvent) => this._stagePointerMove(e)}
|
@pointermove=${(e: PointerEvent) => this._stagePointerMove(e)}
|
||||||
@pointerup=${(e: PointerEvent) => this._stagePointerUp(e)}
|
@pointerup=${(e: PointerEvent) => this._stagePointerUp(e)}
|
||||||
@pointercancel=${(e: PointerEvent) => this._stagePointerUp(e)}>
|
@pointercancel=${(e: PointerEvent) => this._stagePointerUp(e)}>
|
||||||
@@ -3920,7 +3938,9 @@ class HouseplanCard extends LitElement {
|
|||||||
private _renderRoomLabel(
|
private _renderRoomLabel(
|
||||||
r: RoomCfg, space: SpaceModel, view: { x: number; y: number; w: number; h: number }, disp: SpaceDisplay,
|
r: RoomCfg, space: SpaceModel, view: { x: number; y: number; w: number; h: number }, disp: SpaceDisplay,
|
||||||
): TemplateResult | typeof nothing {
|
): TemplateResult | typeof nothing {
|
||||||
if (!r.name) return nothing;
|
// audit/feedback: rooms without a name still need their gear in the Plan
|
||||||
|
// editor — that is where you name them (field report, 2026-07-27)
|
||||||
|
if (!r.name && !this._markup) return nothing;
|
||||||
const p = this._labelPos(r, space.id);
|
const p = this._labelPos(r, space.id);
|
||||||
const left = ((p.x - view.x) / view.w) * 100;
|
const left = ((p.x - view.x) / view.w) * 100;
|
||||||
const top = ((p.y - view.y) / view.h) * 100;
|
const top = ((p.y - view.y) / view.h) * 100;
|
||||||
@@ -3959,12 +3979,14 @@ class HouseplanCard extends LitElement {
|
|||||||
@pointermove=${(e: PointerEvent) => this._labelMove(e, r, space.id)}
|
@pointermove=${(e: PointerEvent) => this._labelMove(e, r, space.id)}
|
||||||
@pointerup=${() => this._labelUp(r)}
|
@pointerup=${() => this._labelUp(r)}
|
||||||
@pointercancel=${() => this._labelUp(r)}
|
@pointercancel=${() => this._labelUp(r)}
|
||||||
><span class="rlname">${this._markup && r.id
|
>${this._markup && r.id
|
||||||
? html`<ha-icon class="rlgear" icon="mdi:cog-outline"
|
? html`<button class="rlgearbtn" title=${this._t('room.settings_title')}
|
||||||
title=${this._t('room.settings_title')}
|
|
||||||
@pointerdown=${(e: Event) => e.stopPropagation()}
|
@pointerdown=${(e: Event) => e.stopPropagation()}
|
||||||
@click=${(e: Event) => { e.stopPropagation(); this._openRoomEdit(r); }}></ha-icon>`
|
@click=${(e: Event) => { e.stopPropagation(); this._openRoomEdit(r); }}>
|
||||||
: nothing}${r.name}${!this._markup && r.area
|
<ha-icon icon="mdi:cog-outline"></ha-icon>
|
||||||
|
<span class="rlgeartext">${this._t('room.settings_short')}</span>
|
||||||
|
</button>`
|
||||||
|
: nothing}<span class="rlname">${r.name || (this._markup ? this._t('room.unnamed') : '')}${!this._markup && r.area
|
||||||
? html`<ha-icon class="rlgo" icon="mdi:open-in-new"
|
? html`<ha-icon class="rlgo" icon="mdi:open-in-new"
|
||||||
title=${this._t('room.open_area')}
|
title=${this._t('room.open_area')}
|
||||||
@click=${(e: Event) => { e.stopPropagation(); this._clickRoom(r); }}
|
@click=${(e: Event) => { e.stopPropagation(); this._clickRoom(r); }}
|
||||||
|
|||||||
+3
-1
@@ -323,5 +323,7 @@
|
|||||||
"room.name_scale": "Room name size",
|
"room.name_scale": "Room name size",
|
||||||
"room.label_scale": "Metrics size",
|
"room.label_scale": "Metrics size",
|
||||||
"preview.room_name": "Living room",
|
"preview.room_name": "Living room",
|
||||||
"toast.cfg_reload_failed": "Could not reload the plan from the server: {err}"
|
"toast.cfg_reload_failed": "Could not reload the plan from the server: {err}",
|
||||||
|
"room.settings_short": "Room",
|
||||||
|
"room.unnamed": "Unnamed room"
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -323,5 +323,7 @@
|
|||||||
"room.name_scale": "Размер названия",
|
"room.name_scale": "Размер названия",
|
||||||
"room.label_scale": "Размер подписей",
|
"room.label_scale": "Размер подписей",
|
||||||
"preview.room_name": "Гостиная",
|
"preview.room_name": "Гостиная",
|
||||||
"toast.cfg_reload_failed": "Не удалось перечитать план с сервера: {err}"
|
"toast.cfg_reload_failed": "Не удалось перечитать план с сервера: {err}",
|
||||||
|
"room.settings_short": "Комната",
|
||||||
|
"room.unnamed": "Комната без имени"
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-1
@@ -383,6 +383,27 @@ export const cardStyles = css`
|
|||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
font-size: calc(1em * var(--rl-name, 1));
|
font-size: calc(1em * var(--rl-name, 1));
|
||||||
}
|
}
|
||||||
|
.rlgearbtn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--hp-accent);
|
||||||
|
color: var(--text-primary-color, #fff);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
opacity: 0.92;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
.rlgearbtn:hover { opacity: 1; }
|
||||||
|
.rlgearbtn ha-icon { --mdc-icon-size: 14px; display: inline-flex; }
|
||||||
.rlgear {
|
.rlgear {
|
||||||
--mdc-icon-size: 0.9em;
|
--mdc-icon-size: 0.9em;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -406,7 +427,7 @@ export const cardStyles = css`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.55em;
|
gap: 0.55em;
|
||||||
font-size: calc(0.62em * var(--rl-meta, 1));
|
font-size: calc(0.75em * var(--rl-meta, 1)); /* feedback: 0.62 was unreadable on a tablet */
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
|
|||||||
Reference in New Issue
Block a user