mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +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);
|
||||
|
||||
Reference in New Issue
Block a user