Files
houseplan-card/docs/specs/179-device-icons-redesign.md
T
2026-08-19 22:06:25 +03:00

34 KiB
Raw Blame History

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, Dark 104: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-примеры.

Порядок приоритета:

  1. решения владельца в §4;
  2. текстовые документы архива;
  3. SVG-примеры архива;
  4. текущая реализация House Plan — только для поведения, которое пакет не переопределяет.

Статические SVG являются эталонами геометрии и состояний, а не готовыми production-иконками: runtime продолжает использовать динамические MDI glyphs, HA-значения и локализованный текст.

4. Принятые решения владельца

  1. Unavailable. Визуальный hover и motion отсутствуют. Click/tap остаётся доступен и выполняет текущее действие поверхности (More Info либо открытие настроек).
  2. Пользовательские pulse-настройки. Если ripple_color или ripple_size явно сохранены, они сохраняют приоритет. При отсутствии поля используются геометрия и цвет пакета.
  3. Zigbee LQI. 0…40 — red, 41…179 — amber, 180+ — green.
  4. Клавиатура. Интерактивный маркер получает focus; Enter и Space выполняют то же действие, что click на этой поверхности.
  5. Специальная кнопка либо новый action для клавиатуры не вводятся.

5. Цели

  1. Реализовать shell/core, цвета, состояния, focus и motion пакета #179.
  2. Сохранить единую проекцию на полном плане, в editor preview и static card.
  3. Показывать полное динамическое значение без ellipsis в Text/Double layout.
  4. Сделать маркер читаемым в light/dark и на цветном фоне плана.
  5. Обеспечить минимум 44×44 CSS px для интерактивной цели и keyboard parity.
  6. Сохранить существующую 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. Геометрия

Маркер состоит из независимых слоёв:

  1. прозрачная hit area минимум 44×44 CSS px;
  2. внешний shell: прозрачный fill, stroke и внешние shadows;
  3. внутренний core с MDI glyph либо динамическим значением;
  4. semantic/focus/selection decoration;
  5. pulse layer с pointer-events: none;
  6. 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 разрешается в порядке:

  1. валидный live RGB контролируемого light, если он уже является текущим источником pulse color;
  2. semantic green для presence;
  3. semantic amber для running/working/open/unlocked;
  4. 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, которые обязаны быть пойманы тестами:

  1. generic hover снова применяется к .unavail;
  2. LQI 40/180 уходит в соседний диапазон либо меняет room gradient;
  3. Text/Double снова получает ellipsis;
  4. 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. План реализации

  1. Зафиксировать package tokens и pure projection facts unit-тестами.
  2. Расширить presentation lock/LQI/theme facts без изменения semantic/action resolvers.
  3. Перевести renderDeviceFace() на shell/core и общие Text/Double sections.
  4. Реализовать package states, focus, unavailable и light/dark CSS.
  5. Обновить pulse timings/defaults/reduced motion.
  6. Подключить keyboard/hit area/accessibility к интерактивным surfaces.
  7. Обновить preview/static parity, settings default и i18n/docs.
  8. Добавить unit, mutation, targeted smoke и golden fixtures.
  9. Прогнать typecheck, unit, build и named smokes; отправить в S7.
  10. Перед beta выполнить полный golden/smoke/performance gate в Linux CI.

20. Принятые технические предположения

  1. Package SVGs — visual reference; runtime DOM/SVG строится из тех же ratios и tokens, потому что MDI glyph, HA text и theme динамические.
  2. Light/Zigbee LQI Low.svg содержит ошибочный amber; red из textual spec и решения владельца нормативен.
  3. Package не задаёт minimum font size для бесконечно длинного значения: используется floor 0.25 × marker size, затем расширяется shell.
  4. Generic physical open сохраняет текущую отдельную semantic-проекцию; специальные black/amber Lock/Unlock применяются только к lock domain.
  5. static_icon остаётся нереактивным, но default plate становится theme-aware, потому что package задаёт разные Light/Dark defaults.
  6. Отсутствующий pulse color сохраняет существующий validated live-light RGB раньше semantic fallback; это единственный package-compatible способ не потерять текущий пользовательский light-color effect.
  7. Marker LQI меняет только цвет подписи; room LQI fill остаётся непрерывным.
  8. Preview и static card показывают новый дизайн, но не получают ложную интерактивность либо tab stop.
  9. Green continuous variant нормативно означает presence; amber означает running/working/open/unlocked, blue — neutral transition. Это явный mapping трёх package assets, который архив сам не задаёт.
  10. Production Dark использует поставленный Icon Default No Blur fallback: визуально дорогой backdrop-filter: blur(20px) на каждом marker запрещён ради блокирующих View/kiosk поверхностей с 20–200 устройствами.