# #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=` и закоммитить свежий `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 не даёт ревьюеру однозначного визуального доказательства.