Issue: #57 User-Visible: no
23 KiB
CODE-REVIEW-57-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/57
- ТЗ:
docs/specs/057-color-opacity-picker.md(ревьюdocs/reviews/SPEC-REVIEW-57-r1.md, вердикт — зелёный, Medium-1 вынесен в #180, Low-1 — устранён автором прозой §9) - Диапазон:
git diff origin/dev...HEAD/git log --oneline origin/dev..HEAD— коммитыffb1084(ТЗ),32e3a79(документ спек-ревью),299da59(реализация,Issue: #57,User-Visible: yes) - Роль: ревьюер кода (не автор), этап
S7-code-review - Цикл: r1/4
Скоуп ревью
Проверялось соответствие реализации:
- ТЗ
docs/specs/057-color-opacity-picker.md— контракту §7–§14 (единая поверхность, значения/преобразования, pointer/touch, keyboard/a11y, API/ migration, AC1–AC9); docs/SCOPE.md— задача не расширяет продукт за пределы editor usability polish;AGENTS.md/PROCESS.md— классы файлов, трейлеры, оба changelog в одномUser-Visible: yesкоммите, отсутствие новых runtime dependencies;- фактическому поведению
src/hp-color-opacity.ts,src/color-picker.ts,src/floating-surface-controller.ts, вызовам вsrc/houseplan-card.ts; - реально выполненным гейтам (см. ниже), включая один прогон golden-сьюта целиком, не только названного в ТЗ сценария.
Как проверялось
- Прочитан весь diff (
git diff origin/dev...HEAD --statи файлы по отдельности):src/hp-color-opacity.ts(412 изменённых строк), новыйsrc/color-picker.ts(80 строк, чистые функции конверсии), измененияsrc/houseplan-card.ts(8 call sites получили.pickerLabels=${this._colorPickerLabels}),src/i18n/{en,ru}.json(6 новых пар ключейcolor_picker.*),test/color-picker.test.mjs,demo/smoke_color_picker.mjs,demo/smoke_help_affordance.mjs(+3 строки),docs/{CHANGELOG.md,CHANGELOG.ru.md,USER-GUIDE.md,USER-GUIDE.ru.md,TESTING.md}. - Подтверждено построчным поиском
<hp-color-opacityвhouseplan-card.tsи тестом-сканом (test/color-picker.test.mjs:48-49), что все 8 call sites (decor stroke/fill/text/furniture, room/space custom fill, marker Glow) получили новый компонент и локализованныеpickerLabels— совпадает с §12 ТЗ и списком, ранее сверенным в SPEC-REVIEW-57-r1. - Прочитан
src/hp-color-opacity.tsцеликом: подтверждено отсутствие<input type="color">, единая.pickerповерхность (role="dialog",aria-label) рендерится синхронно с saturation/value полем, hue/saturation/value ranges, hex-полем и (приshowOpacity) alpha row+процентом — соответствует §7/§14 AC1 ТЗ дословно. - Прочитан
src/color-picker.ts:normalizeHexColorпринимает 3/6-значный hex с необязательным#,rgbToHsv/hsvToRgb/hsvToHex— конечные, clamped функции без побочных эффектов — соответствует §8 ТЗ. - Прочитан
FloatingSurfaceController(src/floating-surface-controller.ts) и его использование вhp-color-opacity.ts(_floating,registerOverlay,_renderFallback,_supportsPopover) — подтверждает заявленное в ТЗ переиспользование общей инфраструктуры #68, а не параллельную реализацию. - Прочитан весь текст issue #57 и всех комментариев (
gh issue view 57 --comments): исходный запрос, аналитика с Q1, решение владельца по Q1 («один клик — одна поверхность с цветом и прозрачностью, без вложенного system picker»), финальный хендофф автора с цифрами бандла и списком выполненных проверок. Реализация воспроизводит решение владельца дословно (см. «Что проверено и корректно»). - Прогнаны все обязательные дешёвые гейты и часть гейтов «по необходимости»
(перечень и результаты — раздел «Гейты» ниже), включая полный
npm run golden:verify— diff меняет видимый рендер во всех диалогах, использующихhp-color-opacity, что подпадает под критерий «геометрия/ стили/слои» из инструкции к этому ревью. - Для каждого теста, который прогонялся, целенаправленно сломан соответствующий кусок продукта и подтверждено, что тест падает, затем код восстановлен и гейты перепрогнаны на чистом дереве (см. «Гейты»).
- Просмотрены PNG-артефакты (
artifacts/golden/actual/*,artifacts/golden/diff/*) для всех пяти сценариев, показавшихdifferent, чтобы отличить реальную визуальную регрессию от ожидаемого следствия новой поверхности (см. находку Low-1). - Проверены трейлеры и class-принадлежность:
git show 299da593 --stat— класс A/B/C/D-файлы согласованы (продукт + smoke/test + документация + все три копии бандла в одном коммите),Issue: #57,User-Visible: yes, оба changelog в том же коммите. - Проверено отсутствие новых runtime dependencies:
git diff origin/dev...HEAD -- package.json package-lock.json— пусто. - Проверено, что Medium-1 из SPEC-REVIEW-57-r1 действительно заведён
отдельным issue:
gh issue view 180существует, меткиP3/tech-debt/S1-new, ссылается на #57.
Обязательные гейты (всегда)
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
чисто, без вывода |
| Unit-тесты | npm test |
884/884, 0 fail |
| Build + сверка бандлов | npm run build + sha256sum dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js demo/srv/assets/houseplan-card.js |
все три идентичны, 6d7e2b02aee1a3af822f1f634209a0fd62e1348988b87c7a1a4eff0f08fa7490 — совпадает (без учёта регистра) с заявленным автором в хендоффе |
Falsifiability (тест должен уметь падать), применена к обоим прогнанным наборам:
- временно вставлен
<input type="color">вhp-color-opacity.ts→test/color-picker.test.mjsтест «contains no nested native color picker» корректно упал (not ok 50); код восстановлен,npm testснова 884/884. - временно превращён
_setHueв no-op →demo/smoke_color_picker.mjsкорректно провалилhueUpdatesSharedDraftиshiftArrowUsesTenStep; код восстановлен, бандл пересобран, три копии совпадают с исходным SHA-256,git statusчист.
Bundle budget (AC8): raw 1 107 696 → 1 119 200 (+11 504 B, совпадает с
заявленным автором), gzip (gzip -9) 304 081 → 306 755 (+2 674 B) —
близко к заявленным автором +2 728 B (разница из-за конкретного
gzip-инструмента/уровня в отчёте, не в самом бандле); в любом случае это
на порядок меньше бюджета ТЗ ≤15 KiB gzip. Runtime dependencies не
добавлены (package.json/package-lock.json не изменены).
Гейты по необходимости
| Гейт | Почему запускался | Результат |
|---|---|---|
node demo/smoke_color_picker.mjs |
назван в AC1–AC5 ТЗ как способ доказательства | зелёный, все 14 проверок true; falsifiability подтверждена (см. выше) |
node demo/smoke_help_affordance.mjs |
тронут (+3 строки) для проверки color-only Glow и forced-fallback exclusivity с hp-help, назван в AC6/§15 ТЗ |
зелёный, все проверки true, включая fallbackPickerFocusable (forced-fallback ветка для пикера) и glowColorOnlyHasNoAlpha |
npm run golden:verify (полный набор, 66 активных сценариев в текущей матрице) |
diff меняет видимый рендер (новая поверхность вместо input[type=color]) во всех диалогах, использующих hp-color-opacity, — подпадает под критерий «рендер/геометрия/стили» из инструкции к ревью, а не только названный в ТЗ сценарий |
61 passed, 5 different — все пять разобраны построчно, см. находку Low-1 |
node scripts/check-docs.mjs --external |
дёшево, диф трогает 4 файла документации | зелёный, «7 files, 10 external links» |
node scripts/process-gate.mjs --issues |
дёшево, проверяет трейлеры/структуру коммитов диапазона | зелёный, «гейт пройден, предупреждений 0» |
Что проверено и корректно
- Главный контракт владельца по Q1 воспроизведён точно. Один
click/tap/Enter/Space по
.triggerоткрывает единственную.pickerповерхность; внутри нет<input type="color">и второго dialog — прямым чтением кода и тестом-сканом, а не только smoke. - Все 8 call sites обновлены единообразно и не получили адаптеров: decor
stroke/fill/text/furniture, room/space custom fill, marker Glow — тот же
список, что был подтверждён на этапе ТЗ.
showOpacity=falseдля Glow по-прежнему скрывает только alpha-row (ranges().length === 3в smoke). - API и данные не расширены.
color/opacity/showOpacity/disabledи событиеhp-color-opacity-changeс прежней формой{ color, opacity }— без изменений сигнатуры; ни один файл backend/types.ts/custom_componentsне тронут (AC9 — подтвержденоgit diff --stat). - Round-trip и валидация значений (AC3) — table-тест по всему цветовому
кубу подтверждает допуск ≤1 канал; невалидный hex-драфт никогда не
эмитится (
invalidHexNeverEmitsв smoke), commit восстанавливает последнее валидное значение — прочитано в_commitHexи подтверждено выполнением. - Touch/pointer (AC4) —
setPointerCapture/releasePointerCapture,pointercancel/lostpointercaptureоткатывают черновик без нового события (pointerCancelAddsNoEventв smoke); второй одновременный pointer игнорируется (_activePointerId !== nullguard) — проверено чтением кода, не отдельным multi-touch smoke (см. «Чего не проверял»). - Keyboard/a11y (AC5) — все четыре ranges имеют
aria-label/aria-valuetext,step=1даёт шаг 1,Shift+Arrowдаёт шаг 10 (shiftArrowUsesTenStepв smoke); hex-поле — подписанный text input сaria-invalid/aria-describedbyбез live-спама по символам; Escape закрывает picker и возвращает фокус на trigger без закрытия диалога (escapeClosesFirstAndRefocuses); focusable-состав включает pickerButton в фокус-трапе диалога и в forced-fallback ветке (shadowFocusables,fallbackPickerFocusable). - Floating lifecycle (AC6) — общий
FloatingSurfaceControllerиспользуется без дублирования логики #68; forced-fallback ветка (когдаshowPopover/hidePopoverнедоступны) реально рендерит picker в теневой портал и остаётся focusable — подтверждено выполнениемsmoke_help_affordance.mjs, не только чтением. - i18n — новые ключи
color_picker.{title,hue,saturation,value,hex, invalid_hex}добавлены синхронно вen.json/ru.json; компонент остаётся presentation-only (pickerLabels— свойство, не читаетhass.localeнапрямую) —cardLanguageOwnsCopyв smoke подтверждает изоляцию поconfig.languageна уровне карточки. - Trailers/классы файлов/changelog — коммит
299da59несётIssue: #57,User-Visible: yes; оба changelog (docs/CHANGELOG.md,docs/CHANGELOG.ru.md) редактируются в этом же коммите; ни новых runtime dependencies, ни измененийpackage.json. - Medium-1 из SPEC-REVIEW-57-r1 закрыт корректно — issue #180 существует, правильно размечен и ссылается на #57; реализация не пытается тайно расширить или сузить скоуп относительно принятого ТЗ.
- Bundle budget (AC8) — независимо пересобранный бандл даёт тот же
SHA-256 и raw-дельту, что заявил автор; gzip-дельта на порядок меньше
лимита
≤15 KiBнезависимо от точного инструмента измерения.
Находки
Low-1 — реализация меняет визуальный рендер ещё в 4 golden-сценариях, не названных нигде в ТЗ/TESTING.md как ожидающие ревью
Файлы: docs/TESTING.md:69-89 (раздел «Unified color and opacity picker
(#57)», называет только decor-color-popover-mobile-ru); фактически
затронуты также device-dialog-desktop-en, device-dialog-mobile-ru,
device-help-popover-light-ru, tray-narrow-tool-ru.
Полный прогон npm run golden:verify (66 активных сценариев в текущей
матрице) даёт 5 different, не 1: помимо названного в TESTING.md
decor-color-popover-mobile-ru, отличаются ещё device-dialog-desktop-en,
device-dialog-mobile-ru (marker Glow trigger — новый 40×40 swatch/picker
вместо старого layout), device-help-popover-light-ru (тот же Glow trigger в
соседстве с hp-help) и tray-narrow-tool-ru (decor color trigger в узкой
панели инструментов). Просмотр artifacts/golden/actual/*.png для всех пяти
подтверждает: рендер корректен, не обрезан, не сломан — разница целиком
объясняется новым размером/видом trigger-кнопки (checkerboard 40×40)
против прежнего инлайн input[type=color] + range, что каскадно сдвигает
контент диалога на 12–20 CSS px вниз. Это ожидаемое и безопасное следствие
явно принятого технического решения (§6 ТЗ), не дефект поведения.
Почему это находка, а не просто ожидаемое поведение. docs/TESTING.md
(единственное место, которое перечисляет пре-бета golden-обязательства этой
задачи) называет только один сценарий. Тот, кто перед бетой примет решение
golden:accept только по названному списку, рискует пропустить, что
реальный список отличающихся baseline — впятеро шире. Пре-бета гейт (полный
golden:verify) всё равно покажет все 5 расхождений и не даст принять
неполный набор молча — риск чисто в неполноте самодокументирования задачи,
не в скрытом от процесса дефекте.
Решение ревьюера: Low, не блокирует S8-merged. Не требует отдельного
issue — это не новая функциональность и не дефект поведения, а неполнота
одной строки в docs/TESTING.md, целиком описывающей последствия этой же
задачи. Оставляю с этой записью, правка необязательна: golden:verify
перед бетой в любом случае предъявит все 5 сценариев ревьюеру беты, а не
только названный.
Чего не проверял
- Multi-touch (второй одновременный pointer) как отдельный smoke — §9
ТЗ требует, чтобы второй pointer «отменял первый либо игнорировался
безопасно»;
demo/smoke_color_picker.mjsпроверяет толькоpointercancelодного pointer. Ветка «второй pointer игнорируется» (_svPointerDown:if (this._activePointerId !== null) return;) проверена чтением кода, не исполнением — как и просил бы обычный, не light-track процесс при отсутствии реального multi-touch harness в окружении ревью. - 200% браузерный zoom на 390 CSS px (AC7/§10 ТЗ) — ни один smoke, ни
один golden-сценарий (
demo/golden/matrix.mjs) не эмулирует page-level 200% zoom; существующиеzoom: 0.4/2.5в матрице — это масштаб плана внутри канваса, не zoom страницы. CSS-контракт (width: min(292px, calc(100vw - 16px)),overflow: auto) читает как корректный, но это не исполненное доказательство для этой конкретной строки AC7 — фиксирую честно, а не выдаю чтение кода за тест. Не блокирует: остальные AC7-грани (light/dark, checkerboard, отсутствие прыжка layout при disabled) подтверждены выполнением/чтением. golden:acceptне запускал и не должен — принятие новых baseline (для всех 5 «different», не только названного) принадлежит пре-бета этапу (PROCESS.md,npm run golden:accept -- --reviewedтолько на полном Linux CI-артефакте), не циклу код-ревью.python -m pytest tests_backend -q— не запускал: diff не трогает ни одного файлаcustom_components/**/*.py(подтвержденоgit diff --stat), гейт неприменим.- performance-профили (
benchmark:*) — не запускал: AC явно не называет производительность как критерий (только «no new long task» в §15 плана автотестов, не в нумерованных AC1–9), diff не трогает canvas/ рендер-цикл плана — rAF-коалессинг в pointermove-пути прочитан в коде (_queueHsvEmit) и оценивается как корректный по конструкции, но не измерен профайлером. - Полный построчный обзор
src/houseplan-card.ts(~18500 строк) на предмет иных мест, не относящихся к call siteshp-color-opacity— не требуется этим циклом: контракт ТЗ (после Medium-1 → #180) явно ограничен уже перечисленным списком, иtest/color-picker.test.mjs:48формально фиксирует ожидаемое число (8) вызовов компонента.
Вердикт
Зелёный. High: 0, Medium: 0, Low: 1 (не блокирует, запись оставлена в этом
документе, правка необязательна — см. Low-1). Реализация корректно и
проверяемо закрывает контракт ТЗ и решение владельца по Q1: единая
поверхность цвета и прозрачности без вложенного системного picker, на всех
восьми существующих call sites, без изменения API/схемы/данных и без новых
runtime dependencies, в рамках заявленного бюджета bundle size. Все
обязательные гейты и относящиеся к тронутым поверхностям smoke/golden гейты
выполнены лично ревьюером (не переиспользована декларация автора), включая
собственноручную проверку «тест умеет падать» для одного unit- и одного
browser-smoke теста. Единственная находка — неполнота одной строки в
docs/TESTING.md относительно фактического числа golden-сценариев,
меняющихся вместе с этой задачей — не блокирует, поскольку пре-бета гейт
сам по себе предъявит полный список расхождений до golden:accept.