Skip to content

Codebase Review

Purpose. A snapshot of the LCARdS codebase as it stands today, intended as the planning input for follow-up PRs. Findings reference real files and line numbers; recommendations are sliced into reviewable PR-sized chunks.

Scope of this review. Source under src/, documentation under doc/, build/validation tooling under scripts/ and .github/workflows/. The backend custom component (custom_components/lcards/) is out of scope.

Compiled. April 2026, against dev at the time of the accompanying docs/refactor PR.


1. Executive Summary

LCARdS is a Lit-based suite of LCARS-themed custom Lovelace cards for Home Assistant, organised around a global singleton core (window.lcards.core.*) that owns themes, datasources, rules, animations, validation, presets, and asset loading. Cards are thin Lit web components that talk to that core; an editor framework on top of LCARdSBaseEditor exposes every card config in the Lovelace UI.

Strengths

  • Clean architectural seams. The BaseService → core singleton → LCARdSCard → card hierarchy is consistent and easy to reason about. HASS propagation, lifecycle hooks (_handleFirstUpdate, _handleHassUpdate, _renderCard, _onRulePatchesChanged), and cleanup are centralised on LCARdSCard, so individual cards stay small in surface area.
  • Documented internal contracts. .github/copilot-instructions.md and .github/instructions/*.md describe the singleton model, template evaluator, datasources, animation system, and editor patterns with concrete do/don't examples — unusually thorough for a HA custom card project.
  • Validator infrastructure already exists. scripts/validate-css-vars.js is a 4-pass governance gate on --lcars-*, --lcards-*, and theme: paths, wired into npm run build and CI. This sets a precedent for the doc-example validator added in this PR.
  • Unified evaluator and resolver. UnifiedTemplateEvaluator (JS / token / datasource / Jinja2) and ThemeTokenResolver (with lighten/darken/ alpha/base computed expressions) consolidate what would otherwise be scattered string-mangling logic. The "two-step pattern" for canvas/SVG colour resolution is documented and consistently used.
  • Comprehensive VitePress docs. 92 markdown pages spanning user-facing card reference, core feature docs, architecture, and API reference. A custom markdown rule wraps {{...}} in <span v-pre> so HA template syntax renders cleanly.

Highest-Risk Tech Debt

  1. Two deepMerge implementations with different semantics.src/utils/deepMerge.js MUTATES its target; src/core/config-manager/merge-helpers.js returns a new object. Both are used — sometimes within the same render path. Consolidating without regressions requires a behaviour-preserving migration with tests, which is why it is recommended as a P0 follow-up rather than collapsed in the current PR.
  2. YAML examples in docs were never validated against code. This PR adds npm run validate:doc-examples and fixed real bugs along the way (type: custom:lcards-msdlcards-msd-card, type: custom:lcards-button-cardlcards-button). 41 snippet-style blocks remain as parse warnings; bringing them to zero requires careful per-block decisions (split into multiple blocks vs. mark yaml no-validate).
  3. Large card files. Several cards exceed 4 000 LOC (lcards-button.js 6 847, lcards-slider.js 4 150, lcards-elbow.js 3 604, lcards-card.js base 4 381). Risk: lifecycle hooks, rendering, and zone math all live in the same file, which makes targeted review and unit testing harder. Splitting is a series of focused refactor PRs.
  4. Editor styles partially shared. editor-styles.js (320 lines) and editor-component-styles.js (184 lines) provide canonical tokens, but ~10 editor components still declare their own static get styles() without importing them. Drift is likely.
  5. No automated test infrastructure. Validation happens at build time (CSS-vars, types, hassfest) and via manual smoke testing in HA. There is no Vitest/Jest harness; introducing one would unlock the deepMerge consolidation, schema drift checks, and template-evaluator regression tests.
  6. Pre-existing TypeScript errors. npm run typecheck reports several TS2339 errors in panels/lcards-config-panel.js (Lit types not pulled into @ts-check JSDoc) and utils/state-color-resolver.js. These are not introduced by this PR but should be cleared before adding any stricter type gates.

2. Repository Map

lcards/
├── custom_components/lcards/   HA backend (Python integration; out of scope here)
├── doc/                        VitePress source (92 md files)
│   ├── .vitepress/config.mts   Nav + sidebar definitions
│   ├── architecture/           Internals & subsystem reference
│   ├── cards/                  Per-card user docs
│   ├── configuration/          HA-side setup & integration
│   ├── core/                   Per-card features (presets, rules, themes…)
│   ├── development/            Developer guides + API reference
│   ├── dev/                    Internals (this report lives here)
│   └── getting-started/        Onboarding & migration
├── scripts/                    Build-time tooling (CSS-var validator, version, doc-example validator)
├── src/
│   ├── api/                    Public-surface helpers
│   ├── base/                   LCARdSCard (4 381 LOC), LCARdSNativeCard, LCARdSActionHandler
│   ├── cards/                  All Lovelace cards (button/elbow/slider/chart/data-grid/select-menu/msd/alert-overlay)
│   ├── charts/                 ApexCharts adapter
│   ├── core/                   Singleton subsystems
│   │   ├── animation/          Anime.js v4 wrapper, presets, registry, timeline resolver
│   │   ├── components/         Component registry (SVG/templates)
│   │   ├── config-manager/     Layered config merge, provenance, schema fan-out
│   │   ├── data-sources/       DS registry, processors, polling, history
│   │   ├── helpers/            Cross-cutting helpers
│   │   ├── packs/              Style/theme packs (textures, backgrounds, sounds)
│   │   ├── presets/            StylePresetManager + preset trees
│   │   ├── rules/              RulesEngine (conditional patches)
│   │   ├── screen-effects/     Full-screen effects pipeline
│   │   ├── services/           HA service wrappers
│   │   ├── sound/              Sound system
│   │   ├── systems-manager/    Overlay registry, lifecycle
│   │   ├── templates/          UnifiedTemplateEvaluator
│   │   ├── themes/             ThemeManager + ThemeTokenResolver
│   │   └── validation-service/ Schema registry + per-domain validators
│   ├── editor/
│   │   ├── base/               LCARdSBaseEditor, editor-styles.js, editor-component-styles.js
│   │   ├── cards/              One editor per card
│   │   ├── components/         Reusable tabs / pickers / list editors / yaml mode
│   │   ├── dialogs/            Modal dialogs
│   │   └── utils/              FormField, color helpers, schema-driven form
│   ├── msd/                    MSD card pipeline (multi-overlay rendering)
│   ├── panels/                 LCARdSConfigPanel (HA panel UI)
│   ├── strategies/             Lovelace strategies
│   ├── types/                  TS type stubs
│   ├── utils/                  Cross-cutting utilities (deepMerge, css-length, theme, fileutils, hashing…)
│   ├── lcards-vars.js          Allowlist for `--lcards-*` CSS variables
│   └── lcards.js               Entry point: registers cards, boots core
└── tools/                      Dev helpers (e.g. `create-editor.js`)

Mapping to the workstream lens

Workstream areaPrimary location
Core systemssrc/core/**
Cardssrc/cards/**, src/base/LCARdSCard.js, src/msd/**
Configuration panelsrc/panels/lcards-config-panel.js, src/strategies/**
Editorssrc/editor/**

3. Findings

3.1 Structure & modularity

  • Singleton boot order is implicit. src/lcards.js registers customElements.define(...) for all cards inside a try block, then window.customCards.push(...). The order of singleton creation lives in src/core/lcards-core.js#_performInitialization. This works, but a diagram or sequence outline (Mermaid) in doc/architecture/systems-arch.md would shorten the onboarding curve.
  • Large card files conflate concerns. Button (6 847 LOC), Slider (4 150), Elbow (3 604) include preset resolution, zone math, SVG generation, action wiring, and rule integration in a single class. Candidate splits:
    • <card>-zones.js (zone math; already a defined contract on the base)
    • <card>-svg.js (markup helpers)
    • <card>-presets.js (preset normalisation)
  • LCARdSCard is the centre of gravity. 4 381 LOC. Many helpers (_resolveThemeToken, _processTextFields, _rebuildZones, _getMergedStyleWithRules, _pxToGridUnits) could move into focused mixins or helper modules without changing the public contract.
  • MSD coordinator vs. core. src/msd/pipeline/MsdCardCoordinator.js directly imports deepMerge from the config-manager helpers, while most cards import a different deepMerge from utils/deepMerge.js. See 3.2.
  • Editor base + tabs are well-factored. LCARdSBaseEditor, FormField, editor-styles, and editor-component-styles already encode "the canonical way" — the gap is enforcement, not design.

3.2 Duplication hotspots

#PatternWhereNotes
1deepMerge (mutating)src/utils/deepMerge.js:18Used by lcards-slider, lcards-button (immutable variant), lcards-chart, ApexChartsAdapter, lcards-slider-editor, core/animation/presets, core/animation/resolveTimelines, StylePresetManager.
2deepMerge (immutable)src/core/config-manager/merge-helpers.js:29Used by LCARdSCard, LCARdSBaseEditor, RulesEngine, MsdCardCoordinator, CoreConfigManager. Different semantics from #1.
3clampinline in lcards-elbow.js:2545, core/animation/resolveAnimParams.js:138, packs/textures/effects/LevelTextureEffect.js:14, packs/backgrounds/BackgroundAnimationRenderer.js:244, plus linearMap.js accepts a clamp flagEach is a different signature; canonicalising on (v, lo, hi) is safe.
4isPlain / isPlainObjectutils/deepMerge.js:10, core/config-manager/merge-helpers.js:158Behaviourally equivalent for plain objects; differ on class instances. Pick one in a shared util.
5Structural clone via JSON.parse(JSON.stringify(...))59 occurrences across src/Loses Date/Map/undefined; risks silent bugs when configs grow. Replace with a single cloneConfig() helper that uses structuredClone where available.
6getNestedValue / setNestedValuecore/config-manager/merge-helpers.js:174 exists; ad-hoc dot-path walks recur in RulesEngine, LCARdSBaseEditor, lcards-button, provenance-trackerMove all dot-path access through the existing helpers.
7Static static get styles() blocks that don't import editor-styles10 components in src/editor/components/**Drift risk on focus rings, button styling, label spacing.
8Per-card "preset normalisation" pipelineslcards-button._resolvePreset, lcards-slider._resolvePreset, lcards-elbow._resolvePreset, lcards-select-menu._resolvePresetAll do: load preset → deepMerge user overrides → resolve theme tokens. Candidate for a BasePresetResolver.
9Animation easing/duration default fallbacksscattered in core/animation/resolveAnimParams.js, core/animation/presets.js, lcards-animation-editor.js, lcards-background-animation-editor.jsDefaults should live in one place, exported as a frozen constant.
10setupActions(...) boilerplateevery cardAlready centralised on LCARdSCard, but each card calls it identically with tap_action/hold_action/double_tap_action. A higher-order helper bindStandardActions(this, host) would remove ~5 LOC per card.

3.3 CSS / styling consistency

  • Canonical sources exist. editor-styles.js (~320 lines) and editor-component-styles.js (~184 lines) define typography, button, field, panel, and tab tokens. editorStyles is consumed by all cards' editors and ~half of the shared components.
  • Drift list (components missing the import).lcards-chart-live-preview.js, lcards-msd-live-preview.js, lcards-shape-texture-editor.js, pack-explorer/lcards-pack-explorer-tab.js, templates/lcards-template-sandbox.js, editors/lcards-color-selector.js, editors/lcards-unified-segment-editor.js, editors/lcards-grid-row-editor.js, editors/lcards-border-editor.js, editors/lcards-slider-range-visualizer.js.
  • Config panel vs. editor parity. lcards-config-panel.js and the per-card editors share a similar tab/section layout (tab bar → panel → field group → row of fields), but they don't share CSS. A panel-tab-styles.js module re-using the editor tokens would eliminate duplicated padding/border definitions.
  • --lcards-* allowlist is well-policed. The CSS-var validator prevents new variables from being added without an allowlist update. Keep this gate.

3.4 Testing gaps

  • No unit tests at all. No vitest, jest, or mocha in devDependencies. Build-time gates (CSS vars, hassfest, version) and manual smoke tests in HA are the only checks.
  • High-leverage candidates for a first test pass:
    • deepMerge (both variants) — pin current semantics before consolidation.
    • ThemeTokenResolver — token resolution and computed expression evaluation (lighten/darken/alpha/base).
    • UnifiedTemplateEvaluator — JS / token / datasource / Jinja2 phases.
    • RulesEngine — patch ordering and merge semantics.
    • core/config-manager — layer ordering and provenance tracking.
    • validate-doc-examples.js — golden fixture for the doc validator.
  • No CI gate for npm run typecheck. Pre-existing errors would block enabling it; clean those up first.

3.5 Documentation gaps

Resolved in this PR:

  • VitePress sidebar pointed /architecture/internals/datasource-buffers at a file that lives at /architecture/animations/datasource-buffers (broken link, masked by ignoreDeadLinks: true).
  • doc/cards/msd/index.md, doc/architecture/msd/index.md, and doc/architecture/animations/rule-based-animations.md referenced type: custom:lcards-msd (the registered element is lcards-msd-card).
  • doc/cards/msd/line-overlay.md referenced type: custom:lcards-button-card (correct value is lcards-button).
  • A Developer / Internals nav section now exists, linking this report.
  • A repo-native npm run validate:doc-examples command was added and is documented in doc/development/index.md.

Remaining gaps (recommendations below):

  • No top-level "What can each card do?" feature matrix; users have to read each card page to discover capabilities.
  • No "common patterns" cookbook (e.g. "alert-coloured button that blinks when a sensor is unavailable") — most knowledge lives in the examples folder.
  • doc/development/custom-card.md is light on the new card recipe (CARD_TYPE, getStubConfig, registration in src/lcards.js, _getCardSize).
  • 41 snippet-style YAML blocks (multi-key "alternative values" examples) parse as duplicate-key warnings under js-yaml. They aren't bugs in user-facing terms, but they aren't valid HA YAML either.

4. Prioritised Recommendations Backlog

PriorityAreaRecommendationEffortRiskSuggested PR SliceFiles / Paths
P0coreConsolidate deepMerge into one canonical, documented immutable variant; migrate all callers. Ship with a tiny test fixture pinning current semantics.MMed"Unify deepMerge" — single PR with table of before/after call sites + golden test.src/utils/deepMerge.js, src/core/config-manager/merge-helpers.js, every importer (see §3.2 row 1-2).
P0toolingIntroduce a unit-test runner (Vitest, no transpile config required for ESM). Seed with deepMerge, ThemeTokenResolver, UnifiedTemplateEvaluator.MLow"Bootstrap Vitest" — config + 3 small specs only; no CI changes.vitest.config.js, tests/, package.json.
P0docs✅ Done. Tagged all 41 snippet-style YAML blocks with ```yaml alternatives so the doc validator skips YAML parsing (but still checks type refs) for those blocks. Then enable --strict in CI.SLow"Make doc YAML zero-warning" — tagged blocks with the new alternatives meta hint.doc/core/colours.md, doc/core/text-fields.md, doc/core/rules/*, doc/cards/msd/line-overlay.md, doc/configuration/browser-mod.md.
P1coreReplace ad-hoc clamp and isPlainObject clones with shared utils.SLow"Shared maths/object utils" — add src/utils/numberUtils.js, re-export isPlain.§3.2 rows 3, 4.
P1coreReplace 59 JSON.parse(JSON.stringify(...)) clones with cloneConfig() (using structuredClone when available, fallback otherwise).MMed"Introduce cloneConfig()" — incremental: add helper, replace high-traffic call sites first.src/utils/cloneConfig.js, callers found via grep -rn "JSON.parse(JSON.stringify" src/.
P1cardsExtract preset-normalisation into a BasePresetResolver mixin/helper.MMed"Preset resolver shared helper" — one card per PR (start with lcards-button).src/cards/lcards-button.js, lcards-slider.js, lcards-elbow.js, lcards-select-menu.js.
P1editorsMake editorStyles import mandatory across all editor components; add an ESLint-style grep gate to validate-css-vars to catch drift.SLow"Editor CSS consistency" — one-line imports + remove duplicated tokens.The 10 components listed in §3.3.
P1docsAdd a "Cards feature matrix" page (rows = cards, columns = capabilities like presets / zones / actions / animations / rules).SLow"Feature matrix" — single new page, link from /cards/.doc/cards/feature-matrix.md, doc/.vitepress/config.mts.
P1toolingWire npm run validate:doc-examples into .github/workflows/docs.yml once warnings are zero.XSLow"Gate docs build on YAML validator" — single workflow edit..github/workflows/docs.yml.
P2coreSplit LCARdSCard (4 381 LOC) into focused mixins: zones, text fields, rules patching, theme token glue, action wiring.LHigh"LCARdSCard mixin split" — one mixin per PR, behaviour-preserving.src/base/LCARdSCard.js, new src/base/mixins/*.js.
P2cardsSplit lcards-button.js into preset / zone / SVG / class modules.LHigh"lcards-button modularisation" — multi-PR series, one extracted module each.src/cards/lcards-button.js, new src/cards/button/*.js.
P2tooling✅ Done. Imported all 7 non-MSD card schemas directly into scripts/validate-doc-examples.js (Node-importable ESM). Pre-processes each schema with stripSchemaRestrictions to remove empty enum: [], dangling $ref, and additionalProperties: false before compiling with ajv@8. Validates every card config node found in parsed YAML blocks (including nested cards). Schema issues reported in a separate ⚠️ schema warning(s) section; --strict promotes them to errors.src/cards/schemas/*.js, scripts/validate-doc-examples.js.
P2toolingClear the 9 pre-existing TypeScript errors and gate npm run typecheck in CI.MMed"Typecheck clean + CI gate."src/panels/lcards-config-panel.js, src/utils/state-color-resolver.js.
P2configShare styling between the HA LCARdSConfigPanel tabs and the per-card editor tabs.MLow"Panel/editor CSS parity" — one new shared module.src/panels/lcards-config-panel.js, src/editor/base/editor-styles.js.
P2docsCookbook of recipes (alert-coloured button, datasource-driven sparkline, rule-driven flash, theme override).MLow"Cookbook section" — one or two recipes per PR.doc/cards/cookbook/**, sidebar.

5. Proposed Follow-up PR Plan

Sized for review-in-an-hour. Each builds on the previous but is standalone if the order shifts.

PR 1 — Bootstrap Vitest (P0)

  • Goal. Make tests possible.
  • Scope. Add Vitest config, tests/utils/deepMerge.spec.js pinning both current implementations' semantics, README note.
  • Key files. vitest.config.js, tests/utils/deepMerge.spec.js, package.json (add test script).
  • Validation. npm test green, no production code touched.

PR 2 — Unify deepMerge (P0, depends on PR 1)

  • Goal. One canonical, documented immutable deepMerge; mutating call sites either migrate to immutable or use a clearly-named mergeInto helper for the rare case where mutation is the contract.
  • Scope. Move canonical impl into src/utils/deepMerge.js, re-export from src/core/config-manager/merge-helpers.js for back-compat, migrate all importers.
  • Key files. §3.2 rows 1-2.
  • Validation. Vitest specs from PR 1, build, manual smoke test of preset inheritance and rules patching.

PR 3 — Doc YAML zero-warning + CI gate (P0/P1)

  • Goal. npm run validate:doc-examples --strict exits clean and is enforced by docs.yml.
  • Scope. ✅ All 41 snippet-style blocks tagged with ```yaml alternatives (parse skipped; type refs still checked). The next step is to add the --strict flag and the workflow step.
  • Key files. §3.5 list, .github/workflows/docs.yml.
  • Validation. npm run validate:doc-examples:strict exits 0; npm run docs:build succeeds.

PR 4 — Shared maths/object utils (P1)

  • Goal. Eliminate clamp and isPlainObject duplicates.
  • Scope. New src/utils/numberUtils.js with clamp. Promote isPlain to the canonical predicate.
  • Validation. Vitest spec for clamp edge cases (NaN, equal bounds, reversed bounds).

PR 5 — cloneConfig() (P1, depends on PR 1)

  • Goal. One safe deep-clone helper; remove JSON.parse(JSON.stringify(...)).
  • Scope. Add helper, replace top 10 highest-traffic call sites (rules engine, presets, theme manager, datasource subscribers).
  • Validation. Spec covering Date/Map/undefined handling.

PR 6 — Editor CSS consistency (P1)

  • Goal. Every editor component imports editorStyles. Add a small grep validator to catch regressions.
  • Scope. §3.3 drift list; add a check to validate-css-vars.js (or a sibling script) that fails if an .editor*.js file declares static get styles() without importing editorStyles.
  • Validation. Validator green; visual smoke test of each editor.

PR 7 — Cards feature matrix + cookbook seed (P1)

  • Goal. A single page that answers "what can each card do?" and one worked recipe.
  • Scope. New md pages, sidebar entries, cross-links from card pages.
  • Validation. npm run docs:build, npm run validate:doc-examples.

PR 8 — BasePresetResolver for buttons (P1)

  • Goal. Extract preset normalisation from lcards-button.js into a reusable helper; prove the shape on one card before generalising.
  • Scope. New src/cards/preset/BasePresetResolver.js; refactor button only.
  • Validation. Visual smoke + Vitest spec for preset merge order.

PR 9 — Apply BasePresetResolver to slider/elbow/select-menu (P1)

  • Goal. Roll out the helper from PR 8.
  • Scope. One card at a time inside this PR if diff is small.

PR 10 — Typecheck clean + CI gate (P2)

  • Goal. npm run typecheck is clean and enforced.
  • Scope. Fix the 9 pre-existing errors, add the workflow step.

6. How These Conclusions Were Validated

Commands run

sh
# Repo overview
git log --oneline origin/dev..HEAD
git diff --stat origin/dev..HEAD
find src -type f -name '*.js' | wc -l        # 305
wc -l src/cards/*.js src/base/*.js           # see §1, §3.1

# Build-time validators
npm run validate:css-vars                    # ✓ 305 files, all valid
npm run validate:doc-examples                # ✓ added in this PR
npm run typecheck                            # 9 pre-existing errors

# Targeted searches
grep -rn 'function deepMerge\|const deepMerge\|deepMerge =' src/ --include='*.js'
grep -rn 'from.*merge-helpers\|from.*deepMerge' src/ --include='*.js'
grep -rn 'function clamp\|const clamp =' src/ --include='*.js'
grep -rn 'function isPlain\|function isPlainObject' src/ --include='*.js'
grep -rn 'JSON.parse(JSON.stringify' src/ --include='*.js' | wc -l   # 59
grep -roh 'type: custom:lcards-[a-z-]*' doc/ --include='*.md' | sort -u
grep -rn 'static CARD_TYPE' src/cards/ --include='*.js'
grep -rL 'editorStyles\|editor-styles\|editor-component-styles' src/editor/components/ --include='*.js' \
  | xargs grep -l 'static get styles\|static styles'

What was specifically searched for

  • deepMerge duplication. Confirmed two implementations with different mutation semantics, in active use across 14+ files (§3.2 row 1-2).
  • CSS-token reuse. editorStyles import audit across src/editor/components/** (§3.3).
  • Schema validators in production code.src/core/validation-service/index.js, SchemaRegistry.js, OverlayValidator.js, ValueValidator.js, plus the schemas in src/core/validation-service/schemas/. Confirmed schemas are registered at runtime by cards (registerSchema() static methods), which is why a Node-side import of the production validator is non-trivial — recorded as P2 follow-up.
  • Doc/code drift. Cross-referenced customElements.define(...) in src/lcards.js against every type: custom:lcards-* value found in doc/**.md. Discrepancies fixed in this PR.
  • Sidebar reachability. Cross-referenced every *.md under doc/ against the link list in doc/.vitepress/config.mts; found one broken sidebar link (/architecture/internals/datasource-buffers), fixed in this PR.

Limits of this review

  • No runtime profiling. Statements about "high-traffic" call sites rely on import graph and code-reading, not measured frame budgets.
  • No deep dive into MSD pipeline. src/msd/ is large and was treated as a black box for the purposes of this review; targeted refactor recommendations there are best made by an MSD-area owner.
  • The doc validator now performs schema validation. It checks unknown card types, parse failures, and validates parsed card configs against the real JSON Schemas for 7 card types (lcards-msd-card excluded). Schema issues are reported as warnings by default and promoted to errors with --strict. The 10 current schema warnings reflect minor gaps between the schemas and the doc examples (e.g. background object form not in schema, layers as array vs object).

This document is intended to evolve. PR authors should update entries here as items are completed and add new findings as they surface.