# Issue #212 — размер, форма, нажатие и touch-hover маркеров устройств - **Issue:** https://github.com/Matysh/houseplan-card/issues/212 - **Связанные issue, реализуемые целиком здесь:** #22, #154, #181 - **Приоритет:** P1 - **Тип:** bug/feature/polish, обычный трек - **Пользовательское изменение:** да - **Touch editor:** best effort / intentionally degraded ## 1. Сценарий Домочадец смотрит план с телефона, настенной панели либо компьютера и нажимает на устройство. Маркер должен занимать меньше места, точно показывать длинное значение, сразу подтверждать принятую команду и не оставаться визуально «наведённым» после tap. Администратор видит ту же геометрию в редакторе устройств и его preview, не теряя сохранённую позицию или настроенный размер. ## 2. Что человек увидит до и после До изменения новые маркеры выглядят крупнее требуемого, длинное значение имеет эллиптическую внутреннюю плашку, команда может не дать немедленного ответа, а touch иногда оставляет hover. После изменения маркер на 10% компактнее, внутренняя и внешняя капсулы эквидистантны, выполненное действие даёт короткий мягкий отклик, а hover существует только у настоящей мыши. ## 3. Проблема и подтверждённая причина Аудит текущего `dev` подтвердил четыре независимых дефекта/пробела, которые по решению владельца выпускаются одной задачей: 1. Общий renderer использует `--dev-size` как диаметр icon core и строит shell размером `1.26875 × core`. После перехода на пакет #179 итоговый marker footprint стал визуально слишком крупным; отдельной общей коррекции 0,9 нет. 2. `display: value` расширяет `.device-core` по тексту, но оставляет `border-radius: 50%`. Для широкого прямоугольника это эллипс. Внешний shell использует постоянный радиус, равный половине своей высоты, поэтому зазор между двумя формами меняется по дуге; это видно на скриншоте #212. 3. `_clickDevice()` выполняет action либо открывает surface, но не создаёт отдельного transient feedback между принятием input и новым HA state. 4. `_notePointer()` только навсегда защёлкивает статический `_touchSeen` и закрывает `_tip`. `_hoverRoom` продолжает устанавливаться из mouse events, View/shared CSS содержит негейтированные `:hover`, а настоящий mouse после touch не может вернуть hover без reload. Это подтверждает #154. #181 также подтверждён: каноническое описание View hover находится в `docs/UX-MODES.md`, а не в `docs/CANVAS.md`. ## 4. Нормативные источники и приоритет При расхождении требования применяются в таком порядке: 1. тело #212 и прямое решение владельца выполнить здесь весь scope #22/#154/#181; 2. это ТЗ после зелёного SPEC-REVIEW; 3. дизайн-пакет #179 версии 1.1.1 и принятые уточнения #211; 4. `docs/SCOPE.md`, `docs/TOUCH-SUPPORT.md`, `docs/UX-MODES.md` и `docs/USER-GUIDE.ru.md`; 5. текущая реализация как compatibility baseline. Числа #212 новее старого ТЗ #22 и имеют приоритет: минимальный scale равен `0.95`, а полный цикл длится `200 ms`. Остальной принятый продуктовый контракт #22 перенесён в §§9–11 и не требует чтения незамерженной ветки той задачи. ## 5. Scope В задачу входят: - уменьшение полной визуальной геометрии каждого device marker на 10% поверх действующих global/per-device размеров; - общий renderer full View/kiosk, Device preview и `houseplan-space-card`; - исправление формы основного `display: value`; - click/tap/keyboard feedback только реально исполняемых marker actions; - confirmation, rapid repeat, reduced-motion и lifecycle feedback; - единый instance-local pointer-modality contract для full card и эквивалентный локальный contract для интерактивной кнопки static card; - room/device/opening/vacuum/header/dialog/shared-control hover в View; - touch, pen, desktop mouse и hybrid touch → mouse; - обновление канонических пользовательских/UX/testing документов; - unit, source-contract, browser smoke, golden и performance evidence. ## 6. Не входит в задачу - изменение дизайн-пакета, цветов, state priority, glyph viewport, pulse geometry, LQI thresholds или lock security; - изменение сохранённых `icon_size`, marker `size`, `angle`, layout coordinates либо автоматическая migration конфигурации; - optimistic смена HA state, icon, semantic color или `aria-pressed`; - feedback для info/more-info, открытия редактора, ссылки, no-op, secure, unavailable/missing target либо отменённого жеста; - новый пользовательский toggle интенсивности/длительности feedback; - новая поддержка touch-редакторов сверх действующего safety floor; - реализация room click-to-fit #152; - закрытие #22, #154, #181 или #212 исполнителем; - принятие golden baseline на Windows либо выпуск beta без команды владельца. ## 7. Уменьшение визуального размера на 10% ### 7.1. Формула Пусть действующий размер до #212 равен: ```text current = resolved card icon_size × resolved per-marker size ``` Новый визуальный base size: ```text visual = current × 0.90 ``` Коэффициент применяется после разрешения сохранённых настроек. Поэтому default, явно заданный global `icon_size` и per-marker `size` уменьшаются одинаково на 10%, а их относительные различия сохраняются. ### 7.2. Что масштабируется От нового visual base зависят: - icon/value core, shell, glyph и text; - Double/value/legacy sections; - state/focus/selection rings, border widths и shadows; - marker-owned LQI, disabled/new badges и activity geometry; - vacuum puck, поскольку он использует уменьшенный package base icon; - preview/static projection того же face. Glow radius, room geometry, sun, decor, label font, plan camera и координаты не масштабируются. Сохранённая точка остаётся центром icon core. Минимальная интерактивная область остаётся не меньше 44×44 CSS px и не уменьшается вместе с painted face. ### 7.3. Совместимость настроек Значения в storage и UI не переписываются и не материализуются. Открытие и сохранение старой настройки не добавляет множитель. House Plan применяет визуальную коррекцию одинаково к старому и новому плану. Пользователь по-прежнему может компенсировать размер существующими global/per-marker controls. ## 8. Геометрия `display: value` Основной value marker остаётся Text shell из пакета #179: - высота внутреннего core равна visual base size; - ширина растёт по полному тексту и padding, без ellipsis; - радиус внутреннего core равен половине его **высоты**, а не половине ширины; - внешний shell повторяет ту же stadium/capsule форму с радиусом половины своей высоты; - расстояние между core и shell равно package inset на прямых сторонах и на полукруглых торцах с допустимой растровой погрешностью не более 0,5 CSS px на QA-размерах 32, 56 и 96 px до коэффициента 0,9; - сохранённый anchor остаётся в центре исходного icon core, а расширение идёт симметрично влево/вправо. Исправление относится к основному `display: value`. Double value section уже имеет нормативный фиксированный pill radius `0.39375 × core`; её форма не переопределяется, кроме общего уменьшения 10%. ## 9. Кому показывается feedback Источник истины — финальный outcome существующего action resolver/path, а не icon, domain или одно наличие `tap_action`. Feedback получает activation, который после всех guards: - переключает хотя бы одну доступную HA target либо virtual light; - выполняет допустимую cover operation; - запускает разрешённый script/scene/automation action; - отправляет другую уже поддерживаемую безопасную service command. Feedback отсутствует, если activation: - открывает House Plan info, HA more-info, Device editor, link или settings; - только открывает confirmation и ещё не подтверждён; - является secure/no-target/unavailable/missing/unsupported no-op; - отменён pan, pinch, drag, long press, context click, вторым pointer, `pointercancel` либо потерей capture. Для partial group достаточно одной реально dispatch-нутой target. При confirmation feedback начинается только после положительного подтверждения и повторной валидации target, одновременно с фактическим dispatch. ## 10. Визуальный contract нажатия Один accepted action создаёт один цикл: ```text scale 1.00 → 0.95 → 1.00; total duration 200 ms ``` - движение плавное, с minimum scale около середины цикла и без паузы; - масштабируется только painted device body/shell относительно собственного центра; hit target, saved anchor, LQI, badges, activity waves и Glow не сжимаются и не сдвигаются; - feedback композируется с общей коррекцией 0,9, per-marker size, angle, Text/Double layout и semantic state, не перезаписывая их transform; - fill, glyph, semantic color, state ring, opacity и z-index не используются для имитации успешного HA state; - цикл начинается в том же interaction turn, что dispatch, и не ждёт HA state; - повторный отдельный action перезапускает один текущий цикл с реально видимого значения, не создавая очередь и не блокируя второй допустимый service call; - state update не повторяет и не продлевает цикл. Pointerdown потенциально actionable marker может сохранять обычный browser pressed/active cue, но канонический цикл выше принадлежит только accepted dispatch. Touch compatibility click и keyboard activation создают не более одного цикла. ## 11. Keyboard, reduced motion и lifecycle feedback - Enter/Space на интерактивном marker идут через тот же `_clickDevice()` и дают тот же цикл ровно один раз при dispatch. - Focus и `:focus-visible` остаются видимыми во время/после feedback. - При `prefers-reduced-motion: reduce` scale tween отсутствует. Accepted action получает краткий theme-safe outline/brightness accent минимум на один painted frame без смены semantic цвета или unavailable opacity. - Быстрый mode/space change, visibility hidden, disconnect/remount и удаление marker отменяют незавершённую animation без остаточного class/style. - Feedback не меняет action debounce, confirmation или service payload. ## 12. Pointer modality и sticky-hover ### 12.1. Authority Каждый экземпляр интерактивной карточки имеет локальное состояние: ```text unknown | mouse | touch | pen ``` - начальное `unknown` не включает pointer-only hover; - настоящий `PointerEvent(pointerType='mouse')` включает `mouse`; - `touch` и `pen` немедленно выключают CSS/JS hover и очищают transient state; - следующий настоящий mouse/trackpad pointer event возвращает `mouse` без reload и без произвольного timeout; - compatibility `MouseEvent` после touch не меняет authority; - media query не является authority, но CSS hover разрешён только когда фактическая modality — `mouse` **и** среда сообщает hover/fine capability; - modality не хранится, не синхронизируется между карточками и не живёт в global mutable singleton. `houseplan-space-card` применяет тот же pure resolver в своём экземпляре для footer button. Статический план остаётся pointer-inert. ### 12.2. Cleanup Один idempotent helper очищает только transient hover: - `_hoverRoom` и hover-owned `_tip`; - DOM gate, разрешающий CSS hover; - transient hover ownership вложенных shared components. Он вызывается на touch/pen `pointerdown`, terminal `pointerup`/`pointercancel`, `lostpointercapture`, начале/окончании multi-touch/pinch, terminal tap/click, mode/space change, `visibilitychange` в hidden и disconnect. Cleanup не снимает DOM focus, не закрывает dialog/help popover, не отменяет action, pointer capture другого owner либо semantic/selected/pressed state. ### 12.3. JS room/device tooltip - room hover использует pointer events либо delegated pointer path и записывает `_hoverRoom` только при разрешённой mouse modality; - device/room tooltip не создаётся из touch/pen либо synthetic mouse; - mouse leave очищает только state своего текущего target; - следующий настоящий mouse move/enter заново hit-test-ит target, а не восстанавливает stale room/device id; - будущий #152 обязан использовать canonical room hit resolver, а не `_hoverRoom`; #212 сам room click-to-fit не реализует. ## 13. CSS hover inventory Все пользовательски заметные `:hover` в full card получают один явный modality gate. Это включает: - View rooms, device markers, openings, room labels/links/gears и vacuum; - header tabs, zoom/settings buttons и kiosk/shared controls; - shared menus/options/candidates/actions, close controls и links; - `hp-dialog`, `hp-help` trigger и `hp-color-opacity` trigger, когда они используются внутри full card; - footer action `houseplan-space-card`. Rules, объединяющие `:hover` и `:focus-visible`, разделяются: hover требует mouse gate, keyboard focus — нет. Alarm/working/open/lock/unavailable/selected state не зависят от modality. Unavailable по-прежнему не получает visual hover. Чисто editor-only handles могут оставаться best effort, но naked selector допустим только если source-contract test документирует, что target никогда не появляется в View/shared component. Предпочтительный безопасный вариант — гейтить весь общий `cardStyles` единообразно, не меняя editor actions. Открытый по click/tap `hp-help` popover остаётся намеренно toggled surface; его жизненный цикл не считается hover. Гейт касается только визуального hover его trigger. ## 14. UX, accessibility и touch policy - View и kiosk на desktop/touch остаются блокирующими поверхностями. - Device/Plan/Background editors остаются desktop-first; общие controls получают no-sticky-hover автоматически, но новая touch parity не обещается. - Touch target marker не меньше 44×44 CSS px после уменьшения face. - Accessible name, role, tabindex, focus order и secure confirmation не меняются. - Screen reader announcement о feedback не добавляется: input принят, но HA ещё не подтвердил изменение состояния. - Touch → dialog → close не возвращает hover marker из старого кадра. - Pan, pinch, long press и pointer cancellation не запускают action/feedback и не оставляют hover. - В Flat/hidden Isometric, light/dark и kiosk действует одна политика. ## 15. Данные, migration и i18n Новых config/layout/localStorage/backend полей нет. Существующие размеры, координаты, display, value, pulse, LQI и action settings round-trip без записи. Migration не нужна. Новых пользовательских строк не ожидается. Если реализация потребует строку, это расширение scope: обязательны RU/EN parity и отдельное решение владельца. ## 16. Архитектурный contract и затронутые файлы Обязательная цепочка сохраняется: ```text HA/config state → resolveDevicePresentation → renderDeviceFace → one shared face geometry on full / preview / static surfaces PointerEvent → one instance-local modality resolver → JS transient-hover owner + explicit CSS/child-component gate accepted _clickDevice dispatch → one retargetable visual feedback owner ``` Ожидаемые файлы/модули: - `src/device-face.ts` — общий face DOM только при необходимости отдельного press body; - `src/styles.ts` — коэффициент 0,9, Text pill, press и gated shared hover; - `src/houseplan-card.ts` — modality/cleanup, room tooltip, action feedback и lifecycle; - новый малый pure helper `src/pointer-modality.ts` либо эквивалентный модуль; - `src/hp-dialog.ts`, `src/hp-help.ts`, `src/hp-color-opacity.ts` — явный nested hover gate без собственного global detector; - `src/space-card.ts` — локальный gate footer button и shared face parity; - `src/hp-device-preview.ts`/`src/space-render.ts` — только если parity требует явной передачи, второй renderer запрещён; - unit tests, targeted browser smokes, golden matrix и документация из §19. Конкретное имя helper/class и выбор CSS attribute являются техническими деталями. Нельзя создавать второй action resolver, второй device face renderer или global session latch наподобие текущего `_touchSeen`. ## 17. Edge cases - marker sizes 32/56/96 px до коэффициента 0,9, per-marker scale 0.5…3; - короткое, длинное, кириллическое и CJK value, `0`, `false`, unit symbol; - Text, Double right/left/top/bottom, legacy third section и LQI; - active/alarm/lock/unlock/selected/focus/virtual/unavailable во время feedback; - explicit pulse color/size, reduced motion и continuous pulse; - click/tap с медленным HA, rejected Promise после dispatch и state update раньше окончания 200 ms; - confirmation accept/cancel и target, изменившийся пока dialog открыт; - rapid double action, touch compatibility click и Enter/Space; - tap → dialog → close, long press, pan, pinch, second pointer, `pointercancel`, lost capture; - touch-only browser, браузер с ложным `(hover: hover)`, hybrid touch → mouse; - mouse pointer впервые входит без предварительного click; - mode/space/visibility/remount во время hover либо press; - два full card и static card на одной странице: modality одного экземпляра не изменяет другой; - Light/Dark, Flat/hidden Isometric, View/kiosk/Device editor/static card. ## 18. Acceptance criteria 1. **AC1 — 10% geometry.** Painted core/shell/glyph/value/sections/rings/badges, marker LQI/activity geometry и vacuum puck имеют линейный размер `0.90 ± 0.005` от pre-#212 reference при одинаковых settings на full, preview и static surfaces. **Доказательство:** unit/source facts + computed-style browser smoke + reviewed 32/56/96 golden/reference matrix. 2. **AC2 — anchor и hit area.** Saved coordinates, Text/Double anchor и relative per-marker scale не меняются; интерактивный target остаётся минимум 44×44 CSS px. **Доказательство:** unit geometry + full View browser smoke. 3. **AC3 — value capsule.** Широкий `display: value` имеет внутренний и внешний stadium radius по половине соответствующей высоты и равномерный inset с погрешностью ≤0,5 CSS px; полный текст не обрезан. **Доказательство:** exact style/layout assertions + long-value Light/Dark golden. 4. **AC4 — accepted feedback.** Реально dispatch-нутый click/tap/Enter/Space даёт один цикл `1 → .95 → 1` общей длительностью `200 ± 20 ms`, не ожидая HA state. **Доказательство:** fake-clock unit + desktop/touch/keyboard smoke. 5. **AC5 — точный gate действия.** Info/more-info/editor/link, secure/no-target/ unavailable no-op, cancel confirmation и отменённый gesture не получают канонический цикл; accepted confirmation получает его при dispatch. **Доказательство:** resolver/outcome matrix + browser smoke. 6. **AC6 — composition и bounded lifecycle.** Feedback не меняет state colors, pulse, Glow, badges, LQI, angle, hit area или service payload; repeat retargets один effect без очереди, lifecycle cleanup не оставляет residue. **Доказательство:** unit lifecycle/composition + semantic-state smoke. 7. **AC7 — reduced motion и focus.** В reduce нет scale tween, accepted action виден минимум один frame через несемантический accent; focus-visible и DOM focus сохраняются. **Доказательство:** media-query/keyboard smoke + golden. 8. **AC8 — modality authority.** `unknown/mouse/touch/pen` переходы instance-local; touch/pen выключают hover, настоящий mouse возвращает его без reload, compatibility mouse не возвращает. **Доказательство:** pure unit matrix + real-touch/hybrid browser smoke. 9. **AC9 — JS hover cleanup.** После touch terminal event, cancel, lost capture, pinch, mode/space/hidden/disconnect `_hoverRoom` и hover-owned `_tip` очищены не позднее следующего painted frame. **Доказательство:** unit/source contract + browser state assertions. 10. **AC10 — CSS/nested hover.** В View/shared surfaces нет naked visual `:hover`; device/room/opening/vacuum/header/dialog/help/color/static-button styles требуют mouse gate, а focus/semantic state не требуют его. **Доказательство:** CSS inventory test + touch computed-style smoke. 11. **AC11 — desktop/hybrid parity.** Desktop mouse hover впервые включается на pointer entry и выглядит как до #212; hybrid touch → mouse восстанавливает текущий hit, не stale state. **Доказательство:** mouse/hybrid smoke + reviewed hover golden. 12. **AC12 — gesture и action safety.** Pan/pinch/long press/second pointer/ pointercancel не вызывают ложный click, feedback или hover; secure lock и confirmation invariants не меняются. **Доказательство:** existing gesture and secure-action smokes + targeted regression. 13. **AC13 — surface parity.** Full View, kiosk, Device preview и static card используют один face contract; static plan остаётся non-interactive, Device editor остаётся desktop-first. **Доказательство:** renderer unit + named preview/static/kiosk smokes. 14. **AC14 — no data/i18n change.** Open → Save и serialization не меняют size/display/action data; backend и RU/EN key set неизменны. **Доказательство:** serialization/i18n unit + code review. 15. **AC15 — documentation address.** Hover/pressed/semantic ownership обновлён в `docs/UX-MODES.md`; `docs/CANVAS.md` не получает несвойственный раздел. **Доказательство:** docs diff + `check-docs`. 16. **AC16 — performance.** 200 markers не получают per-frame JS, per-marker media listener, layout loop либо рост постоянных animation layers; pointer move не вызывает full Lit render на каждый пиксель. **Доказательство:** source review + pre-beta performance profile. ## 19. План тестирования и release-артефакты ### 19.1. Цикл реализации до S7 Обязательны: ```bash npm run typecheck npm test npm run build node scripts/check-docs.mjs --external ``` Targeted browser smokes: - расширенный `demo/smoke_device_icon_design.mjs` — 0,9 и value capsule; - `demo/smoke_device_preview_parity.mjs`; - `demo/smoke_static_icon.mjs`; - `demo/smoke_state_value.mjs`; - `demo/smoke_icon_scale.mjs`; - новый/расширенный device action feedback smoke; - расширенный `demo/smoke_touch_tips.mjs` либо отдельный real-touch hover reset; - `demo/smoke_long_press_gesture.mjs`; - secure confirmation/lock and opening/vacuum hover regressions. Перед S7 нужны failing-before-fix доказательства как минимум для коэффициента размера, value radius, accepted feedback и touch sticky-hover. Для критичных guards добавляются mutation checks либо эквивалентное документированное mutation proof. ### 19.2. Golden и visual QA Проверить Light/Dark: - state-table 32/56/96 с новым размером; - длинный основной Value рядом с reference capsule; - normal press midpoint и reduced-motion accent; - desktop hover, post-touch no-hover и focus-visible; - semantic active/alarm/selected во время feedback. Локальный Windows capture диагностический. Baseline принимается только из reviewed полного Linux CI artefact перед beta по release runbook. ### 19.3. Перед beta Выполняются полный browser smoke, Linux golden и performance gates из `HOUSEPLAN-CODEX-SESSION-RUNBOOK.md`. Полный HA harness каноничен в Linux CI из-за `fcntl`. ### 19.4. Документация и changelog User-visible implementation commit с `User-Visible: yes` одновременно обновляет: - `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #212; - `docs/USER-GUIDE.ru.md` — меньший marker face, accepted-action feedback и отсутствие sticky hover; - `docs/TOUCH-SUPPORT.md` — event-derived modality и hybrid contract; - `docs/UX-MODES.md` — hover/pressed/semantic ownership, закрывая #181; - `docs/TESTING.md` — named size/value/feedback/real-touch/hybrid evidence; - документационные screenshots, если их source fingerprint/вид изменился. `docs/CANVAS.md` не обновляется ради hover ownership. #22/#154/#181 не закрываются и не получают `S8-merged` автоматически: владелец обработает их после merge #212. ## 20. Риски и откат | Риск | Мера | | --- | --- | | Пользовательские tuned sizes уменьшаются дважды | ровно один общий factor после resolver; computed parity tests | | Press обещает успех вместо принятого input | неизменный semantic state; start только при dispatch | | Scale ломает translate/angle/Text shell | независимый visual layer/individual transform + composition test | | Touch compatibility event возвращает hover | PointerEvent authority + CSS gate, без timeout | | Gate убирает keyboard focus | hover/focus selectors разделены | | Cleanup снимает selection/action | helper владеет только `_tip`/`_hoverRoom`/gate | | Две карточки влияют друг на друга | instance-local controller, без static singleton | | Pointer move вызывает render storm | imperative host attribute/state transition only on modality change | | Общий CSS пропускает nested shadow component | явный child property/attribute + source inventory | Откат одним implementation revert возвращает прежний visual factor, value radius, feedback и hover path. Данных, backend migration и rollback storage нет. Возврат к sticky touch hover либо неверной value geometry не является допустимым визуальным fallback. ## 21. План реализации 1. Добавить failing tests/fixtures для 0,9, value capsule, feedback и modality. 2. Вынести instance-local pointer modality/cleanup contract и подключить full card, nested shared components и static footer. 3. Перевести room tooltip/hover на gated pointer path и закрыть CSS inventory. 4. Применить один visual factor к shared face, сохранив 44×44 hit/anchor. 5. Исправить радиус основного Value core. 6. Подключить один retargetable 200 ms feedback в фактические dispatch points. 7. Обновить docs/changelogs/screenshots и выполнить цикл §19.1. 8. Передать один полный диапазон #212 в S7; linked issue не закрывать. ## 22. Принятые технические предположения Следующее принято предположительно и может быть свободно изменено ревьюером без нового продуктового решения, если сохраняются AC: 1. Factor 0,9 реализуется одной CSS custom property в общем face, а не переписыванием config defaults. 2. Value capsule исправляется постоянным радиусом `visual core / 2`. 3. Pointer modality живёт в малом pure resolver; DOM hover gate меняется императивно только при смене состояния, без requestUpdate на каждый move. 4. Nested shadow components получают boolean/attribute от owning full card и не создают собственный global listener. 5. Press feedback может использовать Web Animations API либо отдельную inner shell property, если поддерживает restart/cancel и не меняет hit geometry. 6. Smooth 200 ms curve может использовать package easing `cubic-bezier(.22,.61,.36,1)`; exact easing не является config surface. 7. Reduced-motion accent использует существующий theme/focus token и один painted-frame owner, не новую пользовательскую строку. 8. Формулировка #22 `plan-wide setting` означает одну общую политику feedback для всех actionable markers плана, а не новую пользовательскую настройку либо per-marker override.