fix: settle editor chrome and opening render regressions

This commit is contained in:
Matysh
2026-08-09 15:14:58 +03:00
parent caf3c44ad9
commit faa1f4ea9a
25 changed files with 978 additions and 731 deletions
+17
View File
@@ -8,6 +8,23 @@ House Plan is one HACS package with two parts plus a demo harness:
Standard commands live in `package.json` scripts, `CONTRIBUTING.md`, and `docs/DEVELOPMENT.md`. Read `docs/ARCHITECTURE.md` and `docs/STATUS.md` before non-trivial changes.
## Canonical backlog
GitHub is the only active backlog for House Plan:
- [GitHub Issues](https://github.com/Matysh/houseplan-card/issues) are the
canonical task records: problem, scope, acceptance criteria and discussion.
- [GitHub Projects (v2)](https://github.com/users/Matysh/projects/1) is
the canonical prioritization and workflow-status view. Every open in-scope
issue must be present there.
Before starting planned work, find or create its issue and keep its description,
labels and Project status current as decisions and implementation state change.
Close an issue only after the result is verified. Specs, audits and ADRs may
remain under `docs/`, but must link to their issue and must not become a parallel
task list. When repository documentation disagrees with Issues or Project v2,
the GitHub backlog wins.
## Cursor Cloud specific instructions
The startup update script already runs `npm ci`, provisions a Python 3.13 backend venv at `.venv-backend`, and installs Playwright Chromium. You do not need to reinstall dependencies.
+10
View File
@@ -16,6 +16,16 @@ writing code? The **[Telegram chat @ha_houseplan](https://t.me/ha_houseplan)**
is the quickest route to the author and other users. Bugs and concrete feature
requests still belong in [issues](https://github.com/Matysh/houseplan-card/issues).
## Backlog and work status
[GitHub Issues](https://github.com/Matysh/houseplan-card/issues) and the linked
[GitHub Project v2](https://github.com/users/Matysh/projects/1) are the
only active project backlog. Issues own scope and acceptance criteria; Project
v2 owns prioritization and workflow status. Before starting planned work, link
it to an existing issue or create one, add it to the Project, and keep both
surfaces current until the verified result is closed. Design specs and ADRs may
support an issue, but they do not replace it or maintain a separate checklist.
## Five-minute setup
```bash
+1 -1
View File
@@ -6,7 +6,7 @@
[![Live demo](https://img.shields.io/badge/демо-попробовать-00c853?logo=homeassistant&logoColor=white)](https://demo.houseplan.tech)
[![Telegram chat](https://img.shields.io/badge/Telegram-чат-2CA5E0?logo=telegram&logoColor=white)](https://t.me/ha_houseplan)
📘 **[Полное руководство пользователя](docs/USER-GUIDE.ru.md)** · **[Продуктовый аудит и план улучшений](docs/PRODUCT-IMPROVEMENT-PLAN.ru.md)**
📘 **[Полное руководство пользователя](docs/USER-GUIDE.ru.md)** · 🗂 **[Беклог проекта](https://github.com/users/Matysh/projects/1)**
**Превратите Home Assistant в живую интерактивную карту дома.** Загрузите или
нарисуйте план этажа, обведите комнаты мышкой — и умные устройства появятся на
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -2,7 +2,9 @@
This layer catches visual regressions that DOM smokes cannot: wall seams and
end caps, thick opening tunnels, Glow/sun clipping, hover contours, editor
chrome, long dialog titles/footers, mobile clipping, themes and zoom/remount.
chrome, the open contextual tray at wide/medium/narrow widths in English and
Russian (selection, tool options, group and palette), long dialog
titles/footers, mobile clipping, themes and zoom/remount.
## Safety contract
+51
View File
@@ -143,6 +143,55 @@ export async function prepareGoldenScenario(page, scenario) {
card.requestUpdate();
await card.updateComplete;
}
if (scenario.editorTray) {
let expectedKind = '';
if (scenario.editorTray === 'plan-selection') {
card._physicalSel = { kind: 'partition', id: 'geo-partition-h' };
expectedKind = 'selection';
} else if (scenario.editorTray === 'plan-tool') {
card._physicalSel = null;
card._tool = 'draw';
expectedKind = 'tool';
} else if (scenario.editorTray === 'decor-selection') {
card._decorTool = 'select';
card._decorSel = 'geo-axis-h';
expectedKind = 'selection';
} else if (scenario.editorTray === 'decor-tool') {
card._decorSel = null;
card._decorTool = 'line';
expectedKind = 'tool';
} else if (scenario.editorTray === 'furniture-palette') {
card._decorSel = null;
card._furnPalette = null;
card._editorSecondary.openPalette();
card._decorTool = 'furniture';
expectedKind = 'palette';
} else if (scenario.editorTray === 'group') {
const group = {
id: 'golden-group', label: 'Arrange', icon: 'mdi:shape-outline', items: [
{ id: 'align', label: 'Align', icon: 'mdi:format-align-center', role: 'command', invoke: () => undefined },
{ id: 'distribute', label: 'Distribute', icon: 'mdi:format-horizontal-align-center', role: 'command', invoke: () => undefined },
],
};
Object.defineProperty(card, '_editorToolbarGroups', {
configurable: true,
get: () => [group],
});
card.requestUpdate();
await card.updateComplete;
card._editorSecondary.toggleGroup(card._editorToolbarGroups, group.id);
expectedKind = 'group';
} else {
throw new Error(`unknown golden editor tray: ${scenario.editorTray}`);
}
card.requestUpdate();
await card.updateComplete;
await frame();
const tray = card.renderRoot.querySelector(
`.editor-secondary-host.open .editor-secondary.kind-${expectedKind}`,
);
if (!tray) throw new Error(`golden editor tray did not open: ${scenario.editorTray}`);
}
if (scenario.hoverRoom) {
const room = card._spaceModel().rooms.find((item) => item.id === scenario.hoverRoom);
if (!room) throw new Error(`golden hover room missing: ${scenario.hoverRoom}`);
@@ -185,6 +234,8 @@ export async function prepareGoldenScenario(page, scenario) {
mode: card._mode,
devices: card._devices.length,
dialog: !!card.renderRoot.querySelector('hp-dialog'),
editorTray: card.renderRoot.querySelector('.editor-secondary-host.open .editor-secondary')
?.className || '',
};
}, { fixture, scenario });
}
+21 -1
View File
@@ -1,5 +1,5 @@
/** Data-only HP-QA-01 capture matrix. Bump when framing or scenarios change. */
export const GOLDEN_MATRIX_VERSION = 2;
export const GOLDEN_MATRIX_VERSION = 3;
const stage = { capture: 'stage', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0005 } };
const page = { capture: 'page', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0008 } };
@@ -15,6 +15,26 @@ export const GOLDEN_SCENARIOS = Object.freeze([
theme: 'dark', viewport: { width: 1180, height: 900 }, ...page },
{ id: 'geometry-decor-editor-dark', fixture: 'visual', space: 'golden-geometry', mode: 'decor',
theme: 'dark', viewport: { width: 1180, height: 900 }, ...page },
// HP-UX-11 visual contract: every adaptive width is captured in both
// languages while the six materially different tray contents are open.
{ id: 'tray-wide-selection-en', fixture: 'visual', space: 'golden-geometry', mode: 'plan',
editorTray: 'plan-selection', language: 'en', theme: 'dark',
viewport: { width: 1180, height: 900 }, ...page },
{ id: 'tray-wide-tool-ru', fixture: 'visual', space: 'golden-geometry', mode: 'plan',
editorTray: 'plan-tool', language: 'ru', theme: 'dark',
viewport: { width: 1180, height: 900 }, ...page },
{ id: 'tray-medium-group-en', fixture: 'visual', space: 'golden-geometry', mode: 'plan',
editorTray: 'group', language: 'en', theme: 'dark',
viewport: { width: 760, height: 820 }, ...page },
{ id: 'tray-medium-selection-ru', fixture: 'visual', space: 'golden-geometry', mode: 'decor',
editorTray: 'decor-selection', language: 'ru', theme: 'dark',
viewport: { width: 760, height: 820 }, ...page },
{ id: 'tray-narrow-palette-en', fixture: 'visual', space: 'golden-geometry', mode: 'decor',
editorTray: 'furniture-palette', language: 'en', theme: 'dark',
viewport: { width: 390, height: 760 }, ...page },
{ id: 'tray-narrow-tool-ru', fixture: 'visual', space: 'golden-geometry', mode: 'decor',
editorTray: 'decor-tool', language: 'ru', theme: 'dark',
viewport: { width: 390, height: 760 }, ...page },
{ id: 'geometry-diagonal-45-opening-dark', fixture: 'visual', space: 'golden-geometry', mode: 'view',
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
{ id: 'openings-thick-wall-dark', fixture: 'visual', space: 'golden-lighting', mode: 'view',
+9
View File
@@ -15,6 +15,15 @@ const res = await page.evaluate(async () => {
out.planActive = c._mode === 'plan' && tabs()[0].classList.contains('active');
out.planBar = !!sr().querySelector('.editbar');
out.planBarClose = !!sr().querySelector('.editbar .barclose');
const barClose = sr().querySelector('.editbar .barclose');
const barCloseIcon = barClose?.querySelector('ha-icon');
const barCloseRect = barClose?.getBoundingClientRect();
const barCloseIconRect = barCloseIcon?.getBoundingClientRect();
out.planBarCloseIconIsCentered = !!barCloseRect && !!barCloseIconRect
&& Math.abs((barCloseIconRect.left + barCloseIconRect.width / 2)
- (barCloseRect.left + barCloseRect.width / 2)) <= 1
&& Math.abs((barCloseIconRect.top + barCloseIconRect.height / 2)
- (barCloseRect.top + barCloseRect.height / 2)) <= 1;
out.tabCross = !!tabs()[0].querySelector('.closex');
// 3) повторный клик по активной вкладке — ничего
tabs()[0].click(); await c.updateComplete;
+10
View File
@@ -72,6 +72,14 @@ const res = await page.evaluate(async () => {
out.washerIsInThePalette = !!item('washer');
const prevD = item('sofa')?.querySelector('svg.furnprev path')?.getAttribute('d') || '';
out.previewIsDrawn = prevD.length > 10;
const previewEl = item('sofa')?.querySelector('svg.furnprev');
const previewRect = previewEl?.getBoundingClientRect();
const itemRect = item('sofa')?.getBoundingClientRect();
out.previewStaysInsideItsButton = !!previewRect && !!itemRect
&& getComputedStyle(previewEl).position !== 'absolute'
&& previewRect.width <= 42 && previewRect.height <= 42
&& previewRect.left >= itemRect.left - 1 && previewRect.right <= itemRect.right + 1
&& previewRect.top >= itemRect.top - 1 && previewRect.bottom <= itemRect.bottom + 1;
out.namesAreLocalised = /\S/.test(item('sofa')?.querySelector('span')?.textContent || '');
// An unarmed explicit palette closes on the first press anywhere outside
@@ -287,6 +295,8 @@ const res = await page.evaluate(async () => {
ev('pointerdown', stageEl(), 320, 320); // символ НЕ выбран
await c.updateComplete;
out.noSymbolNoStamp = c._decorList.length === n0;
out.unarmedCanvasDismissReturnsToSelect = c._decorTool === 'select' && !pal();
out.unarmedCanvasDismissLeavesNoClickSuppression = c._suppressClick === false;
c._decorTool = 'erase'; await c.updateComplete;
ev('pointerdown', el(sofaId), 300, 300);
await c.updateComplete;
File diff suppressed because one or more lines are too long
+239 -227
View File
File diff suppressed because one or more lines are too long
+10 -4
View File
@@ -199,6 +199,14 @@ cancelled or one-hour-timed-out Validate withholds the asset. Bump the version
everywhere in sync: `src/houseplan-card.ts` (CARD_VERSION), `package.json`,
`custom_components/houseplan/manifest.json`, `custom_components/houseplan/const.py`.
Validate intentionally runs on branch pushes, not tag pushes, so an annotated
release tag does not duplicate the expensive browser/performance matrix. Every
tagged SHA must therefore already be pushed to a branch and have a completed
green exact-SHA Validate run. For an owner-approved emergency hotfix, push a
temporary `hotfix/*` branch and wait for Validate before creating the tag;
never tag a detached or otherwise unpushed commit, because the release gate
will wait for a run that cannot exist and then fail closed after one hour.
The GitHub Release body is a concise **bilingual user summary**, not a copy of
the exhaustive changelog. Put Russian first and English second, with equivalent
meaning in both sections. Give separate bullets only to significant features
@@ -211,13 +219,11 @@ bodies of skipped releases. The full detail remains in both
two explicit links, one to each language version of the changelog.
```md
## Русский
## Основное
- Значимое изменение.
- Мелкие исправления и улучшения.
---
## English
## Highlights
- Significant change.
- Small fixes and improvements.
+1 -1
View File
@@ -6,7 +6,7 @@
|---|---|---|
| [Полное руководство пользователя](USER-GUIDE.ru.md) | Пользователи и администраторы Home Assistant | Установка, первая настройка, пространства, комнаты, стены, проёмы, устройства, визуальные состояния, заливки, подложка, солнце, пылесосы, киоск, обслуживание и диагностика |
| [Редактор подложки и декора](DECOR-EDITOR.md) | Пользователи, тестировщики и разработчики | Инструменты, единое выделение и трансформации, физические размеры, магнит, Undo/Redo, поведение картинки-подложки и совместимость старых полей |
| [Продуктовый аудит и план улучшений](PRODUCT-IMPROVEMENT-PLAN.ru.md) | Владелец и разработчики продукта | Нелогичные, дублирующие и устаревшие функции, UX-риски, технический долг и поэтапный план рефакторинга |
| [GitHub Issues](https://github.com/Matysh/houseplan-card/issues) и [Project v2](https://github.com/users/Matysh/projects/1) | Владелец, разработчики и контрибьюторы | Единственный актуальный backlog: задачи, приоритеты, решения и статусы выполнения |
Старые тематические документы в этом каталоге остаются инженерными спецификациями и историей решений. При расхождении пользовательского описания с интерфейсом текущей версии приоритет имеет новое руководство, а при расхождении с фактическим поведением — код текущей версии.
+6 -3
View File
@@ -3,9 +3,12 @@
Goal: a **proper Home Assistant component** built to HA developer-docs patterns —
not a hardcoded feature. Universal (any house, any language, any plan format),
flexible (options instead of assumptions), and measured against the official
**Integration Quality Scale** even though custom integrations are not formally graded
(see `docs/PRODUCT-IMPROVEMENT-PLAN.ru.md` for the current product plan; the
original market rationale is archived at `legacy/docs/PRODUCT-2026-07-05.md`).
**Integration Quality Scale** even though custom integrations are not formally graded.
Current tasks and priorities live only in
[GitHub Issues](https://github.com/Matysh/houseplan-card/issues) and the linked
[Project v2](https://github.com/users/Matysh/projects/1); this file keeps
historical engineering direction. The original market rationale is archived at
`legacy/docs/PRODUCT-2026-07-05.md`.
Phases 0–6 of the original plan (server-side config, room markup editor, device
management, virtual devices, publication) are **done** — see CHANGELOG v1.3.0–v1.11.2.
+5 -3
View File
@@ -3,9 +3,11 @@
*Fixed with the owner on 2026-07-22. This document is a guard rail: features are
built, improved and accepted **only** if they serve a job listed here. When a new
idea appears, first find its row in this file; if there is none — it belongs to
HA core, to another card, or nowhere. Companion documents:
PRODUCT-IMPROVEMENT-PLAN.ru.md (current product audit and order of work),
ROADMAP.md (engineering/quality backlog), UX-MODES.md (interaction model).
HA core, to another card, or nowhere. The current order of work lives only in
[GitHub Issues](https://github.com/Matysh/houseplan-card/issues) and the linked
[Project v2](https://github.com/users/Matysh/projects/1). Companion
documents: ROADMAP.md (historical engineering direction), UX-MODES.md
(interaction model).
TOUCH-SUPPORT.md fixes the input-support contract: touch is a guaranteed View
surface, while every editor is desktop-first and best effort on touch.
The old market snapshot is archived at `legacy/docs/PRODUCT-2026-07-05.md`.*
+10 -12
View File
@@ -9,7 +9,8 @@
> `docs/CHANGELOG.md` (English) and `docs/CHANGELOG.ru.md` (Russian, since
> v1.42.0 — the user base is largely Russian-speaking, see the Telegram chat)**, STATUS.md for state changes
> (versions, publication, infrastructure), DEVELOPMENT.md for new gotchas,
> ARCHITECTURE.md for design changes, ROADMAP.md when plans move.
> ARCHITECTURE.md for design changes. Work scope and status live in GitHub
> Issues + Project v2, not in a parallel backlog document.
**Promotion rule (2026-08-08):** every new feature or material behaviour
change must pass through a published beta/RC before stable. Stable release
@@ -23,7 +24,7 @@ metadata). Only an explicit owner-approved emergency hotfix may skip this gate.
| Version | **v1.60.3-beta.2** everywhere (manifest, const.py, package.json, CARD_VERSION) — editor-context and touch-safety pre-release after v1.60.3-beta.1; `main` remains on stable v1.60.2 while the beta is published from `dev` |
| Current local cycle | v1.60.3-beta.2 adds the touch pinch/misclick guard and HP-UX-11 editor chrome: primary toolbars are stable with pinned Close, while selection actions, transient tool controls and palettes use one stage-owned context tray. The review follow-up covers reduced motion, honest stale/group tests, vacuum-fit gesture isolation, global palette/group dismissal, focus and accessibility details. HP-PERF-01 now has same-runner base-vs-candidate reports and a blocking CI gate; HP-QA-01 keeps the reviewed Linux baseline of 20 deterministic scenarios. |
| Workflow | Owner's rule since 2026-08-07: ordinary fixes/features are made **locally, without tests and without commits**. A requested pre-release gets a production build plus the smallest targeted unit/smoke set covering the changed surfaces, one tested `dev` commit/tag and a GitHub Release with `prerelease=true`; `main` stays untouched. The complete local frontend/backend/smoke gate runs only before a stable release, after which `main` is fast-forwarded to the exact tested `dev` SHA and the GitHub Release uses `prerelease=false`. Release bodies are short and bilingual (Russian first): only significant user changes get individual bullets, while minor/code-only work is grouped as `Мелкие исправления и улучшения` / `Small fixes and improvements`; every body ends with separate links to the Russian and English changelogs. Nothing is copied to the home instance by hand |
| GitHub | https://github.com/Matysh/houseplan-card — `main` carries stable releases; pre-release tags may point directly at `dev`. Work lands on `dev` and is merged into `main` for a stable release, so `dev` is normally equal to or ahead of `main`, never behind. Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
| GitHub | https://github.com/Matysh/houseplan-card — [Issues](https://github.com/Matysh/houseplan-card/issues) are the canonical task records and the linked [Project v2](https://github.com/users/Matysh/projects/1) is the canonical priority/status view; both must stay current. `main` carries stable releases; pre-release tags may point directly at `dev`. Work lands on `dev` and is merged into `main` for a stable release, so `dev` is normally equal to or ahead of `main`, never behind. Push via SSH key `ha_jb` (remote git@github.com:…); API releases via the fine-grained PAT in `~/.git-credentials` (Contents R/W, issued 2026-07-23) |
| CI | v1.60.3-beta.2 uses the owner-approved targeted pre-release gate locally. Exact-SHA Ubuntu Validate remains mandatory and runs the broader matrix; `release.yml` withholds assets until every matching run finishes green |
| HACS | Custom repository works. **Inclusion PR: hacs/default#9004** — open, valid, labeled, mergeable clean, never drafted. Queue: 1212 open, 835 older than ours. Merge rate COLLAPSED: 75 in July but almost all in the first decade, 0 in the last week (checked 2026-07-29) — maintainers process in rare bursts; ETA unknowable, months at best. Nothing actionable on our side |
| Home instance | ha.jbstudio.pro (SSH port **22222**, key `ha_jb`; HA config root is `/mnt/data/supervisor/homeassistant` — `/config` does NOT exist in this SSH environment), last direct copy was **v1.57.0**; from v1.58.0 on it updates itself through HACS by tag (no scp) |
@@ -215,16 +216,13 @@ metadata). Only an explicit owner-approved emergency hotfix may skip this gate.
## Open items / watchlist
0. **Current product audit (2026-08-06)** — [`PRODUCT-IMPROVEMENT-PLAN.ru.md`](PRODUCT-IMPROVEMENT-PLAN.ru.md)
(UX, functional integrity, architecture debt and prioritized roadmap). The
previous v1.58.0 market/quality pack is preserved under
[`legacy/docs/audit-v1.58.0`](../legacy/docs/audit-v1.58.0/AUDIT.md). Key
takeaway: engineering depth is strong; **easy-floorplan ≈430★** is now the
traction threat; P0s are demo GIF + forum posts + write-policy UI↔API align.
Agents: read the pack before proposing features or refactors.
**Partial follow-up from the archived audit landed on `dev` (2026-08-05):** P0-3 README diff, P0-4
write-policy (`can_write` + admin_only default on), P3-4/P3-5 validation &
hygiene. Still open: P0-1 demo GIF/video, P0-2 forum/Reddit posts.
0. **Canonical backlog** — [GitHub Issues](https://github.com/Matysh/houseplan-card/issues)
contain task scope and acceptance criteria; the linked
[Project v2](https://github.com/users/Matysh/projects/1) contains the
current priority and workflow status. Every open in-scope issue belongs in
the Project. The former local product plan is preserved only as a snapshot at
[`legacy/docs/PRODUCT-IMPROVEMENT-PLAN.ru.md`](../legacy/docs/PRODUCT-IMPROVEMENT-PLAN.ru.md)
and must not be updated or used as a backlog.
1. **hacs/default PR #9004** — accepted by the bot into the review queue ('New default
repository' label). Minor issues ⇒ the bot drafts the PR (fix and re-ready).
2. GitHub auth: fine-grained PAT (Contents R/W, issued 2026-07-23) in the sandbox
@@ -1,9 +1,9 @@
# Open spans + wall-centric Delete — design
> Superseded on 2026-08-06 by UX-02 in
> `docs/PRODUCT-IMPROVEMENT-PLAN.ru.md`: opening a boundary, closing a boundary,
> merging rooms, and deleting a room are now separate tools. This file remains
> only as the historical rationale for the original open-span implementation.
> Superseded on 2026-08-06 by the accepted UX-02 decision: opening a boundary,
> closing a boundary, merging rooms, and deleting a room are separate tools.
> This file remains only as the historical rationale for the original open-span
> implementation; current work is tracked in GitHub Issues + Project v2.
Status: **approved in chat** (2026-08-05). Implementation plan follows after
spec review.
+2 -1
View File
@@ -8,6 +8,7 @@
|---|---|
| `docs/audit-v1.58.0/` | Снимок аудита v1.58.0; заменён аудитом и планом улучшений v1.59.0-rc.1 |
| `docs/PRODUCT-2026-07-05.md` | Старая продуктовая оценка с устаревшей таблицей рынка |
| `docs/PRODUCT-IMPROVEMENT-PLAN.ru.md` | Последний снимок локального backlog; все актуальные пункты перенесены в GitHub Issues + Project v2 9 августа 2026 года |
| `docs/SUN-CONTRAST.md` | Отклонённая модель солнечного контраста; реализована только описанная в актуальном `docs/SUN.md` кромка |
| `docs/implementation-plans/` | Завершённые пошаговые планы реализации; действующие спецификации остаются в `docs/superpowers/specs/` |
| `demo/dbg_click.mjs` | Одноразовая диагностика старой проблемы клика |
@@ -16,6 +17,6 @@
Актуальные точки входа:
- [пользовательская документация](../docs/README.ru.md);
- [продуктовый аудит и план улучшений](../docs/PRODUCT-IMPROVEMENT-PLAN.ru.md);
- [GitHub Issues](https://github.com/Matysh/houseplan-card/issues) и [Project v2](https://github.com/users/Matysh/projects/1) — единственный актуальный backlog;
- [статус проекта](../docs/STATUS.md);
- [архитектура](../docs/ARCHITECTURE.md).
@@ -1,4 +1,9 @@
# House Plan — актуальный план улучшений продукта
# House Plan — архивный снимок плана улучшений продукта
> **Архивировано 9 августа 2026 года.** Все актуальные пункты этого снимка
> перенесены в [GitHub Issues](https://github.com/Matysh/houseplan-card/issues)
> и Project v2. Этот файл больше не является backlog, не обновляется и хранится
> только для истории решений.
Срез: **v1.60.3-beta.2**, 9 августа 2026 года.
+4 -2
View File
@@ -1,7 +1,9 @@
# Project audit — index
> **Archived:** this is the v1.58.0 snapshot. The current product audit is
> [`docs/PRODUCT-IMPROVEMENT-PLAN.ru.md`](../../../docs/PRODUCT-IMPROVEMENT-PLAN.ru.md).
> **Archived:** this is the v1.58.0 snapshot. Current work is tracked in
> [GitHub Issues](https://github.com/Matysh/houseplan-card/issues) and Project v2;
> the last local product-plan snapshot is archived at
> [`legacy/docs/PRODUCT-IMPROVEMENT-PLAN.ru.md`](../PRODUCT-IMPROVEMENT-PLAN.ru.md).
>
> **Audience:** future humans and agents. Read this before proposing features,
> refactors, or go-to-market work. Snapshot date: **2026-08-05**. Product
+4 -9
View File
@@ -3720,15 +3720,10 @@ class HouseplanCard extends LitElement {
private _stagePointerDown(ev: PointerEvent): void {
if (this._vacFit) return; // no pan/swipe while fitting the robot map
// Furniture deliberately supports stay-open-on-canvas while a symbol is
// armed. With no symbol selected, the same outside press is only dismiss:
// consume it so it cannot also select or begin another canvas operation.
if (this._mode === 'decor' && this._decorTool === 'furniture' && !this._furnPalette) {
ev.preventDefault();
this._suppressClick = true;
this._decorTool = 'select';
return;
}
// The shared secondary controller owns palette dismissal in window
// capture, including the matching synthetic click. Do not duplicate that
// lifecycle here: the former fallback left `_suppressClick` stuck when a
// pointerup was lost or a pointercancel followed the dismiss press.
if (this._kiosk) {
this._cyclePausedUntil = Date.now() + 60000;
if (this._pointers.size === 0) {
+15 -3
View File
@@ -314,7 +314,7 @@ export const cardStyles = css`
opacity: 0;
}
.bootveil .boothouse {
position: static; /* .stage svg pins itself to inset:0 — not this one */
position: static; /* the main .zoomwrap SVG is pinned to inset:0 — not this icon */
width: 56px;
height: 56px;
fill: var(--hp-accent);
@@ -417,7 +417,12 @@ export const cardStyles = css`
font-weight: 600;
pointer-events: none;
}
.stage svg {
/* Only the plan canvas owns the whole zoom wrapper. Context trays now live
inside .stage as well, and may contain small SVG previews (furniture in
particular); the old descendant-wide stage-SVG selector stretched every
preview over the tray and stacked them into one blocking artefact. The
vacuum trail/fit overlays have their own explicit geometry below. */
.zoomwrap > svg {
position: absolute;
inset: 0;
width: 100%;
@@ -1404,11 +1409,18 @@ export const cardStyles = css`
}
.modetab .closex:hover { opacity: 1; }
.editbar .barclose {
padding: var(--sp-2) var(--sp-3);
box-sizing: border-box;
width: 40px;
height: 40px;
padding: 0;
margin: 0;
min-width: 40px;
min-height: 40px;
justify-content: center;
gap: 0;
line-height: 0;
}
.editbar .barclose ha-icon { flex: none; margin: 0; }
.modetab.active {
background: var(--hp-accent);
color: var(--text-primary-color, #fff);
+35 -3
View File
@@ -1867,16 +1867,48 @@ export interface OpeningTunnelGeometry {
}
function tunnelFacePath(side: -1 | 1, pieces: OpeningWallPiece[]): string {
const eps = 1e-9;
const compact: OpeningWallPiece[] = [];
for (const piece of [...pieces]
.filter((candidate) => candidate.x1 > candidate.x0 && candidate.half > 0)
.sort((a, b) => a.x0 - b.x0 || a.x1 - b.x1 || a.half - b.half)) {
const tail = compact[compact.length - 1];
// Atomic wall profiles may split one visually continuous wall at room or
// model vertices. Painting every atom as its own SVG rectangle exposes
// their shared edges as hairlines at fractional zoom. Equal-depth atoms
// are one physical tunnel surface, so collapse them before rendering.
if (tail
&& piece.x0 <= tail.x1 + eps
&& Math.abs(piece.half - tail.half) <= eps) {
tail.x1 = Math.max(tail.x1, piece.x1);
} else {
compact.push({ ...piece });
}
}
const parts: string[] = [];
for (const p of pieces) {
if (!(p.x1 > p.x0) || !(p.half > 0)) continue;
for (let i = 0; i < compact.length; i++) {
const p = compact[i];
const y = side * p.half;
// Both faces overlap symmetrically across y=0 by a tiny amount. They are
// subpaths of one SVG fill, so this closes fractional-zoom hairlines
// without adding opacity or escaping the physical outer faces.
const seam = Math.min(p.half * 0.02, 0.05);
const y0 = -side * seam;
parts.push(`M ${p.x0} ${y0} L ${p.x1} ${y0} L ${p.x1} ${y} L ${p.x0} ${y} Z`);
let x0 = p.x0;
let x1 = p.x1;
const previous = compact[i - 1];
const next = compact[i + 1];
// A real thickness step cannot be collapsed into one rectangle. Overlap
// only its touching internal edge; all strips remain subpaths of one fill,
// so the overlap cannot stack alpha and never extends a tunnel endpoint.
if (previous && Math.abs(previous.x1 - p.x0) <= eps) {
x0 -= Math.min(Math.min(previous.half, p.half) * 0.02, 0.05);
}
if (next && Math.abs(p.x1 - next.x0) <= eps) {
x1 += Math.min(Math.min(next.half, p.half) * 0.02, 0.05);
}
parts.push(`M ${x0} ${y0} L ${x1} ${y0} L ${x1} ${y} L ${x0} ${y} Z`);
}
return parts.join(' ');
}
+20 -1
View File
@@ -19,7 +19,8 @@ test('golden matrix covers required geometry, rendering and adaptive surfaces',
const ids = GOLDEN_SCENARIOS.map((scenario) => scenario.id).join(' ');
for (const token of ['geometry', 'diagonal-45-opening', 'openings', 'openings-hidden',
'fill-light', 'fill-temp', 'fill-lqi', 'lighting', 'hover', 'zoom-040', 'zoom-250',
'warm-remount', 'dialog-mobile', 'color-popover'])
'warm-remount', 'dialog-mobile', 'color-popover', 'tray-wide', 'tray-medium',
'tray-narrow'])
assert.equal(ids.includes(token), true, token);
assert.equal(new Set(GOLDEN_SCENARIOS.map((scenario) => scenario.mode)).has('plan'), true);
assert.equal(new Set(GOLDEN_SCENARIOS.map((scenario) => scenario.mode)).has('devices'), true);
@@ -27,3 +28,21 @@ test('golden matrix covers required geometry, rendering and adaptive surfaces',
assert.equal(new Set(GOLDEN_SCENARIOS.map((scenario) => scenario.theme)).has('light'), true);
assert.equal(new Set(GOLDEN_SCENARIOS.map((scenario) => scenario.theme)).has('dark'), true);
});
test('editor tray golden contract covers every adaptive width in English and Russian', () => {
const tray = GOLDEN_SCENARIOS.filter((scenario) => scenario.editorTray);
assert.equal(tray.length, 6);
const widths = {
wide: (scenario) => scenario.viewport.width >= 1000,
medium: (scenario) => scenario.viewport.width >= 600 && scenario.viewport.width < 1000,
narrow: (scenario) => scenario.viewport.width < 600,
};
for (const [name, predicate] of Object.entries(widths)) {
const languages = new Set(tray.filter(predicate).map((scenario) => scenario.language));
assert.deepEqual(languages, new Set(['en', 'ru']), name);
}
assert.deepEqual(new Set(tray.map((scenario) => scenario.editorTray)), new Set([
'plan-selection', 'plan-tool', 'group', 'decor-selection',
'furniture-palette', 'decor-tool',
]));
});
+17
View File
@@ -417,9 +417,26 @@ test('openingTunnelGeometry: mixed atomic thickness clips each piece to its real
assert.ok(g);
assert.match(g.faces[0].d, /-1(?:\.0+)?\b/, '10 cm half-depth is present');
assert.match(g.faces[0].d, /-2(?:\.0+)?\b/, '20 cm half-depth is present');
assert.match(g.faces[0].d, /M -0\.02\b/,
'touching thickness steps overlap instead of exposing an antialiasing seam');
closeTo(g.maxY, 2);
});
test('openingTunnelGeometry: equal atomic strips collapse into one path without hairlines', () => {
const rooms = [{ id: 'r', poly: [[0, 0], [10, 0], [10, 6], [0, 6]] }];
const walls = [[0, 3], [3, 5], [5, 7], [7, 10]].map(([x0, x1]) => ({
key: wallKey([x0, 0], [x1, 0], pitch), a: [x0, 0], b: [x1, 0], cm: 15,
}));
const g = openingTunnelGeometry(
rooms, { x: 5, y: 0, angle: 0, length: 6 }, walls, [], pitch, 5, 1,
);
assert.ok(g);
for (const face of g.faces) {
assert.equal((face.d.match(/\bM /g) || []).length, 1,
'one continuous wall face must not expose the three internal SVG strip edges');
}
});
test('openingTunnelGeometry: virtual, zero-thickness, orphan and draft-only walls do not paint', () => {
const rooms = [{ id: 'r', poly: [[0, 0], [10, 0], [10, 6], [0, 6]] }];
const opening = { x: 5, y: 0, angle: 0, length: 2 };