Compare commits

..
Author SHA1 Message Date
claude[bot] 4b46a62503 docs: review document for #154
Validate / changes (push) Successful in 48s
Validate / provenance (push) Successful in 55s
Validate / smoke (push) Skipped
Validate / process-gate (push) Successful in 54s
Validate / hacs (push) Skipped
Validate / hassfest (push) Skipped
Validate / performance_smoke (push) Skipped
Validate / backend (push) Skipped
Validate / frontend (push) Skipped
Validate / golden (push) Skipped
Issue: #154
User-Visible: no
2026-08-18 19:31:44 +00:00
Sergey Matyunin cd3c7752a8 docs: specify touch hover reset
Issue: #154
User-Visible: no
2026-08-15 03:55:30 +03:00
4 changed files with 596 additions and 479 deletions
+295
View File
@@ -0,0 +1,295 @@
# Ревью ТЗ — issue #154, цикл r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/154
- **ТЗ:** `docs/specs/154-touch-hover-reset.md`, коммит `cd3c7752a8846c11508a7a6b4c46b6d990624408`
- **Этап:** spec (PROCESS.md §2.4)
- **Вердикт:** жёлтый · цикл r1/4 · High: 3 · Medium: 1
## Скоуп ревью
Прочитаны в указанном порядке: `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md`
(§1–§4, §7, §12), тело issue #154 и оба комментария владельца (аналитика
2026-08-15: P1/bug/polish, обычный трек, «trivial запрещён из-за touch
impact», «вопросов нет»; публикация ТЗ), `docs/USER-GUIDE.ru.md` (термины
«hover», раздел ограничений «Комнатные подсказки основаны на hover»),
`docs/TOUCH-SUPPORT.md` (input classification, deliberate degradation),
`docs/UX-MODES.md` (contract View: «room hover highlight, hover tooltips»),
`docs/CANVAS.md` (проверка, действительно ли это канонический документ для
hover-контракта — нет, см. Medium-1) и `docs/TESTING.md` (существующий
контракт «No hover tooltips on touch», v1.43.3/v1.42.2).
Issue не помечен `small`/`trivial`; ТЗ корректно лежит файлом в
`docs/specs/154-touch-hover-reset.md`, а не в теле issue. Формат ревью —
полный документ, не комментарий.
Для калибровки того, что этот репозиторий уже считает обязательным
оформлением §7.1, сверился с `docs/specs/138-adjacent-room-autoclose.md`
(разделы «1. Сценарий и продуктовый контекст» / «2. Что человек увидит до и
после») и его ревью `docs/reviews/SPEC-REVIEW-138-r1.md` — оно отдельной
строкой подтверждает наличие обоих разделов как условие «обязательные
разделы §7.1 на месте».
## Как проверялось
Ревью также читало код, на который ТЗ опирается как на текущее поведение —
чтобы отличить обоснованное утверждение от догадки, выданной за факт:
- `src/houseplan-card.ts:5442–5470` (`_notePointer`) — подтверждает
формулировку issue и ТЗ: touch/pen действительно закрывает `_tip`
(`5446`), но не трогает `_hoverRoom`. Совпадает.
- `src/houseplan-card.ts:659,662,1759–1760` — `_tip`/`_hoverRoom` как
реактивный state; `14619–14646` — room hover действительно навешан через
`@mouseenter`/`@mouseleave` на path/polygon/rect комнаты, без
`pointerenter`/`pointerleave`. Совпадает с §6 ТЗ.
- `src/houseplan-card.ts:1285,5443,9494,11167` — уже существует
`_boundaryPointerType`, session-local поле «последний реальный
pointerType», используемое для decor-snap радиуса (`_decorSnap`) и
touch-детекции (`9494`). ТЗ §5 говорит «Houseplan вводит один
session-local источник фактической modality», не упоминая, что подобное
поле уже есть. Это не продуктовая двусмысленность (реализация — дело
автора, PROCESS §7.1), но реальный риск дублирования authority; отмечено
ниже как **Low**, без отдельного issue.
- `grep ':hover'` по `src/` — 32 вхождения в 6 файлах
(`houseplan-card.ts`, `styles.ts`, `space-card.ts`, `hp-dialog.ts`,
`hp-device-preview.ts`, `hp-color-opacity.ts`, `hp-help.ts`). Ни один не
назван в ТЗ — см. **High-3**.
- `docs/CANVAS.md` — единственное вхождение слова «hover» (строка 563,
«hover is only a preview and never authoritative») относится к preview
hit-резолвера архитектурного connection overlay в редакторе Плана
(endpoint/line snapping), а не к View-контракту room/device hover,
который меняет issue #154. `docs/UX-MODES.md:69–70` прямо перечисляет
«room hover highlight, hover tooltips» как часть допустимых
взаимодействий View — см. **Medium-1**.
- `docs/TESTING.md:175–176,750–752` — существующий, уже протестированный
контракт «a hover tooltip never appears after ANY touch/pen pointer
event, even if the browser claims hover: hover» — ТЗ корректно описывает
его как частично реализованный и подлежащий расширению.
- issue #152 (`gh issue view 152`) — действительно ещё `S4-spec-review`, не
реализован; ссылка ТЗ на «будущий #152» и решение не читать `_hoverRoom`
из его hit-резолвера корректны и не создают ложной зависимости.
## Находки
### High-1 — нет обязательных разделов «Сценарий» и «Что человек увидит до/после» (§7.1)
**Где:** `docs/specs/154-touch-hover-reset.md`, весь документ; ближе всего
раздел «1. Проблема и требуемый результат» (строки 9–19).
PROCESS.md §7.1 требует ТЗ начинать с двух продуктовых разделов: «какая
персона (`docs/SCOPE.md`), на какой поверхности, в какой момент это
встретит» и «что человек увидит до и после, одной фразой без терминов
реализации» — и подчёркивает, что «два первых раздела — продуктовые, и они
идут первыми не случайно». Это не стилистическая рекомендация: сам
репозиторий уже применяет это буквально в `docs/specs/138-...md`
(разделы «1. Сценарий и продуктовый контекст» / «2. Что человек увидит до и
после»), и предыдущее ревью того ТЗ отдельно проверяло их наличие как
условие «обязательные разделы §7.1 на месте».
В ТЗ #154 такого раздела нет вообще. Раздел 1 сразу входит в техническое
описание бага (`_notePointer()`, CSS `:hover`, `mouseenter`/`mouseleave`), а
второй абзац, который мог бы быть «что человек увидит», написан в терминах
реализации: «показывает только краткий pressed feedback», «настоящие mouse и
trackpad», «на гибридном устройстве последующий mouse input восстанавливает
его» — это не «одна фраза без терминов реализации», а пересказ технического
контракта.
Персона и поверхность из `docs/SCOPE.md` — не абстракция: J1–J3 явно относят
View к household members/guests на wall tablet и companion-приложении
(«View mode is the product for two of the three personas»), а issue
явно называет ещё и «мобильный браузер / HA Companion». Отсутствие
явного раздела означает, что читатель ТЗ вынужден реконструировать это
сам, а вопрос «какая персона важнее при конфликте» (например, «жертвуем ли
чем-то ради гибридного admin-устройства ради гарантии touch у household
member») в тексте не решён явно — только подразумевается порядком risk-строк
§16.
**Почему High:** это единственная площадка процесса, где владелец мог бы
опротестовать выбор персоны/поверхности/степени деградации ДО написания
кода (PROCESS §7.1: «Владелец отвечает на продуктовые вопросы… их место —
этап ТЗ»); без явного раздела с этим согласиться (или оспорить) нечего —
ревьюер не может подтвердить, что «что человек увидит» действительно решено,
а не растворено в технической формулировке.
**Что нужно поправить:** добавить два раздела по образцу #138 — «Сценарий»
(персона/поверхность/момент, со ссылкой на J1–J3 `docs/SCOPE.md`) и «Что
человек увидит до/после» одной фразой без «modality», «pointerType»,
«hybrid», «pressed feedback» (например: «раньше подсветка комнаты/маркера
после тапа могла остаться до следующего действия; теперь она гаснет сразу
после отпускания пальца, а мышь на компьютере работает как раньше»).
### High-2 — критерии приёмки не указывают способ доказательства (DoR §2.5)
**Где:** `docs/specs/154-touch-hover-reset.md` §12 «Acceptance criteria»
(строки 197–212), сверено с §13 «План тестирования» (214–252).
PROCESS.md §2.5 (вход в «Готово к разработке») требует буквально: «AC1…ACn —
пронумерованные проверяемые критерии приёмки; **у каждого указано, чем он
доказывается**: `unit` / `backend` / `smoke` / `golden` / «ревью кода»» — и
«если хоть один пункт не выполнен — статус не «Готово к разработке»».
Все 12 пунктов §12 сформулированы как утверждения о поведении (хорошо,
проверяемо и однозначно сформулированы — отдельная претензия к ним
отсутствует), но ни один не помечен способом доказательства. §13 группирует
тесты по категориям (Unit/source contract, Browser smoke, Golden,
Performance), но не сопоставляет их с номерами AC — например, неясно из
текста, доказывается ли AC7 («desktop mouse hover не изменён») smoke-тестом,
golden-скриншотом, или обоими одновременно, и то же для AC9 (keyboard focus)
и AC12 (naked hover selector — очевидно «ревью кода» по source-inventory,
но это нигде не написано явно).
**Почему High:** это не редакторская придирка, а буквальный пункт входного
чек-листа перед `S5-ready`. Без явной привязки разработчик и код-ревьюер
(другая модель, без контекста этой сессии — AGENTS.md «Two-agent workflow»)
должны будут заново реконструировать, каким тестом каждый AC закрыт, что
именно и должен исключать этот пункт DoR.
**Что нужно поправить:** добавить к каждому из 12 пунктов §12 тег
доказательства, например `[unit]`, `[smoke]`, `[golden]`, `[unit+smoke]`,
`[ревью кода]` — материал для этого уже есть в §13, требуется только явное
сопоставление 1:1.
### High-3 — не перечислены затронутые файлы и модули (DoR §2.5)
**Где:** весь документ; ближайший к теме раздел — «14. План реализации»
(строки 254–262).
Тот же пункт DoR §2.5 требует отдельно: «перечислены затронутые файлы и
модули». В ТЗ этого списка нет — §14 ограничивается общими шагами
(«провести inventory JS state и View/shared CSS hover selectors», «ввести
component-local pointer modality authority»), не называя ни одного
конкретного файла.
Это не тривиальный однофайловый фикс: `grep ':hover' src/` даёт 32
вхождения в 6 файлах — `houseplan-card.ts`, `styles.ts`, `space-card.ts`,
`hp-dialog.ts`, `hp-device-preview.ts`, `hp-color-opacity.ts` (плюс
`hp-help.ts`, у которого есть top-level `mouseenter`/`_notePointer`-подобная
логика — исключён по контракту §4, но это тоже стоило бы явно назвать).
Раз §3 ТЗ прямо говорит, что «общие компоненты редакторов получают
исправление, если используют тот же pointer-modality gate», а этот gate
должен быть «единым» и «передан в обязательные shadow child components»
(§5), явно неясно без списка файлов, какие из этих 6+ компонентов входят в
обязательный охват, а какие — только в «если не требуют отдельной сложной
адаптации» (§3, необязательная часть).
**Почему High:** без списка файлов нельзя проверить полноту заявленного
«обязательного охвата» View (issue, раздел «Обязательный охват») — то же
самое, что нельзя проверить AC без способа доказательства.
**Что нужно поправить:** добавить раздел «Затронутые файлы» с явным списком
(минимум все 6 файлов с `:hover`, плюс модуль(и), где будет жить pointer
modality authority), и отметить для каждого — «обязательный охват» или
«если дёшево, без специальной адаптации» (§3).
### Medium-1 — план документации (§15) называет неверный канонический документ для hover-контракта
**Где:** `docs/specs/154-touch-hover-reset.md` §15 (строки 264–276), пункт
«`docs/CANVAS.md` — hover/pressed/semantic ownership».
`docs/CANVAS.md` — канонический документ infinite canvas (координаты,
content frame, zoom/pan, grid, drag limits, snap contract, wall geometry).
Слово «hover» встречается там ровно один раз (строка 563) и относится к
preview hit-резолверу architectural connection overlay в редакторе Плана
(«the same resolver runs again on click, so hover is only a preview and
never authoritative») — это про предпросмотр примыкания к стене при
рисовании, а не про View-контракт room/device/opening hover, который меняет
эта задача.
Настоящий канонический источник контракта — `docs/UX-MODES.md`, раздел
«View — display and device interaction only» (строки 63–70): «room hover
highlight, hover tooltips (name, clean-floor area, temperature, signal)» —
именно эта строка перечисляет ровно то поведение, которое ТЗ #154 меняет
(добавляет pointer-modality gate и запрещает sticky/synthetic hover). ТЗ
§15 не включает `docs/UX-MODES.md` в список документов на обновление
вообще.
**Последствие, если не исправить:** implementer, следуя §15 буквально,
допишет в `docs/CANVAS.md` абзац не по адресу (документ и так не про
интеракции), а `docs/UX-MODES.md` продолжит говорить просто «room hover
highlight, hover tooltips» без указания на pointer-modality/no-sticky-hover
контракт — расхождение документации с реальным поведением ровно там, где
его будут искать в первую очередь (`UX-MODES.md` явно значится и в
AGENTS.md как канонический документ подсистемы).
**Что нужно поправить:** заменить `docs/CANVAS.md` на `docs/UX-MODES.md` в
списке §15 (либо добавить `UX-MODES.md` к списку и явно решить, нужен ли
`CANVAS.md` вообще — по факту нет, там нет ownership hover).
Заведён отдельный issue: см. итог ниже.
## Что проверено и корректно
- **Проблема описана по реальному коду, не по догадке.** `_notePointer()`
(`houseplan-card.ts:5442`) действительно закрывает только `_tip`, room
hover действительно висит на `mouseenter`/`mouseleave`
(`14619–14646`) — оба факта, на которых строится вся мотивация ТЗ,
подтверждены чтением, не голословны.
- **Границы transient hover (§2) чёткие и без скрытого расширения скоупа.**
Working/alarm/unavailable/Glow/pulse, `hp-help`, selected/checked,
keyboard focus прямо исключены из очистки — совпадает с исключениями,
которые перечислил сам владелец в теле issue («Исключения»).
- **Synthetic-mouse policy (§6) корректно закрывает главный технический
риск бага** («мобильный браузер может синтезировать mouse-события») —
явно требует trusted `PointerEvent` с реальным `pointerType`, запрещает
произвольный wall-clock timeout как основной механизм. Это прямое,
проверяемое техническое решение, а не «наверное сработает».
- **Не создаёт ложной зависимости от нереализованного #152.** Проверено:
#152 (`click-to-fit`) сам ещё в `S4-spec-review`; §9 ТЗ прямо пишет, что
#152 должен использовать canonical hit resolver независимо от
`_hoverRoom`, не читая его как заменитель выбора — корректно, не
предвосхищает нерешённый ТЗ #152.
- **«Принятые предположения» (§17) — действительно технические или уже
решённые владельцем, не спрятанные продуктовые вопросы.** Пункт «touch и
pen следуют одинаковой no-hover policy» дословно совпадает с «Pen tap
следует touch-политике» из тела issue (владелец уже решил это), а не
является домыслом автора ТЗ; «media query — только второй gate»,
«mouse modality — только trusted PointerEvent» — реализационные решения,
которые PROCESS §7.1 явно отдаёт на усмотрение автора.
- **Accessibility (§11) не оставляет открытых вопросов**: DOM focus,
`:focus-visible`, `aria-*`, dialog focus trap явно выведены из-под
touch-cleanup — совпадает с «Keyboard focus и `:focus-visible` сохраняют
штатное поведение» из тела issue.
- **i18n и модель данных закрыты корректно и коротко, а не отпиской.** §15
прямо говорит: новых строк не ожидается, если появятся — обе локали и
parity test обязательны; §16 — «Config, storage и backend data не
меняются; миграция и data rollback не нужны» — соответствует тому, что
задача чисто presentation/pointer-lifecycle, backend не тронут.
- **Откат (§16) реалистичен**: «возвращает прежние JS/CSS hover paths» —
для чисто клиентской, не мигрирующей данные задачи это адекватный и
проверяемый уровень детализации отката.
- **Трек и трейлеры соответствуют аналитике.** Комментарий 2026-08-15
корректно исключил `trivial` (touch impact) и `small` (несколько
поверхностей: room/device/opening/controls/dialogs); полный трек и файл
в `docs/specs/` — правильный выбор, подтверждён `S4-spec-review` без
`small`.
## Чего не проверял
- Реализацию — её нет, это ревью ТЗ, а не кода (PROCESS.md §2.4).
- Golden/browser smoke живьём — на этапе ТЗ не запускаются; план в §13
выглядит технически осмысленным (real touch context, CDP touch input,
`(any-hover: hover)` контекст), но существование конкретных сценариев не
верифицировалось запуском.
- Производительность — утверждения §13 «Performance» (pointermove не
вызывает full Lit update и т.п.) не профилировались; на этапе ТЗ это не
требуется, оценка отложена до реализации/пре-релизного гейта.
- Полный список всех `:hover`-селекторов в `styles.ts` построчно (нужен ли
каждому modality gate) — за пределами ревью ТЗ; сосчитано только число
файлов/вхождений, чтобы обосновать High-3.
- Реализуемость unit-теста «compatibility MouseEvent не включает mouse
modality» в текущем test harness (`test/**`) — не проверялась: на этапе
спецификации это заявление о контракте, а не код.
## Вывод
Технический контракт (§5–11, §16–17) продуман, обоснован кодом и не
содержит выданных за факт догадок — это сильная сторона документа. Но три
independent High-находки — отсутствие обязательных продуктовых разделов
§7.1, отсутствие способа доказательства у каждого AC и отсутствие списка
затронутых файлов — это три отдельных, буквальных пункта DoR §2.5,
неотвеченных полностью. Ни один из них не требует пересмотра решения, все
три чинятся добавлением текста в это же ТЗ без изменения контракта.
Возврат в «ТЗ в работе» (`S3-spec`), цикл r1/4.
Medium-1 заведён отдельным issue со ссылкой на #154 (не блокирует переход
сам по себе, но должен быть закрыт до релиза документации).
@@ -1,477 +0,0 @@
# #109 — отдельные markers для каналов многоканального HA-устройства
- Issue: [#109](https://github.com/Matysh/houseplan-card/issues/109)
- Приоритет: P2
- Тип: bug
- Ветка: `issue/109-multichannel-entity-binding`
- Статус документа: полное ТЗ подготовлено; issue остаётся в `S3-spec`, review не запущено по прямому указанию владельца
- Основание: отчёт пользователя и принятые владельцем defaults от 2026-08-15
## 1. Пользовательская проблема и результат
Home Assistant может представить один физический многоканальный выключатель как
одно device с несколькими независимыми entities. В исходном случае два канала
света управляются через MQTT: в House Plan устройство находится, но отдельный
`mqtt light` второго канала не находится даже в режиме выбора сущностей, хотя
соседние `mqtt sensor` того же устройства видны. В результате на план можно
поставить только один общий marker и управлять только первым каналом.
После #109 администратор дома включает «Показывать сущности», находит каждый
активный канал по понятному имени или точному `entity_id` и создаёт для него
отдельный marker. Каждый marker показывает состояние и выполняет действие только
для своей exact entity. House Plan не создаёт несколько markers автоматически и
не меняет семантику общего `device:*` marker.
## 2. Персона, поверхность и before/after
- **Основная персона:** Home admin с многоканальным реле/выключателем в HA.
- **Настройка:** Device editor в desktop browser; touch остаётся best effort по
общему контракту редактора.
- **Использование результата:** Full View, Static card и существующие быстрые
действия exact entity marker.
- **До:** один HA device фактически схлопывает два независимых канала, а второй
канал невозможно гарантированно найти и разместить.
- **После:** два канала одного HA device независимо находятся, сохраняются и
управляются как два exact entity markers.
## 3. Итог аналитики
### 3.1 Ценность, сложность и риск
- пользовательская ценность: 7/10 — без исправления часть реального освещения
нельзя разместить и управлять с плана;
- ценность для продукта: 5/10 — устраняется разрыв между entity-моделью HA и
уже существующей exact-binding моделью House Plan;
- сложность: 4/10 — формат данных и runtime exact binding уже существуют;
- риск: 5/10 — discovery использует registry, live states, tombstones,
дедупликацию и ограничение выдачи, поэтому локальный фильтр без единого
контракта может открыть disabled/удалённые ссылки или снова скрыть sibling.
Классификация остаётся **P2 bug, обычный процесс**. Задача входит в J1, J3 и J6
`docs/SCOPE.md`: достоверный live-план, безопасное действие с плана и сохранение
актуальности плана при развитии дома.
### 3.2 Подтверждённая техническая база
На момент подготовки ТЗ:
- marker уже хранит точную привязку в виде `entity:<entity_id>`;
- exact entity marker уже имеет собственную identity, state и action target;
- Device editor показывает device candidates всегда, а individual entities —
после флага `showEntities`;
- `_bindingCandidates()` отдельно проходит активную registry-проекцию,
исключает уже занятые exact bindings и фильтрует результат перед лимитом 200;
- `resolveHaBindingStatus()` уже является каноническим источником статуса
`active`, `ha_disabled`, `orphaned` и `unverified`;
- `activeRegistryHass()` намеренно сохраняет live states без registry row, но
текущий individual-entity candidate loop проходит только `h.entities`, из-за
чего критерии «entity активна» и «entity можно выбрать» расходятся;
- текущий smoke проверяет включение «Показывать сущности» и выбор одной entity,
но не два sibling channels одного device, точный поиск, лимит 200 и
независимое повторное добавление.
### 3.3 Связанные задачи и границы
- #29 — общий lifecycle/inbox устройств; не дубликат.
- #88 — выбор ведущей сущности **внутри одного** forced-light marker; не меняет
создание отдельных markers и не дублирует #109.
- #117 — parity registry-less YAML entity у архитектурного проёма; остаётся
отдельной задачей, потому что #109 не меняет `opening.contact`/`opening.lock`
и рендер проёмов.
## 4. Нормативные продуктовые решения
1. Один независимо управляемый канал = один явно созданный пользователем exact
entity marker.
2. Автоматически разворачивать один HA device в несколько markers запрещено.
3. Наличие `device:*` marker не занимает и не скрывает его отдельные
`entity:*` channels; занята только совпадающая exact binding.
4. Две entities одного device не дедуплицируются между собой по parent device,
имени, area или domain.
5. «Показывать сущности» остаётся явным opt-in: при выключенном флаге обычные
child entities устройства не заполняют список.
6. После включения флага каждая активная exact entity должна быть находима по
полному `entity_id`, friendly/registry name, domain и имени parent device.
7. Поиск применяется до лимита выдачи. Точное совпадение за пределами первых
200 нефильтрованных строк всё равно должно появиться в результате.
8. HA-hidden, но enabled entity не равна HA-disabled. В advanced-режиме
«Показывать сущности» она остаётся доступна через поиск.
9. Явно disabled entity, entity disabled вместе с parent device, orphaned либо
unverified binding не предлагается для нового marker.
10. Exact live entity без registry row может быть кандидатом, если канонический
binding-status resolver классифицирует её как `active`; отсутствие registry
metadata не должно само по себе скрывать рабочую сущность.
11. Marker с `removed: true` сохраняет существующий re-add contract: его exact
binding можно выбрать снова, и новая запись заменяет tombstone.
12. Уже размещённая exact entity не предлагается повторно, кроме текущей
привязки редактируемого marker.
13. Если требуемая entity не может быть подтверждена как active, UI не создаёт
полурабочую ссылку и не подменяет её первым sibling channel.
## 5. Scope
### 5.1 Discovery и поиск
- единый чистый resolver кандидатов для device/entity binding picker;
- union подтверждённых active registry entities и допустимых active live-only
exact entities без потери sibling channels;
- поиск по name, `entity_id`, domain и parent device name;
- детерминированная сортировка и limit-after-filter;
- сохранение существующих helper/group candidates и device candidates;
- согласованная обработка taken, removed, disabled и limited-registry states.
### 5.2 Сохранение и runtime
- создание отдельных markers с bindings `entity:<channel-a>` и
`entity:<channel-b>`;
- независимая marker identity и layout position;
- существующие exact-entity presentation/state/action paths без выбора первого
sibling устройства;
- отсутствие автоматической мутации других markers и parent device marker.
### 5.3 Проверки и документация
- unit matrix для candidate resolver;
- browser smoke для двух каналов одного устройства;
- regression existing device/helper/group/tombstone behavior;
- RU user guide: как разместить каналы многоканального устройства отдельно;
- архитектурная документация единого active-candidate contract.
## 6. Non-scope
- автоматическое создание markers для всех каналов устройства;
- bulk placement, grouping либо визуальная связь sibling markers;
- изменение HA/MQTT integration, Entity Registry или device/entity model HA;
- выбор ведущей light entity внутри одного marker — это #88;
- изменение агрегации состояния и service targets общего `device:*` marker;
- изменение `opening.contact`, `opening.lock` или registry-less рендера проёмов
— это #117;
- повторное использование одной exact entity двумя markers;
- новый вид marker, новые иконки, badges, animations или настройки действий;
- изменение публичности скрытого изометрического режима;
- автоматическая миграция существующих device markers в entity markers.
## 7. Контракт кандидатов
### 7.1 Вход и выход
Candidate resolver получает неизменяемый snapshot текущего HA frame и контекст
диалога:
```ts
interface BindingCandidateContext {
hass: HomeAssistant;
registrySnapshot: HaRegistrySnapshot;
markers: MarkerCfg[];
devices: DevItem[];
editingMarkerId?: string;
editingBinding?: string;
showEntities: boolean;
filter: string;
limit: number; // UI default: 200
}
interface BindingCandidate {
value: `device:${string}` | `entity:${string}`;
label: string;
sub: string;
}
```
Конкретные имена types/functions не нормативны. Нормативны один общий resolver,
чисто тестируемые решения и отсутствие второго расходящегося entity-фильтра в
render method.
### 7.2 Eligibility exact entity
Entity допустима как новый candidate, когда одновременно выполнено:
1. `resolveHaBindingStatus(hass, entity:<id>, snapshot).kind === 'active'`;
2. exact binding не занята другим не-removed marker;
3. это не удалённая HA entity и не tombstone другого binding;
4. включён `showEntities`, либо entity уже относится к существующей категории
standalone group/helper, которая показывается по старому контракту;
5. строка проходит непустой пользовательский фильтр, если он задан.
Registry metadata используется для name, parent device и platform. Если строки
нет, candidate строится из live state: label = `friendly_name || entity_id`,
domain берётся из `entity_id`, parent device name отсутствует. Это не означает,
что любой текстовый id принимается: требуется положительный active status.
HA `hidden_by`/эквивалентный presentation flag не является `disabled_by` и не
исключает active entity из advanced exact search. В #109 отдельный warning/badge
для hidden-by-HA не вводится.
### 7.3 Taken и sibling semantics
- `entity:light.bath` занимает только `entity:light.bath`;
- он не занимает `entity:light.toilet`, даже если обе registry rows имеют один
`device_id`;
- `device:dual_relay` не занимает ни одну child `entity:*` binding;
- существующие name/area dedup rules применяются только к device candidates и
не применяются к entity candidates;
- tombstone `removed: true` не считается занятой binding и остаётся доступным
для явного восстановления;
- при редактировании marker его текущая binding остаётся видимой/выбранной, но
не разрешает создать отдельный дубль.
### 7.4 Label, secondary text, search и sort
Для registry entity:
- `label = registry name || live friendly_name || entity_id`;
- `sub` содержит domain, локализованное «Сущность», parent device name при его
наличии и точный `entity_id`;
- две одинаковые labels различимы по `entity_id`.
Нормализация поиска: Unicode lowercase + trim; поиск — substring по объединению
label, secondary text и exact value. Дополнительная транслитерация/fuzzy search
не требуется.
Порядок вычисления:
1. построить eligible candidates;
2. применить filter;
3. отсортировать по localized label, затем по exact value как tie-breaker;
4. применить limit 200.
## 8. UX-поток
1. Пользователь открывает Device editor и «Добавить».
2. Включает существующий флаг «Показывать сущности».
3. Вводит `light.bath`, `light`, «Ванная» либо имя parent device.
4. Выбирает первый канал, сохраняет marker и размещает его.
5. Снова открывает «Добавить»: первый exact channel отсутствует как занятый,
второй sibling остаётся в выдаче.
6. Выбирает второй канал и сохраняет второй marker.
7. В View действие по первому marker адресует только первый entity id, действие
по второму — только второй.
Если channel исчез/disabled до Save, обычная повторная validation не должна
сохранять его как active candidate. Тихо выбирать parent device или sibling
запрещено. Отдельный новый modal/error text не требуется, если существующий
refresh/validation flow честно снимает candidate и не делает partial save.
## 9. Данные, миграция и совместимость
Новый persisted field и migration step не нужны. Канонические записи уже имеют
достаточную форму:
```json
{
"markers": [
{ "id": "entity_light_bath", "binding": "entity:light.bath" },
{ "id": "entity_light_toilet", "binding": "entity:light.toilet" }
]
}
```
Требования:
- существующие `device:*`, `entity:*`, `virtual` markers читаются без rewrite;
- import/export сохраняет обе exact bindings буквально;
- layout остаётся keyed существующей marker identity;
- сохранение второго sibling marker не переписывает первый и parent marker;
- rollback к версии до #109 остаётся data-safe: старый runtime уже понимает
exact entity markers, хотя старый Add picker может не найти их заново;
- неизвестные sibling config fields и порядок незатронутых markers сохраняются
по общему compatibility contract.
## 10. State, presentation и действия
#109 не создаёт новый resolver состояния или действий. После выбора используются
существующие exact-entity authorities:
- state/presentation получают только bound entity;
- universal toggle проверяет domain и безопасность этой entity;
- service target не расширяется до всех entities parent device;
- unavailable/unknown остаются по существующему fail-safe contract;
- secure domains остаются no-op/guarded по общим правилам;
- parent device и sibling states не подменяют состояние exact marker.
## 11. I18n, accessibility и touch
- новые пользовательские строки для базового исправления не обязательны;
- если реализация добавляет строку вместо переиспользования существующей, RU/EN
key parity и осмысленный перевод обязательны в том же commit;
- checkbox, search input и candidate rows сохраняют keyboard focus, label и
screen-reader semantics;
- exact `entity_id` доступен как текст, а не только tooltip;
- одинаковые labels различимы без опоры только на цвет;
- Device editor остаётся desktop reference; на touch существующий поток должен
оставаться выполнимым без уменьшения touch targets.
## 12. Performance, security и observability
- candidate resolver работает линейно от количества devices + entities + live
states, сортировка — только от отфильтрованной выдачи;
- нельзя добавлять unbounded cache либо пересобирать registry snapshot на каждый
keypress;
- memoization/invalidation учитывает registry revision, live collection identity,
markers, showEntities и filter;
- filter выполняется до limit, но это не отменяет общий physical cap UI;
- entity ids показываются только уже авторизованному пользователю HA в локальном
editor; новые внешние запросы, permissions и telemetry не добавляются;
- diagnostic/test logs не должны включать пользовательские names, coordinates
или полный config; fixture использует синтетические ids;
- отдельный runtime metric не нужен, но regression должен быть виден общему
pre-beta performance gate.
## 13. Acceptance criteria и доказательства
| AC | Критерий | Обязательное доказательство |
|---|---|---|
| AC1 | Две enabled `light.*` entities одного device одновременно видны после «Показывать сущности» | unit fixture candidate resolver + browser smoke screenshot/log |
| AC2 | Поиск находит каждый канал по full entity_id, friendly/registry name, domain и parent device name | параметризованный unit test |
| AC3 | Фильтр применяется до лимита 200: точный канал находится среди >200 исходных entities | unit regression |
| AC4 | После сохранения channel A он исключён, но sibling channel B остаётся; после сохранения B существуют два exact markers | unit state transition + smoke |
| AC5 | `device:*` marker того же parent не скрывает child entity candidates и не заменяется автоматически | unit negative regression + smoke |
| AC6 | Действие каждого marker адресует только его bound entity, без sibling/parent fan-out | existing exact-toggle unit suite + targeted regression |
| AC7 | Disabled entity, disabled parent, orphaned и unverified candidate не предлагаются; removed tombstone можно добавить заново | unit status matrix |
| AC8 | Active live-only entity без registry row находится по entity_id; registry-less rendering openings не меняется | unit candidate test + source boundary assertion |
| AC9 | HA-hidden enabled entity доступна в advanced search, но HA-disabled — нет | unit registry metadata matrix |
| AC10 | При `showEntities=false` обычные device-owned entities скрыты, а devices/helpers/groups сохраняют прежнее поведение | existing + new unit/smoke regression |
| AC11 | Две одинаковые labels остаются отдельными и различимыми по entity_id, sort детерминирован | unit snapshot |
| AC12 | Конфиг не мигрирует; full/space export-import сохраняет обе bindings буквально | config round-trip unit |
| AC13 | RU/EN parity, typecheck, unit и build проходят; pre-beta smoke/performance проходят в предусмотренный процессом момент | CI/command evidence |
## 14. Тест-план
### 14.1 TypeScript unit
Синтетический authoritative registry fixture:
- device `dual_relay`;
- `light.bath` и `light.toilet` с одним `device_id`;
- sibling `sensor.dual_relay_lqi`;
- две entities с одинаковым display name;
- enabled hidden entity;
- disabled entity и entity disabled через parent;
- removed/taken bindings;
- live-only exact entity без registry row;
- более 200 шумовых entities и искомый channel после них.
Проверить AC1–AC3, AC5, AC7–AC11. Отдельно проверить последовательность
markers empty → A saved → A+B saved, неизменность исходных inputs и стабильный
tie-breaker.
Existing exact entity tests дополняются утверждением, что state и service target
двух sibling markers не пересекаются. Небезопасный domain не становится
переключаемым из-за новой discoverability.
### 14.2 Browser smoke
Расширить `demo/smoke_binding_ui.mjs` либо добавить узкий сценарий:
1. открыть Add с fixture многоканального device;
2. подтвердить, что без checkbox виден device, но не child channels;
3. включить checkbox и найти оба канала;
4. выбрать A, сохранить и повторно открыть Add;
5. убедиться, что A скрыт как taken, а B доступен;
6. сохранить B и проверить две разные bindings/позиции;
7. выполнить безопасное действие по каждой и зафиксировать разные targets;
8. проверить одинаковые labels, keyboard selection и точный secondary id.
Smoke не должен зависеть от реального MQTT broker или приватного HA diagnostic.
### 14.3 Golden и manual
Новая визуальная модель не вводится, поэтому новые golden baselines не нужны.
Если реализация изменит разметку candidate row, это расширение scope требует
обоснования и review соответствующего golden/screenshot evidence.
Manual sanity на реальном HA перед бетой:
- многоканальный MQTT/Zigbee device с двумя `light.*`/`switch.*`;
- поиск по обоим entity ids;
- отдельное размещение и переключение;
- disabled child отсутствует;
- sibling sensors/device candidates не регрессировали.
### 14.4 Команды и момент запуска
В цикле реализации:
```text
npm run typecheck
npm test
npm run build
```
Golden, smoke и performance выполняются перед бетой по release runbook. Полный
HA harness канонически выполняется в Linux CI; Windows `fcntl` не заменяется
локальным обходом.
## 15. План реализации
1. Вынести pure candidate resolution из render-класса либо создать равнозначную
чисто тестируемую authority без дублирования правил.
2. Сформировать entity universe из registry metadata и live states, применяя
канонический binding status к exact ids.
3. Развести device dedup и entity exact identity; сохранить taken/removed/edit
semantics.
4. Сделать label/sub/search/sort/limit порядок нормативным и детерминированным.
5. Подключить resolver к существующему Add dialog без изменения persisted model.
6. Добавить unit matrix и multi-channel smoke.
7. Обновить пользовательскую и архитектурную документацию, release artifacts.
## 16. Release-артефакты
Исправление пользовательски видимо. В том же class A/B commit обязательны:
- `docs/CHANGELOG.md`;
- `docs/CHANGELOG.ru.md`;
- `docs/USER-GUIDE.ru.md` — короткий сценарий отдельного размещения каналов;
- `docs/ARCHITECTURE.md` — единый binding-candidate contract;
- при изменении smoke routing — `docs/TESTING.md`;
- issue/PR evidence с unit и smoke результатами.
Новых screenshots/golden не требуется, пока внешний вид row не меняется.
Терминальные trailers продуктового commit:
```text
Issue: #109
User-Visible: yes
```
## 17. Риски и меры
| Риск | Мера |
|---|---|
| Entity universe откроет disabled rows | status matrix через единую binding authority |
| Device dedup снова скроет sibling channel | exact entity identity и отрицательный fixture с одним parent |
| Первые 200 строк скроют точный результат | filter-before-limit regression |
| Одинаковые имена станут неразличимы | обязательный entity_id и stable tie-breaker |
| Registry-less fallback создаст битую ссылку | только positive `active` status, без textual guess |
| Повторное добавление создаст дубликат | exact taken set и последовательный smoke |
| Рефакторинг сломает helpers/groups | отдельные regression cases старого always-visible contract |
| Action уйдёт во все channels device | targeted exact service-target test |
| Большой registry замедлит ввод | pure resolver, revision-aware memoization, общий performance gate |
## 18. Откат
Код откатывается обычным revert candidate resolver и его подключения. Новых
полей/версий модели нет, поэтому уже созданные exact entity markers продолжают
читаться старой версией. Откат может вернуть дефект повторного discovery, но не
должен удалять, перепривязывать или объединять сохранённые markers. Если причина
регрессии только в live-only/hidden branch, допустим узкий feature rollback этой
ветки при сохранении multi-channel registry siblings и exact taken semantics.
## 19. Принятые технические предположения
Следующие мелкие решения приняты без дополнительного вопроса владельцу:
- существующий checkbox «Показывать сущности» остаётся единственной advanced
точкой входа; новый toggle не нужен;
- HA-hidden enabled entity доступна в explicit advanced search, потому что
`hidden_by` не является `disabled_by`;
- active live-only state допустим для marker picker; это не расширяет #109 до
архитектурных openings и не закрывает #117;
- exact `entity_id` всегда показывается во secondary text для различимости;
- sort tie-breaker — exact candidate value;
- пустой filter оставляет лимит 200, непустой filter применяется до лимита;
- при исчезновении entity до Save достаточно существующего refresh/fail-safe
flow, отдельный modal не требуется;
- visual row и persisted config schema не меняются, поэтому golden и migration
не нужны.
+299
View File
@@ -0,0 +1,299 @@
# Issue #154 — transient hover не залипает после touch
- **Issue:** https://github.com/Matysh/houseplan-card/issues/154
- **Статус документа:** готово к будущей реализации; issue остаётся на `S3-spec`
- **Приоритет:** P1
- **Тип:** bug/polish, обычный трек
- **Пользовательское изменение:** да
## 1. Проблема и требуемый результат
Мобильный браузер после tap может синтезировать mouse-события и сохранять CSS
`:hover`. Одновременно room hover в View управляется `mouseenter`/`mouseleave`,
а touch sequence не гарантирует `mouseleave`. Текущий `_notePointer()` закрывает
`_tip`, но не очищает `_hoverRoom` и не нейтрализует CSS hover.
После исправления touch/pen показывает только краткий pressed feedback на время
жеста. После его завершения transient hover отсутствует. Настоящие mouse и
trackpad сохраняют обычный hover, а на гибридном устройстве последующий mouse
input восстанавливает его после touch без reload.
## 2. Термины и границы состояния
**Transient hover** существует только из-за текущего hover-capable pointer:
- `_hoverRoom`, room fill/outline и room tooltip;
- device marker lift/shadow/brightness;
- opening, room label и control hover styles;
- dialog close/control hover;
- аналогичные чисто визуальные CSS `:hover` состояния общих View-компонентов.
Не являются transient hover и не очищаются этой задачей:
- `active`, `pressed` во время незавершённого pointer sequence;
- selected/checked/current состояния редакторов;
- working, unavailable, alarm, warning, Glow/pulse и иное HA-derived состояние;
- keyboard focus и `:focus-visible`;
- открытый по click/tap toggle-popover `hp-help`;
- открытый dialog либо выполненное action.
## 3. Scope
Обязательный охват основного View:
- room floor/fill/outline/label/tooltip;
- device marker, opening и vacuum marker;
- controls поверх плана и close/action controls диалога;
- tap комнаты из будущей #152;
- long press, pinch, multi-touch и pointer capture cancellation;
- touch → mouse/trackpad на гибридном устройстве;
- смена режима, visibility и lifecycle компонента.
Общие компоненты редакторов получают исправление, если используют тот же
pointer-modality gate без специальной адаптации. Полная поддержка touch-editing
остаётся вне scope согласно `docs/TOUCH-SUPPORT.md`.
## 4. Не входит в задачу
- изменение semantic device/room state;
- новый visual design hover/pressed/focus;
- изменение действий tap, long press или click;
- превращение `hp-help` в transient tooltip;
- исправление touch settings из #149;
- полная переработка gesture arbiter #152;
- глобальный `blur()`, снятие DOM focus либо synthetic click suppression,
способный отменить пользовательское action.
## 5. Единый pointer-modality authority
Houseplan вводит один session-local источник фактической modality:
```text
unknown | mouse | touch | pen
```
Начальное значение — `unknown`; pointer-only hover при нём выключен. Переходы:
- trusted `PointerEvent` с `pointerType === 'mouse'` включает `mouse`;
- trusted `touch` включает `touch` и немедленно очищает transient hover;
- trusted `pen` включает `pen` и следует touch policy;
- следующий настоящий mouse/trackpad pointer event переводит touch/pen → mouse;
- смена mode/space, `visibilitychange` в hidden и disconnect очищают transient
hover, не подменяя semantic state.
Media queries не являются authority: браузер может ошибочно сообщать
`(hover: hover)` после touch. Для включения CSS hover одновременно нужны:
1. последняя фактическая modality `mouse`;
2. hover/fine-pointer capability среды.
Компонент применяет единый class/data-attribute gate к View tree; дочерние
shadow components получают ту же modality явным property/attribute contract.
Независимые локальные детекторы и глобальный mutable singleton запрещены.
Modality не сохраняется в localStorage/config и не синхронизируется между
карточками. Она не обязана быть reactive product state, если один безопасный
DOM gate и явная очистка обновляются без лишнего full render.
## 6. Synthetic mouse policy
Touch-generated compatibility `MouseEvent` не может включить mouse modality.
Authority дают только Pointer Events с фактическим `pointerType`; существующие
`mouseenter`/`mouseleave` не меняют modality.
JS room hover переводится на `pointerenter`/`pointerleave` либо общий delegated
pointer path и устанавливается только для разрешённого mouse modality. Touch и
pen никогда не записывают `_hoverRoom`/`_tip` через hover path.
Не использовать произвольный таймаут «после touch игнорировать mouse N ms» как
основной механизм: он ломает быстрый touch → mouse сценарий и зависит от
браузера. Если конкретный браузер отправляет compatibility event как
`PointerEvent(pointerType='mouse')`, допускается только детерминированная
проверка provenance/capabilities этого же input sequence с unit/browser
доказательством; wall-clock suppression без идентичности sequence запрещён.
## 7. Очистка transient hover
Единый idempotent helper очищает только transient hover state. Он вызывается:
- в начале каждого touch/pen `pointerdown`;
- на соответствующих `pointerup` и `pointercancel`;
- при `lostpointercapture`;
- после terminal click/action path до следующего painted frame;
- при начале multi-touch/pinch и после его завершения/cancel;
- при mode/space change;
- при `document.visibilityState === 'hidden'`;
- при disconnect/remount boundary.
Повторный вызов — no-op и не должен создавать render loop. Pointer capture
снимается только владельцем gesture по текущему contract; hover cleanup не
отменяет service call, dialog open или room fit.
После touch terminal event pressed feedback очищается обычным gesture owner.
Transient hover не остаётся дольше одного animation frame и не используется для
имитации pressed.
## 8. CSS contract
Все пользовательски заметные View `:hover` selectors получают общий modality
gate. Как минимум это:
- room overlay/yard/styled fill;
- device normal/alarm lift, shadow и brightness;
- opening outline;
- room-label controls;
- stage controls/options;
- dialog close/action controls и общие card controls, видимые в View.
Один selector не должен случайно смешивать hover с semantic state. Если текущая
rule объединяет `:hover` и `:focus-visible`, её разделяют: mouse hover получает
modality gate, focus-visible остаётся без него. `:active`/explicit pressed styles
также остаются независимы.
Selectors редакторских handles можно оставить вне обязательного охвата, если
они не используются в View/shared component; решение фиксируется inventory в
review. Naked View `:hover` после изменения считается source-contract ошибкой.
## 9. JS hover и tooltip contract
- `_hoverRoom` и `_tip` не устанавливаются touch/pen событиями.
- `pointerleave` реальной мыши очищает принадлежащее target состояние.
- touch `pointerdown` очищает старый mouse hover до выполнения tap action.
- открытие/закрытие dialog не восстанавливает старый hover snapshot.
- tooltip, открытый keyboard focus либо explicit click contract, не должен
ошибочно классифицироваться как hover; owner хранится явно.
- следующий mouse enter/move заново вычисляет current hit и показывает hover,
а не восстанавливает устаревший room/device id.
Room fit #152 использует canonical hit resolver непосредственно из gesture
sequence и не зависит от `_hoverRoom`; очистка hover не должна терять room tap.
## 10. Touch, gestures и lifecycle
- Второй pointer немедленно исключает single-tap activation по действующему
gesture contract и очищает hover.
- Pinch/long press могут выполнить своё существующее действие, но после terminal
event не оставляют hover.
- `pointercancel`/lost capture всегда безопасны, даже если target удалён renderом.
- Tap → dialog → close не возвращает marker lift/shadow из пред-dialog frame.
- Pen tap следует touch policy; hover stylus не входит в обязательный контракт.
- Kiosk, light/dark theme, Flat/Isometric используют одинаковую modality policy.
Listener `visibilitychange` регистрируется/удаляется симметрично lifecycle и не
создаёт утечку при повторных mount/unmount.
## 11. Accessibility
- DOM focus не снимается touch cleanup helper.
- `:focus-visible` и keyboard tooltip/action продолжают работать независимо от
последней pointer modality.
- Touch cleanup не меняет `aria-expanded`, `aria-pressed`, selection либо dialog
focus trap.
- Mouse hover не является единственным способом получить обязательную
информацию или действие.
- Reduced motion не влияет на state machine; pressed feedback #22 остаётся
кратким и не превращается в hover.
## 12. Acceptance criteria
1. После touch tap на каждом обязательном View target transient hover исчезает
не позднее следующего frame после завершения pressed feedback.
2. `_hoverRoom`/hover-owned `_tip` равны null после touch terminal event.
3. Device lift/shadow и CSS hover controls отсутствуют после tap/dialog close.
4. Working/alarm/unavailable/Glow/pulse/selected state не изменяется.
5. Pinch, long press, multi-touch, cancel и lost capture не оставляют hover.
6. Браузер с touch и ложным `(hover: hover)` не показывает sticky hover.
7. Настоящий desktop mouse hover визуально и функционально не изменён.
8. На hybrid device touch → mouse восстанавливает hover первым настоящим mouse
pointer event без reload и без произвольной задержки.
9. Keyboard focus и `:focus-visible` сохраняются.
10. Cleanup не отменяет click action, dialog, #152 room fit или help popover.
11. Mode/space/visibility/disconnect очищают transient hover без listener leak.
12. В View/shared CSS не остаётся naked hover selector из inventory.
## 13. План тестирования
### Unit/source contract
- transitions `unknown → mouse/touch/pen → mouse`;
- compatibility MouseEvent не включает mouse modality;
- touch/pen down/up/cancel и lost capture вызывают idempotent cleanup;
- mode/space/visibility/disconnect lifecycle;
- cleanup очищает только hover-owned state;
- room hover gate и #152 hit независимы;
- CSS inventory: View hover selectors требуют modality gate, focus-visible — нет.
### Browser smoke
- реальный touch context/CDP touch input: room, device, opening, label, control;
- tap → dialog → close и tap при активном semantic device state;
- browser context с touch и `(any-hover: hover)`;
- two-finger pinch, long press, pointercancel и lost capture;
- touch room click-to-fit #152 без sticky room highlight;
- touch → hardware/simulated mouse pointer: hover снова появляется;
- desktop mouse enter/leave и keyboard Tab/Enter regression;
- Flat/Isometric, kiosk, light/dark и visibility round-trip.
Тест проверяет computed styles и JS state после painted frame, а не только
отсутствие class. `dispatchEvent(new MouseEvent(...))` не заменяет настоящий
touch browser scenario.
### Golden
- touch post-tap screenshot без hover, но с прежним semantic state;
- desktop mouse-hover и keyboard-focus screenshots;
- visual diff не переакцептует unrelated colors/shadows.
### Performance
- pointermove не вызывает full Lit update на каждый пиксель;
- modality gate не добавляет unbounded listeners/observers;
- canonical pan/pinch smoke сохраняет frame responsiveness;
- performance profile перед beta подтверждает отсутствие новых long tasks.
## 14. План реализации
1. Провести inventory JS state и View/shared CSS hover selectors.
2. Ввести component-local pointer modality authority и lifecycle cleanup.
3. Перевести room hover на pointer events с mouse gate.
4. Ввести общий CSS modality gate, разделив hover/focus/semantic rules.
5. Передать modality в обязательные shadow child components.
6. Добавить unit/source-contract и real-touch/hybrid browser tests.
7. Прогнать typecheck, unit и build; перед beta — smoke, golden, performance.
## 15. Документация и release-артефакты
Поскольку bug виден пользователю, implementation commit обязан иметь
`User-Visible: yes` и в том же коммите обновить:
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`;
- `docs/TOUCH-SUPPORT.md` — pointer modality и отсутствие sticky hover;
- `docs/CANVAS.md` — hover/pressed/semantic ownership;
- `docs/TESTING.md` — real-touch и hybrid smoke contract.
Нужны reviewed touch post-tap, desktop-hover и keyboard-focus golden artifacts,
а также browser smoke report. Новых пользовательских строк не ожидается; если
появятся, обе локали и parity test обязательны.
## 16. Риски и откат
| Риск | Мера |
| --- | --- |
| Touch cleanup отменяет action | state-only helper, gesture smoke |
| Mouse hover пропадает на hybrid | event-derived transition back to mouse |
| Focus styling попадает под gate | отдельные selectors/source contract |
| Semantic state очищается как hover | явный inventory и state ownership tests |
| Pointermove вызывает rerender storm | DOM gate без full render per move |
| Lifecycle listener течёт | symmetric connect/disconnect test |
Откат возвращает прежние JS/CSS hover paths. Config, storage и backend data не
меняются; миграция и data rollback не нужны.
## 17. Принятые предположения
- `touch` и `pen` следуют одинаковой no-hover policy;
- initial `unknown` не включает pointer-only hover до фактической мыши;
- mouse modality определяется trusted PointerEvent, не compatibility MouseEvent;
- media query используется только вторым gate, а не источником modality;
- #152 room activation не должна читать `_hoverRoom` как selected hit;
- `hp-help`, focus-visible и semantic states не относятся к transient hover.
+2 -2
View File
@@ -1,6 +1,6 @@
# Спецификации задач P1 и P2
Актуально на 2026-08-15.
Актуально на 2026-08-14.
GitHub Issues и GitHub Projects (v2) остаются единственным каноническим backlog проекта. Этот каталог содержит развёрнутые ТЗ: каждое ТЗ ссылается на issue, а issue — на соответствующий файл. Статус, приоритет и факт завершения меняются только в GitHub.
@@ -48,6 +48,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#131](https://github.com/Matysh/houseplan-card/issues/131) Полный первый кадр View у read-only-пользователя | [131-readonly-cold-start.md](131-readonly-cold-start.md) |
| [#138](https://github.com/Matysh/houseplan-card/issues/138) Автозамыкание комнаты по существующей стене | [138-adjacent-room-autoclose.md](138-adjacent-room-autoclose.md) |
| [#146](https://github.com/Matysh/houseplan-card/issues/146) Четырёхфазный фон «Следует за Солнцем» | [146-four-phase-sun-background.md](146-four-phase-sun-background.md) |
| [#154](https://github.com/Matysh/houseplan-card/issues/154) Touch: hover-состояние залипает после tap | [154-touch-hover-reset.md](154-touch-hover-reset.md) |
| [#156](https://github.com/Matysh/houseplan-card/issues/156) Регрессии Full Performance перед v1.64.0 stable | [156-full-performance-regressions.md](156-full-performance-regressions.md) |
## P2
@@ -82,7 +83,6 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#94](https://github.com/Matysh/houseplan-card/issues/94) Универсальное действие «Переключить состояние» | [094-universal-state-toggle.md](094-universal-state-toggle.md) |
| [#101](https://github.com/Matysh/houseplan-card/issues/101) Плавный переход View ↔ редакторы | [101-view-editor-transition.md](101-view-editor-transition.md) |
| [#107](https://github.com/Matysh/houseplan-card/issues/107) Переключение виртуального источника света «Всегда» | [107-virtual-light-toggle.md](107-virtual-light-toggle.md) |
| [#109](https://github.com/Matysh/houseplan-card/issues/109) Отдельные markers для каналов многоканального HA-устройства | [109-multichannel-entity-binding.md](109-multichannel-entity-binding.md) |
| [#122](https://github.com/Matysh/houseplan-card/issues/122) Изометрический режим Stage 2: скрытый режим и визуальная полировка | [122-isometric-stage2.md](122-isometric-stage2.md) |
| [#123](https://github.com/Matysh/houseplan-card/issues/123) Split из вершины не меняет наружную геометрию стен | [123-corner-split-wall.md](123-corner-split-wall.md) |
| [#137](https://github.com/Matysh/houseplan-card/issues/137) Узлы и линии привязки в редакторе Плана | [137-plan-snap-overlay.md](137-plan-snap-overlay.md) |