feat: отполировать главную панель редакторов (#660)

Issue: #660
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-09-26 08:51:15 +00:00
committed by claude[bot]
parent cff83e43ec
commit d3c88f06f9
14 changed files with 225 additions and 63 deletions
+34 -4
View File
@@ -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)
+7 -3
View File
@@ -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');
+27 -11
View File
@@ -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);
+8
View File
@@ -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
+9
View File
@@ -8,6 +8,15 @@
## Не выпущено
- Основная панель стала компактнее и перестала дёргаться: крестик **×**
редактора теперь занимает зарезервированное место сразу после активной
кнопки редактора, расстояние до масштаба уменьшено вдвое, а общая ширина не
меняется при переключении режимов. На широком экране настройки и показ
сводной панели доступны во всех редакторах (сама панель появляется только в
Просмотре), а меню телефона остаётся прежним и показывает их только в
Просмотре. Escape теперь выходит из любого редактора, предварительно закрыв
активный инструмент, выбор, перетаскивание, меню или диалог
([#660](https://github.com/Matysh/houseplan-card/issues/660)).
- При холодной загрузке дашборда включённый объёмный план (2.5D) больше не
успевает показать обычный плоский вид и не считает все полы тёмными на первом
кадре. Нейтральная заставка House Plan остаётся до готовности 2.5D-рендера и
+7 -2
View File
@@ -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
+9 -3
View File
@@ -252,6 +252,8 @@ title: План дома
Редакторы переключаются напрямую. Новая панель инструментов коротко проявляется,
а её фактическая высота плавно подстраивается даже при переносе кнопок на несколько строк;
при системной настройке уменьшения движения переход отключается.
Escape сначала отменяет активный диалог, меню, перетаскивание, выбор или
инструмент; следующее нажатие в нейтральном состоянии возвращает в Просмотр.
| Режим | Основное назначение | Устройства | Комнаты/стены | Декор и проёмы |
|---|---|---|---|---|
@@ -274,7 +276,8 @@ title: План дома
просмотре любому пользователю — **Настройки сводной панели** и показ или
скрытие сводной панели. Пункт делает то же, что прежняя кнопка, и закрывает
меню; Escape или касание вне меню просто закрывают его — касание при этом не
нажимает ничего на плане. В редакторе крестик закрытия остаётся в строке. На
нажимает ничего на плане. В редакторе крестик закрытия остаётся в строке, хотя
сами кнопки редакторов находятся только в меню. На
более широком экране шапка прежняя, в киоске её нет.
### Сводная панель
@@ -327,8 +330,11 @@ Escape и закрытие диалога отбрасывают изменен
снизу. В слишком маленькой карточке она временно скрывается и возвращается при
увеличении. Выключенная общая опция **Отображать на мобильных устройствах**
также скрывает панель, когда Home Assistant сообщает об узком режиме; кнопка
остаётся включённой, потому что локальный выбор не сброшен. В трёх редакторах
и статической карточке панели и её кнопок нет.
остаётся включённой, потому что локальный выбор не сброшен. Сама панель в трёх
редакторах и статической карточке не показывается. На экране шире 480 px обе её
кнопки в шапке остаются доступны и в редакторах: можно открыть настройки или
изменить локальный выбор показа, но панель появится только после возврата в
Просмотр. В телефонном меню эти два пункта по-прежнему есть только в Просмотре.
Подсветка комнаты сохраняется в Просмотре и киоске. Чтобы оставить подсветку,
но убрать всплывающее описание комнаты, выключите **Общие настройки →
+17 -9
View File
@@ -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
+64 -8
View File
@@ -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
View File
@@ -41,7 +41,7 @@ export type HeaderMenuInput = {
kiosk: boolean;
mode: HeaderMode;
hasFixedFloor: boolean;
/** Пункты сводной панели (её рантайм отдаёт их только в просмотре). */
/** Пункты сводной панели; мобильное меню добавляет их только в просмотре. */
summary: HeaderMenuItem[];
t: (key: I18nKey) => string;
actions: HeaderMenuActions;
+13 -13
View File
@@ -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">
+16 -6
View File
@@ -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;
+2 -3
View File
@@ -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"
+11
View File
@@ -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 {