Release/v2.0.7#12
Open
cayossarian wants to merge 12 commits intomainfrom
Open
Conversation
Replace every HA custom-element reference with an in-house Lit element so
the card cannot break when HA renames or removes a frontend component
(per developers.home-assistant.io/blog/2026/03/25 guidance).
New elements (src/core/, src/panel/, src/chart/):
- <span-icon>: inline-SVG renderer backed by @mdi/js, sized via
--mdc-icon-size (the same CSS var ha-icon honored), with a curated
19-icon path map covering every mdi: token currently referenced.
- <span-switch>: track + thumb toggle mirroring HA's visual conventions,
with checked/disabled props, keyboard activation, and a bubbling
composed change event so existing listeners continue to fire.
- <span-menu-button>: hamburger that dispatches the documented
hass-toggle-menu event (event API, not component API). Hidden on
wide viewports via :host([narrow]) to mirror HA's drawer behaviour.
- <span-chart>: ECharts wrapper using the SVG renderer. Tree-shaken to
LineChart + GridComponent + TooltipComponent + SVGRenderer to keep
the bundle small. Same charting library HA itself uses inside
ha-chart-base, so visual feel matches and the option shape is the
one already produced by chart-options.
Existing wrappers swapped:
- <ha-icon> -> <span-icon> (~25 sites)
- <ha-switch> -> <span-switch> (3 sites in side-panel.ts; HaSwitchElement
type renamed to SpanSwitchElement)
- <ha-card> -> <div class="span-card"> + .span-card CSS rule. Theme
variables (--ha-card-border-radius, --ha-card-border-color,
--ha-card-border-width, --ha-card-box-shadow) stay because they are
HA's stable theme contracts, not the deprecated component APIs.
- <ha-menu-button> -> <span-menu-button>
- <ha-chart-base> -> <span-chart>
Truncation-driven uniform fold (src/core/truncation-fold.ts):
- New observer measures whether each row's name is currently
ellipsizing and toggles a uniform .is-folded class across every row
in the container. Pixel breakpoints can't pick the right moment
because name length varies per circuit ("Spa" vs
"Commissioned PV System"); per-row decisions left the grid
visually uneven. Container-uniform decision keeps row heights
consistent across the view.
- Hysteresis on the unfold side prevents oscillation around the
trigger width.
- Wired in list-view-controller (.list-row, .list-circuit-name) and
tab-dashboard (.circuit-slot, .circuit-name).
CSS additions (src/card/card-styles.ts):
- .list-row.is-folded and .circuit-slot.is-folded grid layouts: name
takes the full first row (paired with the chevron on list rows);
badges/controls/reading/gear drop to a second row.
- min-width: 0 on .list-row so it shrinks reliably in CSS-grid cells.
- .span-card replaces the ha-card rule, keeping the same theme
variables.
Build (rollup.config.mjs):
- Adds @rollup/plugin-replace to substitute process.env.NODE_ENV at
build time. ECharts (and some Lit internals) reference it for
dev-warning code; without the replacement the bundle throws
ReferenceError: process is not defined and the panel fails to
mount.
Bundle delta: span-panel.js 232 KB -> 758 KB; span-panel-card.js
199 KB -> 724 KB. Dominated by ECharts + a small @mdi/js subset.
Acceptance: grep -rE 'ha-(card|icon|switch|menu-button|chart-base)\b'
src/ returns only theme-variable references and migration comments;
typecheck + 222 tests + rollup build all green.
Followups to be051ae raised by an independent code review: Critical: - Guard customElements.define for span-icon, span-switch, span-chart, and span-menu-button. The integration registers BOTH span-panel.js and span-panel-card.js as resources, so any user with the Lovelace card placed on a dashboard alongside the HA panel will load both bundles on the same document. The second to load would otherwise throw NotSupportedError on the duplicate define and break whichever view loaded second. Mirrors the same pattern already used in span-side-panel.ts. Important: - span-chart now re-mounts in updated() if options arrives after firstUpdated. Without this, any future caller that assigned options asynchronously would see a permanently blank chart. Current callers set options synchronously so the bug isn't reachable today; this is defensive against future regression. - span-chart connectedCallback re-mounts the ECharts instance after a disconnect+reconnect (move, adoptNode). firstUpdated fires only once per element lifetime, so without this branch a reattached span-chart would render its template but never re-create the underlying chart. Cleanup: - Drop the redundant SpanSwitchElement interface in side-panel.ts. span-switch.ts already augments HTMLElementTagNameMap, so document.createElement("span-switch") infers SpanSwitch directly and querySelector takes the SpanSwitch type generic. - Tighten truncation-fold's MutationObserver to subtree:false. Rows appear in the container only via full re-renders that mutate the container's direct child; the deeper subtree traffic from row expand/collapse mutations was wasted work. - Bump truncation-fold default hysteresis from 24px to 48px so borderline-fitting names don't oscillate around the trigger boundary. Tests (new): - tests/truncation-fold.test.ts (10 tests): empty/single/multi-row fold decisions, zero-width name treated as truncated, hysteresis prevents oscillation, ResizeObserver-driven re-evaluation, MutationObserver picks up rows added later, both observers disconnect cleanly. - tests/span-icon.test.ts (4 tests): registration, known-icon SVG path render, empty-icon no-op (no warning), unknown-icon warns once. - tests/span-switch.test.ts (5 tests): registration, ARIA role + tabindex on connect, aria-checked + [checked] attribute reflection, click toggles + dispatches bubbling composed change, disabled blocks toggle and event, Space/Enter keyboard activation. - tests/span-menu-button.test.ts (3 tests): registration, hass-toggle-menu CustomEvent dispatch (bubbles, composed), narrow property attribute reflection. - vitest.config.ts: switch test environment to happy-dom so the Lit-element tests have a DOM to mount in. Pure-function tests are unaffected. 245 tests pass (was 222, +23 new); typecheck clean; rollup build clean. Bundle size unchanged.
Reverts the entity-routing rule from d65ac95 (frontend dist sync of
that change was 4c56e92). The previous fix preferred a circuit-UUID
favorite when an entity's unique_id encoded one — even for entities
device-info-attached to a BESS or EVSE sub-device. That made the
EVSE feed-circuit power/current sensors land as circuit favorites
no matter how the user reached them, which produced two visible
problems:
1. Heart on the EVSE device card in By Panel created a circuit
favorite, so the EVSE never appeared as a device card in the
Favorites view — only as a circuit row representing the feed.
2. There was no way to favorite an EVSE-as-device through any
entity with a circuit UUID; the user had to hunt for an EVSE
status sensor (no circuit UUID) for the device-card outcome.
The simpler rule: any entity attached to a sub-device favorites the
sub-device. The device card on the dashboard already represents both
the EVSE/BESS status sensors AND its feed-circuit power, so a circuit
favorite for the same physical thing would render it twice in the
Favorites view (once as a card, once as a row). Routing all sub-device
entities to the device favorite eliminates that duplication and
preserves the user's natural mental model: clicking the heart on
anything attached to the EVSE means 'favorite the EVSE'.
The list views (By Activity / By Area on a real panel) are unaffected
— they iterate topology.circuits flat, so the EVSE feed circuit still
appears as a row alongside other circuits in those views (where there
is no device-card layer to provide an alternative representation).
Test updated to assert sub-device favorite for an EVSE feed-circuit
sensor; 27 favorites tests pass.
There was a problem hiding this comment.
Pull request overview
This PR updates the SPAN panel/card frontend to reduce/avoid Home Assistant frontend UI component dependencies by introducing local span-* replacements, and adds truncation-driven folding logic to keep circuit names stable/visible when layouts resize.
Changes:
- Replace
ha-icon,ha-switch,ha-menu-button, andha-chart-baseusage with local custom elements (span-icon,span-switch,span-menu-button,span-chart). - Add truncation-driven folding via
observeFold()and update CSS to apply.is-foldedlayouts based on real truncation (with hysteresis). - Add Vitest + happy-dom test setup and new unit tests for the new components/behaviors.
Reviewed changes
Copilot reviewed 29 out of 32 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.ts | Configures Vitest to run in a DOM-like environment (happy-dom). |
| tests/truncation-fold.test.ts | Adds tests for truncation-driven folding behavior and hysteresis. |
| tests/span-switch.test.ts | Adds tests for the span-switch custom element behavior/accessibility. |
| tests/span-menu-button.test.ts | Adds tests for the span-menu-button custom element event behavior. |
| tests/span-icon.test.ts | Adds tests for span-icon rendering and warn-on-unknown behavior. |
| tests/header-renderer.test.ts | Updates expectations to match span-icon output. |
| src/panel/tab-dashboard.ts | Attaches/detaches the fold observer for dashboard grid rows. |
| src/panel/span-panel.ts | Imports/registers new custom elements and swaps menu button usage. |
| src/panel/span-menu-button.ts | Implements span-menu-button as an HA menu button replacement. |
| src/panel/favorites-summary.ts | Replaces ha-icon usage with span-icon. |
| src/core/truncation-fold.ts | Introduces observeFold() to toggle .is-folded based on truncation + hysteresis. |
| src/core/tab-bar-renderer.ts | Renders tab icons using span-icon. |
| src/core/sub-device-renderer.ts | Replaces sub-device gear icon with span-icon. |
| src/core/span-switch.ts | Implements span-switch as an HA switch replacement. |
| src/core/span-icon.ts | Implements span-icon with a curated @mdi/js icon map. |
| src/core/side-panel.ts | Swaps ha-switch/ha-icon usage to span-switch/span-icon and updates types/CSS selectors. |
| src/core/list-view-controller.ts | Attaches/detaches fold observer for list rows on render/unbind. |
| src/core/list-renderer.ts | Replaces list row/search icons with span-icon. |
| src/core/header-renderer.ts | Replaces header legend/gear/lock icons with span-icon. |
| src/core/grid-renderer.ts | Replaces grid icons with span-icon. |
| src/core/favorites-store.ts | Updates documentation/comments for favorite resolution behavior. |
| src/core/error-banner.ts | Uses span-icon for banner icons. |
| src/core/dashboard-controller.ts | Updates DOM queries for lock icon and chart element cleanup (span-chart). |
| src/chart/span-chart.ts | Adds span-chart custom element built on ECharts (SVG renderer). |
| src/chart/chart-update.ts | Updates chart updater to render/update <span-chart> instead of <ha-chart-base>. |
| src/card/span-panel-card.ts | Replaces <ha-card> with .span-card and imports/registers new elements. |
| src/card/card-styles.ts | Updates styling selectors and adds .is-folded CSS layouts for truncation-driven folding. |
| rollup.config.mjs | Adds replace plugin to inline process.env.NODE_ENV for browser bundles. |
| package.json | Adds @mdi/js, echarts, happy-dom, and rollup replace plugin dependencies. |
| package-lock.json | Locks new dependencies. |
Four valid findings fixed: - truncation-fold MutationObserver now watches subtree:true with a filter that triggers attachAll only when added/removed nodes contain row-matching elements. Tying observer correctness to the current 'rows always re-render via container.innerHTML' invariant was fragile — any future incremental-row code would silently regress the fold. The filter keeps non-row mutations (chart-container churn, side-panel insert/remove, expand/collapse content) from triggering needless re-attach work. - span-menu-button button now has explicit type="button" so it cannot trigger a form submission if ever placed inside a <form>. Browser default for <button> is type="submit". - span-icon test suite now restores the console.warn spy in afterEach via vi.restoreAllMocks(). vitest reuses workers across files; without restore the spy leaked into other tests that re-spy console.warn and corrupted call counts. - truncation-fold test suite captures the original ResizeObserver before installing its shim and restores it in afterEach. The install was unconditional and never undone, so other test files depending on happy-dom's real ResizeObserver could see our stub in order-dependent ways. 245 tests pass; rollup build clean; bundle unchanged.
On very narrow viewports (iPhone portrait) the By Panel breaker
cells were clipping their relay toggle and gear icon. The cell was
being squeezed by two sources of outer-frame overhead:
- 28px gutter columns on each side for the tab-number labels —
more than twice what a two-digit number actually needs.
- 8px column-gap applied between every adjacent column, including
between each gutter column and its cell.
Restructure the grid as a 5-column layout with an explicit 8px
spacer column between the two cells and a column-gap of 0: tab
column, cell, 8px spacer, cell, tab column. Tab columns narrow to
14px (enough for the '32' two-digit breaker number at 0.85em).
Total width reclaimed per row: 44px (~22px per cell), enough to
bring the relay toggle and gear back on-screen in the narrow-cell
.is-folded layout. Right-cell grid-column references updated from
3 to 4, right tab-label grid-column from 4 to 5, row-span layout
from '2 / 4' to '2 / 5'.
Double-pole (240V) breaker cells use circuit-col-span which sets min-height: 280px so the cell spans two breaker rows in the panel-grid. Inside the .is-folded layout the three rows (name, controls, chart) were all auto-sized; with no fr unit to absorb extra height, the default align-content:stretch distributed the 280px equally across all three. Result: the badge and relay-toggle got vertically inflated to fill the controls row and the chart was pushed down to the bottom of the cell. Add grid-template-rows: auto auto 1fr so name and controls keep their content height and the chart absorbs all leftover cell height — matching the user's expectation: the chart rises to meet the controls instead of the controls stretching to push the chart down.
Two layout cleanups for the folded breaker-cell controls row: 1. .breaker-badge horizontal padding 7px -> 3px. The blue '15A' / '30A' pill was visually wider than the digits needed; trimming the left/right padding reclaims ~8px per cell and lines the pill up tighter against neighboring badges/icons. 2. The .is-folded grid's 1fr slack column moves from between the shedding icon and the relay toggle to between the relay toggle and the power reading. Effect: the relay sits adjacent to the shedding icon (eliminating a wasted-looking gap the user pointed out on wider cells) while power + gear stay pinned to the right edge. Both .list-row.is-folded and .circuit-slot.is-folded updated for consistency.
Four valid follow-up findings: - truncation-fold's already-folded branch now also applies the fold class to every row in the container, not just the existing ones. Without this, rows added under a folded grid (filter clear, area regroup, etc.) rendered unfolded against an otherwise-folded grid and broke the uniform-fold guarantee. - truncation-fold's attachAll always defers an evaluate() now, not only when new rows are observed. The MutationObserver also fires for row removals, where stale fold state needs a re-check even though no rows were newly attached. - span-menu-button.test drops the no-op beforeAll() — the side-effect import already triggers the @CustomElement registration, so the empty hook was misleading. - side-panel.ts comment grammar: 'an span-switch' -> 'a span-switch'. 245 tests pass; bundle unchanged (CSS / DOM-runtime changes only).
The orange left border (.circuit-custom-monitoring) and the orange gear-icon tint were both driven by hasCustomOverrides(), which returns true whenever the integration sends a continuous_threshold_pct value. The integration sends that field for every circuit even when nothing is overridden, so the indicator was firing on default-config circuits. The indicator is unwanted by the user regardless of the false-positive, so remove the visual completely: - circuit-state.ts no longer adds the .circuit-custom-monitoring class. - grid-renderer.ts and list-renderer.ts always render the gear at the neutral #555 (no MONITORING_COLORS.custom branch). - card-styles.ts drops the .circuit-custom-monitoring border-left rule. - hasCustomOverrides remains exported (still has unit-test coverage) but is no longer wired into any render path.
…ollision span-error-banner was registered via @CustomElement, which calls customElements.define unconditionally. The card and panel are two separate bundles, each shipping its own copy of this module. When both land in the same page (user on a Lovelace dashboard first — card bundle loads as a resource — then switches to the Span panel sidebar item — panel bundle loads), the second bundle's define throws DOMException: the name "span-error-banner" has already been used. That throw aborts the panel bundle's module execution *before* @CustomElement("span-panel") runs, so <span-panel> stays an un-upgraded HTMLElement with no shadow root and no Lit state. The sidebar view is permanently blank until a hard refresh loads the panel bundle first. Every other shared element already uses the bottom-of-file guarded customElements.define pattern (side-panel, span-icon, span-switch, span-chart, span-menu-button, span-panel-card-editor). This one was missed. Adopt the same pattern. Add a regression test that imports each shared module twice with the module cache cleared between imports, mirroring the two-bundle scenario. Without the guard, the second import would throw. The prior "fixes" (retry on visibility restore, recover if tab-content empty, location.reload when panel not connected, coalesce + supersession tokens) were all defending against symptoms of this bug. None of that code ever actually executed, because the element was never upgraded. They can be removed in a follow-up now that the root cause is fixed.
4491c9c fixed the real bug — span-error-banner was registered via @CustomElement unconditionally, so when both card and panel bundles loaded in the same page the second define threw and aborted panel bundle execution before @CustomElement("span-panel") ran. That left <span-panel> an un-upgraded HTMLElement (permanent blank sidebar). Several rounds of "fixes" had been layered on top trying to recover from this symptom, but none of them ever ran — the element was never upgraded, so its Lit lifecycle code was unreachable. Remove them: - src/panel/index.ts: module-level visibilitychange + location.reload fallback (plus the _panelConnected prototype-patch dance). The comment claimed "HA removes <ha-panel-custom> after WS reconnect while the browser tab is backgrounded" — a misread of the real failure mode. - src/panel/span-panel.ts: _onVisibilityChange handler, _recoverTimer state, and the entire _recoverIfNeeded retry-with-backoff method (plus its teardown in disconnectedCallback). It detected empty tab-content on visibility restore and retried render at 2s/4s/6s backoff — defending against a failure mode that only ever fired because the element itself wasn't a LitElement. - src/card/span-panel-card.ts: _onVisibilityChange handler that re-ran recordSamples + updateDOM on tab restore. updated() already does this on every hass change, which HA pushes on reconnect — fully redundant. No test changes: grep confirmed none of the removed symbols are referenced by the suite.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Avoid HA frontend dependencies per the HA dev blog for custom cards
Fix name preservation on first row on resize