docs: specify unified color picker coverage

Issue: #180
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-19 11:22:36 +03:00
parent 25ea8fefab
commit 3540d24f18
2 changed files with 334 additions and 0 deletions
+333
View File
@@ -0,0 +1,333 @@
# #180 — Единый picker во всех местах выбора цвета
Статус: готово к ревью ТЗ
Issue: [#180](https://github.com/Matysh/houseplan-card/issues/180)
Связано: [#57](https://github.com/Matysh/houseplan-card/issues/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-opacity` API и 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
1. Рекурсивный scan `src/**/*.ts` запрещает native `input[type=color]`.
2. Все новые `hp-color-opacity` передают localized `pickerLabels`.
3. Matrix фиксирует `showOpacity=true` только для FillColors/roomColor и `false`
для ripple/global-bg/space-bg.
4. Event handlers обновляют только ожидаемые draft keys и сохраняют alpha.
5. Nullable effective color не материализуется без change event.
6. Existing `color-picker.test.mjs` conversions/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. Принятые технические предположения
1. Пять новых source instances `hp-color-opacity` — один в
`_renderColorRow()` и четыре прямых — являются предпочтительной структурой;
helper можно выделить, если source-invariant и AC сохраняются.
2. `opacity=1` у color-only consumer является presentation input и не должен
попадать в parent draft/config.
3. Layout-классы можно уточнить локально; публичными считаются поведение,
accessible names и отсутствие overflow, а не DOM nesting.
4. Existing `hp-color-opacity` не расширяется reset callback: default/inherit
остаются parent-owned actions.
5. Точные golden scenario names и pixel thresholds выбираются по Linux artifact,
но обязаны отдельно доказать general settings, marker и space surfaces.
+1
View File
@@ -111,6 +111,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| Issue | ТЗ |
|---|---|
| [#57](https://github.com/Matysh/houseplan-card/issues/57) Единый выбор цвета и прозрачности | [057-color-opacity-picker.md](057-color-opacity-picker.md) |
| [#180](https://github.com/Matysh/houseplan-card/issues/180) Единый picker во всех местах выбора цвета | [180-all-color-call-sites.md](180-all-color-call-sites.md) |
## Правило актуализации