Files
houseplan-card/demo/smoke_settings_dialog_cards.mjs
T
Claude de71bd7967 feat(general): «Общие настройки» по референсу — серия 2 из четырёх (#600)
§5 SPEC.md референса. Семь карточек #598 остались, менялось содержимое:

- Display — две полные строки с иконкой, подписью и тумблером справа; пояснение
  радара по §5.2 стало подписью строки (текст прежний, ключ переехал в ленивый
  словарь как gs.radar_show_live_hint, «?» у строки больше нет);
- Zigbee links — заголовок и «?» функции (topology.help) рисует карточка, а
  блок hp-zigbee-topology-settings получил режим embedded: строка-тумблер,
  callout «сохраните настройку», подзаголовки ZHA / Zigbee2MQTT, поле тем и
  строки действий контролами набора. Заголовок больше не удваивается
  (дефект 1). Лист набора вносится в теневой корень компонента; токены
  --hpf-* наследуются с хоста. Прежний render без embedded не тронут — его
  держит контракт #459;
- Room fill colors — три подзаголовка (Lights / Temperature / Zigbee signal,
  Q6) и плитки цвета по три в ряд: название на свотче, hex, число
  прозрачности; свотч — trigger прежнего hp-color-opacity (Q5);
- Light-source glow — две плитки и поле радиуса с единицей внутри рамки;
- Plan — плашка цвета стен, сегмент фона, при Static — плашка фона с Reset и
  подсказкой (тема / свой цвет);
- Sun — gs.sun_missing остаётся callout'ом (К5); север: поле градусов с «°»,
  ссылка Clear, «N» и компас 44 px справа — индикатор набора, но тянуть его
  по-прежнему можно (smoke_sun гоняет настоящий drag; старый 120-px SVG и его
  CSS в plan.styles.ts удалены); лучи — строка-тумблер с подписью; источник
  лучей — сегмент (sun-settings-view.ts переписан с <select> на segmented,
  логика та же);
- Data — подзаголовки с «?» и строки действий набора.

К10 (Q1): Save только при изменениях и без ошибок (радиус > 0, север 0–359),
статус в футере, вопрос при закрытии с изменениями; снимок при открытии —
editors/general-form-state.ts поверх общего editors/dialog-baseline.ts (на него
же переведён space-form-state.ts). _settingsDialog не получил новых ключей.

Удалено мёртвое: _renderColorRow (рантайм и карточка), .sunrow/.suncol,
ключи gs.wall_group, gs.bg_group, gs.bg_theme, gs.radar_show_live.help(.aria)
в четырёх локалях. Девять новых ключей — в i18n/settings.

Смоки переписаны осознанно, с пометкой #600: smoke_general_settings (инвентарь
по классам набора, подзаголовки .hpf-sub h4, пояснения под «?» и подписью,
ни одного .rhint), smoke_sun (сегмент источника лучей вместо select, поле
#gs-north, radius drag 18 px), smoke_backup_transfer (подзаголовок и строка
действий), smoke_settings_dialog_cards (callout вместо .rhint). Новый фокусный
свидетель demo/smoke_general_settings_form.mjs: семь карточек и отсутствие
старой разметки, один заголовок у Zigbee, десять плиток, компас 44, цели 44,
запись плиток/плашки/радиуса/севера/сегментов в свои ключи, Save/dirty/вопрос
при закрытии.

Мутанты: general-save-enabled-without-changes,
general-tile-opacity-writes-a-neighbour-key, general-north-clear-writes-zero,
topology-embedded-draws-its-own-heading; якоря state-callout-hidden-under-help
и dialog-card-loses-its-heading переставлены на новую разметку.

Контрактные тесты: houseplan-source.mjs приводит `host.` к `this.` и в телах
вынесенных диалогов; color-picker — три явных showOpacity=true (плитки, плашка
стены, цвет комнаты); счётчик производных .aria 35 → 34.

Бюджет: стартовый граф 290 711 — потолок ОПУЩЕН 292 500 → 292 200 (ушёл
120-px компас из eager-листа и мёртвые ключи); ленивый редакторский 237 929 —
потолок 236 300 → 238 600 с разбором в bundle-budget.mjs.

Гейты: typecheck, build, bundle:sync, npm test 2792 pass, mutation-gate
--check, семь смоков «Общих», smoke_color_picker_consumers без правок.

Issue: #600
User-Visible: no
2026-09-20 11:04:39 +03:00

84 lines
5.5 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.
// #598: три диалога настроек собраны в карточки-группы. Свидетель отдельный, а
// не дописанный к существующим: девять смоков с `.srcrow` объявлены в ТЗ
// неприкосновенными (AC2), и правка любого из них стоила бы ревью опоры —
// отличить «свидетель поправлен под новую разметку» от «свидетель ослаблен под
// сломанный код» по диффу такого файла нельзя.
//
// Здесь проверяется то, чего раньше не проверял никто: состав карточек,
// клавиатура новых сегментов, размер цели и — главное — что два сообщения о
// СОСТОЯНИИ остались на виду, а не уехали под «?» заодно с пояснениями (К5).
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch();
const out = await page.evaluate(async () => {
const o = {};
const c = window.__card;
const sr = () => c.shadowRoot || c.renderRoot;
const cards = () => [...sr().querySelectorAll('hp-dialog .hpf-card')];
const headings = () => cards().map((card) => card.querySelector('.hpf-head h3')?.textContent.trim());
// --- «Общие настройки»: карточки, сегмент фона, сообщение о состоянии ----
c._openSettingsDialog(); await c.updateComplete;
o.generalCardsInOrder = JSON.stringify(headings()) === JSON.stringify([
c._t('gs.card_display'), c._t('gs.card_zigbee'), c._t('gs.card_fills'),
c._t('gs.glow_group'), c._t('gs.card_plan'), c._t('gs.sun_group'), c._t('gs.card_data'),
]);
o.everyGeneralCardHasContent = cards().every((card) => card.querySelector('.hpf-head + *'));
// Сегмент фона: настоящая радиогруппа с доступным именем и целью ≥ 44 px.
const bgSeg = sr().querySelector('hp-dialog .hpf-seg[role="radiogroup"]');
const bgRadios = bgSeg ? [...bgSeg.querySelectorAll('input[type="radio"]')] : [];
o.bgSegmentIsRadioGroup = !!bgSeg && !!bgSeg.getAttribute('aria-label') && bgRadios.length === 2;
// Цель нажатия ≥ 44 px. Проверка нашла настоящий дефект: лист набора
// подключается вызовом `ensureFormKitStyles`, и без него сегмент рисовался
// высотой 14 px — вся доступность держалась на стиле, которого не было.
o.bgSegmentTargetIsBigEnough = bgRadios.length > 0 && bgRadios.every((radio) => {
const box = radio.closest('label')?.getBoundingClientRect();
return !!box && box.height >= 44;
});
// Клавиатура: стрелка переводит выбор на соседний вариант, как в списке,
// который сегмент заменил. Значение черновика обязано поехать следом.
const bgBefore = c._settingsDialog.bgMode;
const other = bgRadios.find((radio) => !radio.checked);
other?.focus();
o.bgSegmentTakesFocus = !!other && bgSeg.contains(sr().activeElement ?? document.activeElement);
other?.click(); await c.updateComplete;
o.bgSegmentWritesItsOwnKey = c._settingsDialog.bgMode !== bgBefore
&& c._settingsDialog.northDeg === null;
// `gs.sun_missing` — сообщение о состоянии. Убираем sun.sun и требуем, чтобы
// абзац был виден, а не спрятан под «?» (К5).
const states = { ...c.hass.states };
delete states['sun.sun'];
c.hass = { ...c.hass, states };
await c.updateComplete;
// #600 §3.1: сообщение о состоянии — callout набора, а не абзац `.rhint`.
const sunNote = [...sr().querySelectorAll('hp-dialog .hpf-callout')]
.find((node) => node.textContent.trim() === c._t('gs.sun_missing'));
o.sunMissingStaysVisible = !!sunNote && sunNote.getBoundingClientRect().height > 0
&& !sr().querySelector('hp-dialog hp-help[data-help-key="gs.sun_missing.help"]');
c._settingsDialog = null; await c.updateComplete;
// --- «Пространство»: карточки и три сегмента (стены, заливка, фон) -------
c._openSpaceDialog('edit', c._space); await c.updateComplete;
o.spaceCardsInOrder = JSON.stringify(headings()) === JSON.stringify([
c._t('space.card_basics'), c._t('space.display_section'),
c._t('space.roomcard_section'), c._t('space.card_sun'),
]);
const spaceSegs = [...sr().querySelectorAll('hp-dialog .hpf-seg[role="radiogroup"]')];
// #600 §4.2: стиль нулевых стен, режим заливки и фон плана — три сегмента.
o.spaceHasThreeSegments = spaceSegs.length === 3
&& spaceSegs.every((seg) => !!seg.getAttribute('aria-label'));
const zeroBefore = c._spaceDialog.zeroWallStyle;
const titleBefore = c._spaceDialog.title;
const zeroOther = [...sr().querySelectorAll('hp-dialog input[name="space-zero-wall-style"]')]
.find((radio) => !radio.checked);
zeroOther?.click(); await c.updateComplete;
o.zeroWallSegmentWritesItsOwnKey = c._spaceDialog.zeroWallStyle !== zeroBefore
&& c._spaceDialog.title === titleBefore;
c._spaceDialog = null; await c.updateComplete;
return o;
});
await finish(browser, checkAll(out));