mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 20:29:00 +00:00
feat: отполировать главную панель редакторов (#660)
Issue: #660 User-Visible: yes
This commit is contained in:
committed by
claude[bot]
parent
cff83e43ec
commit
d3c88f06f9
@@ -15,8 +15,8 @@ const res = await page.evaluate(async () => {
|
||||
// 1) две вкладки, Просмотра нет, крестиков в неактивных нет
|
||||
out.twoTabs = tabs().length === 3; // третья — Редактор подложки (v1.33.0)
|
||||
out.labels = tabs().map((t) => t.textContent.trim());
|
||||
// #647: X no longer lives inside a mode tab; its own slot after the tabs is
|
||||
// empty (but keeps its size) outside an editor.
|
||||
// #660: X does not live inside a mode button; its slot follows the active
|
||||
// button and moves to the group end (empty but same size) in View.
|
||||
out.noCrossIdle = sr().querySelectorAll('.modetab .closex').length === 0
|
||||
&& !sr().querySelector('.editor-close-slot .closex');
|
||||
out.startView = c._mode === 'view';
|
||||
@@ -51,6 +51,14 @@ const res = await page.evaluate(async () => {
|
||||
&& Math.abs((barCloseIconRect.top + barCloseIconRect.height / 2)
|
||||
- (barCloseRect.top + barCloseRect.height / 2)) <= 1;
|
||||
out.tabCross = !!headerCross() && !tabs()[0].querySelector('.closex');
|
||||
const planSummary = sr().querySelector('.summary-control');
|
||||
const summaryToggle = planSummary?.querySelector('button:last-child');
|
||||
const summaryBefore = summaryToggle?.getAttribute('aria-pressed');
|
||||
summaryToggle?.click(); await c.updateComplete;
|
||||
out.summaryControlsStayInPlan = planSummary?.querySelectorAll('button').length === 2
|
||||
&& summaryToggle?.getAttribute('aria-pressed') !== summaryBefore
|
||||
&& !sr().querySelector('.summary-overlay');
|
||||
summaryToggle?.click(); await c.updateComplete;
|
||||
// 3) повторный клик по активной вкладке — ничего
|
||||
tabs()[0].click(); await c.updateComplete;
|
||||
out.reclickNoop = c._mode === 'plan';
|
||||
@@ -198,8 +206,8 @@ const res = await page.evaluate(async () => {
|
||||
out.barCloseWorks = c._mode === 'view'
|
||||
&& getComputedStyle(chrome).visibility === 'hidden'
|
||||
&& chrome.getBoundingClientRect().height < 1;
|
||||
// 7) #647 (+#195): the header X sits in a fixed 24 x 24 slot after the mode
|
||||
// tabs; the slot is its hit target around a 13 px glyph and has the same size
|
||||
// 7) #660 (+#195): the header X sits in a fixed 24 x 24 slot after the active
|
||||
// mode; the slot is its hit target around a 13 px glyph and has the same size
|
||||
// outside an editor, so the header never changes width. Exercise the slot's
|
||||
// edge rather than calling the element's centre programmatically.
|
||||
const tabCrossChecks = [];
|
||||
@@ -210,6 +218,8 @@ const res = await page.evaluate(async () => {
|
||||
const slot = sr().querySelector('.editor-close-slot').getBoundingClientRect();
|
||||
const glyph = cross.querySelector('ha-icon');
|
||||
const hit = sr().elementFromPoint(rect.left + 1, rect.top + rect.height / 2);
|
||||
const editorSummaryVisible = sr().querySelectorAll('.summary-control button').length === 2
|
||||
&& !sr().querySelector('.summary-overlay');
|
||||
const glyphStays13 = getComputedStyle(glyph).getPropertyValue('--mdc-icon-size').trim() === '13px';
|
||||
hit?.dispatchEvent(new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
@@ -226,6 +236,7 @@ const res = await page.evaluate(async () => {
|
||||
footprint: Math.abs(slot.width - idleSlot.width) <= 0.5
|
||||
&& Math.abs(slot.height - idleSlot.height) <= 0.5 && Math.abs(slot.width - 24) <= 0.5,
|
||||
edgeHit: !!hit && (hit === cross || cross.contains(hit)),
|
||||
summary: editorSummaryVisible,
|
||||
closed: c._mode === 'view',
|
||||
});
|
||||
}
|
||||
@@ -234,6 +245,7 @@ const res = await page.evaluate(async () => {
|
||||
out.tabCrossKeepsLayoutFootprint = tabCrossChecks.every((check) => check.footprint);
|
||||
out.tabCrossExpandedEdgeWorks = tabCrossChecks.every((check) => check.edgeHit && check.closed);
|
||||
out.tabCrossWorks = tabCrossChecks.every((check) => check.closed);
|
||||
out.summaryControlsStayInEveryEditor = tabCrossChecks.every((check) => check.summary);
|
||||
|
||||
// Closing the editor only drops session state: every accepted segment was
|
||||
// already persisted as an ordinary partition by its own click.
|
||||
@@ -358,6 +370,24 @@ const res = await page.evaluate(async () => {
|
||||
c._roomDialog = savedRoomDialog;
|
||||
c._nameSel = savedNameSel;
|
||||
c._areaSel = savedAreaSel;
|
||||
|
||||
// #660: an internal Plan/Background action consumes the first Escape; the
|
||||
// next neutral Escape leaves the editor. Device editor exits immediately.
|
||||
c._setMode('plan', false); await settleMode();
|
||||
c._tool = 'opening'; c.requestUpdate(); await c.updateComplete;
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); await c.updateComplete;
|
||||
out.planEscapeKeepsInternalPriority = c._mode === 'plan' && c._tool === 'draw';
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); await settleMode();
|
||||
out.planNeutralEscapeExits = c._mode === 'view';
|
||||
c._setMode('devices', false); await settleMode();
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); await settleMode();
|
||||
out.deviceNeutralEscapeExits = c._mode === 'view';
|
||||
c._setMode('decor', false); await settleMode();
|
||||
c._decorTool = 'line'; c.requestUpdate(); await c.updateComplete;
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); await c.updateComplete;
|
||||
out.decorEscapeKeepsInternalPriority = c._mode === 'decor' && c._decorTool === 'select';
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); await settleMode();
|
||||
out.decorNeutralEscapeExits = c._mode === 'view';
|
||||
return out;
|
||||
});
|
||||
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||
|
||||
@@ -28,9 +28,12 @@ const header = () => page.evaluate(() => {
|
||||
title: shown(head.querySelector(':scope > .title')),
|
||||
tabs: shown(head.querySelector(':scope > .tabs')),
|
||||
zoom: shown(head.querySelector(':scope > .zoomctl')),
|
||||
modeSlot: shown(head.querySelector(':scope > .modes'))
|
||||
&& Math.abs(head.querySelector(':scope > .modes').getBoundingClientRect().width - 24) <= 0.5,
|
||||
cross: shown(head.querySelector('[data-hp="editor-close"]')),
|
||||
gear: shown(gear),
|
||||
gear44: !!g && g.width >= 44 && g.height >= 44,
|
||||
hiddenInline: ['.modes', '.header-action', '.summary-control', '.tabedit', '.tabadd']
|
||||
hiddenInline: ['.modes .modetab', '.header-action', '.summary-control', '.tabedit', '.tabadd']
|
||||
.filter((selector) => [...head.querySelectorAll(selector)].some(shown)),
|
||||
tabEditInDom: !!head.querySelector('.tabedit, .tabadd'),
|
||||
inlineVisible: ['.header-action', '.summary-control'].filter((selector) => [...head.querySelectorAll(selector)].some(shown)),
|
||||
@@ -85,7 +88,7 @@ for (const width of [390, 320]) {
|
||||
await size(width);
|
||||
const h = await header();
|
||||
out[`admin${width}_oneRow56`] = h.oneRow && h.height <= 56;
|
||||
out[`admin${width}_rowContent`] = !h.title && h.tabs && h.zoom && h.gear && h.gear44;
|
||||
out[`admin${width}_rowContent`] = !h.title && h.tabs && h.modeSlot && h.zoom && h.gear && h.gear44;
|
||||
out[`admin${width}_inlineHidden`] = h.hiddenInline.length === 0 ? true : h.hiddenInline.join(',');
|
||||
await openMenu();
|
||||
const m = await menuState();
|
||||
@@ -135,7 +138,8 @@ await clickAt('[data-hp="header-menu-item"][data-id="mode-plan"]');
|
||||
await page.waitForTimeout(300);
|
||||
out.editorFromMenu = (await mode()) === 'plan';
|
||||
const inEditor = await header();
|
||||
out.editorOneRow56 = inEditor.oneRow && inEditor.height <= 56 && inEditor.hiddenInline.length === 0;
|
||||
out.editorOneRow56 = inEditor.oneRow && inEditor.height <= 56 && inEditor.modeSlot
|
||||
&& inEditor.cross && inEditor.hiddenInline.length === 0;
|
||||
await openMenu();
|
||||
const editorMenu = await menuState();
|
||||
out.editorMenuMarksCurrent = editorMenu.current.join() === 'mode-plan' && !editorMenu.ids.includes('summary-toggle');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// #647: основная панель — нет счётчика устройств, крестик редактора в своём
|
||||
// слоте, ширина шапки и положение кнопок режимов не меняются при входе,
|
||||
// #647/#660: основная панель — нет счётчика устройств, крестик редактора в
|
||||
// постоянном слоте внутри группы режимов, ширина шапки и группы не меняются,
|
||||
// выходе и переключении редакторов; кликабельная зона × не меньше 24 × 24
|
||||
// (#195); × виден на обычных ширинах (721–1100 px — там раньше скрывался
|
||||
// счётчик). Режимы переключаются настоящими кнопками через фасад #629;
|
||||
@@ -8,7 +8,7 @@ import { launch, check, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch({ width: 1400, height: 820 }, 1);
|
||||
|
||||
const WIDTHS = [1400, 1000, 768, 390];
|
||||
const WIDTHS = [1400, 1200, 1000, 768, 620, 481, 390];
|
||||
const out = {};
|
||||
|
||||
for (const width of WIDTHS) {
|
||||
@@ -31,13 +31,22 @@ for (const width of WIDTHS) {
|
||||
// Позиции — относительно левого края шапки: прокрутка страницы их не меняет.
|
||||
const measure = () => {
|
||||
const origin = head().getBoundingClientRect().left;
|
||||
let nextEl = slot()?.nextElementSibling;
|
||||
while (nextEl && !nextEl.getClientRects().length) nextEl = nextEl.nextElementSibling;
|
||||
const modes = sr().querySelector('.modes');
|
||||
const zoom = sr().querySelector('.zoomctl');
|
||||
const modesBox = modes?.getBoundingClientRect();
|
||||
const zoomBox = zoom?.getBoundingClientRect();
|
||||
const active = modes?.querySelector('.modetab.active');
|
||||
return {
|
||||
head: head().getBoundingClientRect().width,
|
||||
tabs: tabs().map((t) => t.getBoundingClientRect().left - origin),
|
||||
modesLeft: modesBox ? modesBox.left - origin : null,
|
||||
modesWidth: modesBox?.width ?? null,
|
||||
zoomLeft: zoomBox ? zoomBox.left - origin : null,
|
||||
gap: modesBox && zoomBox ? zoomBox.left - modesBox.right : null,
|
||||
slot: slot()?.getBoundingClientRect(),
|
||||
next: nextEl ? nextEl.getBoundingClientRect().left - origin : null,
|
||||
slotInside: slot()?.parentElement === modes,
|
||||
slotOrder: c._mode === 'view'
|
||||
? slot() === modes?.lastElementChild
|
||||
: slot()?.previousElementSibling === active,
|
||||
};
|
||||
};
|
||||
const visibleTabs = tabs().filter((t) => t.getBoundingClientRect().width > 0).length === 3
|
||||
@@ -56,8 +65,9 @@ for (const width of WIDTHS) {
|
||||
o.idleSlotKeepsSize = !!idle.slot && Math.abs(idle.slot.width - 24) <= 0.5 && Math.abs(idle.slot.height - 24) <= 0.5;
|
||||
|
||||
const same = (a, b) => Math.abs(a.head - b.head) <= 1
|
||||
&& a.tabs.every((left, i) => Math.abs(left - b.tabs[i]) <= 1)
|
||||
&& (a.next === null || Math.abs(a.next - b.next) <= 1);
|
||||
&& Math.abs(a.modesLeft - b.modesLeft) <= 1
|
||||
&& Math.abs(a.modesWidth - b.modesWidth) <= 1
|
||||
&& Math.abs(a.zoomLeft - b.zoomLeft) <= 1;
|
||||
const steps = [];
|
||||
const hits = [];
|
||||
// View → каждый редактор → View, и прямые переключения между редакторами.
|
||||
@@ -66,7 +76,9 @@ for (const width of WIDTHS) {
|
||||
await hp.setMode(next);
|
||||
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
const now = measure();
|
||||
steps.push({ next, stable: same(idle, now) });
|
||||
const expectedGap = width <= 620 ? 40.5 : 50;
|
||||
steps.push({ next, stable: same(idle, now), inside: now.slotInside,
|
||||
order: now.slotOrder, gap: phone || Math.abs(now.gap - expectedGap) <= 1 });
|
||||
if (next !== 'view' && cross()) {
|
||||
// На 390 px карточка демо-стенда шире окна (так и на dev) — докрутить × в видимую область.
|
||||
cross().scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||
@@ -81,8 +93,12 @@ for (const width of WIDTHS) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// AC2/AC3: ширина шапки и левые границы кнопок режимов и контрола справа стабильны.
|
||||
// AC2/AC3: ширина шапки, группы режимов и контрола справа стабильны.
|
||||
o.widthAndTabsStable = steps.every((s) => s.stable);
|
||||
o.closeSlotLivesInsideModes = idle.slotInside && idle.slotOrder
|
||||
&& steps.every((s) => s.inside && s.order);
|
||||
o.modeToZoomGapMatchesSpec = (phone || Math.abs(idle.gap - (width <= 620 ? 40.5 : 50)) <= 1)
|
||||
&& steps.every((s) => s.gap);
|
||||
// AC5/AC6: × виден, ≥ 24 × 24, глиф 13 px, центр слота — это ×, кнопки режимов без ×.
|
||||
o.crossVisibleAndSized = hits.length === 7 && hits.every((h) => h.visible && h.glyph13);
|
||||
o.crossHitTarget = hits.every((h) => h.centreHitsCross);
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- The main toolbar is steadier and more compact: its editor close **×** now
|
||||
occupies the reserved place immediately after the active editor button, the
|
||||
gap before zoom is halved, and the header width stays fixed while switching
|
||||
modes. On desktop the summary-panel settings and show/hide buttons remain
|
||||
available in every editor (the panel itself still appears only in View),
|
||||
while the phone menu remains View-only. Escape now leaves any editor after
|
||||
first dismissing its active tool, selection, drag, popover or dialog
|
||||
([#660](https://github.com/Matysh/houseplan-card/issues/660)).
|
||||
- A cold dashboard load with 2.5D enabled no longer briefly shows the flat plan
|
||||
or classifies every floor as dark on its first frame. The neutral House Plan
|
||||
loading surface now stays in place until the 2.5D renderer and the actual plan
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- Основная панель стала компактнее и перестала дёргаться: крестик **×**
|
||||
редактора теперь занимает зарезервированное место сразу после активной
|
||||
кнопки редактора, расстояние до масштаба уменьшено вдвое, а общая ширина не
|
||||
меняется при переключении режимов. На широком экране настройки и показ
|
||||
сводной панели доступны во всех редакторах (сама панель появляется только в
|
||||
Просмотре), а меню телефона остаётся прежним и показывает их только в
|
||||
Просмотре. Escape теперь выходит из любого редактора, предварительно закрыв
|
||||
активный инструмент, выбор, перетаскивание, меню или диалог
|
||||
([#660](https://github.com/Matysh/houseplan-card/issues/660)).
|
||||
- При холодной загрузке дашборда включённый объёмный план (2.5D) больше не
|
||||
успевает показать обычный плоский вид и не считает все полы тёмными на первом
|
||||
кадре. Нейтральная заставка House Plan остаётся до готовности 2.5D-рендера и
|
||||
|
||||
+7
-2
@@ -240,6 +240,8 @@ product surfaces after setup.
|
||||
## 5. Interface modes
|
||||
|
||||
View is the state with no editor open. Close the active editor to return to it.
|
||||
Escape first cancels the current dialog, popover, drag, selection or tool; press
|
||||
it again after the editor is neutral to return to View.
|
||||
|
||||
| Mode | Devices | Geometry | Background/openings |
|
||||
|---|---|---|---|
|
||||
@@ -298,8 +300,11 @@ bottom otherwise. It temporarily hides when the card cannot fit a readable
|
||||
panel, and restores itself after the card grows. Turning off **Display on
|
||||
mobile devices** also hides it whenever Home Assistant reports a narrow view;
|
||||
the show/hide button remains pressed because the local choice was not erased.
|
||||
The panel and its controls are absent from all three editors and from the
|
||||
static space card.
|
||||
The panel itself is absent from all three editors and from the static space
|
||||
card. On a screen wider than 480 px, its two header buttons stay available in
|
||||
the editors: you can open settings or change the local show/hide choice there,
|
||||
but the panel appears only after you return to View. On a phone those two gear
|
||||
menu items remain available only in View.
|
||||
|
||||
The room highlight remains available in View and kiosk. To keep that highlight
|
||||
but hide the floating room summary, turn off **General settings → Show the room
|
||||
|
||||
@@ -252,6 +252,8 @@ title: План дома
|
||||
Редакторы переключаются напрямую. Новая панель инструментов коротко проявляется,
|
||||
а её фактическая высота плавно подстраивается даже при переносе кнопок на несколько строк;
|
||||
при системной настройке уменьшения движения переход отключается.
|
||||
Escape сначала отменяет активный диалог, меню, перетаскивание, выбор или
|
||||
инструмент; следующее нажатие в нейтральном состоянии возвращает в Просмотр.
|
||||
|
||||
| Режим | Основное назначение | Устройства | Комнаты/стены | Декор и проёмы |
|
||||
|---|---|---|---|---|
|
||||
@@ -274,7 +276,8 @@ title: План дома
|
||||
просмотре любому пользователю — **Настройки сводной панели** и показ или
|
||||
скрытие сводной панели. Пункт делает то же, что прежняя кнопка, и закрывает
|
||||
меню; Escape или касание вне меню просто закрывают его — касание при этом не
|
||||
нажимает ничего на плане. В редакторе крестик закрытия остаётся в строке. На
|
||||
нажимает ничего на плане. В редакторе крестик закрытия остаётся в строке, хотя
|
||||
сами кнопки редакторов находятся только в меню. На
|
||||
более широком экране шапка прежняя, в киоске её нет.
|
||||
|
||||
### Сводная панель
|
||||
@@ -327,8 +330,11 @@ Escape и закрытие диалога отбрасывают изменен
|
||||
снизу. В слишком маленькой карточке она временно скрывается и возвращается при
|
||||
увеличении. Выключенная общая опция **Отображать на мобильных устройствах**
|
||||
также скрывает панель, когда Home Assistant сообщает об узком режиме; кнопка
|
||||
остаётся включённой, потому что локальный выбор не сброшен. В трёх редакторах
|
||||
и статической карточке панели и её кнопок нет.
|
||||
остаётся включённой, потому что локальный выбор не сброшен. Сама панель в трёх
|
||||
редакторах и статической карточке не показывается. На экране шире 480 px обе её
|
||||
кнопки в шапке остаются доступны и в редакторах: можно открыть настройки или
|
||||
изменить локальный выбор показа, но панель появится только после возврата в
|
||||
Просмотр. В телефонном меню эти два пункта по-прежнему есть только в Просмотре.
|
||||
|
||||
Подсветка комнаты сохраняется в Просмотре и киоске. Чтобы оставить подсветку,
|
||||
но убрать всплывающее описание комнаты, выключите **Общие настройки →
|
||||
|
||||
+17
-9
@@ -53,12 +53,14 @@ inert everywhere outside its editor).
|
||||
background and editor-specific layers move on one short timeline, so opening
|
||||
an editor never flashes a default scale or a mixed dark/white frame. A rapid
|
||||
second choice retargets from the visible intermediate frame. Reduced-motion
|
||||
preferences apply the same final state immediately. The header X sits in
|
||||
its own fixed 24 × 24 px slot right after the mode tabs (#647): the slot keeps
|
||||
its size, empty and hidden from assistive technology, outside the editors,
|
||||
so entering, leaving or switching editors never changes the header width or
|
||||
moves a tab; the X keeps its compact 13 px glyph and the whole slot is its
|
||||
hit target (≥ 24 × 24 px, #195).
|
||||
preferences apply the same final state immediately. The header X sits in a
|
||||
fixed 24 × 24 px slot inside the segmented control, immediately after the
|
||||
active editor (#660); in View the same empty, assistive-technology-hidden
|
||||
reserve sits at the end. The group width therefore stays fixed while buttons
|
||||
to the right of the active editor yield the reserved place. The X keeps its
|
||||
compact 13 px glyph and the whole slot is its hit target (≥ 24 × 24 px,
|
||||
#195). On a phone the mode buttons remain in the gear menu, while this one
|
||||
slot stays visible in the header so the editor still has a direct exit.
|
||||
- An editor's primary toolbar contains only persistent tools. Close is pinned
|
||||
in its own end cap. Selection actions, active-tool parameters, operation
|
||||
hints and palettes appear in one translucent context tray over the top of the
|
||||
@@ -121,8 +123,11 @@ local show and View-size controls only. In a landscape/square House Plan stage
|
||||
the panel is on the right; in a portrait stage it is centred at the bottom.
|
||||
Native HA `narrow` controls the independent mobile permission. Insufficient
|
||||
measured stage space temporarily hides the overlay without changing the saved
|
||||
local choice. All summary surfaces disappear in Plan, Devices and Background;
|
||||
`houseplan-space-card` never renders them.
|
||||
local choice. The overlay remains View-only. On windows wider than 480 px its
|
||||
settings and local show/hide buttons stay in the header in all three editors:
|
||||
settings can be opened there and the local choice can be changed, but the
|
||||
overlay appears only after returning to View. The phone gear keeps those two
|
||||
entries View-only. `houseplan-space-card` never renders any summary surface.
|
||||
|
||||
The admin-only **General settings → Show Zigbee links on device hover** option
|
||||
is off by default. Once an admin explicitly reads cached ZHA data or updates a
|
||||
@@ -142,7 +147,10 @@ Removed from this mode (they move, not die):
|
||||
- every edit button in the header (+device, 👁 show-all, ↺ reset, ⬡ rules, ⚙ general,
|
||||
per-space gear, markup toggle).
|
||||
|
||||
Header in View: space tabs, summary control and zoom cluster; on a phone (≤ 480 px) space tabs, zoom and one gear whose menu holds the rest (#616).
|
||||
Header in View: space tabs, summary control and zoom cluster. On wider screens
|
||||
the summary control also remains available in editors; the fixed distance from
|
||||
the editor group to zoom is half the former gap. On a phone (≤ 480 px): space
|
||||
tabs, the editor-close reserve, zoom and one gear whose menu holds the rest.
|
||||
|
||||
## Plan — geometry and appearance of the space
|
||||
|
||||
|
||||
@@ -7568,8 +7568,8 @@ const MUTANT_DEFINITIONS = [
|
||||
because: '#647 AC1: the header must not show a device count in any mode or locale',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: " <span class=\"editor-close-slot\" aria-hidden=",
|
||||
replace: " <span class=\"count\">${this._devices.length} dev.</span><span class=\"editor-close-slot\" aria-hidden=",
|
||||
find: " const editorClose = html`<span class=\"editor-close-slot\" aria-hidden=",
|
||||
replace: " const editorClose = html`<span class=\"count\">${this._devices.length} dev.</span><span class=\"editor-close-slot\" aria-hidden=",
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -7588,8 +7588,8 @@ const MUTANT_DEFINITIONS = [
|
||||
because: '#647 AC3: an X inside the active mode tab widens that tab and moves its neighbours',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: " <ha-icon icon=${ic}></ha-icon><span class=\"ml\">${this._t(('mode.' + m) as any)}</span>\n </button>`,",
|
||||
replace: " <ha-icon icon=${ic}></ha-icon><span class=\"ml\">${this._t(('mode.' + m) as any)}</span>${this._mode === m ? html`<ha-icon class=\"closex\" icon=\"mdi:close\"></ha-icon>` : nothing}\n </button>`,",
|
||||
find: " <ha-icon icon=${ic}></ha-icon><span class=\"ml\">${this._t(('mode.' + m) as any)}</span>\n </button>${this._mode === m ? editorClose : nothing}`",
|
||||
replace: " <ha-icon icon=${ic}></ha-icon><span class=\"ml\">${this._t(('mode.' + m) as any)}</span>${this._mode === m ? html`<ha-icon class=\"closex\" icon=\"mdi:close\"></ha-icon>` : nothing}\n </button>${this._mode === m ? editorClose : nothing}`",
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -7598,8 +7598,8 @@ const MUTANT_DEFINITIONS = [
|
||||
because: '#647 AC4: outside an editor the reserve is inert and hidden from assistive technology',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: " <span class=\"editor-close-slot\" aria-hidden=${this._mode === 'view' ? 'true' : nothing}>",
|
||||
replace: " <span class=\"editor-close-slot\" tabindex=\"0\">",
|
||||
find: "<span class=\"editor-close-slot\" aria-hidden=${this._mode === 'view' ? 'true' : nothing}>",
|
||||
replace: "<span class=\"editor-close-slot\" tabindex=\"0\">",
|
||||
}],
|
||||
},
|
||||
{
|
||||
@@ -7622,14 +7622,70 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: " .editor-close-slot .closex ha-icon { --mdc-icon-size: 13px; }\n @media (max-width: 1100px) { .editor-close-slot .closex { display: none; } }",
|
||||
}],
|
||||
},
|
||||
// #660: compact stable header, desktop summary controls and neutral Escape.
|
||||
{
|
||||
id: 'toolbar-close-slot-leaves-mode-group',
|
||||
guard: 'node demo/smoke_toolbar_stable_width.mjs',
|
||||
because: '#660 AC2: the one fixed X reserve belongs inside the segmented mode group, after the active button',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' </button>${this._mode === m ? editorClose : nothing}`',
|
||||
replace: ' </button>`',
|
||||
}, {
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: " ${this._mode === 'view' ? editorClose : nothing}\n </div>`",
|
||||
replace: ' </div>${editorClose}`',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'toolbar-mode-zoom-gap-regresses',
|
||||
guard: 'node demo/smoke_toolbar_stable_width.mjs',
|
||||
because: '#660 AC3: the modes-to-zoom distance is the specified compact 50/40.5 px, not the old flexible gap',
|
||||
patches: [{
|
||||
file: 'src/styles/chrome.styles.ts',
|
||||
find: ' .head > .spacer { flex: 0 0 30px; }',
|
||||
replace: ' .head > .spacer { flex: 0 0 80px; }',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'editor-summary-controls-return-to-view-only',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test test/summary-panel-runtime.test.mjs',
|
||||
because: '#660 AC1: wide-screen inline summary actions remain usable in all three editors',
|
||||
patches: [{
|
||||
file: 'src/summary-panel-runtime-loaded.ts',
|
||||
find: " if (kiosk && this.host._mode !== 'view') return nothing;",
|
||||
replace: " if (this.host._mode !== 'view') return nothing;",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'phone-editor-close-hidden-with-mode-buttons',
|
||||
guard: 'node demo/smoke_mobile_view_header.mjs',
|
||||
because: '#660 AC4: hiding desktop mode buttons on a phone must leave the editor X slot visible',
|
||||
patches: [{
|
||||
file: 'src/styles/chrome.styles.ts',
|
||||
find: ' .head > .title, .head > .spacer, .head > .header-action,',
|
||||
replace: ' .head > .title, .head > .modes, .head > .spacer, .head > .header-action,',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'editor-neutral-escape-does-not-exit',
|
||||
guard: 'node demo/smoke_editor_tabs.mjs',
|
||||
because: '#660 AC5: after inner actions have had priority, a neutral Escape returns every editor to View',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: " e.preventDefault();\n this._setMode('view');\n }\n /** Remove the last transient point",
|
||||
replace: " e.preventDefault();\n }\n /** Remove the last transient point",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'phone-header-shows-title',
|
||||
guard: 'node demo/smoke_mobile_view_header.mjs',
|
||||
because: '#616 AC1: on a phone the card title alone takes a row; the one-row header must hide it',
|
||||
patches: [{
|
||||
file: 'src/styles/chrome.styles.ts',
|
||||
find: ' .head > .title, .head > .modes, .head > .spacer, .head > .header-action,',
|
||||
replace: ' .head > .modes, .head > .spacer, .head > .header-action,',
|
||||
find: ' .head > .title, .head > .spacer, .head > .header-action,',
|
||||
replace: ' .head > .spacer, .head > .header-action,',
|
||||
}],
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export type HeaderMenuInput = {
|
||||
kiosk: boolean;
|
||||
mode: HeaderMode;
|
||||
hasFixedFloor: boolean;
|
||||
/** Пункты сводной панели (её рантайм отдаёт их только в просмотре). */
|
||||
/** Пункты сводной панели; мобильное меню добавляет их только в просмотре. */
|
||||
summary: HeaderMenuItem[];
|
||||
t: (key: I18nKey) => string;
|
||||
actions: HeaderMenuActions;
|
||||
|
||||
+13
-13
@@ -2764,7 +2764,6 @@ export class HouseplanCard extends LitElement {
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
// close the topmost open dialog; info popups first, then editors
|
||||
if (this._tapConfirm) { this._tapConfirm = null; return; }
|
||||
if (this._vacCalConfirm) {
|
||||
if (!this._vacCalConfirm.busy) this._vacCalConfirm = null;
|
||||
@@ -2858,8 +2857,6 @@ export class HouseplanCard extends LitElement {
|
||||
e.preventDefault();
|
||||
if (this._decorDraft) this._decorDraft = null;
|
||||
else if (this._decorMove || this._dtDrag || this._bdDrag) this._cancelDecorGesture();
|
||||
// The palette is one explicit surface: Escape closes it and returns
|
||||
// to Select in one step, regardless of whether a symbol was armed.
|
||||
else if (this._decorTool === 'furniture') {
|
||||
this._editorRuntime?._clearFurniturePreview();
|
||||
this._editorRuntime?._furnShiftDetach(); // #369(д) r2-H1
|
||||
@@ -2885,9 +2882,10 @@ export class HouseplanCard extends LitElement {
|
||||
this._undoDevicePosition();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Escape' && this._deviceDrag) {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
this._cancelDeviceDrag();
|
||||
if (this._deviceDrag) this._cancelDeviceDrag();
|
||||
else this._setMode('view');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -2998,9 +2996,11 @@ export class HouseplanCard extends LitElement {
|
||||
e.preventDefault();
|
||||
if (this._tool === 'opening') this._clearOpeningPlacement(true);
|
||||
this._tool = 'draw';
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
this._setMode('view');
|
||||
}
|
||||
|
||||
/** Remove the last transient point or undo the terminal persisted wall. */
|
||||
private _undoPoint(): void {
|
||||
if (!this._path.length) return;
|
||||
@@ -10743,6 +10743,10 @@ export class HouseplanCard extends LitElement {
|
||||
);
|
||||
const transitionStageBg = modeVisual?.stageColor || stageBg;
|
||||
const transitionBrightness = modeVisual?.sceneBrightness ?? 1;
|
||||
const editorClose = html`<span class="editor-close-slot" aria-hidden=${this._mode === 'view' ? 'true' : nothing}>${this._mode !== 'view' ? html`<button
|
||||
class="closex" title=${this._t('title.close_editor')} aria-label=${this._t('title.close_editor')} data-hp="editor-close" data-editor-navigation="view"
|
||||
@click=${(e: Event) => { e.stopPropagation(); this._setMode('view'); }}><ha-icon icon="mdi:close"></ha-icon></button>`
|
||||
: nothing}</span>`;
|
||||
return html`
|
||||
<ha-card
|
||||
data-hp-state=${this._booting ? 'booting' : 'ready'} data-hp-mode=${this._mode}
|
||||
@@ -10816,14 +10820,10 @@ export class HouseplanCard extends LitElement {
|
||||
title=${this._t(('mode.' + m + '_tip') as any)}
|
||||
@click=${() => this._setMode(m)}>
|
||||
<ha-icon icon=${ic}></ha-icon><span class="ml">${this._t(('mode.' + m) as any)}</span>
|
||||
</button>`,
|
||||
</button>${this._mode === m ? editorClose : nothing}`,
|
||||
)}
|
||||
</div>${''/* #647: X owns a fixed slot (the old device-count place): header width never changes */}
|
||||
<span class="editor-close-slot" aria-hidden=${this._mode === 'view' ? 'true' : nothing}>${this._mode !== 'view'
|
||||
? html`<button class="closex" title=${this._t('title.close_editor')} aria-label=${this._t('title.close_editor')}
|
||||
data-hp="editor-close" data-editor-navigation="view"
|
||||
@click=${(e: Event) => { e.stopPropagation(); this._setMode('view'); }}><ha-icon icon="mdi:close"></ha-icon></button>`
|
||||
: nothing}</span>`
|
||||
${this._mode === 'view' ? editorClose : nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
<span class="spacer"></span>
|
||||
<div class="zoomctl">
|
||||
|
||||
@@ -147,10 +147,22 @@ const chromeCoreStyles = css`
|
||||
.header-menu-item.on { color: var(--hp-accent); font-weight: 600; }
|
||||
:host([data-pointer-hover]) .header-menu-item:hover { background: rgba(127, 127, 127, 0.14); }
|
||||
.header-menu-item:focus-visible { outline: 2px solid var(--hp-accent); outline-offset: -2px; }
|
||||
.head > .spacer { flex: 0 0 30px; }
|
||||
@media (max-width: 620px) and (min-width: 481px) {
|
||||
.head > .spacer { flex-basis: 28.5px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.hdr > .head { flex-wrap: nowrap; padding: 5px 8px; gap: 6px; }
|
||||
.head > .title, .head > .modes, .head > .spacer, .head > .header-action,
|
||||
.head > .title, .head > .spacer, .head > .header-action,
|
||||
.head > .summary-control { display: none; }
|
||||
.head > .modes {
|
||||
flex: 0 0 24px;
|
||||
width: 24px;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.head > .modes .modetab { display: none; }
|
||||
.head > .tabs {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
@@ -163,7 +175,7 @@ const chromeCoreStyles = css`
|
||||
.head > .tabs .tab { flex: none; white-space: nowrap; max-width: 100%; }
|
||||
.head > .tabs .tabtitle { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
.head > .tabs .tabedit, .head > .tabs .tabadd { display: none; }
|
||||
.head > .zoomctl, .head > .editor-close-slot { flex: none; }
|
||||
.head > .zoomctl, .head > .modes > .editor-close-slot { flex: none; }
|
||||
.header-menu-wrap { display: inline-flex; }
|
||||
}
|
||||
.decorbar .dcolor {
|
||||
@@ -221,10 +233,8 @@ const chromeCoreStyles = css`
|
||||
.tab.dragging { cursor: grabbing; opacity: 0.55; }
|
||||
.tab.drop-before { box-shadow: inset 2px 0 0 0 var(--primary-color, #03a9f4); }
|
||||
.tab.drop-after { box-shadow: inset -2px 0 0 0 var(--primary-color, #03a9f4); }
|
||||
/* #647: the editor X owns a fixed slot right after the mode tabs (where
|
||||
the device count used to be). One size for both states, so entering,
|
||||
leaving or switching editors never changes the header width; the slot is
|
||||
the X's pointer target (>= 24 x 24, #195) around a 13 px glyph. */
|
||||
/* #660: the fixed X slot lives inside the mode group, immediately after
|
||||
the active editor; in View the same reserve sits at the group end. */
|
||||
.editor-close-slot {
|
||||
--hp-editor-close-size: 24px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -234,8 +234,7 @@ export class LoadedSummaryPanelRuntime {
|
||||
</aside>`;
|
||||
}
|
||||
|
||||
/** #616: the same two actions as `renderControls` — items of the phone header menu
|
||||
* (the menu itself offers them only in View, as `renderControls` does). */
|
||||
/** #616: the same two actions as `renderControls`; the phone menu offers them only in View. */
|
||||
public menuItems(): HeaderMenuItem[] {
|
||||
const toggleTitle = this.toggleTitle();
|
||||
return [
|
||||
@@ -260,7 +259,7 @@ export class LoadedSummaryPanelRuntime {
|
||||
}
|
||||
|
||||
public renderControls(kiosk = false): TemplateResult | typeof nothing {
|
||||
if (this.host._mode !== 'view') return nothing;
|
||||
if (kiosk && this.host._mode !== 'view') return nothing;
|
||||
const toggleTitle = this.toggleTitle();
|
||||
const stop = (event: Event) => event.stopPropagation();
|
||||
return html`<div class="summary-control ${kiosk ? 'kiosk' : ''}" role="group"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { nothing } from 'lit';
|
||||
|
||||
import { SUMMARY_PANEL_API_VERSION } from '../test-build/summary-panel-api.js';
|
||||
import { SUMMARY_PANEL_LEGACY_SCALE_KEY, summaryLocalKey } from '../test-build/summary-panel.js';
|
||||
@@ -72,6 +73,16 @@ const hostFixture = () => {
|
||||
};
|
||||
};
|
||||
|
||||
test('#660 desktop summary controls stay available in editors while kiosk controls stay View-only', () => {
|
||||
const host = hostFixture();
|
||||
const runtime = new LoadedSummaryPanelRuntime(host);
|
||||
for (const mode of ['plan', 'devices', 'decor']) {
|
||||
host._mode = mode;
|
||||
assert.notEqual(runtime.renderControls(false), nothing, `${mode}: desktop controls`);
|
||||
assert.equal(runtime.renderControls(true), nothing, `${mode}: kiosk controls`);
|
||||
}
|
||||
});
|
||||
|
||||
test('#561 unresolved Masonry stays session-only and never imports an old DOM-path preference', () => {
|
||||
const browser = installBrowserGlobals();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user