mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 19:28:46 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b46a62503 | ||
|
|
cd3c7752a8 |
@@ -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
|
||||
не нужны.
|
||||
@@ -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.
|
||||
@@ -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) |
|
||||
|
||||
Reference in New Issue
Block a user