AC0 задачи: пока референс лежит вложением в issue, ревью сравнивает продукт с макетом «на глаз», и ровно это дало выпущенный в 1.77.0-beta.2 ложный паритет — карточки снаружи, старая разметка внутри. Содержимое архива `houseplan-settings-design-2026-09-18.zip` (SHA-256 06f07fc212947ee9da1a39aaab2d14cbd41154cc9effe39bc48e1c2e1517255b, сверен побайтово с вложением #591) разложено по образцу #505: - `SPEC.md`, `IMPLEMENTATION-GUIDE.md`, `OPEN-POINTS.md`, карта полей Space — с этого коммита часть ТЗ: расхождение называется ссылкой на пункт документа; - `reference/` — свёрстанный прототип четырёх форм; - `screenshots/` — четыре формы целиком, 560 px, 2x; - `README.md` — происхождение, отпечаток, границы использования; - `ACCEPTANCE.md` — место для парных кадров «референс / продукт» по AC1. Автор — сотрудник владельца (подтверждено 20.09 в #600), внешний грант не нужен. Windows-лаунчеры `START.cmd` и `START.ps1` не переносились: `node serve.mjs` делает то же самое. `room-fill.test.mjs` перенесён как документ намерения, CI его не запускает — под `docs/**` не попадает ни один тестовый glob. В бандл отсюда не попадает ничего; `repo-hygiene` и полный набор зелёные (2793 pass). Issue: #600 User-Visible: no
17 KiB
Implementation guide: settings dialogs redesign (for the implementing Claude session)
Read this before touching code. It tells you where the four dialogs live in Matysh/houseplan-card, what the prototype in reference/ means, which prototype control maps to which product state key, and what you must not change.
Verified against dev 9683a59 (v1.76.0, 2026-09-18). Re-check line numbers with grep; they drift.
0. Ground rules
- UI only, no global code changes. Only the markup and styles of the four dialogs change. Every product state key, config key, resolver, save path, permission check and side effect stays exactly as it is. A new control must write the same value to the same key the old control wrote. If a visual decision in the prototype needs a behaviour change, do not improvise and do not ask the repository owner: read
OPEN-POINTS.md(known cases with proposed defaults) and ask JB in the chat before implementing that dialog; keep current behaviour meanwhile. - Process.
AGENTS.mdRule #1: product code changes only when the issue carriesS5-ready,S6-in-progressorS7-code-review(gh issue view <NN> --repo Matysh/houseplan-card --json labels). Branchissue/<NN>-settings-dialogs. Commit trailersIssue: #<NN>andUser-Visible: yes. Changelog entries indocs/CHANGELOG.mdanddocs/CHANGELOG.ru.mdin the same commit. Class D paths (dist/**,custom_components/houseplan/frontend/**) only vianpm run bundle:sync. - Prototype JS is a design reference, not code to port. Read
reference/*.jsto learn the markup, class names, states and copy; do not copy localStorage, demo data, the gallery shell orroom-fill.jsinto the product. - Priority when they disagree:
SPEC.md> existing House Plan contracts (data, safety, a11y, hp-dialog, touch) > prototype visuals > prototype implementation details.
1. Where things are in the product
| Surface | Renderer | State | Styles | Notes |
|---|---|---|---|---|
| Space settings (edit/create) | src/houseplan-editor-runtime.ts _renderSpaceDialog() (~13614) |
SpaceDialogState in src/space-dialog.ts; host._spaceDialog |
src/styles/dialogs.styles.ts (.srcrow, .dispsection, .rhint, .colorrow, .areasel, .namein, .helpfieldlabel) |
hp-dialog data-kind="space" wide; create mode has import progress and Skip; switchSpacePlanSource / touchSpaceDisplay guard displayTouched |
| Space display section in onboarding | src/houseplan-onboarding-runtime.ts (~700–780) |
same keys | same | duplicates Display + Room card rows; must get the same controls |
| General settings | _renderSettingsDialog() (~10340) + _renderColorRow() (~10090) |
host._settingsDialog { colors, glowRadius, bgColor, northDeg, bgMode, sunRays, sunRayOrigin, showRoomTooltip, zigbeeTopology, radarShowLive, busy } |
same | data-kind="settings"; Zigbee block is src/hp-zigbee-topology-settings.ts; sun rays select in src/sun-settings-view.ts; compass host._renderCompass() in houseplan-card.ts (~10658); _openBackupExport, _pickBackupImport, _openAlignDialog, undo buttons |
| Room settings (edit/create) | _renderRoomDialog() (~13987), _renderRoomSource() (~13941) |
host._nameSel, _areaSel, _roomFill ('' | none | lqi | light | temp | custom), _roomCustomFill, _roomTempMin/_roomTempMax (strings), _roomTempSrc/_roomHumSrc ('' = average), _roomSrcOpen/_roomSrcFilter, _roomNameScale/_roomLabelScale (fractions) |
same, hp-dialog.roomdialog block |
data-kind="room" wide; temperature bounds in src/room-temperature-controls.ts (roomTemperatureControls), parsing in space-dialog.ts (roomTempThresholdDraft) |
| Device on the plan | _renderMarkerDialog() (~12887) + radar/vacuum sections |
host._markerDialog (keys listed in §5) |
same (.markerlightgroup, .markerradios, .markerhelpfield, .ctrlchips, .dropbtn, .droppanel, .candlist, .habindingbanner) |
data-kind="marker"; preview src/hp-device-preview.ts; *Touched / original* flags drive save semantics |
| Help tooltip | src/hp-help.ts, used via this._help('<i18n key>.help') |
– | own | keep as is; only its placement changes |
| Colour field today | src/hp-color-opacity.ts (hp-color-opacity, event hp-color-opacity-change {color, opacity}) |
– | own | new inline field replaces it in the four dialogs only |
| Dialog shell | src/hp-dialog.ts (wide = --hp-dialog-wide-width, 500px) |
– | own | scope width/badge changes to :host([data-kind="…"]) |
| Bool / range inputs | _boolInput() (~10067, ha-switch fallback checkbox), _rangeInput() (ha-slider) |
– | – | keep using them inside the new rows |
| i18n | src/i18n/{en,ru,de,fr}.json, topology strings in src/i18n/topology/*.json, support strings in src/i18n/support/*.json |
– | – | parity tests reject missing keys; placeholders are a contract |
| Tests | test/space-dialog.test.mjs, test/hp-dialog-contract.test.mjs, room/space/marker unit tests; demo/smoke_*.mjs |
– | – | grep -l "_spaceDialog|_settingsDialog|_markerDialog|roomdialog" demo/*.mjs test/*.mjs |
2. Build order (suggested)
- Primitives first, in one new module pair (names are a proposal):
src/settings-form-view.ts(lit template helpers) andsrc/styles/settings-form.styles.ts(scoped underhp-dialog[data-kind="space"], [data-kind="settings"], [data-kind="room"], [data-kind="marker"]). Take sizes and colours fromreference/styles.css, map colours to HA theme variables (§3). Primitives:card(title, help?, body),subsection(title, help?),settingRow(icon, title, description?, help?, control),compactRow(icon, title, control),segmented(name, options, value, onChange, disabled?),valueTiles(items),colorTiles(items),colorField(...),chips(items, onRemove),sourceButton + sourcePanel,footer(left, status, right). - Colour field component (
hp-color-fieldorcompactmode ofhp-color-opacity): swatch =<input type="color">, hex text, optional opacity number (0–100 in UI, fraction in state), optional Reset. Emits the same{color, opacity}shape. Fallback to a hex text input wheninput[type=color]degrades. - Space dialog (edit + create + onboarding). Smallest surface, validates the primitives.
- General settings, including restyling
hp-zigbee-topology-settingsandsun-settings-view. - Room settings (edit + create queue).
- Device on the plan (largest; keep every conditional branch, §5).
- Smokes and unit tests, i18n for four locales, changelogs, docs,
docs/design/<NN>-settings-dialogs/with the reference andACCEPTANCE.md(paired screenshots).
Gates to run before review: npm run typecheck, npm test, npm run gate:small, npm run bundle:budget, i18n parity tests, npm run bundle:sync for class D.
3. Prototype → product token mapping
| Prototype | Product |
|---|---|
accent #4a9ec6, active text #398fb8 |
var(--primary-color) (text: same, or color-mix darkened for contrast on light theme) |
active fill #f3f9fc, active border #b7d5e4 |
color-mix(in srgb, var(--primary-color) 10%, var(--card-background-color)) and 30% |
card background #fff, form canvas #f6f6f6, plate #fafafa |
var(--card-background-color), var(--secondary-background-color), mix of the two |
line #ddd, #e5e5e5 |
var(--divider-color) |
ink #202124, muted #666 / #777, hint #8a949b |
var(--primary-text-color), var(--secondary-text-color) |
danger #db543d |
var(--error-color) |
| Roboto 14 px body, 16 px h3, 20 px h2 | HA font family; keep 14/16/20 px or HA equivalents, never below 14 px body |
| radii 15 (dialog) / 11 (card) / 8 (buttons, tiles) / 7 (fields) | keep |
| 44 px controls, 40 px compact rows, 72 px value tiles, 32 px help button | keep |
Dark theme: every colour comes from the token set; the swatch shows the resolved colour without a checkerboard.
4. Control mapping per dialog
Value contracts. Any deviation is a bug.
4.1 Space settings (SpaceDialogState) — see docs/FIELD-MAP-space.md for the full table
| Prototype control | Key | Value written | Today |
|---|---|---|---|
| Space name text | title |
string | .namein |
| Grid cell size number + unit | cellCm, cellCmInput, cellCmTouched |
via strictNumber + gridCellFieldToCm (imperial kept) |
#space-cell-cm |
| Floor plan choice cards | source |
'draw' | 'file' via switchSpacePlanSource |
radios plansrc |
| Upload / Browse uploaded | planFile, planUrl, pickSaved, saved* |
unchanged handlers | _pickPlanFile, _toggleServerPlans |
| Always show room borders row | showBorders |
via touchSpaceDisplay |
_boolInput |
| Zero-thickness walls segment | zeroWallStyle |
'dashed' | 'solid' |
select |
| Border & name color field | roomColor, roomOpacity |
hex, 0–100 | hp-color-opacity |
| Room fill segment | fillMode |
SPACE_FILL_UI_MODES value |
radios fillmode |
| Fill color field + Reset | customFill |
{c, a} or null |
hp-color-opacity + Reset |
| Comfort range | tempMin, tempMax |
numbers via strictNumber |
.tempin |
| Visible layers compact rows | hideDecor, hideOpenings (UI inverted), showLqi |
booleans | _boolInput |
| Show room names row | showNames |
via touchSpaceDisplay |
_boolInput |
| Values on the card tiles | labelTemp, labelHum, labelLqi, labelLight |
booleans; disabled when !showNames, values kept |
_boolInput |
| Room-card font size slider + number + Reset | cardFontScale |
fraction (UI shows %) | _rangeInput |
| Plan background segment | bgMode |
null | 'static' | 'daynight' |
select |
| Background color field + Reset | bgColor |
hex or null |
hp-color-opacity + Inherit button |
| Sunlight radios | sunRays |
null | true | false |
select |
| North select + degrees + compass | northDeg |
null or 0–359 int; inherited value from northDegOf(settings, {}) |
number input with placeholder |
| Light-source glow row | glowEnabled |
boolean | _boolInput |
| Footer Copy / Delete / Skip / Cancel / Save | – | openSpaceCopyDialog, _deleteSpace, _skipImport, _saveSpaceDialog unchanged |
– |
Removed from the UI (no state change): card preview _renderCardPreview under the font slider; rhint paragraphs (text moves to ?).
4.2 General settings (host._settingsDialog)
| Prototype control | Key | Value written | Today |
|---|---|---|---|
| Display rows | showRoomTooltip, radarShowLive |
booleans | _boolInput |
| Zigbee links card | zigbeeTopology |
unchanged object; savedEnabled from zigbeeTopologySettingsOf(host._settings).enabled gates provider buttons |
hp-zigbee-topology-settings |
| Colour tiles (8 fills + 2 glow) | colors.<key> |
{c, a}; UI % ↔ fraction |
_renderColorRow |
| Glow radius | glowRadius |
number > 0 via strictNumber; unit gs.unit_m / gs.unit_ft |
#gs-glow-radius |
| Wall fill plate | colors.wall_fill |
{c, a} |
_renderColorRow |
| Plan background segment | bgMode |
'static' | 'daynight' (no null here) |
select |
| Background around the plan field + Reset | bgColor |
hex or null (gs.bg_default) |
hp-color-opacity + button |
| North field + Clear + compass | northDeg |
null or 0–359 int |
input + gs.north_clear |
| Sunlight through windows row | sunRays |
boolean | _boolInput |
| Sun rays segment | sunRayOrigin |
'inner' | 'outer' via renderSunRayOriginSelect callback |
select |
| Data card buttons | – | _openBackupExport, _pickBackupImport, _openAlignDialog, _undoPlanOptimization unchanged; _canEdit gate kept |
– |
| Reset to defaults | whole draft | same reset object as today's gs.reset button |
– |
Keep gs.sun_missing callout when sunStateOf(hass) is falsy.
4.3 Room settings (host fields)
| Prototype control | Field | Value written | Today |
|---|---|---|---|
| Display name | _nameSel |
string; name auto-filled from area when empty (unchanged) | .namein |
| Home Assistant area select | _areaSel |
area id or '' | .areasel |
| "As the space" toggle | _roomFill |
on → ''; off → the current space fill mode as starting value (see OPEN-POINTS Q4) |
first radio |
| Fill segment | _roomFill |
ROOM_FILL_MODES value; non-custom clears _roomCustomFill (unchanged) |
radios rfill |
| Fill color field + Reset | _roomCustomFill |
{c, a} or null (Reset) |
hp-color-opacity |
| Comfort range | _roomTempMin, _roomTempMax |
raw strings; shown when effective fill is temp (roomTemperatureControls) |
roomtemprange |
| Source segment + picker | _roomTempSrc, _roomHumSrc, _roomSrcOpen, _roomSrcFilter |
'' or device:… / entity:… from _roomSrcCandidates() |
_renderRoomSource |
| Font sizes | _roomNameScale, _roomLabelScale |
fractions (UI %) | _rangeInput |
| Footer | – | _roomDialogCancel, _saveRoomEdit, _saveRoom, _keepClosedAsPartitions unchanged; Save disabled by the same tempValid / name / canSaveNew rules |
– |
Removed from the UI: _renderCardPreview at the bottom.
4.4 Device on the plan (host._markerDialog)
| Prototype control | Key(s) | Notes |
|---|---|---|
| Name | name |
– |
| Binding segment | bindingMode ('virtual' | 'ha') |
– |
| Binding picker (button + inline panel + search + Show entities checkbox) | binding, bindingOpen, bindingFilter, showEntities |
candidates from the existing list builder; marker.binding_disabled state kept |
| Room select | room, roomTouched |
options marker.room_auto / marker.room_choose + rooms |
| Tap action select | tapAction, tapActionTouched, tapHintAnnouncement |
keep the announcement text (Target / Now) as the hint |
| Entity to toggle select | toggleEntity, toggleEntityTouched |
conditional, unchanged |
| What to run picker | tapTarget, runFilter |
conditional (tap.run), unchanged |
| Ask for confirmation row | tapConfirm |
hidden for tap.none (OPEN-POINTS Q8) |
| Controls other light sources (chips + search list) | controls, controlsFilter |
existing ctrlchips semantics |
| Include the device temperature in the room | useClimateTemp |
conditional (climrow), keep as a row |
| Light source segment | lightRole, lightRoleTouched |
Auto label shows marker.light_role_auto_yes/no as hint |
| Leading light entity select | lightEntity, lightEntityTouched |
conditional, unchanged |
| Glow mode segment, colour field, brightness slider | glowMode, glowColor, glowBrightness, glowColorDrafted, glowBrightnessDrafted, glowTouched |
disabled states and messages (glow_disabled_*, glow_passive_hint) unchanged |
| Glow radius | glowRadius |
empty = general radius |
| Icon field + clear + Pin | icon, autoIcon |
marker.icon_auto, marker.icon_pin_auto |
| Display select + hint | display |
display.* options; marker.display_hint_*; marker.static_alarm_warning |
| Activity pulse colour / size | rippleColor, rippleSize |
conditional on display mode |
| Value source select | valueSource, valueSourceTouched |
conditional on value display modes |
| Value badge toggle, source, position | valueBadgeEnabled, valueBadgeSource, valueBadgePosition, valueBadgeTouched |
warnings value_badge_static / _missing / _duplicate / _empty |
| Display preview | – | hp-device-preview with its Now / Example controls, inside the tinted block |
| Icon size / rotation | size, angle |
two sliders + numbers |
| Model, Link, Description, Manuals | model, link, description, pdfs |
Attach via the existing file input; chips with remove |
| Additional actions: presence radar, vacuum | radar*, _renderRadarSection, _renderVacSection |
same conditions, restyled |
| Banners | habindingbanner, ha_registry_limited, Open in HA |
callouts in Basics |
| Footer | hideFromPlan, delete, cancel, save |
_toggleMarkerDialogVisibility, _deleteMarker, _saveMarker, same disabled conditions |
5. Things the prototype does not show (implement anyway, same style)
Space create mode (import progress, Skip); onboarding Display section; General gs.sun_missing, undo buttons, _canEdit gating; Room create queue (room.queue_progress, Keep as walls) and canSaveNew; Device: Entity to toggle, What to run, Include the device temperature, Leading light entity, Value source, static alarm warning, binding banners, radar and vacuum sections, bindingOpen disabled state, Show button when hidden. Use settingRow / selectField / callout primitives; do not leave them in the old markup.
6. Verification checklist for the PR
- For each dialog: open, change every control once, save, diff the stored config against a run on
devwith the same actions (byte-identical). - Inverted layers: stored
hide_decor: truerenders Decorative layer OFF; toggling ON storesfalse. - Space:
displayTouchedsurvives source switches in create mode; onboarding writes the same keys. - Room:
''vs mode,nullcustom fill until edited, empty temperature strings inherit. - Device: every
*Touchedflag flips only when the user changes that control;original*restored on cancel. - All
?open the same text keys as before; norhinttext lost (grep the removed keys, each must be used by a?or intentionally deleted with a note). - 320 / 390 / 560 / 200% text / RU / DE / FR / dark theme captures.
- Paired screenshots vs
screenshots/*.pngindocs/design/<NN>-settings-dialogs/ACCEPTANCE.md.