Files
houseplan-card/docs/reviews/CODE-REVIEW-180-r1.md
T
2026-08-19 08:52:44 +00:00

90 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Код-ревью #180 — r1
Issue: [#180](https://github.com/Matysh/houseplan-card/issues/180)
ТЗ: `docs/specs/180-all-color-call-sites.md`, ревью ТЗ: `docs/reviews/SPEC-REVIEW-180-r1.md` (зелёное, High 0/Medium 0)
Диапазон: `git diff origin/dev...HEAD` (коммиты `3540d24`, `f69ac71`, `fcee724`), реализация — `fcee724`
Вердикт: **красный** · цикл r1/4 · High: 1 · Medium: 0 → нет
## Скоуп
#180 переводит пять оставшихся source-шаблонов `<input type="color">`
(15 полей: 11 палитр общих настроек, глобальный статичный фон, marker
activity/ripple color, цвет комнаты пространства, фон пространства) на общий
`hp-color-opacity` из #57. Модель данных, backend, i18n-ключи не меняются.
Диапазон правок: `src/houseplan-card.ts`, `src/styles.ts`,
`test/color-picker.test.mjs`, `test/golden-matrix.test.mjs`,
`demo/golden/harness.mjs`, `demo/golden/matrix.mjs`,
`demo/smoke_color_picker_consumers.mjs` (новый), три копии бандла,
`docs/CHANGELOG.md`/`.ru.md`, `docs/USER-GUIDE.md`/`.ru.md`, `docs/TESTING.md`.
## Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | зелёный |
| Unit | `npm test` | 896/896 зелёный |
| Build + сверка бандлов | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js && cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js` | зелёный, `git status` после сборки — пусто (закоммиченные копии уже актуальны) |
| Целевой smoke (новый, AC1-AC5) | `node demo/smoke_color_picker_consumers.mjs` | зелёный, все 11 проверок `true` |
| Целевой smoke (существующий, общий контракт #57) | `node demo/smoke_color_picker.mjs` | зелёный, все 16 проверок `true` |
| Дисциплина «тест умеет падать» | мутация `showOpacity` ripple-picker с `false`→`true`, пересборка, повторный прогон `smoke_color_picker_consumers.mjs`; исходник и бандлы возвращены на место сразу после | `rippleUsesColorOnlyPicker` упал в `false` как и ожидалось → тест реально способен обнаружить регресс. Дерево восстановлено, `git status` пусто |
| Целевой golden capture (AC7, три новых dialog family) | `npm run golden:capture -- --scenario=general-color-popover-desktop-en` / `--scenario=device-ripple-color-popover-mobile-ru` / `--scenario=space-room-color-popover-desktop-ru` | все три рендерятся без runtime-ошибки (`missing-baseline` — ожидаемо для новых сценариев, эталоны не приняты и не должны быть); визуальный осмотр `artifacts/golden/actual/*.png` **вскрыл High-находку ниже** |
| Ручная DOM-проверка ripple-row (391px и RU/EN) | Playwright-скрипт, `getBoundingClientRect()` на `.label` пикера и соседнем `.opl` | подтвердил геометрическое перекрытие независимо от открытого/закрытого picker popover |
| `golden:verify` (полный) | не прогонялся | не нужен для ревью: сравнение с эталонами для новых сценариев невозможно (`missing-baseline` по дизайну — эталоны принимаются только через `golden:accept --reviewed` на Linux CI, это pre-beta шаг), а существующие сценарии, которые могла затронуть правка `styles.ts`, использую другой CSS-путь (`.editor-secondary`, `.gsrow`), затронутых регрессий не нашёл при точечном чтении |
| `python -m pytest tests_backend -q` | не прогонялся | диапазон не трогает `custom_components/**/*.py` |
| Performance-профили | не прогонялись | ни AC, ни diff не называют performance-чувствительный путь; спека §14 явно фиксирует «новых observers/подписок/network нет» |
| Полный набор `demo/smoke_*.mjs` (127 шт.) | не прогонялся | диапазон касается только color-picker consumers; остальные 125 смоков не относятся к затронутым поверхностям (PROCESS.md §8) |
## Находки
### High-1 — перекрывающиеся, нечитаемые подписи в строке "Цвет пульсации / Размер пульсации" на узком экране
**Файл:** `src/houseplan-card.ts:19334-19343` (шаблон), корень — `src/hp-color-opacity.ts:73-83` (`:host { display: inline-flex; }`, `.label { white-space: nowrap; }`)
**Что не так:** до #180 обе подписи в строке marker-ripple (`Активность`/`Размер пульсации`) рендерились как обычные `<span class="opl">` без `white-space: nowrap`, поэтому при недостатке ширины они просто переносились по словам внутри своего flex-элемента. После миграции подпись цвета переехала внутрь `hp-color-opacity`, чей host-стиль жёстко фиксирует `white-space: nowrap` на `.label`. В строке `.colorrow` (flex, `nowrap`, без `min-width:0` на элементах) это не переносит текст, а позволяет ему визуально вылезти за пределы выделенной flex-полосы и лечь на соседний `<span class="opl">` — тот в это время как раз ужимается и переносится в несколько строк на своей урезанной колонке.
Подтверждено чтением и исполнением:
- Скриншот `device-ripple-color-popover-mobile-ru` (390×1000, RU, dark) — сценарий, который сам #180 завёл именно для проверки AC7 на этой поверхности — показывает буквальное наложение текста «Цвет пульсации активности» и «Размер пульсации активности» друг на друга (см. `artifacts/golden/actual/device-ripple-color-popover-mobile-ru.png`, не коммитится, воспроизводится локально командами ниже).
- Прямое измерение `getBoundingClientRect()` (Playwright, диалог устройства, `display: 'icon_ripple'`, viewport 390×1000):
- RU: `label` (цвет) `x: 17..171` (ширина 154, при этом текст `nowrap` — реальная отрисовка шире бокса), `opl` (размер) начинается в `x: 101.875` — то есть контейнер второй подписи стартует **внутри** горизонтального диапазона первой;
- EN (короче текст, тот же экран): `label` `x: 17..116.4`, `opl` начинается в `x: 110.7` — то есть даже на английском перекрытие есть, просто на ~6px, а не на десятки.
- Причина локализована: то же самое поведение `_renderColorRow()`/`.gsrow`-строк не ломается, потому что там `hp-color-opacity` — единственный текстовый элемент строки; выделенная under CSS-правкой `.gsrow > hp-color-opacity { min-width: ...; justify-content: space-between; }` защита относится только к `.gsrow`, а строка ripple использует простой `.colorrow` без этой защиты и без own min-width на соседнем `.opl`.
**Как воспроизвести:**
```bash
npm run build && cp dist/houseplan-card.js demo/srv/assets/houseplan-card.js
npm run golden:capture -- --scenario=device-ripple-color-popover-mobile-ru
# открыть artifacts/golden/actual/device-ripple-color-popover-mobile-ru.png
```
или через прямой DOM-замер: открыть `hp-dialog` устройства с `display: 'icon_ripple'` на viewport 390px, сравнить `getBoundingClientRect()` подписи `hp-color-opacity.label` (shadow DOM) и соседнего `span.opl`.
**Почему это блокирует:** сам AC7 ТЗ #180 требует именно для этой поверхности («marker activity mobile») отсутствия «double labels» и читаемости `ripple-size` control; golden-сценарий `device-ripple-color-popover-mobile-ru` был добавлен в этой же задаче специально для доказательства этого пункта, но реальный рендер ему не соответствует. Хендофф-комментарий сам признаёт, что «browser smoke and golden capture were authored but not executed» — целевой golden не был прогнан автором перед переводом в код-ревью, поэтому дефект не был замечен раньше. Это доказанный чтением+исполнением сбой конкретного, пронумерованного AC — по §2.7/§3(правило 8) High блокирует.
**Не входит в блокировку:** state-модель (цвет/размер/альфа) не задета — `rippleChangeLeavesSizeAndAlphaModelAlone` в смоке зелёный, независимость `rippleSize` от цвета подтверждена. Проблема исключительно визуальная/layout.
## Что проверено и корректно
- **AC1 (полное source-покрытие).** `node --test test/color-picker.test.mjs` (входит в `npm test`) рекурсивно сканирует весь `src/**/*.ts` на `input[type=color]` — подтверждено чтением скрипта сканирования (использует `readdirSync` рекурсивно от `src/`, не только два файла). `grep` подтвердил 0 вхождений `type="color"` в `src/**/*.ts`. Счётчик `<hp-color-opacity` — 13 (8 существующих + 5 новых template instances), совпадает с инвентаризацией ТЗ §4/§17-1.
- **AC2 (opacity внутри единой surface для 11 палитр + room color).** `showOpacity=${true}` встречается ровно 2 раза в источнике (общий `_renderColorRow()` template, room color) — соответствует ТЗ §7.1. Смок `generalColorAndOpacityStayAtomic` и `roomColorAndOpacityStayAtomic` подтверждают атомарное обновление `{color, opacity}` в правильный draft без побочных изменений соседних полей (`temp_cold` не тронут при правке `light_on`).
- **AC3 (color-only без alpha).** `showOpacity=${false}` — 4 раза (Glow — уже из #57, global bg, ripple, space bg). Событие `hp-color-opacity-change` всегда несёт `{color, opacity}` (`src/hp-color-opacity.ts:693-695`), но все три новых color-only handler деструктурируют только `e.detail.color` — прочитано в коде и подтверждено смоками `rippleChangeLeavesSizeAndAlphaModelAlone` (`!Object.hasOwn(..., 'rippleOpacity')`), `globalBackgroundIgnoresAlphaAndResets`, `spaceBackgroundIgnoresAlphaAndCancelPersistsNothing`.
- **AC4 (nullable/default/inherit).** Смоком подтверждено: открытие/закрытие global bg picker без изменения не материализует `null→explicit` (`globalBackgroundOpenCloseDoesNotMaterialize`); explicit-значение корректно откатывается кнопкой `Default`/`Inherit` в `null` (`globalBackgroundIgnoresAlphaAndResets`, `spaceBackgroundIgnoresAlphaAndCancelPersistsNothing`), причём второй тест также подтверждает, что закрытие space-диалога без Save не сохраняет черновик в `_serverCfg`.
- **AC5 (ripple/size независимость), кроме визуального сбоя выше.** State-уровень подтверждён смоком; `rippleSize` не меняется при правке цвета.
- **AC6 (один overlay).** Смок `generalPickersAreExclusive` подтверждает, что открытие второго picker (`tempCold`) закрывает первый (`lightOn`, `aria-expanded=false`) в строке из 12 инстансов. Общий keyboard/Escape/pointercancel/zoom-контракт компонента не тронут диапазоном (сам `hp-color-opacity.ts` не изменён логически, только новые consumers) и остаётся покрыт существующим `smoke_color_picker.mjs`, прогнанным зелёным.
- **AC8 (совместимость).** Модель данных (`FillColors`, `bg_color`, `ripple_color`, `room_color`/`room_opacity`) не изменена — прочитано в диффе, новых полей/ключей нет. `npm test` зелёный (включая i18n EN/RU parity, unaffected — новых ключей нет). Новых зависимостей/сетевых вызовов в диффе нет.
- **Трейлеры и changelog.** Коммит `fcee724`: `Issue: #180`, `User-Visible: yes`; `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` правятся в этом же коммите, формулировки соответствуют факту диффа (color-only поля не получают alpha, Default/Inherited не меняются).
- **Терминология.** `docs/USER-GUIDE.md`/`.ru.md` обновлены в терминах, которые уже использовались для #57 (тот же picker, Default/Inherited), новых терминов не введено.
- **Waived Low из ревью ТЗ.** Автор сдержал обещание из хендоффа: мёртвый CSS-селектор `.colorrow input[type='color']` удалён вместе с последним native input (`src/styles.ts`), подтверждено диффом.
- **Инфраструктурные правки (golden matrix/harness, smoke).** Прочитаны построчно: `demo/golden/harness.mjs` добавляет три новых `dialog` case, каждый ищет конкретный `hp-color-opacity` по `.label` и клика по `.trigger` — соответствует существующему паттерну `decor-color`. `GOLDEN_MATRIX_VERSION` инкрементирован (28→29) вместе с добавлением новых сценариев — обязательное правило `demo/golden/README.md` выполнено.
## Чего не проверял
- **`golden:verify` (полный прогон, включая существующие baseline-сравнения).** Не прогонял: для трёх новых сценариев эталонов нет по дизайну (`missing-baseline` — ожидаемый результат до `golden:accept --reviewed` на Linux CI), а полный набор — pre-beta гейт (PROCESS.md §8). Точечно прочитал изменённый `src/styles.ts` и убедился, что тронутое правило (`.gsrow > hp-color-opacity`) не пересекается с селекторами существующих golden-сценариев (`.editor-secondary hp-color-opacity` у decor); остаётся вероятность более отдалённого регресса, который увидит только полный pre-beta прогон.
- **`python -m pytest tests_backend -q`.** Не запускал — диапазон не касается `custom_components/**/*.py`.
- **Performance-профили.** Не запускал — ни AC, ни diff не называют performance-чувствительный путь.
- **Остальные 125 browser-смоков.** Не прогонялись — диапазон касается только color-picker consumers; смоки других поверхностей (стены, солнце, canvas и т.д.) не пересекаются с диффом.
- **Touch/pointer-специфичное поведение новых call sites (реальное multi-touch, а не эмулированное `pointercancel` в существующем `smoke_color_picker.mjs`).** Компонент не менялся логически, эмулированный прогон на decor-сайте пройден; отдельный touch-смок для новых пяти call sites не заводился ни автором, ни мной.
- **Полная accessible-name проверка (screen reader) для новых 15 полей** — проверено только через `aria-label`/`aria-expanded` в смоках и структурное чтение шаблонов; полноценного aXe-прогона не было (вне обычного объёма гейтов ревью).
## Рекомендация
Вернуть в `S6-in-progress`. Минимально необходимая правка — дать `.label` внутри `hp-color-opacity`, либо соседним `.opl`, возможность переноса/сжатия в контексте `.colorrow` (например, `min-width: 0` на flex-элементах строки и отказ от `white-space: nowrap` там, где рядом есть второй текстовый label), и подтвердить фактическим `golden:capture` для `device-ripple-color-popover-mobile-ru` перед повторным ревью — то есть выполнить собственный пункт §13 ТЗ, который в этом цикле был заявлен, но не исполнен до код-ревью.