docs: specify device marker geometry polish

Issue: #213
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-20 10:19:03 +03:00
parent 9f25f17c83
commit 0c93be9e85
+542
View File
@@ -0,0 +1,542 @@
# Issue #213 — геометрия, размеры, LQI и hover маркеров устройств
- **Issue:** https://github.com/Matysh/houseplan-card/issues/213
- **Связанные завершённые задачи:** #179, #211, #212
- **Нормативный дизайн-пакет:** архив #179, версия 1.1.1
- **Тип:** bug / polish, обычный трек
- **Пользовательское изменение:** да
- **Touch editor:** best effort / intentionally degraded
## 1. Сценарий и персона
**Персона:** домочадец или гость, который читает состояние дома в View/kiosk,
и администратор, который проверяет те же маркеры в редакторе устройств.
**Сценарий:** пользователь меняет масштаб плана либо открывает House Plan на
экране с другим DPR и ожидает, что круг, внешний shell и MDI-глиф останутся
визуально соосными. Маркер со значением должен реагировать на мышь по всей
видимой капсуле, LQI — снова менять цвет плавно, а замок рядом с дверью —
выглядеть как Lock/Unlock из принятого пакета #179.
## 2. Что человек увидит до и после
До изменения на отдельных масштабах внутренний элемент выглядит сдвинутым на
один пиксель относительно кольца, MDI-глиф маловат, дверной замок живёт в старой
визуальной системе, LQI перескакивает между тремя цветами, а часть видимой
капсулы не включает hover. После изменения все концентрические слои остаются
визуально по центру, фактический размер маркера не меняется, глиф становится на
10% крупнее, LQI снова использует плавную шкалу, дверной замок соответствует
#179, а вся капсула является mouse-hover областью.
## 3. Проблема и подтверждённые причины
Текущий renderer после #212 вычисляет core так:
```text
resolved card size × per-marker scale × --device-visual-factor(0.9)
```
Затем shell, border и padding рассчитываются отдельными дробными выражениями.
Flex layout позиционирует core через независимо округлённые shell/padding/border.
На проверенной матрице браузерных размеров геометрические центры отличаются на
layout quantum; на отдельных scale/DPR антиалиасинг превращает его в заметную
асимметрию на один физический пиксель. Постоянный `translate(1px)` исправил бы
только один случай и сломал соседние размеры.
Дополнительно подтверждено:
1. общий `--device-visual-factor: 0.9` остаётся отдельным поздним множителем;
2. MDI viewport равен `0.5 × core`;
3. opening lock использует старый компактный круг, зелёный locked и не использует
shell/core Lock/Unlock пакета #179;
4. `.device-shell` не владеет pointer hit testing, а круглая `dev::before`
область не покрывает вытянутую часть `with-values`;
5. `markerLqiColor()` после #179 возвращает три фиксированных цвета, хотя общий
`lqiColor()` уже содержит прежний непрерывный red→green HSL-gradient.
Текущий smoke проверяет отношения размеров на 32/56/96 px, но не прогоняет
дробную матрицу, несколько DPR и pixel-centroid симметрию.
## 4. Нормативные источники и приоритет
При расхождении применяются:
1. решения владельца в теле #213 и последующих комментариях;
2. это ТЗ после зелёного SPEC-REVIEW;
3. дизайн-пакет #179 версии 1.1.1 и принятые уточнения #211;
4. контракты #212 для фактического размера, 44×44 hit area и pointer modality;
5. `docs/SCOPE.md`, `docs/TOUCH-SUPPORT.md`, `docs/UX-MODES.md` и пользовательское
руководство;
6. текущая реализация как compatibility baseline там, где источники выше ничего
не меняют.
SVG Lock/Unlock из #179 нормативны по форме, отношению shell/core, глифу, цветам,
stroke и shadows. Как и в #179/#211, production использует динамический DOM/MDI,
а не включает reference SVG в runtime bundle. Для Dark применяется принятый
No-Blur fallback; owner override Dark Unlock остаётся amber.
## 5. Цели
1. Убрать scale-зависимое визуальное расхождение центров shell/core/glyph.
2. Удалить отдельный поздний коэффициент `0.9`, сохранив текущие фактические
размеры всех существующих настроек и поверхностей.
3. Увеличить базовый MDI viewport на 10% без отдельного glyph multiplier.
4. Перевести opening Lock/Unlock на принятую визуальную систему #179 без
изменения security/action contract.
5. Сделать всю видимую value/legacy capsule mouse-hover областью.
6. Вернуть marker LQI прежнюю плавную цветовую шкалу, меняя только цвет.
## 6. Scope
В задачу входят:
- общий device face в full View/kiosk, Device editor/preview и
`houseplan-space-card`;
- base-size resolution и fallback/default bases, включая vacuum puck;
- круглый Icon, основной Text и Double/value/legacy sections;
- MDI glyph viewport для динамических и пользовательских MDI-иконок;
- opening lock badge в View и его light/dark/locked/unlocked/unknown projection;
- marker-only LQI color;
- instance-local mouse hover для полной внешней капсулы;
- unit/source-contract, browser smoke, visual/golden и performance evidence;
- пользовательская, архитектурная и testing-документация.
## 7. Не входит в задачу
- изменение сохранённых координат, `icon_size`, per-marker `size`, angle,
display/value/action либо backend schema;
- изменение фактического внешнего размера marker относительно v1.65.0-beta.8;
- изменение размера core/shell из-за увеличения MDI-глифа;
- новые пользовательские scale, LQI или hover настройки;
- изменение источника, агрегации, форматирования, доступности либо числового
значения LQI;
- изменение low/mid/high категорий в aria-label/data attributes;
- изменение непрерывной комнатной LQI-заливки;
- расширение либо сокращение click/tap/action области value capsule;
- новый hover на touch/pen, sticky hover или новая touch-editor parity;
- изменение lock service calls, подтверждения Unlock либо secure invariant;
- backdrop-filter на marker/opening lock;
- принятие golden baseline на Windows или выпуск без команды владельца.
## 8. Контракт размеров
### 8.1. Удаление позднего `0.9`
После изменения `.dev` и vacuum puck не содержат
`--device-visual-factor`, эквивалентного post-resolution multiplier либо цепочки
`resolved × 0.9` внутри face geometry.
Размер, переданный общему face как base, уже является текущим effective base.
Для v1.65.0-beta.8 reference:
```text
new effective core = current effective core
new effective shell = current effective shell
new effective puck = current effective puck
```
Это выполняется для default card, явного `icon_size`, kiosk multiplier,
per-marker `size`, preview и static card. Числовые пользовательские значения и
их относительные отношения не меняются. Нормализация legacy public size units,
если она нужна для совместимости, принадлежит единой границе base resolution;
внутри face нет второго коэффициента.
Base constants меняются на текущие effective values: fallback full/static,
preview и vacuum используют уже уменьшенные значения. Запрещено сохранять
старые bases и маскировать их новым переименованным `0.9` в CSS.
### 8.2. MDI-глиф
Базовый viewport MDI меняется с `0.50 × core` на `0.55 × core`. Это изменение
самого base ratio, без `--glyph-factor`, вложенного `scale(1.1)` либо второй
трансформации.
Правило одинаково для default, custom MDI и state-swapped glyph; marker angle
по-прежнему вращает глиф вокруг центра. Core, shell, anchor, pulse и hit area от
увеличения glyph не меняются. Reference/runtime matrix должна подтверждать, что
глиф не клиппится на минимальном размере.
### 8.3. Совместимость
- storage/card config не переписывается;
- Open → Save не материализует новые поля или bases;
- default и явно настроенные marker sizes сохраняют текущий pixel result с
допуском layout quantum;
- relative ratio между двумя разными per-marker size остаётся прежним;
- координата остаётся центром icon core;
- минимальная интерактивная область остаётся не меньше 44×44 CSS px.
## 9. Контракт соосности и растеризации
### 9.1. Геометрия
Shell и core круглого marker строятся от одной координаты центра. Позиция core
не выводится из независимо округлённой суммы left/top padding и border.
Для Text/Double/value/legacy shell:
- сохранённая точка остаётся центром icon core;
- прямые стороны и полукруглый торец сохраняют inset пакета;
- core и соответствующий ему круглый конец внешней capsule имеют общий центр;
- расширение секций идёт только в выбранную сторону;
- смена стороны не меняет anchor.
Допустимы CSS grid, явные tracks, общий concentric pseudo-layer либо другой
layout без per-size special cases. Запрещены hardcoded `translateX/Y(1px)`,
таблица исключений по viewport и UA/DPR sniffing.
### 9.2. Проверяемая симметрия
На матрице base core от 24 до 112 CSS px, включая дробные значения с шагом не
крупнее 0.25 px, и DPR `1`, `1.25`, `1.5`, `2`:
- DOM-centres core и его shell-end отличаются не более чем на один browser
layout quantum;
- alpha/color-weighted painted centroids расходятся менее чем на 0.5 физического
пикселя по каждой оси;
- противоположные видимые inset не дают устойчивой однопиксельной ступени;
- проверка умеет покраснеть на v1.65.0-beta.8 либо на документированном mutant,
восстанавливающем прежний layout.
Golden остаётся визуальным доказательством на выбранных failing-before-fix
размерах; один DOM `getBoundingClientRect()` без pixel check недостаточен.
## 10. Opening Lock/Unlock
### 10.1. Известные состояния
Значок замка у двери/ворот использует визуальный контракт Lock/Unlock #179:
- package shell/core ratio и concentric geometry;
- Locked: black/`#252525` theme projection и белый locked glyph;
- Unlocked: amber `#F0A00C`, с theme-specific glyph из принятого reference;
- Light/Dark stroke, inner shadow и external shadows из #179/#211;
- Dark No-Blur production fallback;
- корректный closed/open lock MDI path, а не перекраска одного glyph.
Opening badge сохраняет нынешний компактный общий footprint, позицию возле
створки и способ вычисления offset. Package ratios применяются внутри этого
footprint; он не превращается в полноразмерный device marker и не участвует в
device layout/declump.
### 10.2. Unknown и действия
Unknown/unavailable сохраняет нейтральный `lock-question` смысл, но использует
тот же новый shell/core каркас без ложного locked/unlocked цвета.
Клик по opening badge только открывает карточку проёма. Lock выполняется лишь
явной кнопкой карточки, Unlock — после подтверждения. Pointer/keyboard paths
маркеров устройств не получают нового lock action. Скрытый/disabled/missing
exact opening reference ведёт себя по прежним #117 правилам.
## 11. Hover всей внешней capsule
Для `.device-shell.with-values` видимая border-box capsule целиком является
mouse-hover областью, включая:
- core;
- промежуток между core и value section;
- value/legacy sections;
- внутренний padding до внешнего stroke;
- все четыре направления right/bottom/left/top.
Над любой точкой capsule активируются те же visual hover, z-index и tooltip,
что над core. Hover остаётся instance-local и разрешён только canonical
`data-pointer-hover` после настоящей fine mouse. Touch/pen и compatibility mouse
не включают и не оставляют hover; unavailable по-прежнему не получает visual
hover.
Расширение касается hover/tooltip. Click/tap/action hit contract и 44×44 core
target не расширяются скрыто на value sections. Реализация обязана отделить
hover ownership от action ownership, если browser bubbling иначе превратил бы
всю capsule в новую click target.
Preview воспроизводит визуальный hover при переданном mouse gate, но остаётся
неинтерактивным. Static plan marker остаётся read-only; его footer button не
имеет отношения к этой capsule.
## 12. Плавный marker LQI
Цвет числа LQI снова использует прежнюю непрерывную функцию:
```text
<= 40 → hue 0 (red endpoint)
40..180 → linear hue 0..120
>= 180 → hue 120 (green endpoint)
```
Нормативный output совпадает с общим `lqiColor()`:
`hsl(round(hue), 85%, 55%)`. Внутри диапазона соседние значения меняют hue
плавно; на бывших границах `40/41` и `179/180` нет перехода между отдельными
palette colors.
Меняется только `presentation.lqiColor` marker. Сохраняются:
- resolved average/source и число;
- `show_signal`, отсутствие missing/unavailable подписи и LQI=`0`;
- low/mid/high `lqiBand` для понятного aria-label и data attributes;
- позиция строки под shell и Double-bottom offset;
- room fill gradient и room tooltip color.
Три package цвета #179 больше не являются нормативом для marker LQI, но
остаются semantic colors других состояний и pulse.
## 13. UX, accessibility и touch
- View и kiosk на desktop/touch остаются release-blocking;
- essential state/action не зависит от hover;
- focus-visible не зависит от mouse gate;
- aria-label продолжает содержать число LQI и категорию low/mid/high;
- увеличение glyph не меняет accessible name/role/tab order;
- opening lock сохраняет существующий понятный glyph и click-to-info;
- Device editor остаётся desktop-first, а его touch safety floor не меняется;
- pan/pinch/long press/pointercancel не создают hover или action.
## 14. Данные, migration и i18n
Новых config/layout/localStorage/backend полей нет. Schema и сериализация не
меняются, data migration не нужна. Existing card/marker size values должны
читаться с тем же фактическим результатом.
Новых пользовательских строк не ожидается. Существующие RU/EN LQI a11y bands
сохраняются; изменить нужно только описание цветовой шкалы в руководствах. Если
реализация потребует новый текст, обязательна RU/EN parity без расширения
пользовательских настроек.
## 15. Архитектурный контракт
```text
card config / marker scale / surface base
→ one resolved effective base size
→ resolveDevicePresentation
→ renderDeviceFace
→ one concentric face geometry on full / preview / static
resolved marker LQI
→ continuous marker color + unchanged semantic band
→ shared full / preview / static face
opening exact lock state
→ shared #179 lock visual tokens/geometry
→ opening badge → click opens info only
real mouse modality
→ full capsule hover owner
→ visual hover + tooltip; action owner remains unchanged
```
Ожидаемые зоны изменений:
- `src/styles.ts` — base values, concentric layout, glyph base ratio, capsule
hover и opening-lock tokens;
- `src/device-presentation.ts` — continuous marker LQI color при сохранённом band;
- `src/device-face.ts` — только если нужен явный hover/action layer;
- `src/houseplan-card.ts` — resolved full base, opening lock DOM и hover owner;
- `src/space-render.ts`, `src/hp-device-preview.ts` и vacuum styles — только для
base/parity wiring, второй renderer запрещён;
- unit/source tests, targeted smokes, reference capture, docs и changelogs.
Нельзя вводить второй device face renderer, отдельную LQI-функцию с другой
формулой, global pointer latch, per-marker ResizeObserver или per-frame JS.
## 16. Edge cases
- default и explicit global `icon_size`, kiosk scale и per-marker 0.5…3;
- base 24…112 px, дробные quarter-pixel значения, DPR 1/1.25/1.5/2;
- Light/Dark hot switch и цветной план;
- Icon, Text, Double right/left/top/bottom и legacy third section;
- длинное значение, CJK/кириллица, `0`, `false` и unit;
- selected/focus/active/alarm/virtual/unavailable вместе с новой geometry;
- custom MDI, rotated glyph и state-swapped lock/cover/light icons;
- opening lock locked/unlocked/locking/unlocking/unknown/unavailable;
- opening у двери и ворот, hidden openings и registry-less exact lock;
- LQI `0`, `1`, `39`, `40`, `41`, `42`, `178`, `179`, `180`, `181`, `255`,
missing и value badge, который сам показывает LQI;
- mouse впервые входит с capsule section, touch→mouse hybrid, pointerleave,
space/mode/hidden/disconnect cleanup;
- two cards on one page and non-interactive preview/static surfaces;
- 200 markers without added layout loop/compositor layer.
## 17. Acceptance criteria
1. **AC1 — size contract.** Отдельный `--device-visual-factor`/эквивалентный
late multiplier удалён; default, explicit global/per-marker, kiosk, preview,
static и vacuum сохраняют effective v1.65.0-beta.8 size с допуском одного
layout quantum. **Доказательство:** source/unit facts + computed-style browser
matrix + reference capture.
2. **AC2 — glyph.** MDI viewport использует прямой base `0.55 × core`, без
дополнительного multiplier/transform; core/shell/anchor не меняются и glyph
не клиппится. **Доказательство:** source assertion + icon/custom/rotation
browser smoke + 32/56/96 reference.
3. **AC3 — concentric geometry.** Icon/Text/Double shell-end и core выполняют
§9 на дробной size/DPR matrix; нет hardcoded pixel compensation.
**Доказательство:** DOM-centre assertions + pixel-centroid smoke, красный до
фикса или на documented mutant + reviewed golden.
4. **AC4 — anchor/parity.** Saved coordinate, side anchor, relative scale,
Text expansion и 44×44 minimum action area не меняются на full, preview и
static surfaces. **Доказательство:** unit layout facts + preview/static/icon
scale smokes.
5. **AC5 — opening locks.** Door/gate locked и unlocked совпадают с #179 по
package layers/tokens в Light/Dark при сохранённом compact footprint;
unknown нейтрален. **Доказательство:** reference/runtime visual matrix +
opening browser smoke/golden.
6. **AC6 — lock security.** Opening badge только открывает info; lock/unlock
service и confirmation invariant не меняются, disabled/missing references не
рисуют ложный action. **Доказательство:** existing opening binding,
registry-less, lock-action и lock-invariant smokes.
7. **AC7 — capsule hover.** На всех четырёх позициях любая точка внешней
capsule включает один mouse visual hover/z-index/tooltip; unavailable,
touch/pen и compatibility mouse не включают его. **Доказательство:** real
PointerEvent desktop/hybrid browser smoke + computed styles.
8. **AC8 — action boundary.** Value/legacy section не становится новым click/
tap action target; core/44×44 action semantics, confirmation и gesture safety
прежние. **Доказательство:** pointer target/action-count smoke.
9. **AC9 — continuous marker LQI.** Marker color совпадает с `lqiColor()` на
representative и boundary-adjacent values; 40/41 и 179/180 не используют
дискретные palette jumps. **Доказательство:** pure boundary/dense-range tests
+ browser computed color.
10. **AC10 — only color changes.** LQI text/source/format/visibility/position,
low-mid-high aria/data band и room fill/tooltip gradient не меняются.
**Доказательство:** presentation/a11y unit + existing room fill regression.
11. **AC11 — data/i18n compatibility.** Schema, saved settings и RU/EN key set
не меняются; Open → Save не материализует defaults. **Доказательство:**
serialization/i18n unit + code review.
12. **AC12 — performance.** Нет per-frame JS, per-marker observer/media listener,
backdrop-filter, UA/DPR sniffing либо роста постоянных animation layers;
200-marker budget не ухудшен материально. **Доказательство:** source review
+ pre-beta performance smoke.
13. **AC13 — release artifacts.** Оба changelog, RU/EN guide, architecture,
testing, targeted smoke/reference и visual fingerprints актуальны.
**Доказательство:** docs diff + `check-docs` + release review.
## 18. План автотестов
### 18.1. Цикл реализации
```bash
npm run typecheck
npm test
npm run build
node scripts/check-docs.mjs --external
```
Unit/source coverage:
- отсутствие late 0.9 factor и наличие canonical effective bases;
- direct glyph base 0.55;
- shared concentric layout contract без fixed pixel nudge;
- dense continuous LQI color и неизменный band/a11y;
- opening-lock state/token projection;
- hover/action ownership inventory;
- config/i18n parity.
### 18.2. Targeted browser smoke перед S7
Обязательны:
- расширенный `demo/smoke_device_icon_design.mjs`;
- новый либо отдельный pixel-alignment smoke с fractional size/DPR matrix;
- `demo/smoke_device_preview_parity.mjs`;
- `demo/smoke_static_icon.mjs`;
- `demo/smoke_icon_scale.mjs`;
- `demo/smoke_state_value.mjs`;
- capsule hover/action smoke для четырёх badge positions;
- `demo/smoke_touch_tips.mjs` и hybrid pointer regression;
- `demo/smoke_opening_binding.mjs`;
- `demo/smoke_registryless_opening.mjs`;
- `demo/smoke_lock_action.mjs` и `demo/smoke_lock_invariant.mjs`.
До исправления требуется зафиксировать красное доказательство минимум для
alignment, late 0.9 source contract, glyph 0.50, opening-lock visual, capsule
hover и categorical marker LQI. Critical guards получают mutation proof либо
эквивалентное documented failing-before-fix сравнение.
### 18.3. Golden и pre-beta
Light/Dark golden/reference matrix включает:
- 32/56/96 и выбранные дробные failing sizes;
- Icon, Text, Double four-side/legacy;
- default/hover/focus/active/alarm/virtual/unavailable;
- custom/rotated glyph;
- opening Lock/Unlock/Unknown рядом с SVG #179;
- LQI low-end, несколько промежуточных оттенков и high-end;
- desktop hover и post-touch no-hover.
Windows capture диагностический. Baseline принимается только из reviewed полного
Linux CI artifact. Перед beta выполняются full smoke, golden и performance gates
по release runbook; backend не затронут.
## 19. Риски и меры
| Риск | Мера |
| --- | --- |
| Удаление 0.9 увеличит custom sizes | effective-size matrix для default/explicit/kiosk/per-marker до и после |
| Новый layout сдвинет anchor или side capsule | shared anchor facts и four-side smoke |
| Pixel тест окажется привязан к платформе | DOM invariant + alpha centroid; golden canon только Linux |
| Больший glyph клиппится у сложного MDI path | representative custom/state-swapped matrix на min size |
| Opening lock станет слишком крупным | сохраняется текущий compact footprint, меняются внутренние ratios |
| Hover capsule расширит click target | разные hover/action owners и action-count smoke |
| Continuous marker LQI затронет room fill | reuse `lqiColor`, отдельный marker projection и room regression |
| Refactor добавит layout/compositor cost | CSS-only geometry, no observers/blur, pre-beta performance |
## 20. Откат
Откат одним implementation revert возвращает прежний layout, factor, glyph,
opening badge, hover и marker LQI colors. Миграции данных нет, поэтому storage
rollback не нужен. Golden baseline возвращается только через нормативный reviewed
Linux artifact, не ручным принятием Windows capture.
## 21. Release-артефакты
В user-visible implementation commit одновременно обновляются:
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #213;
- `docs/USER-GUIDE.md` и `docs/USER-GUIDE.ru.md`: effective size без отдельного
factor, glyph, smooth marker LQI, opening lock и capsule hover;
- `docs/ARCHITECTURE.md`: base-size boundary, shared concentric face и
marker-LQI band/color separation;
- `docs/TESTING.md`: fractional/DPR pixel alignment, lock reference и
hover/action/LQI regressions;
- reference/golden/source fingerprints и screenshots, если их источник изменён.
Release note: исправлено центрирование новых маркеров на разных масштабах,
увеличены внутренние глифы, дверные замки приведены к новому дизайну, hover
охватывает всю капсулу, а LQI снова использует плавную цветовую шкалу. Сначала
beta; stable — только после promotion rule.
## 22. План реализации
1. Добавить failing source/unit/browser guards для шести подтверждённых проблем.
2. Перенести current effective sizes на единую base-resolution boundary и
удалить late factor из face/puck.
3. Перестроить concentric shell/core tracks и увеличить direct glyph base.
4. Подключить full-capsule hover с отдельным action owner.
5. Вернуть continuous marker LQI color при неизменном band/a11y.
6. Перевести opening lock на #179 tokens/geometry, сохранив compact footprint и
security path.
7. Обновить parity surfaces, docs/changelogs/reference fixtures.
8. Выполнить fast gates и все named smokes §18.2, затем передать в S7.
9. Перед beta выполнить полный Linux golden/smoke/performance gate.
## 23. Принятые технические предположения
Следующее принято предположительно и может быть изменено ревьюером без нового
продуктового решения, если все AC сохраняются:
1. Opening lock сохраняет нынешний компактный footprint и offset; #179 задаёт
его внутренние proportions/tokens, а не новый абсолютный размер.
2. Unknown opening lock использует package shell/default theme и текущий
`lock-question`, потому что архив не содержит отдельного Unknown asset.
3. Запрос владельца расширяет именно hover/tooltip value capsule; click/tap
target остаётся прежним, так как отдельного решения расширить действие нет.
4. Low/mid/high band остаётся только semantic/a11y facet; цвет независимо
разрешается общей continuous `lqiColor()`.
5. Base compatibility может нормализоваться один раз на границе surface size,
но внутри `.dev`, puck и face запрещён любой поздний 0.9-equivalent factor.
6. Pixel-centroid smoke использует controlled solid background и color/alpha
segmentation; точные thresholds калибруются failing-before-fix/mutant и не
становятся browser-vendor whitelist.
7. Общий concentric layout предпочтительнее размера, округлённого через CSS
`round()`: последний зависит от поддержки WebView и не решает shared-center
invariant сам по себе.