Files
houseplan-card/legacy/specs/021-color-css-injection.md
Claudeandclaude[bot] df46fd1c3e docs(hygiene): ТЗ выпущенных задач без живых ссылок — в legacy/specs (#682)
Волна 5 эпика #674, перенос ТЗ (класс C). Из 240 файлов `docs/specs/` в
`legacy/specs/` уехали 219: на них не ссылается ни один живой файл (код,
тесты, скрипты, workflow, документы вне архива и ревью). Остались 21 ТЗ —
на которые ссылаются код, ADR, ISOMETRIC, SUN, RADAR, LIGHT (`docs/specs/067`),
DECOR-EDITOR, support-relay, и те, на которые ссылаются они сами; README
каталога объясняет, где искать остальное. Открытых issue с файлом ТЗ среди
перенесённых нет. Относительные ссылки перенесённых файлов переписаны
(`../X` → `../../docs/X`, соседние оставшиеся ТЗ → `../../docs/specs/…`) —
все 26 резолвятся. Попутно: битая ссылка в
`089-isometric-view-stage1.md:8` на удалённый `089-isometric-view.md` —
теперь команда `git show` по истории. Строка в `legacy/README.md`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:46 +00:00

5.6 KiB

ТЗ #21 — безопасные пользовательские цвета

  • Issue: https://github.com/Matysh/houseplan-card/issues/21
  • Приоритет: P2
  • Статус: реализовано; целевой gate v1.61.0-beta.1 пройден, ожидается публикация
  • Блокирует цветовые части #50 и #56; SVG sanitization из #51 остаётся отдельной задачей

Проблема

Новые записи проходят backend-валидацию, но старый, импортированный или вручную изменённый store может попасть во frontend без повторной проверки. Подтверждённый разрыв: marker.ripple_color доходил до CSS custom property как часть строки inline-style. Lit не позволяет выйти из HTML-атрибута, но строка вида red;position:fixed могла добавить соседние CSS declarations.

Единый контракт

Все сохраняемые цвета Houseplan имеют только вид #RRGGBB:

  • ровно # и шесть шестнадцатеричных цифр;
  • регистр букв любой;
  • без пробелов, короткого #RGB, имени цвета, rgb(), var(), url() и других CSS-функций;
  • неверное значение не исправляется и не записывается автоматически: при чтении используется безопасный штатный fallback.

Это соответствует уже существующим color picker и backend-схеме. Расширять форматы цвета в рамках #21 не требуется.

Реализация

Frontend

Один pure helper safeStoredColor(value, fallback) является общей границей для всех цветов из сохранённой конфигурации:

  • цвет и фон пространства;
  • глобальный фон и палитра заливок, включая стены и Glow;
  • обводка и заливка декоративных объектов и текста;
  • marker.ripple_color;
  • компонент выбора цвета.

Проверка выполняется при проекции данных для рендера, поэтому защищает и старый store, записанный до этого правила. Невалидный пользовательский ripple не должен перекрывать безопасный динамический цвет лампы.

HA может отдавать rgb_color. Это не сохраняемый пользовательский цвет: приложение принимает три конечных числа, ограничивает каждый канал диапазоном 0–255, округляет и само создаёт каноническую строку rgb(R, G, B). На последней границе inline-style разрешены только строгий #RRGGBB и именно такой сгенерированный rgb().

Backend

Все поля цвета используют один _COLOR = ^#[0-9a-fA-F]{6}$. Поведение API не меняется: корректный hex принимается, остальные формы отклоняются.

Совместимость

  • валидные планы визуально не меняются;
  • невалидный старый цвет заменяется только в runtime на штатный fallback;
  • исходный config не мутируется при чтении;
  • при следующем обычном редактировании color picker показывает безопасный fallback, а сохранение записывает валидный цвет.

Обязательные проверки перед пре-релизом

  1. Unit corpus: валидный mixed-case hex; short hex, whitespace, named/rgb, ;, braces, comment, backslash, newline, url(), overlong и non-string.
  2. Проекция устройства: hostile ripple_color заменяется динамическим цветом включённой RGB-лампы либо null, если такого цвета нет.
  3. Последняя style-граница не выводит hostile ripple declaration.
  4. Decor/space/fill resolvers используют fallback и не меняют входной объект.
  5. Backend одинаково отклоняет hostile значение во всех цветовых полях.
  6. Browser smoke на импортированном hostile config: нет overlay, внешнего запроса, новой style declaration и ошибки рендера.

Критерии приёмки

  • Ни одно значение цвета из store не попадает в CSS/SVG без общей проверки.
  • Все новые записи принимают только #RRGGBB.
  • Динамический цвет HA остаётся рабочим и не может содержать произвольный CSS.
  • Визуальное поведение валидных существующих планов не изменено.
  • Целевые frontend/backend тесты и браузерные regression smokes выполнены в gate v1.61.0-beta.1; issue закрывается после зелёного exact-SHA CI и проверки release assets.