Skip to content

Releases: Michailjovic/Room-Card

v6.0.0

Choose a tag to compare

@Michailjovic Michailjovic released this 08 Aug 09:14

[6.0.0] - 2026-08-08

Documentation overhaul + editor UX rebuild milestone

README split into a landing page + two focused docs. The README had grown to ~860 lines,
most of it the configuration reference and the editor walkthrough — useful, but it buried the
pitch, install steps and quick start under a wall of YAML examples. It's now a short landing page
(pitch, feature table, screenshots, install, quick start, links out) plus:

  • docs/CONFIGURATION.md — the full YAML reference: every top-level
    key, the layout engine, conditions, filters, overlays, gauges, blinds & cover control, zones,
    badges, icons/labels, embedded cards, companion cards, light controls, camera/weather, template
    visibility, groups, multi-room, and the complete example. Moved verbatim from the README, not
    rewritten — no behaviour or documented-key changes.
  • docs/EDITOR.md — the GUI editor tab by tab (Image / Elements / Layout /
    Rooms & menu), Edit mode in full, and a new summary section rolling up what the editor UX
    rebuild (below) actually changed and why.

LAYOUT.md and PRESETS.md are unchanged and now cross-linked from both new docs.

Marks the editor GUI/UX revalidation as done. v5.9.0–v5.10.1 shipped four changes from the
EDITOR_UX_REVALIDATION.md review, discussed and decided incrementally rather than implemented
wholesale: the header's Test mode + Drag-edit preview merged into one Edit mode toggle
(v5.9.0); the Layout tab gained Portrait/Landscape sub-tabs with a live mini grid preview,
plus a real fix so Layout edits reach the mounted Edit-mode preview card (v5.9.11); the Rooms &
menu tab
split into four accordions matching the Elements tab's pattern (v5.9.13); and the dead
dock_side blind-editor control, found by the code audit, was removed (v5.10.1). Two proposals
from the same review were explicitly rejected after discussion — reordering Elements sections by
intent (alphabetical-with-icons stays) and a per-section text filter (low value at typical room
sizes). Full history in ROADMAP.md and EDITOR_UX_REVALIDATION.md.

Roadmap re-scope

ROADMAP.md had reserved v6.0.0 for the HACS default-repository submission milestone (decided
2026-08-05), with the version bump meant to land only after a hacs/default PR is accepted. That
plan changed today: v6.0.0 now marks this documentation overhaul instead, and the HACS submission
becomes its own later milestone with a version number chosen at that time — so v6.0.0 isn't
reserved-then-reused, it just means something different than originally planned. ROADMAP.md
updated to reflect this and to correct its stale "current release" line (it still said v5.4.0).

Not done

No code, config keys, or behaviour changed in this release — documentation and versioning only.
ROC_VERSION and package.json bumped to match.

v5.10.1

v5.10.1 Pre-release
Pre-release

Choose a tag to compare

@Michailjovic Michailjovic released this 08 Aug 06:32

[5.10.1] - 2026-08-08

Editor honesty & repo tidy-up (closes the code audit)

The "Dock side" select did nothing — removed. The blind editor offered a left/right dropdown that wrote control.dock_side; coverControlNorm() turned it into a side field that nothing ever read. It could not have worked: a docked cover control is flex:1 1 0 and therefore fills its cover grid region, so there is no free space to align it within — the side comes entirely from where the cover region sits in the Layout tab, per profile. (The README already said exactly that, one bullet below the line documenting dock_side.) Removed rather than reimplemented: a knob that cannot affect anything is worse than no knob, because it sends you hunting for a layout bug that isn't there. No visual impact — it never had an effect. An existing dock_side: key is ignored exactly as before and is dropped the next time the blind is saved from the editor. README corrected; historical CHANGELOG entries left as-is.

Editor escaping unified with the card's. The editor carried its own escaping helper that differed from escA() in two ways, both bugs: it did not escape ' (harmless today — all 136 call sites were checked and every one lands in a double-quoted attribute or a text node — but a trap for the next single-quoted attribute anyone adds), and it used String(s) rather than String(s ?? ''), so an unset field rendered the literal text undefined (or null) into its input box. _e() now delegates to escA(); verified beforehand that the editor uses no inline on* handlers, so escaping apostrophes can't break anything.

Housekeeping. 54 RELEASE_NOTES_v*.md files moved from the repo root to docs/releases/ — they were crowding out the documents people actually open (README, LAYOUT, PRESETS, ROADMAP). Moved with git mv, so git log --follow still works on each; CHANGELOG.md stays the canonical history. Stale test-results/ artifacts deleted — they recorded six "failures" that were only ever a missing Chromium install in a sandbox, and made the suite look broken at a glance.

Testing

8 new assertions in tests/lifecycle.test.js (43 total). Four of them fail against v5.10.0 — they reproduce the bugs rather than just describing the fixes; the other four are regression nets around nullish rendering. Also covered: a legacy dock_side: key in an existing config still loads and saves without error. Full suite green against both the source and the minified bundle.

Not done: the structural refactor

Step 4 of CODE_ANALYSIS_v5.9.13.md (splitting the file into src/) is dropped, and the report's recommendation there was wrong on two counts. It claimed the split would make the pure helpers unit-testable — they already are, via smoke.test.js loading the file with vm.runInContext and exercising them off the sandbox global (100+ assertions). And src/ already existed once as TypeScript + rollup, drifted out of sync with the shipped JS, and was deliberately removed in v1.3.0 with the note that room-overlay-card.js is now the single source of truth; re-splitting would recreate precisely that failure mode. The editor's real problem — 101 fields described three times across _render/_collectConfig/_listen, 1633 lines — is genuine but currently correct: an audit of all 101 ids found no dead or drifted controls. Future maintenance risk, not a present defect; better as a deliberate incremental project than on refactor momentum.

v5.10.0.1

v5.10.0.1 Pre-release
Pre-release

Choose a tag to compare

@Michailjovic Michailjovic released this 08 Aug 05:52

[5.10.0.1] - 2026-08-08

Minified release build (step 3 of the code audit)

The release asset is now minified: 413.8 → 284.8 KB raw, 110.1 → 69.8 KB gzipped (−36.5 %), on a file every dashboard load pulls. The repo keeps the readable source — only the published artifact is compressed, so manual installs from the repo and HACS installs from the release differ in size but not behaviour.

build.js runs terser with property mangling explicitly off, which is the one setting that must never change: mangling properties would rename setConfig, hass, getCardSize and break every HA integration point at once. Class and function names are manglable — custom elements register by string and nothing reads constructor.name. --check fails the build if the output is truncated, not smaller than the source, or missing the version string / element registrations.

npm run build:verify runs all three test tiers against the minified bundle. The tiers already accepted a path argument, so this cost nothing to add — and it's the check that matters, since the bundle is what users install. Source and bundle produce byte-for-byte identical pass/fail output across all 213 assertions. Two tests had to change: the version checks in smoke.test.js and render.test.js matched const ROC_VERSION='…' in the source text, which doesn't survive minification; both now read the version at runtime from the customCards registration.

The release workflow builds before uploading and then downloads the asset back and byte-compares it against the freshly built file, instead of only checking that an asset with the right name exists. v4.6.2 shipped an asset-less release after a transient GitHub 503 and failed in HACS with "unknown error" — an asset with wrong or truncated contents would fail just as opaquely, and name-only verification wouldn't catch it. The sourcemap ships as a second asset. CI builds and verifies on every push, so a terser setting that silently breaks the card fails the PR.

v5.10.0

v5.10.0 Pre-release
Pre-release

Choose a tag to compare

@Michailjovic Michailjovic released this 08 Aug 04:55

[5.10.0] - 2026-08-08

Hot-path performance (step 2 of the code audit)

Pure optimisation — no new features, no config changes, no visible behaviour change. Measured against v5.9.14 on a 3-room config (6 overlays + 5 zones + 8 icons + 8 labels + 4 gauges per room): 300× _update() went from 234 ms / 2 400 querySelector / 300 offsetWidth to 148 ms / 0 / 0 (−37 % time), and a 60-frame resize drag from 32 ms / 480 querySelector / 120 offsetWidth to 1.3 ms / 0 / 60 (−96 % time). jsdom timings aren't browser timings — the percentages are the signal, the DOM-op counts are exact.

_update() no longer touches the DOM to find nodes. Each icon's inner <ha-icon> was re-queried every tick — 20 icons = 20 querySelector calls per state change, for a node that cannot change between renders. Now cached at render time in _icoIconEls, the same pattern badges already used for _biconEls/_blabelEls.

Forced layout is now conditional. _update() read this.offsetWidth unconditionally to resolve %-based icon sizes and label font sizes, even for configs that use none. A per-render _needsCardWidth flag records whether any item actually sizes in % — including sizes hidden in per-profile overrides (portrait: {size: "12%"}) and legacy tier keys. Detection is deliberately conservative (a non-string value counts as "yes"): a false positive costs one reflow, a false negative would break sizing.

Geometry events no longer run a state pass. The ResizeObserver called the full _update() — every condition on every overlay, zone, badge, icon, label and gauge re-evaluated — once per frame during a window drag or an opening keyboard. Only three things in _update() depend on the box, so resize now runs a narrow _applyResizeStyles(). The one entangled case (day/night blind gauges, whose slat gradient derives from both their own height and the state value) still delegates to the full pass, gated on _hasDayNightGauge.

Layout passes coalesce. Several observers can fire in one frame, each previously running its own fit+stage sequence. All now funnel through _requestLayout(), collapsing a burst into one microtask pass — same pattern as the existing _requestPin(). Stage-only requests (.wrap resized but not the card) upgrade to a full fit if a full request lands in the same microtask, mirroring _schedule()'s nav-only/full upgrade.

Layout element refs cached. _layoutFitWrap(), _layoutStage() and _layoutRootHeight() each re-queried .wrap, .content and ha-card per call — 11 querySelector calls per observer callback. Now behind _elWrap()/_elContent()/_elCard(), which re-resolve only when the cached node has left the current shadow tree (getRootNode()!==this.shadowRoot — exactly what innerHTML replacement does to it), so no manual invalidation is needed.

New instance fields are declared in the constructor, restoring the "constructor documents the instance shape" contract the audit flagged.

Testing

tests/lifecycle.test.js → 35 assertions. The hot-path budget is now a hard ceiling: _update() must do zero querySelector calls and zero offsetWidth reads on a 40-item card, so an uncached query or forced read reintroduced in a per-item loop fails CI. The riskiest failure mode of this change — a % size silently ceasing to resolve — is covered behaviourally rather than structurally: a 10% icon on a 400 px card must compute to 40px and re-compute to 80px at 800 px. Coalescing (50 requests → 1 pass) and the absence of _update() from the resize path are asserted directly. Render (178) and smoke tiers unchanged and passing; render output diffed line-by-line against v5.9.14 and identical.

v5.9.14

v5.9.14 Pre-release
Pre-release

Choose a tag to compare

@Michailjovic Michailjovic released this 08 Aug 04:31

[5.9.14] - 2026-08-08

Lifecycle & config-shape hardening (full code audit)

Six real defects found by a full audit of the card, three of which could leave a card permanently dead until a page reload. No new features, no config migrations — all fixes are backwards compatible.

Scalar config values crashed the card. resolveVal() assumed its argument was always a [{condition,value}] list. A bare scalar — label: Kitchen, icon_color: red, color: "#fff", conditions: {opacity: 0.5}, conditions: {filter: "blur(2px)"} — reached conds.find(...) and threw a TypeError out of set hass: the card rendered once, then never updated again, with a console error on every state change. Two call sites (label colour, gauge colour) had been hardened with Array.isArray at some point; five had not. The guard now lives in resolveVal / resolveFilter / resolveFilterInverted themselves, so the scalar form is a supported shorthand everywhere and every future call site is covered. Reachable from the GUI editor, not just hand-written YAML — badges are edited through a free-form label / visible / icon_color / tap_action / group (YAML) textarea.

Four hooks died on every dashboard edit-mode toggle. HA moves the card element when entering/leaving edit mode → disconnectedCallback + connectedCallback. _io, _hlHandler, _relTimer and _orientHandler were nulled on the way out and revived with if(this._x)… on the way back in — dead code once null. Result after one toggle: no IntersectionObserver, no roc-highlight listener (editor→card flash broken), no 30 s ticker (format: relative labels frozen), no device-orientation parallax. Worst case: _visible is set only by the IntersectionObserver, so a card that was off-screen when HA moved it kept _visible=false with nothing alive to flip it back — set hass bailed forever and the card was frozen until a page reload. Revivable handles are now detached rather than nulled; _wireVisibility() and _wireRelTimer() (re)build the observer and the ticker from both _render() and connectedCallback(); and set hass now only trusts _visible while an observer actually exists to correct it, so no future regression of this class can freeze a card. Same defect class as the v4.6.4 layout-observer fix — four siblings were missed then.

Per-room templates never subscribed. _setupTemplates() read this._config while the rest of _render() worked on the merged room view (this._roomCfg). labels/badges/zones/icons/overlays/elements/gauges are all ROOM_KEYS, so roomMerge() replaces them with the active room's copies — meaning in any rooms: config the element ids never matched and every template lookup silently continued. Affected label.template, badge.label_template and visible_template on every element type. _startCamera() already handled this correctly; _setupTemplates was the one that was missed.

label_template did nothing on its own. The badge text span was only emitted when label was defined, so a badge using only label_template — the exact form documented in the README — had no element to write into. Now emitted for either key.

Editor leaked a window listener per open. disconnectedCallback removed roc-pos-update but not roc-room-switch; every open/close left a live listener pinning the dead editor instance and still firing on room switches. The mounted _prevCard preview is released too. Also dropped the unused _lyPvT field.

Unhandled promise rejections from invalid templates. HA rejects the subscription for a bad Jinja template; with no .catch this surfaced as a bare console error with no card or template context. Now caught and logged with the offending template string.

Testing

New tests/lifecycle.test.js (27 assertions) covering the blind spot all six bugs lived in: the existing suite only ever asserted on a card rendered once and left alone, never on one that gets moved. It polyfills IntersectionObserver and ResizeObserver — jsdom ships neither, and without them "the observer is gone" is indistinguishable from "the API never existed", which produces a false PASS on the _io assertions and a false FAIL on _ro. Kept in its own file so those polyfills don't perturb render.test.js. Also enforces a hot-path budget (_update() must not scale querySelector calls with item count) so perf regressions fail CI. Wired into npm test and the CI workflow; the 178 existing render assertions and the smoke tier are unchanged and still pass.

Full audit report, including the performance and structural findings deferred to later releases: CODE_ANALYSIS_v5.9.13.md.

v5.9.13

v5.9.13 Pre-release
Pre-release

Choose a tag to compare

@Michailjovic Michailjovic released this 08 Aug 03:45

[5.9.13] - 2026-08-08

Editor: Rooms & menu tab — sub-accordions (proposal 4 from the editor GUI/UX revalidation)

The Rooms & menu tab was one unbroken flat block — roughly 15 distinct concerns back to back with no sub-headers, the densest tab in the editor. Split into 4 collapsible sec() accordions (the same pattern already used on the Elements tab), decided after comparing a 5-section and a 4-section mockup with the user — 4 won out:

  • Room identity — Room id, Name, Room icon, Area match, Thumbnail chips override. Open by default the first time the editor ever renders (same one-time nudge the Image tab's "Background & basics" section already gets), since it's the section most people reach for first.
  • Presence & follow — room_entity, Follow hold, card_id, Follow mode, room_state_entity, and the "This device" browser_mod mapping (kept here rather than split out — it's just another way of resolving the active room).
  • Navigation menu — style, position, live thumbnails + the conditional Mini-room settings panel, height/width/mobile height, auto breakpoint, wheel switch, follow button, and the Chips/Cards YAML lists. Still the biggest section, but now fenced off from room identity and presence, and collapsible on its own.
  • Deep-linking — the Sync-room-to-URL checkbox + hash key, now with a one-line explanation of what it actually does (#room=<id> in the address bar) that didn't fit anywhere before.

Every field keeps its exact same id — this is a pure DOM regrouping, _collectConfig() needed zero changes.

Testing

7 new render tests: all 4 accordion panels present, Room identity open by default on first render (others closed), and the right fields land inside the right panel (spot-checked room-id / nav-style / url-sync). Full smoke + render suites green (0 FAIL).

v5.9.12

v5.9.12 Pre-release
Pre-release

Choose a tag to compare

@Michailjovic Michailjovic released this 08 Aug 03:28

[5.9.12] - 2026-08-08

Editor: Layout tab — two small follow-ups to v5.9.11, per live user feedback on the rendered result

Portrait/Landscape sub-tab buttons now carry an icon (mdi:crop-portrait / mdi:crop-landscape) alongside the label, so which profile is which is visible at a glance instead of relying on reading the word.

Image fit is now a dropdown, not a free-text field. cover/contain was previously typed as plain text per profile (placeholder cover|contain) — easy to typo silently (the render falls back to cover for anything that isn't exactly contain, so a typo just fails silently rather than erroring). Replaced with a <select> per profile: — same as landscape — / — default: cover —, cover — crop to fill, contain — letterbox. Same underlying config keys and parsing (_collProf already worked generically off .value, so no parsing changes were needed) — purely removes the chance of a bad string.

Testing

3 new render tests: sub-tab buttons contain the right mdi icon each, Image fit renders as <select> for both profiles, and the dropdown offers both cover/contain options. Full smoke + render suites green (0 FAIL).

v5.9.11

v5.9.11 Pre-release
Pre-release

Choose a tag to compare

@Michailjovic Michailjovic released this 08 Aug 03:17

[5.9.11] - 2026-08-08

Editor: Layout tab — Portrait/Landscape sub-tabs, live mini grid preview, and a real live-preview fix

Part of the ongoing editor GUI/UX revalidation (EDITOR_UX_REVALIDATION.md) — this pass covers the Layout tab specifically (proposal 7, "Live preview for the Layout tab"), plus two smaller usability wins for the same tab. The Elements tab's alphabetical-with-icons section ordering (proposal 1) was reviewed and kept as-is — it reads fine and is out of scope for this round.

1. Portrait / Landscape as sub-tabs, not stacked boxes. The two profile editors used to render one after another, doubling the tab's length and making it easy to edit the wrong profile by mistake. They're now behind a small Portrait/Landscape pill toggle (same pattern as the top-level Image/Elements/Layout/Rooms&menu tabs); both profiles stay mounted underneath so field values and focus survive switching, only visibility toggles.

2. Illustrative mini grid preview per profile. Each profile box now shows a small color-coded diagram of its regions (Nav/Cards↑/Image/Lights/Cards↓/Cover), built from the exact same rocGridCss/rocRegionCss functions the real card render path uses — so it can't drift out of sync with the actual CSS Grid semantics (including the 1/6 grid-line span syntax). It repaints on every keystroke via a direct innerHTML swap of just the preview box (the same technique already used for the Light Controls gradient preview), so typing in a field never loses focus or triggers a full editor re-render. Purely a visual aid inside the editor form — it does not touch the real card.

3. Fix: Layout tab edits didn't actually reach the Edit-mode live preview card. Investigated whether the existing Edit-mode preview (the real, interactive <room-overlay-card> instance mounted in the editor when Edit mode is on) already reflected Layout tab changes, as the roadmap note suggested checking before building anything new. It didn't — not even on blur. Root cause: the editor's own setConfig() only re-mounts that preview instance when an item-count diff (same check — zones/icons/labels/badges/etc. array lengths) says something changed; layout was never part of that comparison, so a Layout-only edit could never trigger a remount, at any point, before this fix. Added a dedicated debounced path (_lyDebouncedUpdate, 150ms, mirroring the existing _fireDebounced used elsewhere) that pushes the freshly-collected config straight into the mounted preview card's setConfig(), bypassing the editor's own DOM re-render entirely — so typing in a Layout field now updates the live preview almost immediately, without any risk to input focus.

Testing

9 new jsdom render tests: sub-tab buttons present and default to Portrait, clicking Landscape shows/hides the right panel, both mini-preview divs render, the landscape preview is pre-filled from an auto-migrated config, typing a region's row live-repaints its mini preview without losing the field's own value, and — with fake timers advanced past the debounce — a Layout tab edit (input, no blur) reaches the mounted Edit-mode preview card's _config.layout. Full smoke + render suites green (0 FAIL). Playwright e2e suite unaffected by this change (none of the 6 geometry specs touch the editor) but could not be executed in this environment — the Chromium binary failed to install offline; unrelated to this diff.

v5.9.10

v5.9.10 Pre-release
Pre-release

Choose a tag to compare

@Michailjovic Michailjovic released this 06 Aug 03:52

[5.9.10] - 2026-08-05

Fix: nav.live: full/custom thumbnails were overexposed vs. composite

_updateNav() applied each room's brightness_model/filter_conditions-derived CSS filter
directly to the thumbnail wrapper element (.roc-thumb), regardless of nav.live mode. For
composite (and classic static thumbnails) that's correct — the wrapper paints the room's
background/overlay stack itself, so it needs its own filter. But for full/custom, the wrapper
instead hosts a real, independently-rendering <room-overlay-card> mini instance, which
already applies its own brightness_model/filter_conditions internally (the exact same render
code path as the main card). Setting a CSS filter on the wrapper composited it on top of the
mini's already-correct rendering — brightness stacking on brightness — producing a visibly
overexposed/washed-out thumbnail. Reported live: composite thumbnails looked normal, full/custom
looked overexposed.

Fixed by skipping the wrapper filter entirely when nav.live is full or custom — the mounted
mini instance is fully self-sufficient for its own lighting/filter state. composite and classic
static thumbnails are unaffected.

Testing

4 new render tests (full/custom stay filter-free; composite and classic-static are confirmed
unaffected, still applying filter_conditions to the wrapper as before). Full smoke + render
suites pass.

v5.9.9

v5.9.9 Pre-release
Pre-release

Choose a tag to compare

@Michailjovic Michailjovic released this 05 Aug 22:38

[5.9.9] - 2026-08-05

Revert: v5.9.8's day_night phase change broke the fully-closed look

User-caught regression, tested live the same day: after v5.9.8, a day_night blind at fully
closed (even with top_offset: 0, i.e. no correction at all) rendered visibly "a bit open"
instead of solid. Root cause: the old formula's pct>=1 → offset = one half-period special case
wasn't an arbitrary discontinuity as v5.9.8 assumed — it was the load-bearing anchor that
keeps the two striped background layers in anti-phase at fully closed, which is what makes the
blind render fully opaque (no see-through gaps) when drawn. v5.9.8's "continuous, no special-case"
replacement reached offset 0 at fully closed instead — aligning the two layers instead of
anti-phasing them, letting the gaps show through everywhere, including at 100% coverage.

Reverted the render formula to its exact pre-5.9.8 behavior. Removed the rocDayNightOffset
helper and its tests (smoke + render) added in 5.9.8, since they encoded the incorrect model.

Structural finding (kept for any future attempt — see ROADMAP.md 🅿️ day_night blind model)

While reverting, traced the two-layer compositing precisely: one gradient layer is permanently
fixed at background-position 0, the other scrolls with pct. Because the fixed layer's pattern
always starts solid at position 0, and the fill area always starts at row 0 (top-anchored),
the very first visible row of any day_night fill is unconditionally opaque, at any pct, at any
phase
— the fixed layer guarantees it whenever the scrolling layer happens to be transparent
there. This means a residual sliver that is purely transparent (as opposed to "mostly transparent
with an always-present opaque cap row") is not achievable via phase-tuning alone with the current
two-fixed/one-scrolling rendering approach — it would need a structurally different mechanism. This
is exactly the kind of finding the ROADMAP's parked note asks for before any further day_night
redesign is attempted; documented there for whoever (likely the same author) picks this up next.
top_offset continues to correctly control coverage amount for day_night blinds (that part
of v5.9.7 is unaffected and still correct) — it just doesn't (and currently can't cleanly) also
control the striped pattern's exact phase.

Testing

Full smoke + render suites green (0 FAIL) after the revert.