Files
houseplan-card/docs/specs/372-space-card-empty-title.md
T
2026-08-29 13:16:04 +03:00

260 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.
# #372 — Компактное верхнее кадрирование `houseplan-space-card` без заголовка
Issue: [#372](https://github.com/Matysh/houseplan-card/issues/372)
## Сценарий
Персона **администратор дома** размещает `custom:houseplan-space-card` на
dashboard, где название этажа уже задано секцией или соседней карточкой. Он
явно указывает `title: ""`, чтобы получить компактную read-only схему без
дублирующего заголовка.
Карточка остаётся обычной View-поверхностью на desktop и touch. Редакторы и
полная `custom:houseplan-card` в этом сценарии не участвуют.
## Что человек увидит до и после
Сейчас текст и настоящий header уже исчезают, но над верхней границей плана
остаётся тёмная часть симметричного 5%-го поля кадрирования. После изменения
явно пустой `title` уберёт только это верхнее пустое поле: план начнётся от
верхнего края сцены, а боковые и нижнее поля сохранятся.
## Проблема
`src/space-card.ts` уже не создаёт `.hp-static-title`, когда вычисленный title
пуст: браузерный harness на актуальном `dev` даёт `topGap = 0 px` между
`ha-card` и `.hp-static-stage`. Поэтому отмеченная в issue полоса не является
высотой header.
Полосу создаёт `spaceFrame(..., pad = 0.05)` в static render: content frame
симметрично расширяется на 5% длинной стороны и становится SVG `viewBox`.
Тёмный фон сцены виден в верхней части этого frame раньше, чем начинается
бумага/геометрия плана.
Владелец подтвердил решение: явное `title: ""` становится компактным режимом
верхнего кадрирования. Убирается только верхняя доля штатного padding;
остальные края, footer и все другие варианты `title` сохраняют прежнее
поведение.
## Скоуп
- только `custom:houseplan-space-card`;
- только конфигурация с **явно заданным** `title: ""`;
- удаление верхней части 5%-го content-frame padding;
- сохранение левого, правого и нижнего padding в прежнем размере;
- одинаковое поведение для изображения-плана и нарисованного плана;
- одинаковое поведение в светлой/тёмной теме и на desktop/touch;
- regression-тесты для DOM header и численного SVG frame;
- RU/EN документация и changelog.
## Не-скоуп
- изменение кадрирования полной `custom:houseplan-card`, View, kiosk или
любого редактора;
- удаление бокового или нижнего поля static card;
- новая настройка padding, compact mode или отдельный UI-контрол;
- изменение `spaceFrame` для других потребителей;
- изменение алгоритма content/outlier voting, fit, auto-placement, масштаба
иконок или room labels;
- изменение footer, `show_button`, `button_label`, `button_target` и deep link;
- изменение цвета сцены, бумаги, стен, backdrop, Glow, солнца или устройств;
- исправление произвольных пробелов в строке title: значение, отличное от
точной пустой строки, сохраняет текущую семантику;
- миграция сохранённых Lovelace-конфигураций.
## Контракт поведения
1. `title` отсутствует: карточка, как прежде, подставляет название пространства,
рисует header и использует симметричный 5%-й content-frame padding.
2. `title` содержит непустую строку: карточка, как прежде, рисует эту строку и
использует симметричный 5%-й padding.
3. `title === ""`: `.hp-static-title` отсутствует; `.hp-static-stage` начинается
на верхней границе `ha-card`; верхняя координата SVG `viewBox` совпадает с
верхней координатой выбранного **непаддированного** content frame.
4. В режиме пункта 3 левая, правая и нижняя границы SVG `viewBox` совпадают с
прежним симметрично padded frame. Меняются только `viewBox.y` и вытекающая
из него `viewBox.height`; нижняя координата `y + height` неизменна.
5. Величина сохраняемого padding остаётся текущей: 5% длинной стороны
непаддированного frame. Новая магическая величина не вводится.
6. Выбор main-mass/outlier, fallback для пустого пространства и защита
вырожденных осей остаются прежними. Если content frame отсутствует и
используется stored `view_box`, compact mode не выдумывает новый crop.
7. `show_button` не влияет на frame. Footer сохраняет текущую высоту и
действие; compact mode уменьшает только высоту сцены на удалённую верхнюю
долю padding.
8. Background/day-cycle заполняют получившуюся сцену как раньше; никакой слой
не получает отдельного сдвига. SVG, devlayer и backdrop используют один и
тот же compact `viewBox`.
9. Изменение не скрывает ошибку header CSS: тест отдельно доказывает отсутствие
`.hp-static-title` и нулевой DOM gap, а отдельно — изменение координат
content frame.
## UX и доступность
- Новых контролов, фокусов, hover-состояний и действий нет.
- Static schematic остаётся inert; единственная интерактивная поверхность —
существующая footer-кнопка.
- Порядок DOM не меняется: optional title → body/stage → optional footer.
- Compact mode не добавляет ARIA-узлы и не меняет доступное имя footer-кнопки.
- Удалённое поле находится внутри сцены, поэтому скругление и фон `ha-card`
остаются нативными для темы Home Assistant.
## Модель данных, миграция и совместимость
Публичный интерфейс не расширяется:
```ts
interface SpaceCardConfig {
title?: string;
}
```
- schema/config version не меняется;
- backend и House Plan config store не меняются: `title` принадлежит Lovelace
config экземпляра карточки;
- старые карточки без `title` визуально не меняются;
- старые карточки с непустым `title` визуально не меняются;
- только уже валидная явная пустая строка получает уточнённую семантику;
- YAML и GUI round-trip не переписываются.
## i18n
Новых и изменённых UI-строк нет. `src/i18n/{en,ru,de}.json` не меняются.
## Производительность и bundle
- frame вычисляется один раз на render, как сейчас;
- допустим один pure helper или необязательный параметр существующего frame
resolver; DOM measurement и второй проход по SVG запрещены;
- асимптотика остаётся O(число объектов пространства);
- новых runtime dependencies и сетевых запросов нет;
- bundle budget не повышается.
## Touch и темы
- изменение только визуальное и одинаково для mouse/touch;
- pointer-events static stage остаётся `none`;
- footer tap target и навигация не меняются;
- светлая и тёмная темы используют тот же frame; меняются только действующие
theme colors фона;
- `prefers-reduced-motion`, day-cycle transitions и continuity overlay не
получают новой анимации или ветки поведения.
## Затронутые файлы и модули
- `src/space-card.ts` — отличить точный explicit-empty title и передать compact
top-frame contract в static renderer;
- `src/space-render.ts` — применить per-edge frame только к static card;
- при необходимости узкий pure helper в `src/space-geometry.ts`, без изменения
default-пути остальных потребителей;
- `test/space-geometry.test.mjs` либо новый узкий unit-тест — арифметика
per-edge padding, degenerate/fallback и неизменность трёх краёв;
- `demo/smoke_space_card.mjs` — omitted/non-empty/explicit-empty матрица,
численный DOM/SVG contract и реальный compact render;
- `docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md` — точная семантика `title: ""`;
- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md` — пользовательское изменение.
## Критерии приёмки
- **AC1 — явная пустая строка компактна:** при `title: ""` title-узел отсутствует,
DOM gap между `ha-card` и stage равен 0, а верхняя координата SVG frame
совпадает с непаддированной верхней границей content. **Доказательство:**
`demo/smoke_space_card.mjs` и unit-тест frame math.
- **AC2 — меняется только верх frame:** для одной fixture explicit-empty и
контрольный обычный режим имеют одинаковые left/right/bottom границы;
compact `y` сдвинут ровно на прежний верхний padding, `height` уменьшен на
ту же величину. **Доказательство:** unit + browser smoke по численным
`viewBox` и `getBoundingClientRect()`.
- **AC3 — остальные title-режимы совместимы:** omitted title показывает
название пространства, непустой title показывает заданный текст; оба режима
сохраняют прежний симметричный frame и высоту stage. **Доказательство:**
browser smoke с тремя экземплярами карточки.
- **AC4 — содержимое и footer не расходятся:** SVG, backdrop, devlayer,
устройства, room labels и continuity overlay остаются в одном compact frame;
`show_button: true/false` не меняет frame, а footer deep link работает как
прежде. **Доказательство:** browser smoke и ревью кода.
- **AC5 — edge cases безопасны:** пустое пространство, stored fallback frame,
collinear/degenerate content и outlier fixture не создают нулевой/NaN/
Infinity `viewBox` и не меняют voting. **Доказательство:** unit-тесты
`space-geometry` и существующие canvas-frame tests.
- **AC6 — поддерживаемые поверхности:** compact render не имеет horizontal
overflow на desktop и touch width, работает в light/dark и не возвращает
интерактивность schematic. **Доказательство:** browser smoke на двух widths/
themes; полный Linux smoke/golden перед ревью/бетой.
- **AC7 — документация и релиз:** RU/EN guide описывают различие omitted,
non-empty и explicit-empty title; оба changelog содержат ссылку на #372.
**Доказательство:** docs/process gates и ревью кода.
- **AC8 — сборка и бюджет:** typecheck, unit, build, bundle sync/budget и
`check-docs` зелёные; новых зависимостей нет. **Доказательство:** CI Validate
на точном SHA ветки.
## План автотестов
1. Pure unit: для прямоугольного content frame вычислить прежний uniform frame
и compact-top frame; сравнить left/right/bottom и точный удалённый top pad.
2. Pure unit: повторить для portrait/landscape, degenerate axis, fallback без
content и outlier-набора; все числа конечны, voting одинаков.
3. Расширить `demo/smoke_space_card.mjs` тремя карточками на одной fixture:
omitted, `title: "Named"`, `title: ""`.
4. В браузере проверить наличие title node, DOM top gap, `viewBox`, stage
aspect/height и неизменность footer deep link.
5. Повторить explicit-empty при `show_button: false`, на 390 px dark и 900 px
light; stage остаётся inert и не переполняет карточку по горизонтали.
6. Добавить semantic pixel witness: верхняя строка плана в compact fixture
достигает верхней границы viewport, тогда как control сохраняет штатный
padding. Assertion должен падать при возврате симметричного frame.
7. В цикле реализации: `npm run typecheck`, `npm test`, `npm run build`, затем
целевой `node demo/smoke_space_card.mjs`. Перед передачей в код-ревью —
Linux Validate, полный golden verify и полный browser smoke по процессу.
## Риски
- **Случайно меняется полная карточка.** Снижается параметром default-off только
у static renderer и AC3/ревью диффа.
- **Удаляется padding со всех сторон.** Снижается точной проверкой четырёх
границ AC2.
- **Frame и devlayer получают разные координаты.** Снижается единым `vb` и AC4.
- **Вырожденный frame становится нулевым.** Снижается сохранением текущего
degenerate/fallback resolver и AC5.
- **Тест проверяет только отсутствие title, которое уже работает.** Снижается
отдельным численным и pixel assertion именно верхнего content padding.
- **Docs screenshot source fingerprint устаревает после `src/**`.** Снижается
обязательным canonical Docs screenshots workflow и приёмкой полного artifact.
## Откат
Убрать передачу compact-top режима и новые frame assertions, вернув static
renderer к симметричному `spaceFrame(..., 0.05)`. Данные и Lovelace config не
меняются, поэтому обратная миграция и очистка не нужны. Откат возвращает только
известную верхнюю тёмную полосу у `title: ""`.
## Release-артефакты
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`: компактный `title: ""` со
ссылкой на #372;
- `docs/USER-GUIDE.md` и `docs/USER-GUIDE.ru.md`: таблица/пример трёх режимов
title;
- целевой browser artifact из `demo/smoke_space_card.mjs`: control и
explicit-empty на light/dark и narrow/wide;
- полный `npm run golden:verify` доказывает отсутствие побочных визуальных
изменений существующей матрицы; новый baseline не обязателен, если targeted
semantic pixel witness однозначно доказывает AC1/AC2;
- поскольку меняется `src/**`, запустить canonical workflow **Docs
screenshots**, принять только полный Linux artifact командой
`npm run docs:accept -- --reviewed --from=<artifact>` и закоммитить свежий
`docs/images/screenshots.json`; ожидаемо все канонические PNG остаются
пиксельно неизменными, так как static space card в них не снята;
- backend, migration, security и отдельный performance artifact не требуются;
общий bundle budget и CI Validate обязательны.
## Принято предположительно, поменять свободно
- имя pure helper и точное место per-edge арифметики;
- exact-empty определяется сравнением `config.title === ""`; whitespace-only
сохраняет прежнее поведение и не нормализуется;
- конкретная synthetic fixture и допустимый raster tolerance pixel witness;
- reuse существующего `spaceFrame` через новый optional argument либо отдельный
узкий helper допустимы, если default path побайтово/численно не меняется;
- новый golden baseline добавляется только если targeted smoke не даёт
ревьюеру однозначного визуального доказательства.