34 KiB
Issue #179 — новый визуальный язык маркеров устройств
- Статус: ревизия 2 после жёлтого ревью r1; готово к повторному ревью ТЗ
- Issue: https://github.com/Matysh/houseplan-card/issues/179
- Приоритет: P1
- Тип: feature / polish / accessibility
- Область: frontend, общий renderer маркера, preview, статическая карточка, документация и visual QA
- Нормативный дизайн-пакет:
House Plan Icons — Developer Package, версия 1.1.1, экспорт 2026-08-18 - Архив: https://github.com/user-attachments/files/31235094/House.plan.Icons.-.Developer.Package.2026-08-19.zip
- SHA-256 архива:
63670C73E25D1E59DDAF1BE236F3D7F2FAC827B9B5D6DD4B77125EA9BC012025 - Figma frames: Light
99:1290, Dark104:1539
1. Пользовательский сценарий и персона
Персона: пользователь House Plan, который одновременно контролирует устройства разных доменов, читает значения датчиков и оценивает состояние связи на светлой либо тёмной теме Home Assistant.
Сценарий: пользователь открывает план и за один взгляд различает обычное, активное, тревожное, заблокированное, разблокированное, виртуальное и недоступное устройство; видит полное значение и Zigbee LQI; получает тот же результат в полном плане, preview редактора и статической карточке пространства.
2. Проблема и результат
До реализации
- plate маркера является одним заполненным rounded-square без общего внешнего shell из дизайн-пакета;
- hover применяется и к unavailable-маркеру;
- selected обозначается amber-цветом и конфликтует с семантикой активности;
- value и legacy secondary value рисуются отдельными спутниками, длинные значения обрезаются ellipsis;
- LQI использует непрерывный HSL-gradient, а не три читаемых диапазона;
- ordinary continuous pulse длится 2,4 с и по умолчанию разрастается до 3×;
- маркер не является клавиатурной целью и не активируется Enter/Space;
- light/dark, focus и semantic states не образуют единой дизайн-системы.
После реализации
Все поверхности используют один runtime-renderer нового shell/core, компоновок Text/Double, LQI и motion из пакета. Семантика устройств и действий остаётся прежней; меняется её единое визуальное и доступное представление.
3. Нормативный источник и порядок разрешения расхождений
Единственный дизайн-источник этой задачи — архив #179. Нормативны его
README.md, SPECIFICATION.md, ACTIVE_ANIMATION_SPEC.md,
DEVELOPER_HANDOFF.md, PACKAGE_ANNOTATION.txt, manifest.json и SVG-примеры.
Порядок приоритета:
- решения владельца в §4;
- текстовые документы архива;
- SVG-примеры архива;
- текущая реализация House Plan — только для поведения, которое пакет не переопределяет.
Статические SVG являются эталонами геометрии и состояний, а не готовыми production-иконками: runtime продолжает использовать динамические MDI glyphs, HA-значения и локализованный текст.
4. Принятые решения владельца
- Unavailable. Визуальный hover и motion отсутствуют. Click/tap остаётся доступен и выполняет текущее действие поверхности (More Info либо открытие настроек).
- Пользовательские pulse-настройки. Если
ripple_colorилиripple_sizeявно сохранены, они сохраняют приоритет. При отсутствии поля используются геометрия и цвет пакета. - Zigbee LQI.
0…40— red,41…179— amber,180+— green. - Клавиатура. Интерактивный маркер получает focus; Enter и Space выполняют то же действие, что click на этой поверхности.
- Специальная кнопка либо новый action для клавиатуры не вводятся.
5. Цели
- Реализовать shell/core, цвета, состояния, focus и motion пакета #179.
- Сохранить единую проекцию на полном плане, в editor preview и static card.
- Показывать полное динамическое значение без ellipsis в Text/Double layout.
- Сделать маркер читаемым в light/dark и на цветном фоне плана.
- Обеспечить минимум 44×44 CSS px для интерактивной цели и keyboard parity.
- Сохранить существующую HA-семантику, пользовательские настройки, координаты и безопасный pipeline действий.
6. Не входит в задачу
- новые правила определения
on,working,open, alarm и activity; - новые HA service calls, actions или обход secure confirmation;
- новые display modes и новые пользовательские настройки дизайна;
- смена MDI-иконок либо поставка собственного icon font;
- настройка порогов LQI пользователем;
- автоматическое устранение пересечений соседних маркеров;
- изменение комнатной LQI-заливки, Glow, света, комнатных fills или isometry;
- миграция сохранённых координат и marker config;
- перенос каждого example SVG из архива в production bundle.
7. Визуальная система
7.1. Геометрия
Маркер состоит из независимых слоёв:
- прозрачная hit area минимум 44×44 CSS px;
- внешний shell: прозрачный fill, stroke и внешние shadows;
- внутренний core с MDI glyph либо динамическим значением;
- semantic/focus/selection decoration;
- pulse layer с
pointer-events: none; - LQI и дополнительные секции, если включены.
Для Icon-эталона shell имеет диаметр 101.5/127 viewBox, core — 80/127;
отношение core/shell равно 0.788 с допустимым расхождением не более 0,5 CSS
px на QA-размерах 32, 56 и 96 px. Координата сохранённого маркера остаётся
центром icon core, поэтому включение Text/Double и смена стороны не двигает
устройство на плане.
Light shell: stroke #BCBCBC; core по умолчанию white. У light-варианта
запрещены backdrop blur и inner shadows. Внешние shadows масштабируются от
референса 56 px: 0 1px 2px rgb(37 40 45 / 12%) и
0 4px 8px -1.07px rgb(37 40 45 / 18%).
Dark использует core #252525, stroke и внешние shadows неизменённой
dark-ревизии пакета, но production renderer нормативно следует поставленному
варианту Dark/Icon Default No Blur.svg: backdrop-filter: blur(20px) на
каждом маркере запрещён. Это сохраняет дизайн-пакетный fallback и исключает
дорогой отдельный backdrop-composite layer для 20–200 устройств в View/kiosk.
Тема берётся из актуальных HA theme tokens; переключение темы не требует
remount и не меняет semantic state.
7.2. Цвета
Нормативные semantic colors:
| Смысл | Цвет |
|---|---|
| hover / focus / neutral activity | #0C82F0 |
| active / working / unlocked | #F0A00C |
| alert / low LQI | #F0410C |
| unavailable core | #B5BAC1 |
| locked glyph | black |
| presence activity / high LQI | #1DC21D |
Тема не подменяет semantic colors. Контраст glyph/core и читаемость текста проверяются в light/dark и на светлом, тёмном и насыщенном фоне плана.
7.3. Состояния и приоритет
Нормативный визуальный приоритет:
Alert > Focus > Selected > Hover > semantic state > Default
- Default: theme-default shell/core и обычный glyph.
- Hover: blue decoration пакета; не меняет semantic state и не перезапускает pulse.
- Focus: blue keyboard-focus decoration, видимая без hover.
- Selected: отдельная selection decoration пакета, не amber semantic fill.
- Active/working: amber.
- Lock: locked glyph black; Unlock: unlocked glyph amber.
- Alert: red и всегда выше остальных состояний.
- Unavailable: gray core/glyph по текущему правилу прозрачности; без визуальной реакции на hover и без motion.
- Virtual: пунктирный внешний shell. У обычного virtual default/hover меняется только цвет оформления. Реальная HA unavailable и active pulse для HA-less virtual device не синтезируются.
Текущая semantic-модель neutral/open/working/alarm, activity resolver и
domain-specific HA rules сохраняются. Для lock presentation дополнительно
различает locked/unlocked, не меняя secure action. Generic physical open
для cover/contact/valve не переименовывается в lock state и продолжает
существующую отдельную semantic-проекцию.
display: static_icon получает новую theme-default shell/core геометрию, но
остаётся нереактивным к HA state, values и pulse. На интерактивной поверхности
он сохраняет разрешённые hover/focus/click.
8. Text, Double и дополнительные секции
8.1. Text
В display: value динамический текст находится внутри общего shell. Полная
строка всегда доступна визуально: CSS ellipsis, clipping и скрытие хвоста
запрещены. Сначала шрифт равномерно уменьшается до 0.25 × marker size
(8 CSS px при размере 32); если этого недостаточно, text section расширяет
shell. Измерение выполняется детерминированно и кэшируемо, без layout-loop и
ResizeObserver на каждый маркер.
8.2. Double
Иконка и value badge входят в один общий shell. Поддерживаются сохранённые
позиции right, bottom, left, top; icon core остаётся anchor. Высота
внутренней value-секции равна 0.7875 × icon core, как в референсе.
Если у нетронутой legacy-конфигурации одновременно разрешены два значения, второе становится третьей секцией того же shell, а не отдельным спутником. Новая пользовательская модель второго бейджа не вводится.
Длинное значение проходит тот же auto-fit, что Text: полная строка без ellipsis, а shell при необходимости расширяется. Динамический текст остаётся текстом DOM, а не SVG path; шрифт — HA/system Roboto с weight 600 и безопасным system fallback.
9. Zigbee LQI
Числовое LQI располагается под shell по геометрии пакета и окрашивается категориально:
<= 40—#F0410C;41…179—#F0A00C;>= 180—#1DC21D.
Границы применяются к текущему resolved average LQI без изменения источника,
агрегации, форматирования и флагов show_signal. Значение 0 валидно. Для
missing/unavailable LQI сохраняется текущая политика отсутствия подписи.
Категориальная функция применяется только к marker LQI. Непрерывный HSL-gradient комнатной LQI-заливки остаётся неизменным.
В Light/Zigbee LQI Low.svg архива low ошибочно окрашен amber. Текстовая
спецификация архива и решение владельца имеют приоритет: production low — red.
10. Motion
10.1. Continuous
- одно кольцо;
- duration
3.6s, infinite; - easing
cubic-bezier(.45,.05,.55,.95); - scale
1 → 1.5, opacity.55 → 0; - цвет соответствует resolved ordinary activity:
presence— green; running/working/open/unlocked — amber; neutral transition — blue. Явный пользовательский либо валидный live-light color сохраняет приоритет по §10.4.
10.2. Short
- три кольца;
- каждое длится
1.1s; - delays
0,1.1s,2.2s; - easing
cubic-bezier(.22,.61,.36,1); - общий цикл
3.3s; - новое событие перезапускает цикл через существующий generation/runtime.
10.3. Alert
- red независимо от custom pulse color;
- wave stroke 3 reference units;
- scale до
1.5; - два wave-starts с интервалом
1.2s, цикл2.4s; - easing
cubic-bezier(.22,.61,.36,1); - alert имеет приоритет над ordinary short/continuous.
10.4. Пользовательские настройки и fallback
Явно сохранённые ripple_color и ripple_size сохраняются без миграции.
Отсутствующий ripple_size использует package scale 1.5, отсутствующий
ripple_color разрешается в порядке:
- валидный live RGB контролируемого light, если он уже является текущим источником pulse color;
- semantic green для presence;
- semantic amber для running/working/open/unlocked;
- package blue для neutral transition/activity.
UI-default размера меняется на 1.5, но Open → Save не материализует поле и
не переписывает существующее явно сохранённое значение. Backend допустимый
диапазон и import/export round-trip не меняются.
10.5. Reduced motion
При prefers-reduced-motion: reduce animated rings отсутствуют. Ordinary
activity обозначается статической semantic point из существующего единого
pulse renderer; alert остаётся красным статическим состоянием. Marker не
создаёт собственную media-query subscription: используется одна подписка на
surface/card host.
11. Интерактивность и доступность
- Интерактивные маркеры в View/kiosk и Device editor получают
tabindex="0"и button semantics. - Enter/Space вызывают тот же существующий click handler текущей поверхности: action/More Info в View и открытие настроек в Device editor.
- Space предотвращает прокрутку только когда активирует marker.
- Pointer/touch поведение, long-press/context action и drag в редакторе не меняются; keyboard path не создаёт прямой HA service call.
- Background/Plan, read-only static card и неинтерактивный preview не входят в tab order и не получают ложный button role.
- Minimum hit area 44×44 CSS px центрирована по icon core и не влияет на visual bounds, координату, pulse либо collision geometry.
aria-labelвключает локализованное имя, semantic state, activity/alert и, если LQI показан, число и понятный диапазон «низкий/средний/высокий».- Цвет и motion не являются единственным носителем alert, availability, lock или LQI band.
- Focus после закрытия открытого marker dialog возвращается на исходный marker по существующему dialog contract.
Secure lock/cover/valve actions продолжают проходить через текущий action resolver и confirmation policy. Новый DOM-shell и keyboard handler не имеют отдельного пути обхода подтверждения.
Touch editor: best effort / intentionally degraded. Device editor сохраняет текущие touch drag/tap/pointer-cancel guarantees и увеличенную hit area, но его полный editor workflow остаётся desktop-first. View/kiosk touch — блокирующий и полностью поддерживаемый контракт.
12. Совместимость и данные
- config schema version не меняется;
display,value_badge,value_badge_position,ripple_color,ripple_size,show_signalи legacy fields сохраняют формат и round-trip;- существующие координаты относятся к icon core и не мигрируют;
- явно сохранённые pulse color/size воспроизводятся как раньше, кроме нового shell и motion geometry;
- отсутствующие поля не материализуются при Open → Save;
- старые версии карточки читают тот же config; downgrade меняет только внешний вид;
- runtime не зависит от наличия в bundle статических SVG-примеров;
- ru/en i18n parity обязательна.
13. Архитектурный контракт
HA/device registry + marker config
→ existing semantic/activity resolvers
→ resolveDevicePresentation
→ resolveDevicePulse
→ renderDeviceFace (shell/core/text/LQI/motion)
→ full plan | editor preview | static space card
Один resolved presentation и один renderDeviceFace() остаются источником
истины всех поверхностей. Surface передаёт только interaction/theme context и
не вычисляет semantic state повторно.
Рекомендуемые зоны изменений:
src/device-visual.ts— lock presentation facet без смены action semantics;src/device-presentation.ts— theme/semantic projection, marker-only LQI band, defaults pulse;src/device-pulse.ts— package timings/scale/reduced motion;src/device-face.ts— единый shell и Text/Double/third section DOM;src/styles.ts— tokens, geometry, states, focus, auto-fit и motion;src/houseplan-card.tsи общие surface adapters — role/tabindex/keydown;- i18n, unit, smoke, golden fixtures и документация.
Нельзя вводить surface-specific копию state mapping, formatter либо motion runtime. Text auto-fit не должен читать layout на каждом render/animation frame. Pulse использует transform/opacity и не меняет layout.
14. Эдж-кейсы
- light/dark hot switch во время continuous/short pulse;
- colored room background и минимальный marker size 32;
- marker sizes 32/56/96, user scale 0.5…3 и explicit ripple size 1…20;
- alarm во время hover/focus/selected и снятие alarm;
- unavailable во время active pulse, затем reconnect baseline без false short;
- locked, unlocked, locking и unlocking с secure action;
- virtual default/hover без HA entity и linked virtual with resolved controller;
static_iconпри реальном alarm/working/unavailable;0,false, very long localized string, unit symbol и third legacy value;- Double на четырёх сторонах вместе с LQI;
- LQI exactly
0,40,41,179,180, missing; - rapid short retrigger, short поверх continuous, reduced-motion hot switch;
- keyboard activation после drag, click confirmation и dialog focus return;
- несколько cards одного плана с независимыми runtime и theme context;
- hidden/HA-disabled/orphaned marker и read-only static card.
15. Acceptance criteria
- AC1 — shell/theme. Icon, Text и Double используют package shell/core geometry и Light/Dark tokens; light не содержит blur/inner shadow. Доказательство: unit token assertions + reviewed golden 32/56/96.
- AC2 — states. Default, Hover, Focus, Selected, Active, Lock, Unlock, Alert, Virtual и Unavailable соблюдают §7.3 и нормативный приоритет. Доказательство: presentation unit matrix + light/dark state-table golden.
- AC3 — unavailable. Unavailable не получает visual hover/motion, но click/tap по-прежнему открывает текущее действие поверхности. Доказательство: unit + browser smoke.
- AC4 — secure locks. Locked black, unlocked amber, locking/unlocking continuous; pointer и keyboard проходят один action/confirmation path. Доказательство: unit + secure-action smoke.
- AC5 — virtual/static. Virtual имеет dashed shell; HA-less virtual не
синтезирует unavailable/activity.
static_iconиспользует theme-default shell и не реагирует на live state/value/pulse. Доказательство: unit + existing static-icon smoke. - AC6 — Text/Double. Полные строки видимы без ellipsis; четыре стороны, anchor, auto-fit и третья legacy section соответствуют §8. Доказательство: unit layout facts + browser smoke + long-value golden.
- AC7 — LQI. Marker LQI использует red/amber/green thresholds на границах, а room fill сохраняет continuous gradient. Доказательство: boundary unit tests + room-fill regression test.
- AC8 — motion. Continuous/short/alert timings, ring counts, retrigger, easing, reason→color mapping, scale и colors соответствуют §10; explicit color/size сохраняются. Доказательство: pure resolver tests + DOM/style smoke.
- AC9 — reduced motion. Rings отсутствуют, ordinary point и static red alert остаются; одна subscription на host. Доказательство: unit + reduced-motion browser smoke.
- AC10 — keyboard/a11y. Только интерактивные surfaces получают focus; Enter/Space эквивалентны click, hit area >=44×44, aria-label содержит state/LQI, focus возвращается после dialog. Доказательство: keyboard/touch/browser smoke.
- AC11 — parity. Full plan, editor preview и static card используют один presentation/face renderer и дают одинаковую read-only projection. Доказательство: existing preview/static parity smokes.
- AC12 — совместимость. Schema и saved config не мигрируют; Open → Save не материализует defaults; explicit pulse settings round-trip. Доказательство: frontend/backend config tests.
- AC13 — производительность. Нет per-frame JS, per-marker media query,
ResizeObserver/layout loop или SVG example assets в production bundle;
transform/opacity motion не вызывает layout; ни Light, ни Dark marker не
создаёт per-marker
backdrop-filter/backdrop-composite layer. Доказательство: code assertion +performance_smoke.mjsперед beta. - AC14 — release artifacts. ru/en copy, guides, architecture/testing docs, changelogs, targeted smokes и golden review актуальны.
16. Тестирование
16.1. Цикл реализации
После каждого логического блока:
npm run typecheck
npm test
npm run build
Unit coverage:
- полная state/priority/theme/display matrix;
- lock/unlock и generic open не смешиваются;
- LQI boundaries и неизменность room gradient;
- pulse kinds, timings, fallback/explicit settings и reduced motion;
- Text/Double/third section facts, long values и anchor;
- keyboard surface policy и accessible labels;
- config/default/round-trip compatibility.
16.2. Targeted browser smoke перед S7
Добавить demo/smoke_device_icon_design.mjs и прогнать его вместе с:
demo/smoke_device_preview_parity.mjs;demo/smoke_static_icon.mjs;demo/smoke_state_value.mjs;demo/smoke_disabled_device.mjs;- релевантным secure-action/keyboard smoke из существующего набора.
Новый smoke проверяет light/dark, все состояния, Text/Double, четыре позиции, long value, LQI boundaries, unavailable click, 44×44 hit area, keyboard и reduced motion.
16.3. Mutation gates
Зарегистрировать targeted mutants, которые обязаны быть пойманы тестами:
- generic hover снова применяется к
.unavail; - LQI
40/180уходит в соседний диапазон либо меняет room gradient; - Text/Double снова получает ellipsis;
- keyboard activation вызывает отдельный action path.
16.4. Golden и pre-beta
Golden matrix: desktop/mobile, light/dark, neutral/hover/focus/selected,
active/lock/unlock/alert/virtual/unavailable, Text, четыре Double, third section,
LQI low/mid/high, reduced motion, sizes 32/56/96 и цветной фон.
Комбинации Selected + Hover/Working/Green/Alert и Focus + Alert проверяются
в обеих темах: Light сверяется с прямыми SVG-эталонами, Dark — с тем же
нормативным приоритетом слоёв, поскольку отдельных Dark combo-assets в архиве
нет.
Golden принимается только по Linux CI artifact согласно HP-QA-01. Перед beta
запускаются полный golden, smoke и performance наборы по release runbook;
Windows не является каноном полного HA harness из-за fcntl.
17. Риски и откат
Риски
- рост shell у длинных значений может чаще пересекаться с соседями;
- old explicit ripple size 3+ визуально заметно больше package default;
- Dark No-Blur отличается от основного Dark example отсутствием backdrop-filter, но сохраняет core/stroke/shadow geometry пакета;
- новый focus/tab order увеличивает число клавиатурных остановок на больших планах;
- ошибочная привязка общего
lqiColor()изменит комнатную заливку; - новый DOM может изменить snapshot/golden fingerprint всех поверхностей.
Митигации
- центр icon core и saved coordinates неизменны;
- explicit settings не переписываются скрыто;
- marker-only LQI helper отделён и покрыт regression test;
- интерактивность добавляется только существующим actionable surfaces;
- один renderer и одна token table исключают drift;
- No-Blur Dark не создаёт 20–200 дорогих backdrop-composite layers;
- targeted golden содержит контрастные backgrounds и dense layout.
Откат
Откат выполняется одним revert implementation commit. Миграции данных нет, поэтому rollback возвращает старый renderer без преобразования config. ТЗ и архив остаются диагностическим контрактом.
18. Release-артефакты
В том же user-visible implementation commit обновить:
docs/CHANGELOG.mdиdocs/CHANGELOG.ru.mdсо ссылкой на #179;docs/USER-GUIDE.mdиdocs/USER-GUIDE.ru.md: состояния, Text/Double, LQI, keyboard и static mode;docs/ARCHITECTURE.md: общий presentation/face pipeline и theme context;docs/TESTING.md: новый smoke, mutation и golden matrix;- ru/en i18n для доступных state/LQI descriptions и изменённого static hint;
- screenshot/golden fingerprints и review manifest по HP-QA-01.
Release note: единая новая визуальная система устройств на светлой и тёмной теме, полные значения, читаемый LQI и клавиатурное управление. Поставка сначала в beta; stable — только после release gates.
19. План реализации
- Зафиксировать package tokens и pure projection facts unit-тестами.
- Расширить presentation lock/LQI/theme facts без изменения semantic/action resolvers.
- Перевести
renderDeviceFace()на shell/core и общие Text/Double sections. - Реализовать package states, focus, unavailable и light/dark CSS.
- Обновить pulse timings/defaults/reduced motion.
- Подключить keyboard/hit area/accessibility к интерактивным surfaces.
- Обновить preview/static parity, settings default и i18n/docs.
- Добавить unit, mutation, targeted smoke и golden fixtures.
- Прогнать typecheck, unit, build и named smokes; отправить в S7.
- Перед beta выполнить полный golden/smoke/performance gate в Linux CI.
20. Принятые технические предположения
- Package SVGs — visual reference; runtime DOM/SVG строится из тех же ratios и tokens, потому что MDI glyph, HA text и theme динамические.
Light/Zigbee LQI Low.svgсодержит ошибочный amber; red из textual spec и решения владельца нормативен.- Package не задаёт minimum font size для бесконечно длинного значения:
используется floor
0.25 × marker size, затем расширяется shell. - Generic physical
openсохраняет текущую отдельную semantic-проекцию; специальные black/amber Lock/Unlock применяются только к lock domain. static_iconостаётся нереактивным, но default plate становится theme-aware, потому что package задаёт разные Light/Dark defaults.- Отсутствующий pulse color сохраняет существующий validated live-light RGB раньше semantic fallback; это единственный package-compatible способ не потерять текущий пользовательский light-color effect.
- Marker LQI меняет только цвет подписи; room LQI fill остаётся непрерывным.
- Preview и static card показывают новый дизайн, но не получают ложную интерактивность либо tab stop.
- Green continuous variant нормативно означает
presence; amber означает running/working/open/unlocked, blue — neutral transition. Это явный mapping трёх package assets, который архив сам не задаёт. - Production Dark использует поставленный
Icon Default No Blurfallback: визуально дорогойbackdrop-filter: blur(20px)на каждом marker запрещён ради блокирующих View/kiosk поверхностей с 20–200 устройствами.