mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 04:38:55 +00:00
`.room { transition: 0.12s }` interpolates a room's colour and its
fill-opacity / stroke-opacity independently, and the visible opacity is their
product. The states without a fill kept their alpha in the colour with the
default opacity 1 (.overlay transparent, .yard rgba 0.14, .outlined rgba
0.06 / 0.55, .picked rgba 0.25), while .styled writes an opaque colour plus
fill-opacity: var(--room-fill-op). On a change between the two on the same
node one half rose while the other fell, and mid-way the room was darker than
at either end. Opening the space settings on a floor with no fill (the dialog
shows "no fill" as its own colour at alpha 0) flashed every room grey for
~0.1 s, 0 -> 0.241 -> 0; cancelling the dialog after a preview, entering and
leaving the plan editor briefly darkened the fill (0.18 -> 0.317 -> 0.06).
Every .room state now writes an opaque colour plus *-opacity, and
transparent only together with a zero opacity. The transition itself,
.styled and the --room-* variables are unchanged; the space card takes the
same styles. The resting paint is the same: the witness records each state's
colour and visible opacity as dev drew them, and screenshots of seven resting
states (View without fill, with fill and borders, plan editor, room picked for
a merge, yard with and without borders, yard in the plan editor) are
pixel-identical to dev outside the plan editor's tool hint, whose text shifts
by a sub-pixel between runs on dev too.
Witness: new demo/smoke_room_fill_transitions.mjs, deterministic. A
MutationObserver pauses the room's transitions at their first frame right
after Lit commits, and the smoke seeks them through 0..120 ms in 15 ms steps.
Red on dev: fill overshoot 0.241 / 0.125 / 0.137 / 0.134 on the four paths,
stroke 0.241 / 0.242 on the first two. A real colour change of the same room
still runs a fill transition (catches `transition: none`).
A card-mod rule that sets only `fill` on an unfilled room now meets
fill-opacity 0; CHANGELOG and STYLING-HOOKS say to set the opacity with the
colour (the values are generated, STYLING-HOOKS §3.3).
Issue: #746
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
474 lines
24 KiB
Markdown
474 lines
24 KiB
Markdown
# Styling hooks — the selectors card-mod may rely on
|
||
|
||
Code: `src/houseplan-card.ts`
|
||
(`_renderDevice`, `_renderRoomLabel`, `_renderOpenings`, `_renderDecorLayer`,
|
||
the room shapes in `render()`, the space tabs in the header),
|
||
`src/space-render.ts` (the static `houseplan-space-card`).
|
||
Smoke: `demo/smoke_styling_hooks.mjs`.
|
||
|
||
This document is a **contract**, not a feature. It names the handful of
|
||
attributes and classes that the card promises not to rename, so that a user
|
||
who has already installed [card-mod](https://github.com/thomasloven/lovelace-card-mod)
|
||
can write a rule against the plan and keep it working across our releases.
|
||
|
||
---
|
||
|
||
## §1 The philosophy — we do not sell CSS
|
||
|
||
The card has no "custom CSS" field, no theme editor, no style templates, and
|
||
this document does not introduce any. **We are not building a styling layer,
|
||
and we do not support user CSS.**
|
||
|
||
What we are doing is narrower and cheaper: we stop *getting in the way* of a
|
||
user who already runs card-mod. Their CSS pierces our shadow root on its own;
|
||
all they were missing was something stable to aim at. Until now the plan had
|
||
meaningful class names but almost no identifiers — five `data-` attributes in
|
||
the whole source, every one of them an internal implementation detail
|
||
(`data-corner` on a drag handle, `data-mid` on a vacuum trail). A rule like
|
||
"make the icon of *this* sensor bigger" had to be written against
|
||
`:nth-child(7)` and broke the next time a device was added.
|
||
|
||
The distinction from **ha-floorplan** matters and is deliberate. There, CSS
|
||
is a *required part of the product*: you configure the floorplan by writing
|
||
stylesheets and templates, so every user is a stylesheet author and every
|
||
release is a chance to break one. That is where their 138-post
|
||
"my styles no longer apply" thread comes from. Here, the plan is configured
|
||
by clicking, the card ships finished, and CSS is something a **power user**
|
||
may add **on their own responsibility** — the way a person may add a browser
|
||
userstyle to any web page.
|
||
|
||
Consequences, stated plainly:
|
||
|
||
- We promise the names in §3 and the test hooks in §7 are **stable**. If we ever have to change one, it
|
||
is a breaking change and it goes in the changelog.
|
||
- We promise nothing about **anything not in §3** — internal classes, DOM
|
||
nesting, element order, the presence of a wrapper `<div>`, the internals of
|
||
Home Assistant's own components.
|
||
- We do not review, debug or support user CSS, and "card-mod rule stopped
|
||
working" is not a bug report we can act on unless it names a §3 selector.
|
||
- We will not add hooks on request one by one. §3 covers the objects the plan
|
||
*is* — devices, rooms, openings, decor, labels, floors. That is the whole
|
||
model; there is nothing else on the plan to aim at.
|
||
|
||
---
|
||
|
||
## §2 The model — one attribute answers "what is this"
|
||
|
||
Every object the plan draws now carries the same identity:
|
||
|
||
| Attribute | Meaning |
|
||
| --- | --- |
|
||
| `data-hp` | **what kind of object** this is — the one attribute to branch on |
|
||
| `data-id` | the object's **own id** in the stored config, stable across restarts |
|
||
| `data-kind` | the sub-type, where a kind has one (`door`/`window`/`gate`, `line`/`rect`/…) |
|
||
| `data-entity` | the entity the object **presents**, where it presents one |
|
||
| `data-area` | the Home Assistant **area id**, where the object belongs to one |
|
||
|
||
`data-hp` is the anchor: `[data-hp="device"]` selects every device marker on
|
||
the plan and nothing else, regardless of which layer, mode or element type it
|
||
happens to be rendered as today. It is deliberately one attribute rather than
|
||
a class, so it can never collide with a state class (`.on`, `.sel`, `.ghost`)
|
||
and so `querySelectorAll('[data-hp]')` enumerates the whole contract.
|
||
|
||
**Absent is absent.** An attribute is *not rendered at all* when the object
|
||
has no such value: a virtual marker with no entity has no `data-entity`, a
|
||
room outside any HA area has no `data-area`, and a room drawn before ids
|
||
existed has no `data-id`. There is never a `data-area="undefined"` and never
|
||
an empty string — `[data-area]` means "this one has an area".
|
||
|
||
**Ids are the config's, not the DOM's.** `data-id` is the id stored in
|
||
`houseplan.config` (marker id, room id, opening id, decor shape id). It
|
||
survives reloads, re-mounts and re-orderings; it changes only when the object
|
||
is deleted and drawn again. A device that is auto-discovered and has no
|
||
marker of its own uses the same synthetic id the card uses for it internally.
|
||
|
||
---
|
||
|
||
## §3 The table — element → attributes → class
|
||
|
||
Everything in this table is **public API**.
|
||
|
||
| Object | Element | `data-hp` | Other attributes | Public class |
|
||
| --- | --- | --- | --- | --- |
|
||
| Device marker | `div` (HTML, marker layer) | `device` | `data-id` = marker/device id, `data-entity` = primary entity id, `data-area` = area id, `data-binding-status` = `active` \| `ha-disabled` \| `orphaned` \| `unverified`, `data-disabled-reason` = `device` \| `entity` \| `all-entities` when applicable | `.dev` |
|
||
| Room shape | `polygon` / `rect` / `path` (SVG) | `room` | `data-id` = room id, `data-area` = area id | `.room` |
|
||
| Room card (name + metrics) | `div` (HTML, marker layer) | `room-label` | `data-id` = room id, `data-area` | `.roomlabel` |
|
||
| Door / window / gate | `g` (SVG) | `opening` | `data-id` = opening id, `data-kind` = `door` \| `window` \| `gate` | `.opening` |
|
||
| Coloured tunnel inside a thick opening | `path` / `g` (SVG) | `opening-tunnel` | `data-id` = opening id, `data-kind` = `door` \| `window` \| `gate` | `.opening-tunnel` |
|
||
| Wall body (thickness) | `path` (SVG) | `wall` | `data-id` = segment key, `data-kind` = `shared` \| `outer` | `.wallbody` |
|
||
| Independent partition | `path` (SVG editor hit target) | `partition` | `data-id` = partition id, `data-kind` = `partition` | `.physical-hit` |
|
||
| Wall column | `path` / `circle` (SVG editor hit target) | `wall-column` | `data-id` = column id, `data-kind` = `square` \| `circle` | `.physical-hit` |
|
||
| Stair | `g` (SVG) | `stair` | `data-id` = stair id, `data-kind` = `straight` \| `spiral`, `data-target-state` = `active` \| `missing` \| `self` \| `deleted` \| `fixed` | `.hp-stair` |
|
||
| Decor shape | `line` / `rect` / `ellipse` / `text` (SVG) | `decor` | `data-id` = shape id, `data-kind` = `line` \| `rect` \| `ellipse` \| `text` | `.dshape` (`.dtext` on text); persisted colour/alpha are inline SVG attributes and therefore win over weak CSS selectors |
|
||
| Furniture | `path` (SVG) | `decor` | `data-id` = shape id, `data-kind` = `furniture`, `data-symbol` = the symbol id (`sofa`, `toilet`, …) | `.dshape .dfurn` |
|
||
| Floor / space tab | `button` (HTML, header) | `space-tab` | `data-id` = space id | `.tab` |
|
||
|
||
`data-symbol` is the furniture library's own vocabulary (docs/FURNITURE.md
|
||
§3): the ids in that table are public and stable, and new ones are only ever
|
||
added. It is what lets one rule colour a whole category:
|
||
|
||
```css
|
||
ha-card [data-kind="furniture"][data-symbol="bathtub"],
|
||
ha-card [data-kind="furniture"][data-symbol="toilet"] { stroke: #4fc3f7; }
|
||
```
|
||
|
||
### 3.1 Public classes inside a device marker
|
||
|
||
The marker's own children are part of the contract too — they are what a rule
|
||
usually wants:
|
||
|
||
| Class | What it is |
|
||
| --- | --- |
|
||
| `.dev` | the marker box itself |
|
||
| `.dev ha-icon` | the icon (a Home Assistant element — see §5) |
|
||
| `.valtext` | the value badge of a "value instead of icon" marker |
|
||
| `.tval` | the small temperature plate next to the icon |
|
||
| `.hval` | the small humidity plate |
|
||
| `.lqi` | the Zigbee signal badge |
|
||
| `.ripple` | the presence-ripple rings |
|
||
| `.newdot` | the "new device" dot |
|
||
|
||
Inside a room card: `.rlname` (the name) and `.rlmetrics` / `.rlm` (the
|
||
metric row and one metric in it).
|
||
|
||
**Breaking hook change in #203.** The former SVG fallback
|
||
`text.rlabel[data-hp="room-label"]` has been removed. A space with
|
||
`show_names: false` now emits no permanent room-label element at all, as the
|
||
setting promises. Target the HTML room-card hook
|
||
`[data-hp="room-label"].roomlabel` when room names are enabled. Plan editor
|
||
temporarily renders the same HTML card so it can still be positioned.
|
||
|
||
### 3.2 Public state classes
|
||
|
||
These are set on `.dev` and describe **live state**, so a rule may key off
|
||
them:
|
||
|
||
`.on`, `.off`, `.unavail`, `.alarm`, `.ghost` (hidden device shown in the
|
||
device editor), `.virtual` (a marker with no entity), `.valonly` (value
|
||
instead of icon), `.static-icon` (the always-static display mode), `.noicon`,
|
||
`.sel` (selected in an editor).
|
||
|
||
Unified device activity is exposed by one descendant `.device-pulse` with
|
||
exactly one kind class: `.alarm`, `.short` or `.continuous`. Semantic reason is
|
||
available as `.reason-alarm`, `.reason-event`, `.reason-presence`,
|
||
`.reason-transition` or `.reason-running`. Reduced-motion ordinary activity is
|
||
the compact `.activity-dot`; no static ring is emitted. The legacy
|
||
`.activity-ring` descendant class remains a beta compatibility alias only and
|
||
must not be used by new integrations.
|
||
|
||
The full plan and `houseplan-space-card` emit the same binding-status data
|
||
attributes. A forced-hidden HA-disabled marker is absent from ordinary View;
|
||
the attributes are visible on its service ghost in the Device editor.
|
||
|
||
On a room shape: `.filled` (a fill mode is painting it), `.styled`,
|
||
`.overlay` / `.yard` (drawn over a picture / on bare canvas).
|
||
|
||
A room shape keeps its transparency in `fill-opacity` / `stroke-opacity`,
|
||
never in the colour: every state is an opaque colour plus an opacity, and paint
|
||
that is off has the opacity `0` (#746). A rule that recolours a room therefore
|
||
sets the opacity together with `fill` / `stroke`. The values themselves are
|
||
generated (§3.3).
|
||
|
||
### 3.3 Explicitly NOT the contract
|
||
|
||
These exist in the DOM and **will** change without notice. Do not build on
|
||
them:
|
||
|
||
- **Boot and transition classes** — `.hpboot`, `.hpsettle`,
|
||
`.mode-transition`, `.editorchrome.transitioning`, and the four-phase
|
||
`.daycycle` / `.phase-*` environment classes. Their
|
||
whole job is to be replaced.
|
||
- **Editor chrome and previews** — `.dtframe`, `.dthandle`, `.dtknob`,
|
||
`.dtbox`, `.dtstem`, `.bdhandle`, `.rszhandle`, `.rszicon`, `.rszframe`,
|
||
`.ddraft`, `.vacfithandle`, the align guides, the markup layer (apart from
|
||
the physical-object identity attributes explicitly listed in §3),
|
||
`data-corner`, `data-mid`. These are the drawing tools' own furniture; they
|
||
are redesigned whenever an editor is.
|
||
- **Layout wrappers** — `.stage`, `.zoomwrap`, `.devlayer`, `.decorlayer`,
|
||
`.measurelayer` and their nesting. The layers are how we composite; they
|
||
are not where the objects live.
|
||
- **Dialog classes and nesting** — `.dialog`, `.menuwrap`, `.entrow`,
|
||
`.inforow` and friends. Dialog markup follows the dialog's design, and the
|
||
design changes. Only the sparse semantic test hooks in §7 are stable.
|
||
- **Everything generated** — CSS custom properties starting with `--hp-`,
|
||
`--room-*`, `--dev-*`, `--ripple-*`, `--rl-*` are set inline by the
|
||
renderer. Reading them is fine; overriding them may fight the renderer,
|
||
which writes them again on the next `hass`.
|
||
|
||
---
|
||
|
||
## §4 Examples
|
||
|
||
Real card-mod configuration, on the card that carries the plan. (card-mod is
|
||
a separate community integration — install it first; we do not ship it.)
|
||
|
||
**Hide every Zigbee signal badge.**
|
||
|
||
```yaml
|
||
type: custom:houseplan-card
|
||
card_mod:
|
||
style: |
|
||
ha-card [data-hp="device"] .lqi { display: none; }
|
||
```
|
||
|
||
**Make one specific marker twice the size** — the front door lock, named by
|
||
its entity, so it keeps working after the device is renamed or moved:
|
||
|
||
```yaml
|
||
card_mod:
|
||
style: |
|
||
ha-card [data-hp="device"][data-entity="lock.front_door"] {
|
||
--dev-scale: 2;
|
||
z-index: 5;
|
||
}
|
||
```
|
||
|
||
**Recolour the room captions of one floor**, and make the kitchen's stand
|
||
out — by area, which is the name Home Assistant knows it by:
|
||
|
||
```yaml
|
||
card_mod:
|
||
style: |
|
||
ha-card [data-hp="room-label"] { font-weight: 700; letter-spacing: .02em; }
|
||
ha-card [data-hp="room-label"][data-area="kitchen"] { color: #c62828; }
|
||
```
|
||
|
||
**Dim every window, keep the doors bright** — openings by kind:
|
||
|
||
```yaml
|
||
card_mod:
|
||
style: |
|
||
ha-card [data-hp="opening"][data-kind="window"] { opacity: .45; }
|
||
```
|
||
|
||
The supported Solid / Dashed choice for a decor line lives in its properties
|
||
dialog. For a custom dash pattern beyond those two product styles, card-mod can
|
||
still target one line by its shape id from the config:
|
||
|
||
```yaml
|
||
card_mod:
|
||
style: |
|
||
ha-card [data-hp="decor"][data-kind="line"][data-id="dc_17"] {
|
||
stroke-dasharray: 12 8;
|
||
}
|
||
```
|
||
|
||
> **Disclaimer.** card-mod is not ours: we do not ship it, do not support it,
|
||
> and do not answer for what your CSS does to the card. The examples above are
|
||
> illustrations of the selectors, not supported configuration. If a rule
|
||
> misbehaves, remove it first and then decide whether there is a card bug
|
||
> underneath.
|
||
|
||
---
|
||
|
||
## §5 Limits — where a selector cannot reach
|
||
|
||
**The card's own shadow root is fine.** Everything in §3 is rendered by the
|
||
card into its single Lit shadow root, including all dialogs and menus — we
|
||
never portal a dialog to `document.body`. card-mod's shadow piercing reaches
|
||
all of it.
|
||
|
||
**Home Assistant's own elements are not.** `ha-icon`, `ha-switch`,
|
||
`ha-slider`, `ha-card` are separate custom elements with shadow roots of
|
||
their own, and a descendant selector stops at their boundary. You can style
|
||
the **host** — `.dev ha-icon { color: … ; transform: … }` works, because the
|
||
icon inherits `color` and the host takes the transform — but you cannot reach
|
||
the `<svg>` inside `ha-icon`. That is a browser rule, not our choice, and it
|
||
is why we put the hooks on *our* wrappers rather than expecting a rule to
|
||
target the icon element itself.
|
||
|
||
**`houseplan-space-card` is a different card.** The read-only space card is
|
||
its own custom element with its own shadow root, so it needs its own card-mod
|
||
block. It carries the same `data-hp` attributes for the objects it draws:
|
||
rooms, room labels, device markers, openings (`data-hp="opening"` with
|
||
`data-id`/`data-kind`, class `.static-opening`) and decor images
|
||
(`data-hp="decor"`, class `.dimage`). Vector decor shapes and furniture are not
|
||
drawn there, so their hooks simply are not there.
|
||
|
||
**The kiosk header is visually absent.** The full header is hidden by CSS in
|
||
kiosk mode, but most of its existing children remain in the DOM. A selector
|
||
can therefore still match hidden `space-tab` and zoom elements. Actions which
|
||
the renderer already omits, such as `space-add`, remain absent; test code must
|
||
not confuse a DOM match with visibility or permission.
|
||
|
||
**One rule, both modes.** Editors add classes to the same elements rather than
|
||
re-rendering different ones, so a rule written against the View mode also
|
||
applies while the user is editing. If that is not wanted, scope it: the stage
|
||
carries `mode-view` / `mode-plan` / `mode-devices` / `mode-decor`, and those
|
||
four names are part of the contract for exactly this purpose.
|
||
|
||
---
|
||
|
||
## §6 Values are formatted by Home Assistant
|
||
|
||
A styling contract is about *how* a value looks; this section is about *what*
|
||
the value is, because the two questions arrive together — "why does my sensor
|
||
show `22.4` here and `22,4 °C` everywhere else in HA".
|
||
|
||
Wherever the card prints **one entity's state or attribute**, it now asks
|
||
Home Assistant to format it: `hass.formatEntityState(stateObj)` for a state,
|
||
`hass.formatEntityAttributeValue(stateObj, attr)` for an attribute. That is
|
||
the same call HA's own more-info and entities card make, so the number obeys
|
||
the entity's `display_precision`, the user's decimal separator, and the state
|
||
translations (`on` → *Включено*). One wrapper owns it — `hassValue()` in
|
||
`src/logic.ts` — and every printing site goes through it:
|
||
|
||
| Where | What it prints |
|
||
| --- | --- |
|
||
| Value badge (`display: value`) | the acting entity's state |
|
||
| Decor live text (docs/DECOR-EDITOR.md §5) | the linked entity's state or attribute |
|
||
| Device info card | the primary state and every listed entity |
|
||
|
||
**Fallbacks are silent.** An older Home Assistant without
|
||
`formatEntityState` gets exactly today's behaviour — the raw state, with the
|
||
entity's `unit_of_measurement` appended by us. The same is true of
|
||
`formatEntityAttributeValue`, which is newer still. Nothing throws, nothing
|
||
is blank; the wrapper reports which path it took so the caller knows whether
|
||
a unit is already in the string.
|
||
|
||
**The unit appears exactly once.** HA's formatter normally appends the
|
||
entity's own unit itself — so blindly adding ours would double it, and blindly
|
||
trusting it would drop the unit wherever it does not. One rule survives both:
|
||
strip the entity's own `unit_of_measurement` if it is already the tail (exact
|
||
trailing match, nothing else in the text is touched), then append the unit
|
||
that is actually wanted — the user's explicit one on a decor label, the
|
||
entity's own otherwise. A value with no unit at all — a translated state
|
||
(«Включено») — never grows a suffix. `valueWithUnit()` in `src/logic.ts`.
|
||
|
||
**The °/% plates keep their own form.** The small temperature and humidity
|
||
plates next to an icon (`.tval`, `.hval`), the same numbers in a room card
|
||
(`.rlm`) and in the tooltip are **not** an entity state readout: they are a
|
||
derived reading — an average over every sensor of the area, or a climate
|
||
device's `current_temperature` attribute — rendered as a fixed compact glyph
|
||
(`21.5°`, `48%`) so a plan full of them reads as one instrument panel. There
|
||
is no single entity whose `display_precision` applies to an average, and a
|
||
formatter would put `°C` into a badge the size of a fingernail. They stay
|
||
ours, deliberately (owner's call, 2026-08-05).
|
||
|
||
---
|
||
|
||
## §7 Test hooks — stable selectors for external E2E
|
||
|
||
The object hooks in §3 are also suitable for tests, but a real Home Assistant
|
||
browser test needs a few controls and state boundaries which are not objects on
|
||
the plan. Those selectors are declared here. They are deliberately sparse:
|
||
their purpose is to let a test express user intent without reading private
|
||
fields or depending on layout classes.
|
||
|
||
The machine-readable authority is
|
||
[`data-hp-contract.json`](data-hp-contract.json). It records each value, the
|
||
element types on which it can occur, its first version and whether its audience
|
||
is styling, testing or both. An external test suite should validate its
|
||
selectors against that file before driving the UI.
|
||
|
||
### 7.1 Compatibility promise
|
||
|
||
- A public `data-hp` value, `data-hp-state`/`data-hp-mode` value, documented
|
||
`data-kind`, or documented `data-tool` is not renamed or removed silently.
|
||
- A necessary incompatible change is announced in both changelogs and keeps a
|
||
compatible transition for one following **stable** release.
|
||
- Adding a new value is compatible and does not change `schemaVersion`.
|
||
- The JSON `schemaVersion` changes only when the JSON structure itself becomes
|
||
incompatible.
|
||
- Presence follows the product. If permission, mode or state means that an
|
||
existing control is not rendered, its hook is absent too. No hidden testing
|
||
duplicate is created.
|
||
- These attributes do not grant permission and are not product logic. Removing
|
||
one in DevTools or card-mod must not change how the card behaves.
|
||
|
||
### 7.2 Root state
|
||
|
||
On every rendered `houseplan-card` root `ha-card`:
|
||
|
||
| Attribute | Values | Meaning |
|
||
| --- | --- | --- |
|
||
| `data-hp-state` | `booting` \| `ready` | The existing visual boot barrier is active, or the card has crossed it. An empty configured plan can be ready. |
|
||
| `data-hp-mode` | `view` \| `plan` \| `devices` \| `decor` | The current public mode. `devices` intentionally matches the existing `mode-devices` and `data-editor-navigation="devices"` contracts. |
|
||
|
||
The compact `houseplan-space-card` has its own lifecycle and does not expose
|
||
these full-card attributes.
|
||
|
||
### 7.3 Header and empty state
|
||
|
||
| Selector | Existing element |
|
||
| --- | --- |
|
||
| `[data-hp="settings"]` | General settings button |
|
||
| `[data-hp="pdf"]` | PDF export button |
|
||
| `[data-hp="support"]` | Help and feedback button |
|
||
| `[data-hp="zoom-in"]`, `[data-hp="zoom-out"]`, `[data-hp="zoom-fit"]` | Existing camera buttons |
|
||
| `[data-hp="space-add"]` | Add-space button, when permitted |
|
||
| `[data-hp="space-settings"][data-id="…"]` | Settings gear for one space |
|
||
| `[data-hp="empty"]` | Existing empty, fixed-floor loading or fixed-floor error surface |
|
||
| `[data-hp="create-space"]` | First-space action, only for a user already allowed to see it |
|
||
| `[data-hp="toast"][data-kind="message"]` | Current transient message |
|
||
|
||
Zoom controls remain in the DOM inside the CSS-hidden kiosk header, exactly as
|
||
before. `space-add` keeps its stricter existing rule and is not rendered in
|
||
kiosk mode.
|
||
|
||
### 7.3a Phone header menu (#616)
|
||
|
||
At a window width of 480 px or less the header is one row: space tabs, zoom
|
||
and a gear `[data-hp="header-menu"]` (44 × 44, `aria-haspopup`,
|
||
`aria-expanded`). Everything the row drops — editor tabs, space settings, add
|
||
space, general settings, PDF, help and the summary-panel pair (the projection
|
||
item is gone since #649) — is a `[data-hp="header-menu-item"][data-id="…"]` button of
|
||
its menu (`#hp-header-menu`, `role="group"`), each at least 44 px high; the
|
||
vocabulary of `data-id` is in the JSON inventory. While the menu is open a
|
||
transparent `[data-hp="header-menu-scrim"]` swallows the tap that dismisses it.
|
||
Above 480 px the gear is in the DOM but hidden, and the ordinary buttons are
|
||
unchanged. Kiosk renders neither.
|
||
|
||
### 7.4 Editors
|
||
|
||
Each active editor exposes one primary
|
||
`[data-hp="toolbar"][data-kind="plan|device|decor"]`. Tool and command launchers
|
||
use `[data-hp="tool"][data-tool="…"]`; the complete `data-tool` vocabulary is
|
||
in the JSON inventory. The toolbar X and the header X (its own slot after the mode tabs, #647) use
|
||
`[data-hp="editor-close"]` while retaining `data-editor-navigation="view"`.
|
||
|
||
The three editor mode tabs are `[data-hp="mode-tab"][data-mode="plan|devices|decor"]`
|
||
(#629) and keep `data-editor-navigation`. Like the tabs themselves, the hook is
|
||
present only for a user allowed to edit. Nothing in the card's styles selects
|
||
it; its audience is `test`.
|
||
|
||
The one visible secondary/context surface uses `[data-hp="tray"]`. Its
|
||
`data-kind` is the stable launcher id when a grouped launcher owns it and the
|
||
broad secondary kind otherwise. A closed placeholder does not carry the hook.
|
||
|
||
Undo/Redo, colour pickers and Save/Cancel controls inside a tray are not tools
|
||
and are not assigned `data-tool`.
|
||
|
||
### 7.5 Dialogs
|
||
|
||
Every `hp-dialog` host is `[data-hp="dialog"][data-kind="…"]`. `data-kind` is a
|
||
broad user workflow rather than a private method name; the complete initial
|
||
vocabulary is in the JSON inventory. A workflow may reuse the same kind across
|
||
several steps.
|
||
|
||
An existing principal action which accepts the current dialog result uses
|
||
`[data-hp="dialog-confirm"]`. An existing Cancel/Close action which dismisses
|
||
without accepting uses `[data-hp="dialog-cancel"]`. Alternate accepted outcomes
|
||
may produce more than one confirm button. A step which has no such visible
|
||
button does not gain a synthetic one. The native fallback X is a cancel hook;
|
||
Home Assistant's own close control stays inside HA's private shadow root.
|
||
|
||
### 7.6 Sidebar panel
|
||
|
||
Inside `houseplan-panel`'s own shadow root, `[data-hp="panel-menu"]` opens the HA
|
||
menu and `[data-hp="panel-title"]` identifies the visible House Plan title.
|
||
The rest of the panel shell remains private.
|
||
|
||
### 7.7 Internal diagnostics are not public API
|
||
|
||
The repository also uses `data-hp` for screenshot and diagnostic probes. The
|
||
JSON `internalPrefixes` and `internalExactValues` fields allow the source gate
|
||
to distinguish them from omissions, but do **not** make them stable. Current
|
||
families include `iso-*`, `zigbee-topology-*`, `resize-*`, `plan-snap-*` and
|
||
`hidden-wall-*`. Do not use them from card-mod or an external E2E repository.
|
||
|
||
`room-draft` is listed separately as retired metadata: #478 removed the object
|
||
type itself by making every unfinished chain ordinary partitions. There is no
|
||
current DOM state in which that legacy hook can appear; new tests use
|
||
`partition`.
|