# 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. Состояния и приоритет Нормативный визуальный приоритет: ```text 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. Архитектурный контракт ```text 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. Цикл реализации После каждого логического блока: ```bash 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 устройствами.