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
This commit is contained in:
Claude
2026-09-20 11:04:39 +03:00
parent 91a008db9c
commit de71bd7967
82 changed files with 2080 additions and 1685 deletions
+12 -2
View File
@@ -296,7 +296,13 @@ export const LOW_HEADROOM_WARNING_BYTES = 15_000;
// ленивого support-словаря в основной, потому что «?» читает его через `_t`.
// Код диалогов остался в ленивом графе; вырос словарь, который там же и был.
// Опущен до 291 400 в #593, поднят обратно осознанно — рост объяснён здесь.
export const INITIAL_VIEW_GZIP_CEILING = 292_500;
/**
* #600, серия 2: 292 500 → 292 200 (замер 290 711). Стартовый граф похудел:
* компас «Общих настроек» стал индикатором набора, его 120-px CSS ушёл из
* eager-листа `plan.styles.ts`, а мёртвые ключи основного каталога удалены.
* Потолок опущен, чтобы выигрыш не потерялся.
*/
export const INITIAL_VIEW_GZIP_CEILING = 292_200;
export const INITIAL_VIEW_CEILING_BAND = 2_000;
/**
@@ -401,8 +407,12 @@ export const LAZY_FURNITURE_ART_GZIP_CEILING = 17_900;
* словарь `i18n/settings` на четыре локали (51 ключ × 4 ≈ 3,5 КБ gzip), который
* раньше лёг бы в первый кадр. Старая разметка онбординга (300 строк) ушла,
* но она была в другом графе. Строки и словарь — большая часть роста.
* - серия 2: 236 300 → 238 600 (замер 237 929). «Общие настройки» по §5:
* плитки цвета, встроенный режим блока Zigbee, dirty/валидация; девять новых
* строк словаря на четыре локали. Старые `_renderColorRow`, `.sunrow/.suncol`
* и 120-px компас удалены.
*/
export const LAZY_EDITOR_GZIP_CEILING = 236_300;
export const LAZY_EDITOR_GZIP_CEILING = 238_600;
export const LAZY_GRAPH_CEILING_BAND = 2_000;
/**
+50 -4
View File
@@ -10269,9 +10269,10 @@ const MUTANT_DEFINITIONS = [
}, {
// Оба сообщения о состоянии — один контракт (К5), поэтому и мутант один:
// спрятать под «?» можно каждое, и «поймано» обязано означать оба.
// #600: сообщение — callout набора; спрятать его в title так же легко.
file: 'src/editors/general-settings-dialog.ts',
find: " ? html`<div class=\"rhint\">${this.host._t('gs.sun_missing')}</div>`",
replace: " ? html`<span title=${this.host._t('gs.sun_missing')}></span>`",
find: " ? callout({ kind: 'warning', role: 'status', text: t('gs.sun_missing') })",
replace: " ? html`<span title=${t('gs.sun_missing')}></span>`",
}],
},
{
@@ -10281,8 +10282,8 @@ const MUTANT_DEFINITIONS = [
+ 'Заголовок и есть то, ради чего задача делалась, и его потеря обязана краснеть',
patches: [{
file: 'src/editors/general-settings-dialog.ts',
find: " title: this.host._t('gs.card_fills'),",
replace: " title: '',",
find: " title: t('gs.card_fills'),",
replace: " title: '',",
}],
},
{
@@ -10345,6 +10346,51 @@ const MUTANT_DEFINITIONS = [
replace: " checked: !d.hideDecor, onChange: (v) => set(port, { ...d, hideDecor: v }),",
}],
},
{
id: 'general-save-enabled-without-changes',
guard: 'node demo/smoke_general_settings_form.mjs',
because: '#600 К10/AC6 (Q1) для «Общих настроек»: Save активен только при изменениях; '
+ 'без dirty футер снова врёт о том, есть ли что сохранять',
patches: [{
file: 'src/editors/general-settings-dialog.ts',
find: ' const canSave = dirty && problems.length === 0 && !d.busy;',
replace: ' const canSave = problems.length === 0 && !d.busy;',
}],
},
{
id: 'general-tile-opacity-writes-a-neighbour-key',
guard: 'node demo/smoke_general_settings_form.mjs',
because: '#600 К1/AC3: у плитки цвета своё число прозрачности, и оно обязано писать '
+ 'в свой цвет палитры. Восемь одинаковых плиток подряд — соседний ключ на глаз '
+ 'не отличить, а в конфиг уедет чужая прозрачность',
patches: [{
file: 'src/editors/general-settings-dialog.ts',
find: " onOpacity: (a) => this._setFillColor(key, { c: v.c, a }),",
replace: " onOpacity: (a) => this._setFillColor(key === 'light_on' ? 'light_off' : key, { c: v.c, a }),",
}],
},
{
id: 'general-north-clear-writes-zero',
guard: 'node demo/smoke_general_settings_form.mjs',
because: '#600 §5.2: «Clear» возвращает север в «не задан» (null), а не в 0°: ноль — '
+ 'это направление, и лучи через окна с ним считаются, а с null — нет',
patches: [{
file: 'src/editors/general-settings-dialog.ts',
find: "textLink(t('gs.north_clear'), () => set({ northDeg: null }))",
replace: "textLink(t('gs.north_clear'), () => set({ northDeg: 0 }))",
}],
},
{
id: 'topology-embedded-draws-its-own-heading',
guard: 'node demo/smoke_general_settings_form.mjs',
because: '#600 дефект 1: заголовок «Zigbee links» удваивался — карточка рисовала '
+ 'свой, компонент внутри — свой. Встроенный режим обязан заголовок не рисовать',
patches: [{
file: 'src/hp-zigbee-topology-settings.ts',
find: ' if (this.embedded) return this._renderEmbedded();',
replace: ' if (this.embedded && this.savedEnabled) return this._renderEmbedded();',
}],
},
{
id: 'form-kit-segment-drops-radio-semantics',
guard: 'node --test test/form-kit.test.mjs',