diff --git a/demo/smoke_editor_tabs.mjs b/demo/smoke_editor_tabs.mjs index ae2d29e3..33d3e9a5 100644 --- a/demo/smoke_editor_tabs.mjs +++ b/demo/smoke_editor_tabs.mjs @@ -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) diff --git a/demo/smoke_mobile_view_header.mjs b/demo/smoke_mobile_view_header.mjs index ccc78a78..429160cf 100644 --- a/demo/smoke_mobile_view_header.mjs +++ b/demo/smoke_mobile_view_header.mjs @@ -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'); diff --git a/demo/smoke_toolbar_stable_width.mjs b/demo/smoke_toolbar_stable_width.mjs index e89e7ddd..1416044f 100644 --- a/demo/smoke_toolbar_stable_width.mjs +++ b/demo/smoke_toolbar_stable_width.mjs @@ -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); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index df4c889d..affad3ac 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/docs/CHANGELOG.ru.md b/docs/CHANGELOG.ru.md index d8b96463..6cbd9e60 100755 --- a/docs/CHANGELOG.ru.md +++ b/docs/CHANGELOG.ru.md @@ -8,6 +8,15 @@ ## Не выпущено +- Основная панель стала компактнее и перестала дёргаться: крестик **×** + редактора теперь занимает зарезервированное место сразу после активной + кнопки редактора, расстояние до масштаба уменьшено вдвое, а общая ширина не + меняется при переключении режимов. На широком экране настройки и показ + сводной панели доступны во всех редакторах (сама панель появляется только в + Просмотре), а меню телефона остаётся прежним и показывает их только в + Просмотре. Escape теперь выходит из любого редактора, предварительно закрыв + активный инструмент, выбор, перетаскивание, меню или диалог + ([#660](https://github.com/Matysh/houseplan-card/issues/660)). - При холодной загрузке дашборда включённый объёмный план (2.5D) больше не успевает показать обычный плоский вид и не считает все полы тёмными на первом кадре. Нейтральная заставка House Plan остаётся до готовности 2.5D-рендера и diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 13cc3b7a..5f1e574b 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -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 diff --git a/docs/USER-GUIDE.ru.md b/docs/USER-GUIDE.ru.md index c8f50547..032cf88c 100644 --- a/docs/USER-GUIDE.ru.md +++ b/docs/USER-GUIDE.ru.md @@ -252,6 +252,8 @@ title: План дома Редакторы переключаются напрямую. Новая панель инструментов коротко проявляется, а её фактическая высота плавно подстраивается даже при переносе кнопок на несколько строк; при системной настройке уменьшения движения переход отключается. +Escape сначала отменяет активный диалог, меню, перетаскивание, выбор или +инструмент; следующее нажатие в нейтральном состоянии возвращает в Просмотр. | Режим | Основное назначение | Устройства | Комнаты/стены | Декор и проёмы | |---|---|---|---|---| @@ -274,7 +276,8 @@ title: План дома просмотре любому пользователю — **Настройки сводной панели** и показ или скрытие сводной панели. Пункт делает то же, что прежняя кнопка, и закрывает меню; Escape или касание вне меню просто закрывают его — касание при этом не -нажимает ничего на плане. В редакторе крестик закрытия остаётся в строке. На +нажимает ничего на плане. В редакторе крестик закрытия остаётся в строке, хотя +сами кнопки редакторов находятся только в меню. На более широком экране шапка прежняя, в киоске её нет. ### Сводная панель @@ -327,8 +330,11 @@ Escape и закрытие диалога отбрасывают изменен снизу. В слишком маленькой карточке она временно скрывается и возвращается при увеличении. Выключенная общая опция **Отображать на мобильных устройствах** также скрывает панель, когда Home Assistant сообщает об узком режиме; кнопка -остаётся включённой, потому что локальный выбор не сброшен. В трёх редакторах -и статической карточке панели и её кнопок нет. +остаётся включённой, потому что локальный выбор не сброшен. Сама панель в трёх +редакторах и статической карточке не показывается. На экране шире 480 px обе её +кнопки в шапке остаются доступны и в редакторах: можно открыть настройки или +изменить локальный выбор показа, но панель появится только после возврата в +Просмотр. В телефонном меню эти два пункта по-прежнему есть только в Просмотре. Подсветка комнаты сохраняется в Просмотре и киоске. Чтобы оставить подсветку, но убрать всплывающее описание комнаты, выключите **Общие настройки → diff --git a/docs/UX-MODES.md b/docs/UX-MODES.md index a4242272..dba9cd9c 100644 --- a/docs/UX-MODES.md +++ b/docs/UX-MODES.md @@ -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 diff --git a/scripts/mutation-registry.mjs b/scripts/mutation-registry.mjs index 0d058014..97791924 100644 --- a/scripts/mutation-registry.mjs +++ b/scripts/mutation-registry.mjs @@ -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: " ${this._devices.length} dev.${this._devices.length} dev.${this._t(('mode.' + m) as any)}\n `,", - replace: " ${this._t(('mode.' + m) as any)}${this._mode === m ? html`` : nothing}\n `,", + find: " ${this._t(('mode.' + m) as any)}\n ${this._mode === m ? editorClose : nothing}`", + replace: " ${this._t(('mode.' + m) as any)}${this._mode === m ? html`` : nothing}\n ${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: " ", - replace: " ", + find: "", + replace: "", }], }, { @@ -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: ' ${this._mode === m ? editorClose : nothing}`', + replace: ' `', + }, { + file: 'src/houseplan-card.ts', + find: " ${this._mode === 'view' ? editorClose : nothing}\n `", + replace: ' ${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,', }], }, { diff --git a/src/header-menu.ts b/src/header-menu.ts index aab821ef..a484969e 100644 --- a/src/header-menu.ts +++ b/src/header-menu.ts @@ -41,7 +41,7 @@ export type HeaderMenuInput = { kiosk: boolean; mode: HeaderMode; hasFixedFloor: boolean; - /** Пункты сводной панели (её рантайм отдаёт их только в просмотре). */ + /** Пункты сводной панели; мобильное меню добавляет их только в просмотре. */ summary: HeaderMenuItem[]; t: (key: I18nKey) => string; actions: HeaderMenuActions; diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts index 859805fd..46906934 100755 --- a/src/houseplan-card.ts +++ b/src/houseplan-card.ts @@ -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`${this._mode !== 'view' ? html`` + : nothing}`; return html` this._setMode(m)}> ${this._t(('mode.' + m) as any)} - `, + ${this._mode === m ? editorClose : nothing}`, )} - ${''/* #647: X owns a fixed slot (the old device-count place): header width never changes */} - ${this._mode !== 'view' - ? html`` - : nothing}` + ${this._mode === 'view' ? editorClose : nothing} + ` : nothing}
diff --git a/src/styles/chrome.styles.ts b/src/styles/chrome.styles.ts index 8f63a267..e224ebf7 100644 --- a/src/styles/chrome.styles.ts +++ b/src/styles/chrome.styles.ts @@ -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; diff --git a/src/summary-panel-runtime-loaded.ts b/src/summary-panel-runtime-loaded.ts index 79465c77..7fd61df1 100644 --- a/src/summary-panel-runtime-loaded.ts +++ b/src/summary-panel-runtime-loaded.ts @@ -234,8 +234,7 @@ export class LoadedSummaryPanelRuntime { `; } - /** #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`
{ }; }; +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 {