diff --git a/demo/helpers/README-ha-dialog-505.md b/demo/helpers/README-ha-dialog-505.md index c6875911..c1b25cf3 100644 --- a/demo/helpers/README-ha-dialog-505.md +++ b/demo/helpers/README-ha-dialog-505.md @@ -31,9 +31,12 @@ It opens the real Room settings form and checks the settled WebAwesome surface: 560 px desktop width, the 940 px/viewport height cap, zero duplicate HA body padding, one HA-owned scroller, a distinct form canvas, footer containment and edge-to-edge layouts at 480×800 and 390×844. It also keeps HA's own fullscreen -behaviour for a 1280×480 viewport. `--capture` writes the two reviewed diagnostic +behaviour for a 1280×480 viewport. `--capture` writes the three reviewed diagnostic images under `docs/design/600-settings-dialogs/pairs/`; without it the command -is read-only. The fast Validate witness is +is read-only. The authentic diagnostic explicitly applies the light palette, +then repeats desktop geometry in the dark palette with a 32 px root font and +proves that canvas contrast, the single HA scroller and the footer survive. +The fast Validate witness is `demo/smoke_ha_form_shell_parity.mjs`; neither diagnostic is a golden producer. From a checkout with the usual `npm ci` dependencies and Playwright Chromium: diff --git a/demo/verify_ha_form_shell_609.mjs b/demo/verify_ha_form_shell_609.mjs index 65272a94..7fd10ff6 100644 --- a/demo/verify_ha_form_shell_609.mjs +++ b/demo/verify_ha_form_shell_609.mjs @@ -55,9 +55,15 @@ const inspect = async () => page.evaluate(() => { const cardStyle = getComputedStyle(formCard); const panelBox = panel.getBoundingClientRect(); const overflowNodes = []; + const scrollOwners = []; const visit = (root) => { for (const node of root.querySelectorAll('*')) { const box = node.getBoundingClientRect(); + const style = getComputedStyle(node); + if (node.scrollHeight > node.clientHeight + 1 && /auto|scroll/.test(style.overflowY)) { + scrollOwners.push({ tag: node.localName, id: node.id, className: String(node.className || ''), + scrollHeight: node.scrollHeight, clientHeight: node.clientHeight }); + } // wa-dialog itself stays in document flow at the demo host's 8 px page // inset; its rendered native dialog is fixed and is the geometry users see. if (node.localName !== 'wa-dialog' && box.width > 0 @@ -85,12 +91,32 @@ const inspect = async () => page.evaluate(() => { formBodyOverflowY: formBodyStyle.overflowY, formBodyBackground: formBodyStyle.backgroundColor, cardBackground: cardStyle.backgroundColor, + rootFontSize: getComputedStyle(document.documentElement).fontSize, + colorScheme: getComputedStyle(document.documentElement).colorScheme, + scrollOwners, horizontalOverflow: overflowNodes.length > 0, documentWidth: [document.documentElement.scrollWidth, document.body.scrollWidth], overflowNodes: overflowNodes.slice(0, 20), }; }); +const setPresentation = async ({ theme, textSize }) => page.evaluate(async ({ theme, textSize }) => { + const dark = theme === 'dark'; + const colors = dark ? ['#3ea6ff', '#e6e7eb', '#9aa4ad', '#202126', '#3a3d45', '#11151b', '#2b2d33'] + : ['#0b73b8', '#202124', '#5f6368', '#ffffff', '#d7d9de', '#eef1f4', '#e7eaee']; + ['primary-color', 'primary-text-color', 'secondary-text-color', 'card-background-color', + 'divider-color', 'primary-background-color', 'secondary-background-color'] + .forEach((key, index) => document.documentElement.style.setProperty(`--${key}`, colors[index])); + document.documentElement.style.setProperty('--ha-card-background', colors[3]); + document.documentElement.style.colorScheme = theme; + document.documentElement.style.fontSize = `${textSize}px`; + document.body.style.background = colors[5]; + const card = window.__card; + card.hass = { ...card.hass, themes: { ...card.hass?.themes, darkMode: dark } }; + card.requestUpdate(); + await card.updateComplete; +}, { theme, textSize }); + const clipPanel = async (name, state) => { if (!capture) return; const clip = { @@ -122,6 +148,7 @@ try { return ha?.open === true && ha.shadowRoot.querySelector('wa-dialog')?.shadowRoot.querySelector('dialog')?.matches(':modal'); }); + await setPresentation({ theme: 'light', textSize: 16 }); await settle(); const desktop = await inspect(); @@ -134,10 +161,36 @@ try { assert.equal(desktop.haBodyOverflowY, 'auto'); assert.equal(desktop.formBodyOverflowY, 'visible'); assert.notEqual(desktop.formBodyBackground, desktop.cardBackground); + assert.equal(desktop.cardBackground, 'rgb(255, 255, 255)'); + assert.ok(desktop.colorScheme.includes('light'), `desktop scheme ${desktop.colorScheme}`); + assert.equal(desktop.rootFontSize, '16px'); + assert.equal(desktop.scrollOwners.length, 1, JSON.stringify(desktop.scrollOwners)); assert.ok(desktop.haBodyScroll[0] > desktop.haBodyScroll[1], `desktop scroll ${desktop.haBodyScroll}`); assert.ok(desktop.footer.bottom <= desktop.panel.bottom + 1); await clipPanel('room-ha-light.png', desktop); + await setPresentation({ theme: 'dark', textSize: 32 }); + await settle(); + const darkText200 = await inspect(); + assert.ok(Math.abs(darkText200.panel.width - 560) <= 1, + `dark 200% width ${darkText200.panel.width}`); + assert.ok(darkText200.panel.height <= 941, + `dark 200% height ${darkText200.panel.height}`); + assert.equal(darkText200.rootFontSize, '32px'); + assert.ok(darkText200.colorScheme.includes('dark'), `dark scheme ${darkText200.colorScheme}`); + assert.equal(darkText200.cardBackground, 'rgb(32, 33, 38)'); + assert.notEqual(darkText200.formBodyBackground, darkText200.cardBackground); + assert.equal(darkText200.haBodyOverflowY, 'auto'); + assert.equal(darkText200.formBodyOverflowY, 'visible'); + assert.equal(darkText200.scrollOwners.length, 1, JSON.stringify(darkText200.scrollOwners)); + assert.ok(darkText200.haBodyScroll[0] > darkText200.haBodyScroll[1], + `dark 200% scroll ${darkText200.haBodyScroll}`); + assert.equal(darkText200.horizontalOverflow, false, JSON.stringify(darkText200.overflowNodes)); + assert.ok(darkText200.footer.bottom <= darkText200.panel.bottom + 1); + await clipPanel('room-ha-dark-text-200.png', darkText200); + + await setPresentation({ theme: 'light', textSize: 16 }); + await page.setViewportSize({ width: 480, height: 800 }); await settle(); const edge = await inspect(); @@ -170,6 +223,7 @@ try { authenticHaFrontend: fixture.provenance, capture, desktop, + darkText200, edge480: edge, mobile390: mobile, shortViewport: short, diff --git a/docs/design/600-settings-dialogs/ACCEPTANCE.md b/docs/design/600-settings-dialogs/ACCEPTANCE.md index 1615da2e..ba6ada8c 100644 --- a/docs/design/600-settings-dialogs/ACCEPTANCE.md +++ b/docs/design/600-settings-dialogs/ACCEPTANCE.md @@ -51,12 +51,14 @@ node demo/verify_ha_form_shell_609.mjs --capture | Режим | Поверхность | Проверенный результат | |---|---:|---| | desktop 1600×1000 | 560×940 | канва вокруг карточек, один скроллер `.body` HA, footer внутри | +| dark + text 200%, 1600×1000 | 560×940 | root font 32 px, один HA-скроллер, footer внутри, канва не сливается с карточками | | граница 480×800 | 480×800 | fullscreen, радиус 0, горизонтального переполнения нет | | mobile 390×844 | 390×844 | fullscreen, радиус 0, footer внутри | | низкое окно 1280×480 | 1280×480 | штатный fullscreen HA сохранён | -Снимки приёмки: `pairs/room-ha-light.png` и -`pairs/room-ha-mobile-light.png`. Это диагностические доказательства, а не +Снимки приёмки: `pairs/room-ha-light.png`, +`pairs/room-ha-dark-text-200.png` и `pairs/room-ha-mobile-light.png`. Это +диагностические доказательства, а не golden-базы; 13 нативных golden-сцен остаются неизменными. Быстрый обязательный свидетель тех же публичных CSS-контрактов — `demo/smoke_ha_form_shell_parity.mjs`, а перечень пяти потребителей оболочки diff --git a/docs/design/600-settings-dialogs/pairs/room-ha-dark-text-200.png b/docs/design/600-settings-dialogs/pairs/room-ha-dark-text-200.png new file mode 100644 index 00000000..85580a12 Binary files /dev/null and b/docs/design/600-settings-dialogs/pairs/room-ha-dark-text-200.png differ diff --git a/docs/design/600-settings-dialogs/pairs/room-ha-light.png b/docs/design/600-settings-dialogs/pairs/room-ha-light.png index fc30713b..eefe69cc 100644 Binary files a/docs/design/600-settings-dialogs/pairs/room-ha-light.png and b/docs/design/600-settings-dialogs/pairs/room-ha-light.png differ diff --git a/docs/design/600-settings-dialogs/pairs/room-ha-mobile-light.png b/docs/design/600-settings-dialogs/pairs/room-ha-mobile-light.png index 189e659d..659c2c5f 100644 Binary files a/docs/design/600-settings-dialogs/pairs/room-ha-mobile-light.png and b/docs/design/600-settings-dialogs/pairs/room-ha-mobile-light.png differ diff --git a/scripts/mutation-registry.mjs b/scripts/mutation-registry.mjs index d1e81ead..f0303b92 100644 --- a/scripts/mutation-registry.mjs +++ b/scripts/mutation-registry.mjs @@ -75,7 +75,7 @@ const MUTANT_DEFINITIONS = [ { id: 'ha-form-shell-width-falls-back-to-generic', guard: 'node demo/smoke_ha_form_shell_parity.mjs', - because: '#609 AC1/AC8: the authentic HA branch must consume the reviewed 560 px form ' + because: '#609 AC1: the authentic HA branch must consume the reviewed 560 px form ' + 'width; falling back to HA generic medium width recreates the released/native mismatch', patches: [{ file: 'src/hp-dialog.ts', @@ -97,7 +97,7 @@ const MUTANT_DEFINITIONS = [ { id: 'ha-form-shell-mobile-keeps-desktop-inset', guard: 'node demo/smoke_ha_form_shell_parity.mjs', - because: '#609 AC4/AC8: at 480 px and below the form shell is edge-to-edge; retaining the ' + because: '#609 AC4: at 480 px and below the form shell is edge-to-edge; retaining the ' + 'desktop 48 px height inset clips the reviewed mobile surface and footer', patches: [{ file: 'src/hp-dialog.ts',