20 KiB
#180 — Единый picker во всех местах выбора цвета
Статус: готово к ревью ТЗ
Issue: #180
Связано: #57
1. Сценарий пользователя
Администратор настраивает цвета плана в общих настройках, свойствах пространства или диалоге устройства. В любом из этих мест он нажимает образец и получает один и тот же House Plan picker, уже знакомый по decor, custom fill и Glow.
2. Что человек увидит до и после
До: часть цветов открывается единым picker из #57, а оставшиеся поля вызывают системный color dialog; там, где есть прозрачность, она по-прежнему вынесена в отдельный slider рядом.
После: каждый образец цвета открывает одну и ту же поверхность House Plan. Если значение уже имеет прозрачность, цвет и opacity находятся внутри неё; если поле color-only, строка opacity отсутствует.
3. Проблема
#57 заменила вложенный native picker внутри hp-color-opacity, но не перевела
параллельные native call sites. В результате одинаковое действие зависит от
конкретного диалога, а общий компонент не является настоящей единственной точкой
выбора цвета.
На текущем dev осталось пять source-шаблонов <input type="color">, которые
представляют 15 пользовательских полей. Такое расхождение усложняет touch,
keyboard, темы, локализацию и дальнейшее сопровождение.
4. Инвентаризация и scope
В задачу входят все оставшиеся native call sites:
| Поверхность | Поля | Новый режим |
|---|---|---|
Общие настройки, _renderColorRow() |
11: light on/off/none; temp cold/ok/hot; LQI low/high; Glow base/light; wall fill | color + existing opacity |
| Общие настройки → статичный фон | 1 | color-only + прежний Default/theme |
| Устройство → activity/ripple color | 1 | color-only |
| Пространство → цвет комнат | 1 | color + existing room opacity |
| Пространство → статичный фон | 1 | color-only + прежний Inherit |
Дополнительно входят:
- переиспользование существующего
hp-color-opacityAPI и labels #57; - сохранение live draft, Save/Cancel, default/inherit и nullable semantics;
- запрет native
input[type=color]во всём продуктовом TypeScript; - keyboard/touch/theme/accessibility regression coverage;
- smoke и golden для затронутых dialog families;
- актуализация пользовательской и тестовой документации.
5. Не в scope
- redesign самого picker, HSV/RGB panels, presets, history или eyedropper;
- новое значение opacity для color-only полей;
- изменение палитр, default-цветов либо визуальных правил light/temp/LQI/Glow;
- изменение marker ripple animation, размера либо alarm semantics;
- изменение day/night background, наследования space или room fill algorithms;
- новый формат конфига, backend schema либо migration;
- массовая перестройка layout диалогов вне необходимой замены controls;
- полная touch parity родительских редакторов.
6. Общий interaction contract
Каждый из 15 controls использует существующий hp-color-opacity:
- click/tap/Enter/Space по swatch открывает единственную picker surface;
- внутри нет native
input[type=color]и второго системного dialog; - изменения сразу обновляют только draft текущего родительского диалога;
- Save сохраняет draft прежним путём, Cancel/close его отбрасывает;
- Escape сначала закрывает picker и возвращает focus на trigger, второй Escape принадлежит родительскому dialog;
- открытие другого transient overlay либо picker закрывает предыдущий;
- одновременно в одном dialog видима не более чем одна picker surface;
- disabled/busy parent не получает обходного write через picker.
Все interaction, floating surface, focus, viewport и pointer semantics остаются контрактом #57. #180 добавляет consumers, а не второй вариант компонента.
7. Контракт значений
7.1 Поля с opacity
_renderColorRow() и room color передают текущую пару {color, opacity} в
hp-color-opacity с showOpacity=true. Событие
hp-color-opacity-change: {color, opacity} атомарно обновляет оба draft-поля.
Существующие отдельные alpha slider и процент рядом удаляются: opacity доступна
внутри picker при первом открытии. Значение [0,1], шаг/округление и сохранение
остаются контрактом #57; открытие/закрытие без изменения ничего не округляет.
7.2 Color-only поля
Общий фон, space background и ripple используют showOpacity=false. Они не
получают alpha в UI, config или event handling; передаваемая техническая opacity
не сохраняется.
7.3 Default и inheritance
Nullable background сохраняет прежнюю модель:
- общий
bgColor=nullпоказывает эффективный theme/stage color и прежний текстTheme; выбор цвета создаёт explicit draft; - кнопка
Defaultснова ставитnull; - space
bgColor=nullпоказывает эффективный inherited/global color и прежний текстInherited; выбор создаёт explicit draft; - кнопка
Inheritснова ставитnull.
Picker не получает собственную reset/inherit команду. Действующие кнопки рядом с ним сохраняются и закрывают открытую surface через обычный parent update.
Пустой rippleColor означает действующий accent/default. Открытие picker без
изменения не материализует fallback; первое валидное изменение создаёт explicit
hex draft. Сброс ripple color в #180 не добавляется, потому что его не было в
текущем UI и владелец запросил унификацию picker, а не новый default-action.
8. Layout, theme и accessibility
- На каждой строке пользовательское название отображается ровно один раз; можно убрать внешний label либо внутренний label component, но trigger получает непустой локализованный accessible name.
- У 11 строк общих настроек не остаётся пустого места от удалённого alpha slider; swatch выравнивается с labels и соседними controls без горизонтального overflow.
- Смешанная marker-строка сохраняет ripple-size slider и его значение; picker trigger не уменьшает touch target и не перекрывает соседний control.
- Общий/space default-inherit button остаётся keyboard-доступным и не попадает внутрь floating surface.
- Light/dark theme, forced colors, 390 CSS px и 200% zoom используют уже проверенный visual contract #57.
- Picker остаётся локализован через
.pickerLabels=${this._colorPickerLabels}и.opacityLabel; hardcoded English в новых call sites запрещён.
9. Модель данных и совместимость
Новых полей нет. Без изменений сохраняются:
FillColors[key] = { c: #RRGGBB, a: number };- global
settings.bg_color?: #RRGGBB; marker.ripple_color?: #RRGGBB;space.settings.room_colorиroom_opacity;space.settings.bg_color?: #RRGGBB.
Backend, export/import, config compatibility registry и migration не меняются.
Старые значения открываются и сохраняются в прежнем формате. safeStoredColor
и normalizer #57 остаются единственной границей допустимого hex; #180 не вводит
новый parser.
10. i18n
Новых ключей не требуется. Переиспользуются существующие:
color_picker.*иspace.opacityдля общей picker surface;- текущие labels
gs.*,marker.activity_color,space.room_colorиspace.bg_colorдля конкретного trigger; - текущие Default/Theme/Inherit/Inherited strings.
EN/RU parity scanner должен остаться зелёным.
11. Touch
Touch editor: supported для самого picker; родительские редакторы остаются
best effort / intentionally degraded по docs/TOUCH-SUPPORT.md.
Tap по любому новому trigger открывает ту же surface #57; pointer capture,
multi-touch, pointercancel, viewport rotation и outside tap не создают
дополнительного parent write. Родительский Save/Cancel остаётся safety boundary.
12. Критерии приёмки
AC1 — полное source-покрытие
Все 15 полей из §4 используют hp-color-opacity; в src/**/*.ts отсутствует
input[type=color] в любой допустимой Lit-форме. Уже мигрированные consumers #57
продолжают использовать тот же component.
Доказательство: source-contract unit со scan всего src/**/*.ts, точным
перечнем пяти новых template instances и regression count/allowlist всех
component call sites.
AC2 — opacity перенесена внутрь единой surface
Каждая из 11 FillColors и room color открывает picker с текущим color/opacity; изменение любого из них обновляет правильный draft. Отдельные alpha slider и процент этих строк отсутствуют, но сохранённое значение совпадает с picker.
Доказательство: unit/source contracts + browser smoke общего dialog и space dialog, включая две разные FillColors keys и room opacity 0/37/100%.
AC3 — color-only не получает alpha
Ripple, global background и space background скрывают opacity row, меняют только hex color и не добавляют alpha в config/event projection.
Доказательство: DOM smoke showOpacity=false, save round-trip и negative
schema/source assertions.
AC4 — nullable/default/inherit остаются прежними
При bgColor=null trigger показывает effective color, но одно открытие/закрытие
не материализует его. Первое изменение создаёт explicit value; Default/Inherit
возвращает null. Parent Cancel не сохраняет ни один draft.
Доказательство: browser smoke global + space dialog с config snapshot до/ после open, change, reset и Cancel/Save.
AC5 — ripple и соседний size control независимы
Ripple picker меняет только rippleColor; ripple size slider остаётся видимым,
keyboard/touch-доступным и сохраняет прежнее значение. Пустой fallback не
материализуется без изменения.
Доказательство: marker-dialog browser smoke и config projection unit.
AC6 — один overlay, keyboard и touch
В dialog с 11 swatches открытие второго picker закрывает первый; Escape/focus, outside tap, pointercancel, 390px/200% zoom и fallback portal соответствуют #57. Ни один trigger не вызывает системный picker.
Доказательство: расширенный smoke_color_picker.mjs, включая general
settings multi-picker и representative mobile marker/space cases.
AC7 — visual/theme contract
Общие настройки, marker activity и space appearance не имеют layout overflow или двойных labels в light/dark themes; default/inherit и ripple-size controls остаются читаемыми.
Доказательство: reviewed Linux golden минимум для general settings desktop, marker activity mobile и space appearance в противоположных темах.
AC8 — совместимость и производительность
Persisted schema/values не меняются; новых dependencies/fetch нет. Открытие general settings с 11 component instances не создаёт long task и закрытый dialog не оставляет document listeners/overlays.
Доказательство: round-trip units, dependency diff, целевой smoke после close/reopen; existing performance smoke без нового regression threshold.
13. План автотестов
Unit/source
- Рекурсивный scan
src/**/*.tsзапрещает nativeinput[type=color]. - Все новые
hp-color-opacityпередают localizedpickerLabels. - Matrix фиксирует
showOpacity=trueтолько для FillColors/roomColor иfalseдля ripple/global-bg/space-bg. - Event handlers обновляют только ожидаемые draft keys и сохраняют alpha.
- Nullable effective color не материализуется без change event.
- Existing
color-picker.test.mjsconversions/API остаются зелёными.
Browser smoke
Расширить demo/smoke_color_picker.mjs либо добавить узкий companion, который
проверяет:
- две строки general FillColors, alpha внутри picker и переключение overlays;
- global background effective/default lifecycle;
- marker ripple color-only + неизменный size;
- space room color+opacity;
- space background inherited/explicit/inherit;
- Save/Cancel snapshots, keyboard/Escape, touch/pointercancel;
- native/fallback floating surface и отсутствие системного input.
Golden
- general settings с открытым picker, desktop light;
- marker activity picker, mobile dark;
- space appearance с room/background controls, desktop dark либо mobile light;
- все расхождения принимаются только по полному reviewed Linux artifact перед бетой; существующие baselines #57 не обновляются автоматически.
Реализационные гейты
В цикле: npm run typecheck, npm test, npm run build.
Перед S7-code-review: затронутый color-picker smoke и целевые golden capture/
verify. Полные golden, smoke и performance остаются pre-beta gate. Backend tests
не нужны, если diff действительно не затронет Python/schema.
14. Риски и меры
| Риск | Мера |
|---|---|
| Alpha теряется при замене двух controls одним | атомарный {color, opacity} handler + boundary tests |
| Inherited fallback случайно сохраняется explicit | отсутствие change при open/close + snapshot smoke |
| 11 pickers открываются одновременно | общий exclusive overlay controller #57 |
| Label дублируется или trigger остаётся без имени | ровно один visible label + accessible-name assertion |
| Marker row ломается из-за более крупного trigger | mobile golden + независимый size smoke |
| Native picker возвращается новым call site | recursive source scan, не count одного файла |
| Много component listeners остаётся после dialog | disconnect/reopen smoke и existing cleanup contract |
Производительность: одновременно существует максимум содержимое открытого родительского dialog; floating surface создаётся только у активного picker. Новых observers, subscriptions, animation loops и network requests нет.
15. Откат
Откат возвращает пять native templates и отдельные alpha slider. Поскольку модель данных и API сохранения не меняются, migration/downgrade действий нет. Сам picker #57 и уже существующие consumers остаются работоспособными.
16. Release-артефакты
User-visible implementation commit одновременно обновляет:
docs/CHANGELOG.mdиdocs/CHANGELOG.ru.mdсо ссылкой на #180;docs/USER-GUIDE.ru.md— единый picker во всех общих, space и marker color settings, включая opacity/default/inherit;docs/TESTING.md— source invariant, smoke matrix и golden scenarios;- синхронные
dist, integration frontend и demo bundle.
Перед бетой нужны reviewed golden artifacts для трёх новых dialog families и зелёный targeted smoke. Новых screenshots вне golden, security artifact, backend artifact или отдельный performance profile не требуется.
17. Принятые технические предположения
- Пять новых source instances
hp-color-opacity— один в_renderColorRow()и четыре прямых — являются предпочтительной структурой; helper можно выделить, если source-invariant и AC сохраняются. opacity=1у color-only consumer является presentation input и не должен попадать в parent draft/config.- Layout-классы можно уточнить локально; публичными считаются поведение, accessible names и отсутствие overflow, а не DOM nesting.
- Existing
hp-color-opacityне расширяется reset callback: default/inherit остаются parent-owned actions. - Точные golden scenario names и pixel thresholds выбираются по Linux artifact, но обязаны отдельно доказать general settings, marker и space surfaces.