mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
fix: keep room labels aligned across modes
Issue: #200 User-Visible: yes
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -163,6 +163,24 @@ export function prepareGoldenFixture(scenario) {
|
||||
if (!space) throw new Error(`golden override references missing space: ${scenario.space}`);
|
||||
return space;
|
||||
};
|
||||
if (scenario.roomLabelParity) {
|
||||
const space = requireSpace();
|
||||
if (space.id !== 'golden-lighting' || space.rooms.length !== 2)
|
||||
throw new Error(`golden roomLabelParity requires the two-room lighting fixture: ${space.id}`);
|
||||
space.settings = {
|
||||
...(space.settings || {}),
|
||||
show_names: true,
|
||||
label_temp: true,
|
||||
label_hum: true,
|
||||
label_lqi: true,
|
||||
label_light: true,
|
||||
};
|
||||
fixture.layout = {
|
||||
...(fixture.layout || {}),
|
||||
'rl_light-left': { s: space.id, x: 0.28, y: 0.24 },
|
||||
'rl_light-right': { s: space.id, x: 0.72, y: 0.24 },
|
||||
};
|
||||
}
|
||||
if (scenario.deviceName) {
|
||||
if (!scenario.deviceId || !fixture.devices?.[scenario.deviceId])
|
||||
throw new Error(`golden deviceName references missing device: ${scenario.deviceId || '<empty>'}`);
|
||||
@@ -399,6 +417,14 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
await card.updateComplete;
|
||||
await settleMode(card);
|
||||
}
|
||||
if (scenario.roomLabelParity) {
|
||||
const labels = [...card.renderRoot.querySelectorAll('.roomlabel')];
|
||||
if (labels.length !== 2
|
||||
|| labels.some((label) => !label.querySelector('.rlgo') || !label.querySelector('.rlmetrics'))
|
||||
|| labels.some((label) => !label.querySelector('.rlmetrics')?.textContent?.trim())) {
|
||||
throw new Error(`golden room-label parity core is incomplete: ${scenario.id}`);
|
||||
}
|
||||
}
|
||||
if (scenario.projection === 'iso' && typeof card._setProjection === 'function') {
|
||||
card._setProjection('iso');
|
||||
await card.updateComplete;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fixtureWallKey } from '../fixtures/visual-matrix.mjs';
|
||||
|
||||
/** Data-only HP-QA-01 capture matrix. Bump when framing or scenarios change. */
|
||||
export const GOLDEN_MATRIX_VERSION = 30;
|
||||
export const GOLDEN_MATRIX_VERSION = 31;
|
||||
|
||||
const stage = { capture: 'stage', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0005 } };
|
||||
const page = { capture: 'page', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0008 } };
|
||||
@@ -44,6 +44,14 @@ export const GOLDEN_SCENARIOS = Object.freeze([
|
||||
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'geometry-view-light-fit', fixture: 'visual', space: 'golden-geometry', mode: 'view',
|
||||
theme: 'light', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'room-label-parity-view-dark', fixture: 'visual', space: 'golden-lighting', mode: 'view',
|
||||
roomLabelParity: true, theme: 'dark', viewport: { width: 1000, height: 900 }, ...page },
|
||||
{ id: 'room-label-parity-plan-dark', fixture: 'visual', space: 'golden-lighting', mode: 'plan',
|
||||
roomLabelParity: true, theme: 'dark', viewport: { width: 1000, height: 900 }, ...page },
|
||||
{ id: 'room-label-parity-view-light', fixture: 'visual', space: 'golden-lighting', mode: 'view',
|
||||
roomLabelParity: true, theme: 'light', viewport: { width: 1000, height: 900 }, ...page },
|
||||
{ id: 'room-label-parity-plan-light', fixture: 'visual', space: 'golden-lighting', mode: 'plan',
|
||||
roomLabelParity: true, theme: 'light', viewport: { width: 1000, height: 900 }, ...page },
|
||||
{ id: 'washer-active-cycle-dark', fixture: 'visual', applianceLifecycle: true,
|
||||
space: 'golden-appliance', mode: 'view',
|
||||
stateOverrides: { 'sensor.golden_washer_status': { state: 'start' } },
|
||||
|
||||
+184
-37
@@ -1,38 +1,185 @@
|
||||
import { launch, 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;
|
||||
// включить имена комнат
|
||||
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== c._space ? s : ({
|
||||
...s, settings: { ...(s.settings || {}), show_names: true } })) };
|
||||
c._setMode('view'); c.requestUpdate(); await c.updateComplete;
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
// 1) комната не кликабельна: cursor default, обработчика клика нет
|
||||
const room = sr().querySelector('.room');
|
||||
out.roomCursor = getComputedStyle(room).cursor === 'default';
|
||||
// 2) значок ссылки у комнат с зоной, кликабелен, ведёт в зону
|
||||
const links = sr().querySelectorAll('.rlgo');
|
||||
out.linksShown = links.length > 0;
|
||||
const withArea = c._spaceModel().rooms.filter((r) => r.name && r.area).length;
|
||||
out.linkPerArearoom = links.length === withArea;
|
||||
const icon = links[0];
|
||||
out.linkCursor = getComputedStyle(icon).cursor === 'pointer';
|
||||
out.linkClickable = getComputedStyle(icon).pointerEvents === 'auto';
|
||||
// навигация: перехват history
|
||||
let navTo = null;
|
||||
const orig = history.pushState.bind(history);
|
||||
history.pushState = (a, b, url) => { navTo = url; };
|
||||
icon.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
||||
await c.updateComplete;
|
||||
history.pushState = orig;
|
||||
out.navigates = typeof navTo === 'string' && navTo.includes('/config/areas/area/');
|
||||
// 3) комнаты без зоны — без значка (проверено соотношением выше); в разметке значков нет
|
||||
c._setMode('plan'); await c.updateComplete;
|
||||
out.noneInPlan = sr().querySelectorAll('.rlgo').length === 0;
|
||||
c._setMode('view'); await c.updateComplete;
|
||||
return out;
|
||||
});
|
||||
checkAll(res);
|
||||
await finish(browser, res);
|
||||
|
||||
const checks = {};
|
||||
const geometry = [];
|
||||
|
||||
for (const scale of [1, 2]) {
|
||||
const { page, browser } = await launch({ width: 820, height: 760 }, scale);
|
||||
for (const dark of [false, true]) {
|
||||
const key = `dpr${scale}-${dark ? 'dark' : 'light'}`;
|
||||
const setup = await page.evaluate(async ({ dark }) => {
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const settle = async () => {
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
const deadline = performance.now() + 2000;
|
||||
while (c._modeTransitionBusy && performance.now() < deadline)
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
await c.updateComplete;
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
};
|
||||
const labelById = (id) => [...sr().querySelectorAll('.roomlabel')]
|
||||
.find((label) => label.dataset.id === id);
|
||||
const relativeBox = (label, selector) => {
|
||||
const anchorBox = label.getBoundingClientRect();
|
||||
const box = label.querySelector(selector)?.getBoundingClientRect();
|
||||
if (!box) return null;
|
||||
const anchorX = anchorBox.left + anchorBox.width / 2;
|
||||
const anchorY = anchorBox.top + anchorBox.height / 2;
|
||||
return {
|
||||
left: box.left - anchorX,
|
||||
top: box.top - anchorY,
|
||||
width: box.width,
|
||||
height: box.height,
|
||||
};
|
||||
};
|
||||
const snapshot = (label) => ({
|
||||
name: relativeBox(label, '.rlname'),
|
||||
metrics: relativeBox(label, '.rlmetrics'),
|
||||
metricsText: label.querySelector('.rlmetrics')?.textContent?.replace(/\s+/g, ' ').trim() || '',
|
||||
});
|
||||
|
||||
c.hass = {
|
||||
...c.hass,
|
||||
themes: { ...(c.hass.themes || {}), darkMode: dark },
|
||||
};
|
||||
c._serverCfg = {
|
||||
...c._serverCfg,
|
||||
spaces: c._serverCfg.spaces.map((space) => space.id !== c._space ? space : ({
|
||||
...space,
|
||||
settings: {
|
||||
...(space.settings || {}),
|
||||
show_names: true,
|
||||
label_temp: true,
|
||||
label_hum: true,
|
||||
label_lqi: true,
|
||||
label_light: true,
|
||||
},
|
||||
})),
|
||||
};
|
||||
const targetRoom = c._spaceModel().rooms.find((room) => room.name && room.area);
|
||||
if (targetRoom?.id) {
|
||||
c._layout = {
|
||||
...c._layout,
|
||||
[`rl_${targetRoom.id}`]: { s: c._space, x: 0.16, y: 0.22 },
|
||||
};
|
||||
}
|
||||
c._setMode('view');
|
||||
await settle();
|
||||
|
||||
const labels = [...sr().querySelectorAll('.roomlabel')];
|
||||
const label = labels.find((candidate) => candidate.querySelector('.rlgo')
|
||||
&& candidate.querySelector('.rlmetrics'));
|
||||
if (!label) return { missingFixture: true };
|
||||
const roomId = label.dataset.id;
|
||||
const view = snapshot(label);
|
||||
const links = sr().querySelectorAll('.rlgo');
|
||||
const withArea = c._spaceModel().rooms.filter((room) => room.name && room.area).length;
|
||||
const room = sr().querySelector('.room');
|
||||
const viewIcon = label.querySelector('.rlgo');
|
||||
const viewCursor = getComputedStyle(viewIcon).cursor;
|
||||
const viewPointerEvents = getComputedStyle(viewIcon).pointerEvents;
|
||||
|
||||
let viewNav = null;
|
||||
const viewPushState = history.pushState;
|
||||
history.pushState = (state, title, url) => { viewNav = url; };
|
||||
viewIcon.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
||||
await c.updateComplete;
|
||||
history.pushState = viewPushState;
|
||||
|
||||
c._setMode('plan');
|
||||
await settle();
|
||||
const planLabel = labelById(roomId);
|
||||
const planIcon = planLabel?.querySelector('.rlgo');
|
||||
const plan = planLabel ? snapshot(planLabel) : null;
|
||||
|
||||
let planNav = null;
|
||||
const planPushState = history.pushState;
|
||||
history.pushState = (state, title, url) => { planNav = url; };
|
||||
planIcon?.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
||||
await c.updateComplete;
|
||||
history.pushState = planPushState;
|
||||
|
||||
const iconBox = planIcon?.getBoundingClientRect();
|
||||
window.__roomLinkOriginalPushState = history.pushState;
|
||||
window.__roomLinkDragNav = null;
|
||||
history.pushState = (state, title, url) => { window.__roomLinkDragNav = url; };
|
||||
return {
|
||||
missingFixture: false,
|
||||
roomId,
|
||||
view,
|
||||
plan,
|
||||
roomCursor: getComputedStyle(room).cursor,
|
||||
viewLinkCount: links.length,
|
||||
withArea,
|
||||
viewCursor,
|
||||
viewPointerEvents,
|
||||
viewNav,
|
||||
planLinkCount: sr().querySelectorAll('.rlgo').length,
|
||||
planCursor: planIcon ? getComputedStyle(planIcon).cursor : '',
|
||||
planTitle: planIcon?.getAttribute('title') || '',
|
||||
planNav,
|
||||
planHandles: planLabel?.querySelectorAll('.rlhandle').length || 0,
|
||||
planSettingsButtons: sr().querySelectorAll('.rlgearbtn').length,
|
||||
beforeLayout: c._layout[`rl_${roomId}`] || null,
|
||||
dragTarget: iconBox ? { x: iconBox.left + iconBox.width / 2, y: iconBox.top + iconBox.height / 2 } : null,
|
||||
themeApplied: c.hass.themes.darkMode === dark,
|
||||
};
|
||||
}, { dark });
|
||||
|
||||
if (setup.missingFixture) {
|
||||
checks[`${key}:fixture`] = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let drag = { changed: false, dragNav: 'not-run', stayedInPlan: false };
|
||||
if (setup.dragTarget) {
|
||||
await page.mouse.move(setup.dragTarget.x, setup.dragTarget.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(setup.dragTarget.x + 24, setup.dragTarget.y + 16, { steps: 4 });
|
||||
await page.mouse.up();
|
||||
drag = await page.evaluate(async ({ roomId, beforeLayout }) => {
|
||||
const c = window.__card;
|
||||
await c.updateComplete;
|
||||
const after = c._layout[`rl_${roomId}`] || null;
|
||||
const changed = !!after && (!beforeLayout
|
||||
|| Math.abs(after.x - beforeLayout.x) > 1e-6
|
||||
|| Math.abs(after.y - beforeLayout.y) > 1e-6);
|
||||
const dragNav = window.__roomLinkDragNav;
|
||||
history.pushState = window.__roomLinkOriginalPushState;
|
||||
delete window.__roomLinkOriginalPushState;
|
||||
delete window.__roomLinkDragNav;
|
||||
return { changed, dragNav, stayedInPlan: c._mode === 'plan' };
|
||||
}, { roomId: setup.roomId, beforeLayout: setup.beforeLayout });
|
||||
}
|
||||
|
||||
const values = ['left', 'top', 'width', 'height'];
|
||||
const nameDeltas = values.map((field) => Math.abs(setup.view.name[field] - setup.plan.name[field]));
|
||||
const metricDeltas = values.map((field) => Math.abs(setup.view.metrics[field] - setup.plan.metrics[field]));
|
||||
geometry.push({ key, nameDeltas, metricDeltas });
|
||||
checks[`${key}:fixture`] = !setup.missingFixture;
|
||||
checks[`${key}:room-cursor`] = setup.roomCursor === 'default';
|
||||
checks[`${key}:view-link-per-area-room`] = setup.viewLinkCount === setup.withArea;
|
||||
checks[`${key}:view-link-interactive`] = setup.viewCursor === 'pointer'
|
||||
&& setup.viewPointerEvents === 'auto';
|
||||
checks[`${key}:view-navigates`] = typeof setup.viewNav === 'string'
|
||||
&& setup.viewNav.includes('/config/areas/area/');
|
||||
checks[`${key}:plan-keeps-links`] = setup.planLinkCount === setup.withArea;
|
||||
checks[`${key}:plan-link-is-drag-affordance`] = setup.planCursor === 'grab'
|
||||
&& setup.planTitle === '';
|
||||
checks[`${key}:plan-does-not-navigate`] = setup.planNav == null
|
||||
&& drag.dragNav == null && drag.stayedInPlan;
|
||||
checks[`${key}:plan-icon-drags-label`] = drag.changed;
|
||||
checks[`${key}:core-name-parity`] = nameDeltas.every((delta) => delta <= 0.5);
|
||||
checks[`${key}:core-metrics-parity`] = metricDeltas.every((delta) => delta <= 0.5)
|
||||
&& setup.view.metricsText === setup.plan.metricsText;
|
||||
checks[`${key}:plan-controls-preserved`] = setup.planHandles === 4
|
||||
&& setup.planSettingsButtons > 0;
|
||||
checks[`${key}:theme-applied`] = setup.themeApplied;
|
||||
}
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
checkAll(checks);
|
||||
await finish(null, { checks, geometry });
|
||||
|
||||
File diff suppressed because one or more lines are too long
Vendored
+9
-9
File diff suppressed because one or more lines are too long
@@ -2,6 +2,11 @@
|
||||
|
||||
## v1.65.0-beta.4 — 2026-08-19
|
||||
|
||||
- Room labels in the Plan editor now keep the same name, Home Assistant Area
|
||||
icon and state-row geometry as View, so switching modes no longer shifts a
|
||||
carefully positioned label. The icon still opens the Area only in View; in
|
||||
Plan it remains part of the label's drag surface
|
||||
([#200](https://github.com/Matysh/houseplan-card/issues/200)).
|
||||
- New and directly edited openings in independent walls now keep a physical
|
||||
jamb at each wall end equal to half that wall's thickness. Placement, drag,
|
||||
rebind and length edits share the same limit and explain a rejected edit,
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
## v1.65.0-beta.4 — 2026-08-19
|
||||
|
||||
- Подписи комнат в редакторе Плана теперь сохраняют ту же геометрию имени,
|
||||
иконки зоны Home Assistant и строк состояний, что и в Просмотре, поэтому при
|
||||
переключении режимов аккуратно выставленная подпись больше не сдвигается.
|
||||
Иконка по-прежнему открывает зону только в Просмотре, а в Плане остаётся
|
||||
частью области перетаскивания подписи
|
||||
([#200](https://github.com/Matysh/houseplan-card/issues/200)).
|
||||
- Новые и явно редактируемые проёмы в независимых стенах теперь оставляют у
|
||||
каждого торца физический откос не меньше половины толщины этой стены.
|
||||
Размещение, перетаскивание, перепривязка и изменение длины используют одну
|
||||
|
||||
+7
-4
@@ -929,10 +929,13 @@ separately promised workflows:
|
||||
popover (icon ×0.5–3, room-card font; localStorage per device);
|
||||
cycle: N auto-advances spaces with a 60 s pause after any touch;
|
||||
manual: walk the real wall tablet [auto+manual]
|
||||
- [ ] Room link icon (v1.40.1): clicking empty room space in View does
|
||||
nothing (default cursor); an open-in-new icon after the room name (rooms
|
||||
with an HA area, View only) navigates to the area; no icon in editors or
|
||||
on area-less rooms [auto: smoke_room_link]
|
||||
- [ ] Room link icon (v1.40.1, #200 parity): clicking empty room space in View
|
||||
does nothing (default cursor); an open-in-new icon after the room name
|
||||
appears for rooms with an HA area in View and Plan. In View it navigates
|
||||
to the area; in Plan it has no separate action/title and remains part of
|
||||
the draggable room label. Its name/metrics geometry relative to the
|
||||
saved label anchor matches View within 0.5 CSS px at DPR 1/2 in light and
|
||||
dark themes. Area-less rooms have no icon [auto: smoke_room_link]
|
||||
- [ ] Smart guides (v1.40.0): while drawing (outline, cut, decor shapes) or
|
||||
dragging (icons, room cards, decor) dashed accent guides appear from the
|
||||
nearest object sharing the X and/or Y (max two, with a dot at the
|
||||
|
||||
@@ -33,7 +33,9 @@ View сейчас добавляет к имени иконку перехода
|
||||
`.roomlabel` в обоих режимах получает одну позицию от `_labelPos()` и одну
|
||||
проекцию через `_scenePoint()`. `.rlmetrics` уже вынесен из centering math и
|
||||
имеет одинаковый состав и положение. Различается `.rlname`: условие
|
||||
`!this._markup && r.area` рендерит `.rlgo` только в View.
|
||||
`!this._markup && r.area` скрывает `.rlgo` именно в Plan editor. В остальных
|
||||
режимах, где `_markup` выключен и подписи видимы, иконка уже входит в DOM; это
|
||||
уточнение Low из зелёного ревью ТЗ r1.
|
||||
|
||||
На demo-карточке Living room подтверждено:
|
||||
|
||||
|
||||
@@ -391,6 +391,28 @@ export const MUTANTS = [
|
||||
replace: ' if (!replacingRemoved && prevPos && prevPos.s === targetSpaceId && !roomChanged) {',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'plan-room-area-icon-hidden',
|
||||
guard: 'node demo/smoke_room_link.mjs',
|
||||
because: 'Plan обязан сохранять тот же состав name-row, что и View; возврат прежнего '
|
||||
+ 'условия снова убирает Area icon и сдвигает подпись относительно anchor',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' const showAreaLink = !!r.area;',
|
||||
replace: ' const showAreaLink = !this._markup && !!r.area;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'plan-room-area-icon-navigates',
|
||||
guard: 'node demo/smoke_room_link.mjs',
|
||||
because: 'Area icon в Plan является частью drag-зоны, а не ссылкой; включение View-handlers '
|
||||
+ 'обязано одновременно проявиться навигацией и невозможностью начать drag с иконки',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' const areaLinkInteractive = !this._markup;',
|
||||
replace: ' const areaLinkInteractive = true;',
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
// --- механика ---------------------------------------------------------------
|
||||
|
||||
+31
-5
@@ -5002,6 +5002,24 @@ class HouseplanCard extends LitElement {
|
||||
return this._view && this._view.w ? this._view : fitView(vb, this._stageAspect());
|
||||
}
|
||||
|
||||
/**
|
||||
* Room labels must keep their View screen geometry while the Plan toolbar
|
||||
* borrows height from the stage. Devices continue to scale with the active
|
||||
* editor viewport; only the label core uses the hypothetical View viewport
|
||||
* at the same zoom and total card height (#200).
|
||||
*/
|
||||
private _roomLabelReferenceViewWidth(view: { w: number }): number {
|
||||
if (!this._markup) return view.w;
|
||||
const stage = this._stageEl;
|
||||
const chrome = this.renderRoot.querySelector('.editorchrome') as HTMLElement | null;
|
||||
if (!stage || stage.clientWidth <= 0 || stage.clientHeight <= 0) return view.w;
|
||||
const totalHeight = stage.clientHeight + (chrome?.getBoundingClientRect().height || 0);
|
||||
if (totalHeight <= 0) return view.w;
|
||||
return this._viewForModeTarget(
|
||||
this._zoom, undefined, undefined, stage.clientWidth, totalHeight,
|
||||
).w;
|
||||
}
|
||||
|
||||
/** Screen (sx,sy relative to the scene, px) → vb coordinates per the current view. */
|
||||
private _screenToVb(sx: number, sy: number): number[] {
|
||||
const s = this._stageEl;
|
||||
@@ -15799,7 +15817,7 @@ class HouseplanCard extends LitElement {
|
||||
every marker to a dot. Same expression as the static
|
||||
space-card, so the two renderers agree. The per-device
|
||||
multiplier and the kiosk scales still feed --dev-size. */}
|
||||
<div class="devlayer" style="--icon-size:${iconCqw(iconPct, space, view.w, this._kiosk ? this._kioskScale.icon : 1).toFixed(3)}cqw;--rl-font:${this._kiosk ? this._kioskScale.font : 1}">
|
||||
<div class="devlayer" style="--icon-size:${iconCqw(iconPct, space, view.w, this._kiosk ? this._kioskScale.icon : 1).toFixed(3)}cqw;--rl-icon-size:${iconCqw(iconPct, space, this._roomLabelReferenceViewWidth(view), this._kiosk ? this._kioskScale.icon : 1).toFixed(3)}cqw;--rl-font:${this._kiosk ? this._kioskScale.font : 1}">
|
||||
${devs.map((d) => this._renderDevice(d, view, showLqi))}
|
||||
${this._renderVacuums(devs, view)}
|
||||
${this._renderVacFit(view)}
|
||||
@@ -17134,6 +17152,10 @@ class HouseplanCard extends LitElement {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Plan keeps the same name-row geometry as the other modes, but the Area
|
||||
// icon remains part of the draggable label instead of becoming a link.
|
||||
const showAreaLink = !!r.area;
|
||||
const areaLinkInteractive = !this._markup;
|
||||
return html`<div class="roomlabel ${rows.length ? 'card' : ''}"
|
||||
data-hp="room-label" data-id=${r.id || nothing} data-area=${r.area || nothing}
|
||||
style="left:${left}%;top:${top}%;color:${disp.color};opacity:${op};--rl-scale:${k};--rl-space:${disp.cardFontScale};--rl-name:${clampScale(r.settings?.name_scale)};--rl-meta:${clampScale(r.settings?.label_scale)}"
|
||||
@@ -17141,11 +17163,15 @@ class HouseplanCard extends LitElement {
|
||||
@pointermove=${(e: PointerEvent) => this._labelMove(e, r, space.id)}
|
||||
@pointerup=${() => this._labelUp(r)}
|
||||
@pointercancel=${() => this._labelUp(r)}
|
||||
><span class="rlname">${r.name || (this._markup ? this._t('room.unnamed') : '')}${!this._markup && r.area
|
||||
><span class="rlname">${r.name || (this._markup ? this._t('room.unnamed') : '')}${showAreaLink
|
||||
? html`<ha-icon class="rlgo" icon="mdi:open-in-new"
|
||||
title=${this._t('room.open_area')}
|
||||
@click=${(e: Event) => { e.stopPropagation(); this._clickRoom(r); }}
|
||||
@pointerdown=${(e: Event) => e.stopPropagation()}></ha-icon>`
|
||||
title=${areaLinkInteractive ? this._t('room.open_area') : nothing}
|
||||
@click=${areaLinkInteractive
|
||||
? (e: Event) => { e.stopPropagation(); this._clickRoom(r); }
|
||||
: nothing}
|
||||
@pointerdown=${areaLinkInteractive
|
||||
? (e: Event) => e.stopPropagation()
|
||||
: nothing}></ha-icon>`
|
||||
: nothing}</span>
|
||||
${rows.length ? html`<span class="rlmetrics">${rows}</span>` : nothing}
|
||||
${this._mode === 'plan'
|
||||
|
||||
+1
-1
@@ -873,7 +873,7 @@ export const cardStyles = css`
|
||||
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 * var(--rl-scale, 1) * var(--rl-font, 1) * var(--rl-space, 1));
|
||||
font-size: calc(var(--rl-icon-size, var(--icon-size, 2.5cqw)) * 0.5 * var(--rl-scale, 1) * var(--rl-font, 1) * var(--rl-space, 1));
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -74,6 +74,11 @@ test('golden matrix has stable unique ids and bounded comparison thresholds', ()
|
||||
assert.equal(scenario.mode, 'plan', scenario.id);
|
||||
assert.equal(scenario.capture, 'page', scenario.id);
|
||||
}
|
||||
if (scenario.roomLabelParity) {
|
||||
assert.equal(scenario.space, 'golden-lighting', scenario.id);
|
||||
assert.equal(['view', 'plan'].includes(scenario.mode), true, scenario.id);
|
||||
assert.equal(scenario.capture, 'page', scenario.id);
|
||||
}
|
||||
if (scenario.wallJunctionPreview) {
|
||||
assert.equal(scenario.wallJunctions, true, scenario.id);
|
||||
assert.equal(scenario.mode, 'plan', scenario.id);
|
||||
@@ -102,6 +107,24 @@ test('golden matrix covers required geometry, rendering and adaptive surfaces',
|
||||
assert.equal(new Set(GOLDEN_SCENARIOS.map((scenario) => scenario.theme)).has('dark'), true);
|
||||
});
|
||||
|
||||
test('room-label parity goldens pair View and Plan in light and dark themes', () => {
|
||||
const scenarios = GOLDEN_SCENARIOS.filter((scenario) => scenario.roomLabelParity);
|
||||
assert.deepEqual(scenarios.map(({ mode, theme }) => `${mode}-${theme}`).sort(), [
|
||||
'plan-dark', 'plan-light', 'view-dark', 'view-light',
|
||||
]);
|
||||
for (const scenario of scenarios) {
|
||||
const fixture = prepareGoldenFixture(scenario);
|
||||
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
|
||||
assert.equal(space.settings.show_names, true);
|
||||
assert.equal(space.settings.label_temp, true);
|
||||
assert.equal(space.settings.label_lqi, true);
|
||||
assert.equal(space.settings.label_light, true);
|
||||
assert.deepEqual(Object.keys(fixture.layout).filter((id) => id.startsWith('rl_')).sort(), [
|
||||
'rl_light-left', 'rl_light-right',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test('washer lifecycle goldens pair active and idle snapshots of one composite fixture', () => {
|
||||
const active = GOLDEN_SCENARIOS.find((scenario) => scenario.id === 'washer-active-cycle-dark');
|
||||
const idle = GOLDEN_SCENARIOS.find((scenario) => scenario.id === 'washer-idle-cycle-dark');
|
||||
@@ -211,7 +234,7 @@ test('sun-ray golden requires browser-painted light from a state-only sun entity
|
||||
assert.ok(scenario);
|
||||
const fixture = prepareGoldenFixture(scenario);
|
||||
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 30);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 31);
|
||||
assert.equal(space.settings.sun_rays, true);
|
||||
assert.equal(scenario.northDeg, 90,
|
||||
'the sign-sensitive golden must keep a non-zero north direction');
|
||||
|
||||
Reference in New Issue
Block a user