Files
houseplan-card/demo/capture_design_pairs_600.mjs
T
Claude c3e64dc60a feat(dialogs): финал #600 — мёртвые стили, парные кадры, документация, потолки
Завершение четырёх серий по референсу docs/design/600-settings-dialogs/:

- из eager-листов dialogs.styles.ts и chrome.styles.ts сняты стили прежней
  разметки диалогов (.bindsel/.bindharow/.entcheck, .ctrlchip*, .markerhelp*,
  .helpfieldlabel, .markerradios, .markerlightdisabled, .markerbadgetechnical,
  .markerglowvalue, .fileupload, .descin, .pdfedit/.pdftag, .gsrow,
  .ripple-*row, .radaradditional, .togglehint, футеры marker/room) — 5,8 КБ
  исходника; с разметки сняты уже пустые классы;
- «Additional actions» карточки Details — подзаголовок и строка действий
  набора, без раскрывашки (radar-section.ts, только additional-placement);
- Q3 уточнён по кадру: hp-device-preview стоит без обёртки — у него свой
  заголовок «Display preview · Now», второй над ним удваивался; пустое
  состояние — блок на тинте;
- §6.1: «?» у комнаты стоит у поля Home Assistant area, а не у заголовка
  Basics; подписи сегмента заливки — None · Zigbee · Lights · Temperature ·
  Custom (два коротких ключа room.fill_seg_*), источники — «Average of the
  room’s sensors / Specific device or entity», режим свечения — «Colour and
  brightness» (тексты прежних ключей укорочены под сегменты);
- подпись поля с «?» и без стоят на одной линии в сетке (min-height 32 по
  кнопке hp-help); иконка внутри подсказки — inline;
- парные кадры: demo/capture_design_pairs_600.mjs снимает четыре диалога в
  двух темах на 560 в состояниях макетов → docs/design/…/pairs/ (8 PNG,
  палитра 256, ~0,5 МБ); ACCEPTANCE.md — статус по диалогам, расхождения с
  референсом по каждому со ссылкой на пункт SPEC и принятые адаптации (шапка
  hp-dialog по §3, тумблеры набора по К7, секции радара/пылесоса по §7.2);
- changelog EN/RU («Не выпущено»), USER-GUIDE ru/en: раздел «Как устроены
  диалоги настроек», таблица настроек пространства по новым карточкам,
  диалог комнаты, диалог устройства, ссылка «Как у пространства»;
- бюджет: стартовый граф 289 853 — потолок 291 600 → 290 900 (снятые стили);
  ленивый редакторский 243 149 — итоговый потолок 243 900 с разбором по
  сериям в bundle-budget.mjs.

Golden: 11 сцен диалогов (device-dialog-*, room-temperature-dialog-*,
toggle-entity-dialog-*, device-help-popover-light-ru,
general-color-popover-desktop-en, settings-help-zoom-200-*,
device-ripple-color-popover-mobile-ru, space-room-color-popover-desktop-ru)
прогнаны диагностически (capture, HP_ALLOW_FOREIGN_CAPTURE, только
artifacts/): харнесс проходит все пути без исключений, кадры «different» —
эталоны переснимаются на Linux CI / машине владельца по #455.
Скриншоты документации (06-device-editor, 06-device-display-preview и др.)
устарели по отпечатку — пересъёмка перед кандидатом беты (#479).

Гейты: typecheck, build, bundle:sync (три копии байт в байт), npm test
2793 pass по четырём частям, mutation-gate --check, check-inputs --coverage,
фокусные смоки четырёх форм и смоки диалога устройства.

Issue: #600
User-Visible: yes
2026-09-20 13:01:07 +03:00

103 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// #600 AC1: парные кадры продукта для сравнения с референсом
// docs/design/600-settings-dialogs/ (ACCEPTANCE.md). Диагностическая съёмка, не
// golden: кадры не принимаются golden:accept и не участвуют в golden:verify.
//
// node demo/capture_design_pairs_600.mjs [--out=docs/design/600-settings-dialogs/pairs]
//
// Четыре диалога × две темы, ширина поверхности 560, скроллер раскрыт на всю
// высоту, чтобы кадр показывал форму целиком, как макет референса. Состояния —
// те же, что в макетах: пространство с температурной заливкой и своим севером,
// общие настройки со статическим фоном, комната с эффективной температурной
// заливкой, устройство — привязанный светильник с ролью Always и фиксированным
// свечением.
import { mkdirSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { launch } from './serve.mjs';
const out = resolve(process.argv.find((a) => a.startsWith('--out='))?.slice(6) || 'docs/design/600-settings-dialogs/pairs');
mkdirSync(out, { recursive: true });
const THEMES = {
light: {
'--primary-color': '#0b73b8', '--primary-text-color': '#202124', '--secondary-text-color': '#5f6368',
'--card-background-color': '#ffffff', '--ha-card-background': '#ffffff', '--divider-color': '#d7d9de',
'--secondary-background-color': '#e7eaee', '--primary-background-color': '#eef1f4',
},
dark: {
'--primary-color': '#3ea6ff', '--primary-text-color': '#e6e7eb', '--secondary-text-color': '#9aa4ad',
'--card-background-color': '#202126', '--ha-card-background': '#202126', '--divider-color': '#3a3d45',
'--secondary-background-color': '#2b2d33', '--primary-background-color': '#11151b',
},
};
const { page, browser } = await launch({ width: 1280, height: 3600 });
const applyTheme = (theme) => page.evaluate(({ vars, theme }) => {
for (const [k, v] of Object.entries(vars)) document.documentElement.style.setProperty(k, v);
document.documentElement.style.colorScheme = theme;
document.body.style.background = theme === 'light' ? '#eef1f4' : '#11151b';
const c = window.__card;
c.hass = { ...c.hass, themes: { ...(c.hass.themes || {}), darkMode: theme === 'dark' } };
}, { vars: THEMES[theme], theme });
const openAndExpand = (which) => page.evaluate(async (w) => {
const c = window.__card;
const upd = async () => { c.requestUpdate(); await c.updateComplete; await new Promise((r) => setTimeout(r, 40)); };
c._settingsDialog = null; c._spaceDialog = null; c._markerDialog = null; if (c._roomDialog) c._roomDialogCancel();
await upd();
const spId = c._space;
if (w === 'space') {
c._setMode('view'); c._openSpaceDialog('edit', spId); await upd();
c._spaceDialog = { ...c._spaceDialog, fillMode: 'temp', northMode: 'custom', northDeg: 35 }; await upd();
}
if (w === 'general') {
c._openSettingsDialog(); await upd();
c._settingsDialog = { ...c._settingsDialog, bgMode: 'static', northDeg: 45 }; await upd();
}
if (w === 'room') {
c._serverCfg = { ...c._serverCfg, spaces: c._serverCfg.spaces.map((s) => s.id !== spId ? s
: ({ ...s, settings: { ...(s.settings || {}), fill_mode: 'temp', show_names: true } })) };
c._setMode('plan'); await upd();
c._openRoomEdit(c._curSpaceCfg.rooms[0]); await upd();
c._roomFill = 'temp'; c._roomTempMin = '19'; await upd();
}
if (w === 'device') {
c._setMode('devices'); await upd();
const d = c._devices.find((x) => x.id === 'd_light1') || c._devices.find((x) => x.bindingKind === 'device') || c._devices[0];
c._openMarkerDialog(d); await upd();
c._markerDialog = { ...c._markerDialog, lightRole: 'always', lightRoleTouched: true, glowMode: 'fixed', glowTouched: true }; await upd();
}
const sr = c.shadowRoot || c.renderRoot;
const dlg = sr.querySelector('hp-dialog[form-shell]');
const surface = dlg?.shadowRoot?.querySelector('.surface');
const content = dlg?.shadowRoot?.querySelector('.content');
if (content) { content.style.maxHeight = 'none'; content.style.overflow = 'visible'; }
if (surface) { surface.style.maxHeight = 'none'; surface.style.height = 'auto'; }
const backdrop = dlg?.shadowRoot?.querySelector('.backdrop, .scrim');
if (backdrop) backdrop.style.alignItems = 'flex-start';
// Автофокус первого поля и всплывшая по фокусу подсказка «?» — не часть макета.
let active = document.activeElement;
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
active?.blur?.();
await new Promise((r) => setTimeout(r, 250));
const b = surface?.getBoundingClientRect();
return b ? { x: b.x, y: b.y, w: b.width, h: b.height } : null;
}, which);
const report = [];
for (const theme of ['light', 'dark']) {
await applyTheme(theme);
for (const which of ['space', 'general', 'room', 'device']) {
const b = await openAndExpand(which);
if (!b || !b.w) { report.push(`${which}/${theme}: нет поверхности`); continue; }
const file = resolve(out, `${which}-${theme}.png`);
const shot = await page.screenshot({ clip: {
x: Math.max(0, b.x - 4), y: Math.max(0, b.y - 4), width: Math.min(b.w + 8, 1280), height: Math.min(b.h + 8, 3600),
} });
writeFileSync(file, shot);
report.push(`${which}/${theme}: ${Math.round(b.w)}×${Math.round(b.h)} → ${file}`);
}
}
console.log(report.join('\n'));
await browser.close();