docs: specify color picker confirmation

Issue: #476
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-09-06 12:56:13 +00:00
committed by claude[bot]
parent 7195ad1914
commit 3fbf312bbc
2 changed files with 340 additions and 0 deletions
+339
View File
@@ -0,0 +1,339 @@
# #476 — Явное завершение выбора цвета кнопкой «ОК»
- **Issue:** https://github.com/Matysh/houseplan-card/issues/476
- **Тип / приоритет:** feature + polish / P2
- **Трек:** полный; появляется новый наблюдаемый UX-контракт завершения общей
поверхности выбора цвета, включая desktop, touch и fallback без Popover API
- **Оценка:** пользовательская ценность 7/10; ценность для разработки 6/10;
сложность 3/10; риск 3/10
- **Связано:** #57, #180; `docs/SCOPE.md`, `docs/TOUCH-SUPPORT.md`
## 1. Сценарий
Персона — Home admin из `docs/SCOPE.md`. Поверхность — любой редакторский или
настроечный диалог House Plan, в котором используется стандартный выбор цвета.
Момент — администратор уже настроил цвет и, если доступно, прозрачность, но не
понимает, каким действием завершить работу с открытой поверхностью.
Desktop с мышью и клавиатурой остаётся эталонной средой редакторов. Та же кнопка
доступна на touch, поскольку `hp-color-opacity` уже является общей touch-capable
поверхностью и явное завершение не требует нового жеста.
## 2. Что человек увидит до и после
**До:** после настройки цвета в picker нет явного действия завершения, поэтому
пользователь вынужден догадываться, что поверхность закроется нажатием снаружи,
повторным нажатием на образец или `Escape`.
**После:** внизу picker находится заметная полноширинная кнопка «ОК»; нажатие
сохраняет уже показанный результат и закрывает поверхность, а прежние способы
закрытия продолжают работать.
## 3. Проблема
Единый `hp-color-opacity`, созданный в #57 и распространённый на все места
выбора цвета в #180, применяет корректные изменения сразу через событие
`hp-color-opacity-change`. Он умеет закрываться через trigger, outside pointer,
`Escape`, смену transient overlay и lifecycle родительского диалога, но не
показывает ни одного явного действия внутри самой поверхности.
Технически выбор уже состоялся, но визуально интерфейс выглядит незавершённым.
Это повторяется во всех потребителях общего компонента: decor, room/space,
общих цветах, Glow и ripple.
## 4. Решения владельца
1. Сохраняется нынешнее live-применение. «ОК» не создаёт отдельную транзакцию и
не откладывает изменение родительского draft.
2. Outside click/tap, повторное нажатие swatch и `Escape` сохраняются; каждый
путь оставляет последнее валидное live-применённое значение.
3. При незавершённом невалидном HEX «ОК» не закрывает picker. Последнее валидное
применённое значение сохраняется, а ошибка у HEX-поля остаётся видимой до
исправления.
4. Кнопка — полноширинная primary-кнопка внизу picker.
## 5. Скоуп
- одна кнопка подтверждения внутри общей поверхности `hp-color-opacity`;
- одинаковый interaction contract в native Popover API и portal/fallback;
- одинаковое поведение для `showOpacity=true` и color-only потребителей;
- локализованная подпись кнопки во всех поддерживаемых языках;
- keyboard focus, screen-reader name, forced-colors и touch hit target;
- сохранение действующих live events и всех прежних путей закрытия;
- unit, browser smoke и reviewed golden для светлой/тёмной тем, desktop/touch;
- пользовательская запись в обоих changelog.
## 6. Не входит
- кнопка «Отмена», Reset, история, presets, eyedropper или новый palette UI;
- транзакционный черновик цвета внутри компонента либо откат live-изменений;
- изменение состава HSV/HEX/opacity controls и их порядка;
- изменение формата `#rrggbb`, opacity `[0, 1]` или
`hp-color-opacity-change`;
- изменение Save/Cancel родительских диалогов и серверной конфигурации;
- отдельные правила для decor, room, space, general settings, Glow или ripple;
- новый глобальный keyboard shortcut: `Enter` в HEX-поле сохраняет свой
текущий смысл, а `Enter`/`Space` на самой кнопке работает нативно;
- переработка floating placement, overlay stacking либо touch parity остальных
редакторских операций.
Если реализация потребует изменить момент применения значения, detail события,
родительские drafts или закрытие других transient overlays, задача возвращается
в `S3-spec` как расширение публичного контракта.
## 7. Контракт поведения
### 7.1 Live-применение
Hue, saturation, brightness, валидный HEX и opacity продолжают немедленно
обновлять локальный preview и отправлять существующее bubbling/composed событие
`hp-color-opacity-change` с detail `{ color, opacity }`. Родительский consumer
продолжает обновлять свой draft или сохранённый editor style так же, как до
#476.
Открытие picker и нажатие «ОК» без нового значения не отправляют событие.
Нажатие «ОК» после уже live-применённого изменения также не отправляет
дублирующее событие: кнопка завершает взаимодействие, а не повторно применяет
состояние.
### 7.2 Кнопка «ОК»
Кнопка является последним focusable control в DOM-порядке picker и находится
после opacity-row либо после HEX-control у `showOpacity=false`. Она занимает всю
доступную ширину внутренней области, имеет высоту не менее 40 CSS px и следует
theme tokens House Plan для primary action. В `forced-colors` используются
системные цвета/граница, а не невидимый theme-only fill.
Один click/tap либо нативная активация `Enter`/`Space`:
1. валидирует текущий HEX draft по существующему правилу commit;
2. при валидном draft закрывает picker тем же единым lifecycle-путём, что
остальные close reasons;
3. возвращает keyboard focus на swatch trigger;
4. не передаёт click/tap нижележащему plan, toolbar или родительскому dialog.
Кнопка существует и работает одинаково во всех экземплярах общего компонента;
отдельного opt-in property у consumers нет.
### 7.3 Невалидный HEX
Если на момент нажатия «ОК» HEX draft не нормализуется как 3- или 6-значный
цвет либо поле всё ещё несёт результат предыдущей неуспешной валидации:
- picker остаётся открытым;
- `aria-invalid=true` и существующее локализованное сообщение ошибки остаются
видимыми;
- последнее валидное значение color/opacity и родительский draft не меняются;
- новое `hp-color-opacity-change` не отправляется;
- focus остаётся/переводится в HEX input, чтобы значение можно было исправить.
После ввода валидного HEX повторное «ОК» закрывает picker по §7.2. Точное
существующее правило отображения невалидного draft (нормализация поля к
последнему валидному значению при commit) не меняется этой задачей. При этом
повторное «ОК» без нового валидного пользовательского ввода не имеет права
снять состояние ошибки только потому, что commit уже вернул в поле последнее
валидное значение: поверхность остаётся открытой до реального исправления.
### 7.4 Остальные способы закрытия
Повторное нажатие trigger, pointer down вне surface, `Escape`, открытие другой
exclusive transient surface, закрытие родительского диалога, mode change,
disconnect и потеря валидного anchor продолжают закрывать picker по нынешним
правилам. Они не откатывают уже live-применённое значение и не требуют
предварительно нажать «ОК».
`Escape` из focus внутри picker закрывает сначала picker и возвращает focus на
trigger; второй `Escape` принадлежит родительскому dialog. Outside pointer и
trigger-close сохраняют свой текущий focus contract.
## 8. UX, touch и доступность
- `hp-color-opacity` сохраняет `role="dialog"`, доступное название и одну
floating surface; кнопка — нативный `button type="button"` с видимым текстом.
- Полноширинная цель имеет минимум 40 px высоты. На узком viewport она остаётся
в обычном scroll flow поверхности, не перекрывает HSV/HEX/opacity controls и
не увеличивает ширину за viewport.
- Tap по кнопке закрывает только picker и не превращается в editor action.
Pointer capture HSV-поля и `pointercancel` остаются прежними.
- Light/dark theme, browser zoom, visual viewport, flip/shift placement и
portal fallback используют существующий `FloatingSurfaceController`.
- Новых анимаций нет; `prefers-reduced-motion` не меняется.
Touch editor остаётся best effort по `docs/TOUCH-SUPPORT.md`, но доступная уже
поверхность выбора цвета получает ту же явную кнопку без намеренной деградации.
View, kiosk и device actions не затрагиваются.
## 9. i18n
`ColorPickerLabels` получает обязательное поле `confirm`; его per-card значение
передаётся всем существующим picker instances через уже общий
`_colorPickerLabels`.
Добавить `color_picker.confirm` в синхронные словари `src/i18n/{en,ru,de,fr}.json`:
- RU: `ОК`;
- EN: `OK`;
- DE: `OK`;
- FR: `OK`.
Fallback внутри presentation-only компонента — `OK`. Отдельный aria-only ключ
не нужен: видимый текст является доступным именем.
## 10. Модель данных, миграция и совместимость
Server config, editor drafts, storage/model version и backend schema не
меняются. Новых persisted полей, compatibility aliases и миграции нет.
Публичный consumer API сохраняет свойства `color`, `opacity`, `disabled`,
`showOpacity`, событие и его detail. Дополнение `ColorPickerLabels.confirm`
является внутренним compile-time контрактом House Plan: все production
consumers уже получают единый объект labels. Для defensive runtime fallback
отсутствующее/пустое `confirm` отображается как `OK`, поэтому стороннее ручное
создание custom element со старым объектом labels не ломается.
Downgrade возвращает прежний picker без кнопки и продолжает читать тот же
config. Данные не теряются и обратная миграция не нужна.
## 11. Затронутые файлы и модули
- `src/hp-color-opacity.ts` — label, кнопка, validation/close path и стили;
- `src/houseplan-card.ts` — per-card localized `ColorPickerLabels`;
- `src/i18n/{en,ru,de,fr}.json` — `color_picker.confirm`;
- `test/color-picker.test.mjs` и i18n/source contract tests;
- `demo/smoke_color_picker.mjs` — click/tap, invalid HEX, focus и события;
- при необходимости существующий fallback smoke, без создания второго
component implementation;
- `demo/golden/matrix.mjs`/`harness.mjs` только если существующим color-picker
сценам нужна подготовка для видимой кнопки; принимаемые PNG и golden index;
- `docs/TESTING.md`, `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`;
- generated `dist/**` и `custom_components/houseplan/frontend/**` по
manifest-driven bundle contract.
User Guide и config compatibility docs не меняются: новая кнопка
самообъясняющаяся, а конфигурационный контракт отсутствует. Если в реализации
понадобится объяснять кнопку отдельно в руководстве, это считается сигналом,
что affordance не решил исходную проблему.
## 12. Критерии приёмки
- **AC1 — видимая кнопка и геометрия (unit + golden).** Каждый открытый
`hp-color-opacity`, включая `showOpacity=false`, показывает последним control
полноширинную primary-кнопку с высотой не менее 40 CSS px; она помещается в
light/dark desktop/touch кадрах без clipping и перекрытия controls.
- **AC2 — live parity (unit + smoke).** Hue/SV/HEX/opacity по-прежнему немедленно
отправляют ровно существующий `{ color, opacity }` и обновляют parent draft;
открытие и «ОК» не добавляют event, если значение уже применено.
- **AC3 — успешное завершение (smoke).** Реальный click/tap и keyboard activation
по «ОК» закрывают native popover и fallback, ставят `aria-expanded=false`,
возвращают focus на trigger и не вызывают действие plan/dialog под surface.
- **AC4 — невалидный HEX (unit + smoke).** При невалидном draft «ОК» оставляет
picker открытым, error/`aria-invalid` видимыми, focus в HEX input, последнее
валидное parent value неизменным и не отправляет событие; после исправления
та же кнопка закрывает picker. Повторное «ОК» без нового валидного input также
не закрывает поверхность.
- **AC5 — прежние close paths (smoke).** Outside pointer, trigger и `Escape`
продолжают закрывать surface и сохраняют последнее валидное live-значение;
`Escape` не закрывает родительский dialog тем же нажатием.
- **AC6 — i18n и a11y (unit + smoke).** EN/RU/DE/FR имеют parity, production
instances получают per-card `confirm`, fallback даёт `OK`, видимый текст
является accessible name, Tab достигает кнопки после последнего field.
- **AC7 — touch/fallback/lifecycle (smoke + golden).** На narrow touch кнопка
имеет достаточную цель, tap потребляется surface; Popover API и forced
fallback дают один и тот же результат, disconnect не оставляет portal и
pending emit.
- **AC8 — совместимость, release и бюджеты (unit + docs gate + commands).** Нет
новых config/storage/backend полей и изменений event API; оба changelog
содержат пользовательскую запись, целевые docs/tests актуальны, bundle
проходит действующие initial/editor gzip budgets.
## 13. План автотестов
1. Расширить `test/color-picker.test.mjs`: обязательный `confirm` label,
defensive `OK`, native `type="button"`, последний DOM-control,
полноширинный/40 px CSS contract и неизменный event detail.
2. Расширить `demo/smoke_color_picker.mjs` реальными действиями:
- live изменить hue и opacity, запомнить число событий, нажать «ОК» и
доказать close/focus без дополнительного события;
- открыть снова, ввести невалидный HEX, нажать «ОК», проверить open/error/
focus/no-event, повторным «ОК» доказать отсутствие обхода, исправить и
закрыть;
- повторить color-only consumer;
- доказать сохранение outside/trigger/`Escape`.
3. Forced-fallback path проверить существующим smoke affordance либо узким
расширением color-picker smoke: portal удалён, нижележащий click не вызван.
4. Обновить существующие golden-сцены, уже покрывающие общую surface:
`decor-color-popover-mobile-ru`, `decor-color-popover-desktop-en`,
`general-color-popover-desktop-en`, `device-ripple-color-popover-mobile-ru`,
`space-room-color-popover-desktop-ru`. Вместе они доказывают RU/EN,
light/dark, desktop/touch, opacity/color-only. Не создавать дублирующую сцену
только ради той же кнопки.
5. Защитные мутанты: удалить click handler, повторно emit на confirm, закрыть
surface при invalid HEX, убрать stopPropagation, не передать новый label в
один из языков и сделать кнопку auto-width — соответствующие AC обязаны
покраснеть.
## 14. Release-артефакты
- Пользовательские записи со ссылкой на #476 в `docs/CHANGELOG.md` и
`docs/CHANGELOG.ru.md` в product-коммите.
- `docs/TESTING.md` актуализирует общий color-picker contract.
- Пять существующих color-picker golden обновляются из канонического Linux CI,
явно просматриваются и принимаются с `Baseline-Reviewed` по процессу.
- Документационные screenshots переснимаются/принимаются только если их
visual fingerprint объявлен устаревшим обычным gate; отдельного нового
пользовательского screenshot не требуется.
- Performance/security artifacts не добавляются: кнопка не входит в render
loop, не читает сеть и не меняет данные. Действующие bundle budget и
prerelease performance gates остаются обязательными.
## 15. Производительность и безопасность
Один статический button и один click handler добавляются только в уже открытый
lazy editor picker. Закрытая карточка, View, camera render, geometry и backend
не получают новой работы. Runtime dependency и сетевой запрос не добавляются.
Кнопка не выполняет HA action, не пишет config и не обходит parent Save/Cancel.
Потребление click/tap защищает от случайного действия под закрывающейся
поверхностью. Существующие CSP, permissions и privacy contract не меняются.
## 16. Риски
- **Случайно превратить live picker в transaction.** Снимается AC2 и прямой
проверкой parent draft до «ОК».
- **Повторно отправить последнее значение.** Снимается счётчиком событий до и
после confirm в AC2.
- **Закрыть surface с невалидным HEX.** Снимается единым validation result и
AC4, без проверки только CSS-класса.
- **Разойтись между Popover API и fallback.** Снимается одним `_pickerTemplate`
и реальными smoke обоих путей.
- **Провалить tap в план после удаления portal.** Снимается потреблением события
и sentinel-action в AC3/AC7.
- **Обрезать нижнюю кнопку на touch/zoom.** Снимается существующим viewport
placement, scroll flow и reviewed narrow golden.
- **Оставить один язык без кнопки.** Снимается type/i18n parity и AC6.
## 17. Откат
Feature flag не нужен: изменение локально для общей surface и не меняет данные.
Для отката удаляются button/handler, поле `confirm` и новый i18n key, а прежние
close paths и live event contract остаются рабочими. Родительские consumers,
config и backend откатывать не требуется.
Если после выпуска понадобится другая семантика подтверждения — например,
транзакционный draft с Cancel, — это отдельная продуктовая задача и миграция
interaction contract, а не скрытая правка обработчика #476.
## 18. Принятые предположения
Следующие технические решения предположительны и могут быть свободно изменены
ревьюером без нового продуктового решения владельца:
- рабочее имя нового поля — `ColorPickerLabels.confirm`, ключ —
`color_picker.confirm`;
- confirm вызывает существующий HEX commit helper и использует результат
валидации, после чего вызывает единый `_closePicker(true, ...)`;
- отдельный custom event `confirm` не нужен: родители уже получили live value;
- кнопка стилизуется внутри shadow DOM `hp-color-opacity` theme tokens без
зависимости от HA-private web components;
- тест fallback может быть частью существующего smoke, если это сохраняет
реальное browser proof и не дублирует весь сценарий.
+1
View File
@@ -187,6 +187,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#461](https://github.com/Matysh/houseplan-card/issues/461) Быстрый commit промежуточной точки цепочки стен | [461-wall-draw-click-performance.md](461-wall-draw-click-performance.md) |
| [#464](https://github.com/Matysh/houseplan-card/issues/464) Верхний контекстный слой Zigbee-топологии | [464-zigbee-topology-layer-order.md](464-zigbee-topology-layer-order.md) |
| [#473](https://github.com/Matysh/houseplan-card/issues/473) Свидетели перф-дельты #160 и диффозависимый перф-смок | [473-iso-perf-witnesses-and-smoke.md](473-iso-perf-witnesses-and-smoke.md) |
| [#476](https://github.com/Matysh/houseplan-card/issues/476) Явное завершение выбора цвета кнопкой «ОК» | [476-color-picker-ok.md](476-color-picker-ok.md) |
## P3