Skip to content

Release/v2.0.7#12

Open
cayossarian wants to merge 12 commits intomainfrom
release/v2.0.7
Open

Release/v2.0.7#12
cayossarian wants to merge 12 commits intomainfrom
release/v2.0.7

Conversation

@cayossarian
Copy link
Copy Markdown
Member

Avoid HA frontend dependencies per the HA dev blog for custom cards
Fix name preservation on first row on resize

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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and ha-chart-base usage 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-folded layouts 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.

Comment thread src/core/truncation-fold.ts Outdated
Comment thread tests/span-icon.test.ts
Comment thread src/panel/span-menu-button.ts Outdated
Comment thread tests/truncation-fold.test.ts
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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 32 changed files in this pull request and generated 4 comments.

Comment thread src/core/truncation-fold.ts
Comment thread src/core/truncation-fold.ts
Comment thread tests/span-menu-button.test.ts Outdated
Comment thread src/core/side-panel.ts Outdated
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants