mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
385 lines
38 KiB
Markdown
385 lines
38 KiB
Markdown
# 068 — Подсказки к настройкам: переиспользуемая кнопка помощи в любом диалоге
|
||
|
||
- Issue: **#68**
|
||
- Приоритет: P2, polish
|
||
- Статус ТЗ: **выпущено в v1.62.0-beta.1; локально усилено после ревью** (ревизия 5)
|
||
- Связано: #30 (информационная архитектура диалогов), #31 (доступность),
|
||
#62 (инфраструктура i18n), #85 (тесты должны уметь падать)
|
||
- Область: общий компонент `hp-help`, фабрика вызова Houseplan Card, `hp-dialog`,
|
||
`hp-color-opacity`, ключи i18n
|
||
- Touch editor: **поддерживается для самого affordance** — подсказку можно открыть и
|
||
закрыть тапом; это явная гарантия данной задачи поверх общего best-effort правила
|
||
редакторов из `docs/TOUCH-SUPPORT.md`
|
||
|
||
Задача поставляет **механизм** и три пилотных текста. Остальные подсказки пишутся
|
||
отдельно — см. issue про контент, первая партия описана в §9.
|
||
|
||
Ревизия 4 по code review локальной реализации добавляет единый DOM/lifecycle-
|
||
контроллер fallback-поверхности для `hp-help` и `hp-color-opacity`, включает
|
||
dialog-owned portal в composed focus traversal и запрещает Escape переносить
|
||
фокус на hover-trigger. `aria-describedby` подключается только у открытой
|
||
подсказки: закрытая кнопка объявляет назначение, но не повторяет весь текст при
|
||
каждом попадании фокуса. Статический scanner дополнительно запрещает сочетать
|
||
`title=` и `_help()` на одном host-контроле.
|
||
|
||
Ревизия 5 по полному code review запрещает «мёртвый» affordance: кнопка помощи
|
||
не существует в DOM, если отсутствует или пуста хотя бы одна из двух обязательных
|
||
локализованных строк — текст подсказки либо полная ARIA-подпись. Фабрика проверяет
|
||
пару до создания host, а сам `hp-help` повторяет защиту для прямого и динамического
|
||
использования. Визуальный знак — MDI `help-circle-outline` («вопрос в кружке»), а не
|
||
зависящий от системного шрифта символ `?`.
|
||
|
||
---
|
||
|
||
## 0. Что изменено ревью 2026-08-11
|
||
|
||
| # | Находка | Решение |
|
||
|---|---|---|
|
||
| R1 | **Пилот неработоспособен в исходном виде.** Кнопка внутри `<fieldset disabled>` отключается браузером по спецификации HTML, а пилотный светоблок диалога маркера — это именно `<fieldset ?disabled=${glowControlsDisabled}>` (`houseplan-card.ts:15579`). Подсказка умирала бы ровно там, где она нужнее всего: «почему это серое?» | Нормативно: триггер рендерится **внутри первого `<legend>`** группы либо вне `fieldset`. `<legend>` — единственное исключение из disabled-наследования по стандарту, поэтому решение не требует новых хаков (§3.3) |
|
||
| R2 | **Изобретался второй механизм всплытия.** ТЗ требовало «слой, не обрезаемый диалогом» и своё место в шкале z-индексов, хотя в проекте уже есть решённый тот же случай: `hp-color-opacity` использует native Popover API + `position: fixed` и покрыт golden-сценой `decor-color-popover-mobile-ru` | Нормативно: переиспользовать тот же подход, вынеся его в общий helper. Никакого нового z-слоя не вводится — top layer выше всего по определению (§3.4) |
|
||
| R3 | **Escape не дошёл бы до пузыря.** `hp-dialog._onKeyDown` (`hp-dialog.ts:293`) на Escape делает `stopImmediatePropagation()` и закрывает диалог. Требование «Escape закрывает сначала подсказку» не выполнялось бы само собой | Введён явный реестр открытых поверхностей у `hp-dialog`; Escape отдаётся верхней из них (§3.5), и это отдельный ассерт смока |
|
||
| R4 | **Цифры устарели.** В теле issue: 57 `title=`, 38 hint-ключей | На v1.61.0: **56** `title=`, **34** ключа вида `*_tip`/`*_hint`/`*.hint` из 721 (§1) |
|
||
| R5 | **Обоснование пилота устарело.** «Светоблок всё равно переделывается по #65–#67» — они вышли в v1.61.0 | Пилот сохранён, но по другой причине: это самый трудный случай (disabled-группа + три контрола, которые пришлось объяснять текстом) (§8) |
|
||
| R6 | Не было правил для тача, повторного открытия, прокрутки и клика по label | §3.6–§3.9 |
|
||
| R7 | Проверка паритета ключей не могла работать: ключ мог быть вычисляемым | Нормативно: только строковый литерал в разметке (§6) |
|
||
| R8 | Тесты не проверялись на способность падать | Добавлен обязательный мутант по контракту #85 (§7) |
|
||
| R9 | **Компоненту неоткуда было взять правильный язык.** `hass.locale` недостаточно: карточка поддерживает явный `config.language`, а автономный `hp-help key="…"` этого контекста не видит. При двух карточках с разным языком глобальный localizer также недопустим | `hp-help` становится presentation-only, а Houseplan Card даёт единую фабрику `_help('literal.help')`, которая на каждом рендере передаёт уже локализованные `text` и `ariaLabel`. Вызов по-прежнему занимает одну строку и содержит один литеральный ключ (§3.1, §5) |
|
||
| R10 | Открытие «по фокусу» конфликтовало с кликом: pointer сначала фокусирует кнопку, затем `click` немедленно переключает уже открытый пузырь обратно | Автооткрытие по focus разрешено только при `:focus-visible`; pointer-hover обслуживается только `pointerType === 'mouse'`, а `click` остаётся единым toggle для мыши, тача и `Enter`/`Space` (§3.2) |
|
||
| R11 | Необязательная ссылка противоречила семантике tooltip: `role="tooltip"` не может содержать интерактивный элемент, а non-modal dialog потребовал бы другой фокусный контракт | В первой версии пузырь строго текстовый и не получает фокус. Ссылки вынесены из механизма #68; их можно добавить отдельным расширением с отдельной семантикой. #86 остаётся задачей на короткий самостоятельный текст (§3.10, §10) |
|
||
| R12 | Настоящая кнопка внутри Shadow DOM нового компонента могла выпасть из ручного focus trap нативной ветки `hp-dialog`: текущий `_focusableElements()` видит только light DOM | Нативная ветка `hp-dialog` получает обход focusable-элементов по composed tree/open shadow roots; порядок Tab и wrap проверяются отдельным смоком (§3.12, §7) |
|
||
| R13 | `registerOverlay(close)` не определял порядок, очистку и конкуренцию с пикером цвета | Зафиксирован token/disposer-контракт, LIFO, идемпотентная очистка, pruning отключённых владельцев и одна exclusive transient-поверхность на диалог. `hp-help` и `hp-color-opacity` используют один контракт (§3.5) |
|
||
| R14 | В пилоте подсказка радиуса оставалась бы внутри тела disabled-fieldset и снова отключалась, несмотря на исправление для legend | Радиус вынесен из disabled-fieldset цвета/яркости; disabled назначается самому input, а живой help-trigger остаётся рядом с подписью. Динамическая причина блокировки сохраняется как отдельная status-note (§8) |
|
||
| R15 | Не был определён fallback при отсутствии Popover API и жизненный цикл при resize/orientation; комментарий fallback в `hp-color-opacity` сам по себе этого контракта не гарантирует | Общий floating helper обязан иметь проверяемую ветку без `showPopover`, учитывать visual viewport и закрывать либо безопасно перепозиционировать поверхность при resize. Fallback тестируется принудительно (§3.4, §7) |
|
||
| R16 | Исходное требование «ниже toast» несовместимо с выбранным native top layer: обычный `.toast { z-index: 120 }` всё равно окажется под открытым popover | Появление toast сначала закрывает transient-поверхности текущей карточки. Так toast остаётся единственным верхним сообщением без отдельной миграции toast в top layer (§3.5, §7) |
|
||
|
||
---
|
||
|
||
## 1. Зачем механизм, а не ещё один `title=`
|
||
|
||
Объяснения сейчас живут в трёх несовместимых формах (замеры на v1.61.0):
|
||
|
||
- **56** нативных `title=${this._t(...)}` в `houseplan-card.ts` — невидимы на тач-экране,
|
||
задержка не управляется, оформление не управляется. То есть не работают ровно на
|
||
планшетах и настенных панелях, где и живёт основная аудитория киоска;
|
||
- **16** блоков `.rhint` под контролом — видны всем и всегда, включая тех, кто уже
|
||
разобрался, и навсегда увеличивают высоту диалога;
|
||
- у большинства опций объяснения нет вообще.
|
||
|
||
При этом **34** ключа i18n уже содержат подсказочный текст (`*_tip`, `*_hint`, `*.hint`)
|
||
из 721 ключа. Контент существует кусками, но не имеет единой подачи.
|
||
|
||
## 2. Что видит пользователь
|
||
|
||
После подписи опции — **кружок со знаком вопроса** того же размера, что подпись, в
|
||
приглушённом цвете. По наведению, фокусу с клавиатуры или тапу рядом появляется
|
||
компактный пузырь с объяснением этой опции. Пузырь закрывается уводом курсора, тапом
|
||
мимо, повторным нажатием или Escape — и **не закрывает при этом сам диалог**.
|
||
|
||
Опции без подсказки выглядят ровно как сейчас: кружок не рендерится вовсе.
|
||
|
||
## 3. Механизм
|
||
|
||
### 3.1 Компонент
|
||
|
||
`hp-help` — presentation-only компонент. Он не импортирует словари, не читает
|
||
`hass.locale`, не ищет приватные поля host-карточки и не держит глобальный язык. На вход он
|
||
получает уже разрешённые строки `text` и `ariaLabel`.
|
||
|
||
Houseplan Card предоставляет одну фабрику, которая остаётся единственной точкой вызова:
|
||
|
||
```ts
|
||
${this._help('marker.light_role.help')}
|
||
```
|
||
|
||
Фабрика локализует тело и полную доступную подпись через текущий `_t()` карточки, поэтому
|
||
учитывает как язык профиля HA, так и явный `config.language`, и рендерит:
|
||
|
||
```ts
|
||
html`<hp-help .text=${text} .ariaLabel=${ariaLabel}></hp-help>`
|
||
```
|
||
|
||
Место вызова передаёт **только один литеральный ключ**. Ни пользовательских строк, ни
|
||
разметки, ни callback локализации, ни позиционирования на стороне конкретного диалога.
|
||
Смена языка родительской карточки обычным Lit-рендером обновляет уже открытый пузырь.
|
||
Две карточки с разными языками на одной странице не влияют друг на друга.
|
||
|
||
### 3.2 Триггеры
|
||
|
||
Все три, потому что продукт живёт на десктопе, планшете и телевизоре:
|
||
|
||
- **мышь**: `pointerenter` с `pointerType === 'mouse'` открывает с задержкой 300 мс;
|
||
уход и с триггера, и с пузыря закрывает с задержкой 150 мс. Вход в пузырь отменяет
|
||
таймер закрытия;
|
||
- **клавиатура**: триггер — настоящий `<button type="button">`, доступен по Tab. Фокус
|
||
автоматически открывает подсказку только когда кнопка соответствует `:focus-visible`;
|
||
`Enter`/`Space` через обычный `click` переключают состояние;
|
||
- **мышь или тач по кнопке**: обычный `click` переключает. Эмулированные после тапа
|
||
mouse-hover события игнорируются, потому что hover-ветка принимает только настоящий
|
||
mouse pointer.
|
||
|
||
Pointer-фокус перед `click` не должен давать цикл «открылась по focus → закрылась по
|
||
click». Все таймеры открытия/закрытия отменяются при противоположном действии и в
|
||
`disconnectedCallback`; отложенный callback проверяет, что компонент ещё подключён и
|
||
намерение пользователя не изменилось.
|
||
|
||
### 3.3 Disabled-группы (R1)
|
||
|
||
Триггер **обязан** оставаться доступным, когда группа настроек отключена: половина
|
||
ценности подсказки — объяснить, почему контрол недоступен.
|
||
|
||
Нормативно: вызов `_help()` внутри `<fieldset>` размещается **в первом `<legend>`**. По
|
||
HTML-стандарту `disabled` у `fieldset` не распространяется на содержимое первого
|
||
`legend`, поэтому кнопка остаётся живой без единого обходного приёма. Если у группы нет
|
||
`legend`, триггер выносится за пределы `fieldset`.
|
||
|
||
Запрещено: help-trigger в теле disabled-группы, отключение триггера вместе с контролом,
|
||
дублирование подсказки в `aria-describedby` отключённой группы.
|
||
|
||
### 3.4 Позиционирование (R2)
|
||
|
||
Диалог обрезает содержимое: `overflow: hidden` и на `.surface` (`hp-dialog.ts:138`), и на
|
||
`ha-dialog::part(dialog)` (`:55`). Абсолютно спозиционированный пузырь будет срезан у
|
||
края.
|
||
|
||
Нормативно: **тот же механизм, что у `hp-color-opacity`** — native Popover API
|
||
(`showPopover()`/`hidePopover()`, состояние `:popover-open`) плюс `position: fixed` и
|
||
пересчёт координат при открытии. Всплывающий слой оказывается в top layer, поэтому:
|
||
|
||
- новый z-слой не вводится и шкала `tip 60 < tray 70 < menu 80 < toast 120` не меняется;
|
||
- второй механизм всплытия в проекте не появляется.
|
||
|
||
Логика переворота и сдвига у края (flip/shift) выносится из `hp-color-opacity` в общий
|
||
**чистый** helper и используется обоими компонентами. Helper получает rect якоря, rect
|
||
поверхности, gap, безопасный отступ и границы visual viewport; возвращает координаты и
|
||
выбранную сторону. Он не владеет DOM, слушателями или политикой закрытия. Дублировать
|
||
геометрию копипастой запрещено.
|
||
|
||
Обязательные граничные случаи helper:
|
||
|
||
- обычное размещение снизу;
|
||
- flip наверх, если снизу не помещается;
|
||
- horizontal shift у обоих краёв;
|
||
- поверхность шире/выше доступной области: координата прижимается к safe edge, а сама
|
||
поверхность получает ограничение `max-inline-size`/`max-block-size`;
|
||
- `visualViewport.offsetLeft/offsetTop/width/height`, если API доступен; иначе
|
||
`window.innerWidth/innerHeight`.
|
||
|
||
Popover API — предпочтительная ветка, но не безусловное допущение. Общий контроллер
|
||
делает feature detection. Ветка без `showPopover()` обязана остаться видимой и не
|
||
обрезаться обеими ветками `hp-dialog`; конкретный способ fallback выбирает реализация,
|
||
но «оставить элемент внутри clipping ancestor и назвать это fallback» недостаточно.
|
||
Fallback имеет отдельный принудительный смок.
|
||
|
||
При `resize` окна/visual viewport и смене ориентации открытая поверхность либо один раз
|
||
перепозиционируется через rAF, либо закрывается — выбранная политика одинакова для
|
||
одинаковых причин и покрыта тестом. Исчезновение/отключение якоря всегда закрывает
|
||
поверхность. Scroll-политика подсказки отдельно зафиксирована в §3.8.
|
||
|
||
### 3.5 Escape и порядок закрытия (R3)
|
||
|
||
`hp-dialog` получает публичный реестр открытых внутри него transient-поверхностей. Один
|
||
вызов регистрации имеет следующий смысл (точное имя TypeScript API может отличаться,
|
||
семантика — нет):
|
||
|
||
```ts
|
||
registerOverlay({ owner, close, group: 'transient' }): () => void
|
||
```
|
||
|
||
- регистрация происходит **только после фактического открытия** поверхности;
|
||
- возвращённый disposer идемпотентен и вызывается при любом закрытии и обязательно в
|
||
`disconnectedCallback` владельца;
|
||
- повторная регистрация того же owner не создаёт дубль, а переносит его наверх LIFO;
|
||
- перед использованием реестр удаляет записи с `!owner.isConnected`;
|
||
- открытие новой поверхности в exclusive-группе `transient` сначала закрывает прежнюю
|
||
из этой группы. Поэтому одновременно не остаются help-bubble и color picker;
|
||
- callback `close('escape')` синхронно переводит owner в закрытое состояние и не сам
|
||
генерирует второй Escape.
|
||
|
||
На Escape диалог сначала закрывает верхнюю зарегистрированную поверхность и **не
|
||
закрывает себя**; если реестр пуст — закрывается сам. Событие по-прежнему получает
|
||
`preventDefault()` и `stopImmediatePropagation()` ровно один раз в capture-handler
|
||
диалога. Существующее поведение диалога без поверхностей не меняется.
|
||
|
||
`hp-color-opacity` переводится на тот же реестр в рамках этой задачи: сейчас его пикер и
|
||
диалог конкурируют за Escape. Сам `hp-color-opacity` и `hp-help` не ищут реестр глобально:
|
||
они регистрируются в ближайшем `hp-dialog`; вне диалога сохраняют собственное закрытие по
|
||
Escape и outside-pointer для изолированного demo/test применения.
|
||
|
||
Обычный toast не может перекрыть native top-layer числовым `z-index`. Поэтому перед
|
||
показом toast карточка закрывает все transient-поверхности **только своего render root**;
|
||
сам диалог остаётся открыт. Это сохраняет существующую визуальную иерархию «toast важнее
|
||
подсказки/пикера» без переноса toast в новый механизм.
|
||
|
||
### 3.6 Тач и указатель одновременно (R6)
|
||
|
||
События hover на тач-устройствах эмулируются, поэтому тап иначе открывал бы и тут же
|
||
закрывал пузырь. Нормативно: обработчики наведения игнорируются при
|
||
`event.pointerType !== 'mouse'`; activation живёт в обычном `click`, а не в параллельном
|
||
`touchend`. На hybrid-устройстве реальная мышь сохраняет hover, реальный touch — toggle.
|
||
|
||
### 3.7 Одна подсказка за раз (R6)
|
||
|
||
Открытие любого `hp-help` в диалоге закрывает предыдущую transient-поверхность этого
|
||
диалога, включая открытый `hp-color-opacity`, и наоборот. Разные карточки/диалоги не
|
||
делят глобальное состояние и не закрывают поверхности друг друга.
|
||
|
||
### 3.8 Прокрутка (R6)
|
||
|
||
Пузырь спозиционирован фиксированно и за прокруткой тела диалога не следует.
|
||
Нормативно: прокрутка любого предка закрывает пузырь. Пересчёт координат на каждый кадр
|
||
прокрутки запрещён. Scroll внутри другой независимой карточки не закрывает подсказку;
|
||
контролируется event path/принадлежность к тому же document+dialog, а не глобальный
|
||
`document.scroll` без фильтра.
|
||
|
||
Открытие/закрытие подсказки не меняет `clientHeight`, `scrollHeight` или `scrollTop`
|
||
тела диалога. Вспомогательный текст для `aria-describedby` постоянно остаётся вне
|
||
scroll-flow; его доступность переключается ARIA-состоянием, а не добавлением
|
||
абсолютно спозиционированного узла в overflow-геометрию. Требование одинаково для
|
||
native Popover и portal fallback.
|
||
|
||
### 3.9 Триггер и `<label>` (R6)
|
||
|
||
Help-trigger не размещается внутри `<label>`: клик по нему переключал бы связанный
|
||
контрол. Триггер ставится после закрывающего `</label>` либо в `legend`.
|
||
|
||
### 3.10 Содержимое
|
||
|
||
Первая версия — **только короткий текст**, только из i18n, без HTML и без интерактивных
|
||
элементов. Поверхность имеет `role="tooltip"` и никогда не принимает фокус.
|
||
|
||
Ссылка на документацию в эту версию не входит: интерактивная ссылка несовместима с
|
||
семантикой tooltip и потребовала бы отдельного non-modal-dialog/focus-контракта. Если
|
||
такая ценность подтвердится, это отдельное расширение компонента, а не необязательный
|
||
ключ, меняющий семантику молча.
|
||
|
||
Ширина ограничена, текст переносится. Контент из #86 — одно предложение, максимум два;
|
||
при 200 % масштабе и ширине viewport 390 CSS px он должен помещаться без горизонтальной
|
||
прокрутки. Защитный `max-block-size` допускается, но штатный пилотный текст не должен
|
||
требовать внутренней прокрутки, недоступной визуальному keyboard-only пользователю.
|
||
|
||
### 3.11 Оформление и движение
|
||
|
||
Токены поверхности диалога, обе темы. При `prefers-reduced-motion` анимация появления и
|
||
исчезновения отключается. Видимый знак вопроса — 16–18 px, но зона настоящей кнопки не
|
||
меньше 32×32 CSS px на fine pointer и 40×40 px при `pointer: coarse`. Wrapper подписи
|
||
может честно увеличить высоту строки; отрицательные margins и перекрытие соседних targets
|
||
запрещены. На 200 % подпись и help-trigger могут перенестись вместе на следующую строку,
|
||
но не обрезаются.
|
||
|
||
### 3.12 Shadow DOM и focus trap (R12)
|
||
|
||
`hp-help` инкапсулирует настоящую кнопку в своём Shadow DOM. Нативная fallback-ветка
|
||
`hp-dialog` обязана включать focusable descendants открытых shadow roots в composed
|
||
порядке при initial-focus и Tab wrap. Обход не заходит в закрытые shadow roots и не
|
||
добавляет host отдельным tab-stop, если фокусным является его внутренний control.
|
||
|
||
Это исправление проверяется не только на `hp-help`, но и на существующем
|
||
`hp-color-opacity`: оба trigger доступны Tab, а переход после последнего focusable
|
||
возвращает на close/первый control ровно один раз. HA-ветка продолжает использовать trap
|
||
`ha-dialog`; ручная логика не должна создавать второй trap поверх него.
|
||
|
||
## 4. Доступность
|
||
|
||
- триггер — кнопка с `aria-label` в виде одной цельной строки i18n
|
||
(«Подсказка: радиус свечения»), без склейки из кусков;
|
||
- у кнопки есть `aria-expanded`; открытое состояние подключает
|
||
`aria-describedby` к стабильному уникальному id. В закрытом состоянии связь
|
||
отсутствует, чтобы screen reader не повторял объяснение при каждом фокусе;
|
||
- пузырь имеет `role="tooltip"`, не содержит focusable-элементов и сам не получает
|
||
фокус; фокус всё время остаётся на trigger;
|
||
- Escape закрывает пузырь; если фокус уже был на trigger (keyboard/click path), он там
|
||
остаётся. Hover-подсказка не крадёт текущий фокус и при закрытии его не переносит;
|
||
- screen reader получает и назначение кнопки, и открытое объяснение; одна и та же фраза
|
||
не объявляется дважды из-за параллельного live-region;
|
||
- читаемо при 200 % масштабе браузера.
|
||
|
||
## 5. Соглашение о ключах
|
||
|
||
На один вызов нужны два механически связанных ключа:
|
||
|
||
- `<ключ опции>.help` — текст пузыря: `marker.light_role.help`;
|
||
- `<ключ опции>.help.aria` — **цельная** доступная подпись кнопки, например
|
||
«Подсказка: является ли устройство источником света». Это не конкатенация общего
|
||
«Подсказка» с label во время выполнения.
|
||
|
||
В `_help()` передаётся первый ключ; `.aria` выводится из него механически. Тип фабрики —
|
||
`Extract<I18nKey, `${string}.help`>`, поэтому произвольная строка не проходит typecheck.
|
||
Для опции без обоих ключей trigger не добавляется.
|
||
|
||
## 6. Проверка ключей в CI (R7)
|
||
|
||
Существующий тест паритета i18n расширяется: он сканирует исходники на вызовы
|
||
`_help('…')`, собирает help-ключи и для каждого требует сам ключ и производный `.aria`
|
||
**в обоих** словарях. Отсутствующий/пустой ключ валит CI, а не рендерит ключ или пустой
|
||
пузырь.
|
||
|
||
Нормативно: аргумент `_help()` — строковый литерал в разметке. Вычисляемые ключи запрещены, иначе
|
||
статический сканер их не видит и проверка становится фиктивной. Единственное
|
||
исключение — внутреннее механическое добавление `.aria` внутри самой фабрики.
|
||
|
||
## 7. Тесты
|
||
|
||
- **юниты**: pure flip/shift helper (низ, верх, оба horizontal edge, oversized surface,
|
||
visual viewport offset); разрешение help + `.aria`, обнаружение отсутствующего ключа,
|
||
стабильная связка aria;
|
||
- **паритет i18n**: см. §6;
|
||
- **смок**: открытие по mouse-hover, по `:focus-visible` с клавиатуры, кликом мыши и
|
||
touch tap; pointer-focus+click не закрывает только что открытый пузырь; закрытие тапом
|
||
мимо и по Escape **без закрытия диалога**; второй Escape закрывает уже диалог;
|
||
открытие help закрывает color picker и наоборот; закрытие при прокрутке своего dialog,
|
||
но не чужой карточки; доступность триггера в disabled-группе; bounding box пузыря
|
||
внутри visual viewport для триггера в правом нижнем углу диалога на экране 390 px;
|
||
показ toast закрывает help/color picker своей карточки, но не dialog и не поверхности
|
||
другой карточки;
|
||
отдельный прогон с принудительно отключённой Popover API веткой; явный `config.language`
|
||
отличается от `hass.locale` и определяет текст; две карточки с разными языками не
|
||
заражают друг друга;
|
||
- **focus smoke**: в нативной fallback-ветке Tab доходит до внутренних кнопок `hp-help`
|
||
и `hp-color-opacity` в composed порядке, Shift+Tab идёт обратно, wrap происходит один
|
||
раз; в HA-ветке не появляется второй trap;
|
||
- **golden**: одна сцена с открытым пузырём, тёмная и светлая, с семантическим ассертом
|
||
«в области пузыря есть непрозрачные пиксели текста» — по образцу `warmPixelRegion`
|
||
(`demo/golden/run.mjs`);
|
||
- **мутант по #85** (обязателен): удаление логики flip/shift у края. Смок обязан
|
||
покраснеть на ассерте «пузырь внутри вьюпорта». Без объявленного мутанта задача не
|
||
считается выполненной.
|
||
|
||
## 8. Пилот (R5)
|
||
|
||
Конвертируется **один** блок — светоблок диалога маркера. В нём ровно три help-trigger:
|
||
|
||
1. в legend «Является источником света» — общий смысл Auto/Always/Never;
|
||
2. в первом legend disabled-fieldset «Цвет и яркость свечения» — различие трёх режимов,
|
||
включая правило «минимум 1 %, выключение через Never»;
|
||
3. рядом с подписью «Радиус свечения» — единицы и наследование общего значения.
|
||
|
||
Радиус визуально остаётся частью светоблока, но его row выводится **за пределы**
|
||
disabled-fieldset цвета/яркости. При блокировке disabled получает сам input; help-trigger
|
||
остаётся доступным. Input и fieldset ссылаются через `aria-describedby` на существующую
|
||
динамическую status-note с фактической причиной блокировки.
|
||
|
||
Переносятся и затем удаляются неиспользуемые старые ключи/строки:
|
||
`marker.light_role_tip`, `marker.glow_color_tip`, `marker.glow_brightness_hint` и
|
||
`marker.glow_radius_hint`. Три динамических `marker.glow_disabled_*` **не превращаются**
|
||
в общую подсказку и остаются видимой status-note: это не документация опции, а текущее
|
||
состояние конкретного маркера.
|
||
|
||
Контрол/подпись не может нести одновременно `title=`/старую `.markerlighttip` и новый
|
||
`_help()` — это проверяется сканером исходника. Основание пилота: это самый трудный случай
|
||
в продукте (disabled-группа, условные controls, color picker и динамическая причина), и
|
||
на нём проверяются сразу §3.3–§3.5 и §3.12.
|
||
|
||
## 9. Что дальше (контент)
|
||
|
||
Написание остальных текстов — отдельная задача [#86](https://github.com/Matysh/houseplan-card/issues/86).
|
||
Правило для неё: одна небольшая правка на диалог, пары `.help` + `.help.aria` пишутся
|
||
сразу в обоих словарях. Упоминание необязательной clickable-ссылки в #86 заменяется
|
||
правилом §3.10: ссылка требует отдельного расширения механизма.
|
||
|
||
## 10. Вне области
|
||
|
||
- Написание подсказок для всех опций.
|
||
- Массовая миграция 56 `title=`. Правило на будущее: новые объяснения делаются только
|
||
через `_help()`, старые `title=` мигрируют по мере касания диалога.
|
||
- Clickable-ссылки, HTML/Markdown и произвольный rich content внутри пузыря.
|