Skip to content

Development ​

The card is a single LitElement bundled with Rolldown and distributed through HACS. No framework, no build server — clone, npm install, and build.

Build commands ​

bash
npm install
npm run build      # Rolldown bundle → dist/weather-alerts-card.js
npm run watch      # Rolldown in watch mode
npm run lint       # TypeScript type-check (tsc --noEmit)
npm run test       # Vitest, two projects: pure files on jsdom, every suite that mounts the card or editor in headless Chromium (run `npx playwright install chromium` once)
npm run test:watch # Vitest in watch mode

Run npm run lint and npm run test before committing — both are required status checks on every PR.

The bundle is a single ES module, minified and downleveled to ES2019 so it keeps working in the old Android WebViews some wall panels still ship (#194).

Source layout ​

FilePurpose
src/weather-alerts-card.tsThe main card class. Implements the HA card contract (setConfig(), hass, getCardSize(), getStubConfig(), window.customCards) and wraps output in <ha-card>
src/weather-alerts-card-editor.tsThe visual configuration editor
src/types.tsWeatherAlert (normalized), WeatherAlertsCardConfig, AlertAdapter, raw provider shapes
src/adapters/index.tsAdapter registry and auto-detection — exports getAdapter(provider, attributes)
src/adapters/*.tsOne adapter per provider
src/localize.tsi18n lookup — t(key, lang, params?), strips the region subtag, falls back to English per key
src/translations/One file per locale (en, fr, es, it, de, nl, pl, zh-Hans, pt-BR, sr, sr-Latn)
src/utils.tsPure functions: icon mapping, timestamp parsing, computeAlertProgress(), severity normalization, zone filtering, sorting, reflowAlertText()
src/styles.tsAll CSS, as a Lit css tagged template

The adapter pattern ​

Every provider is an adapter that converts raw entity attributes into a normalized WeatherAlert[]. The card UI only ever consumes WeatherAlert — never raw provider data. That is what keeps every feature working identically across nine feeds, and it is the constraint to respect when adding a tenth.

An adapter declares how to detect itself from an attribute signature, and getAdapter() resolves either an explicit config.provider or the first adapter that claims the attributes. Adapters that collect per-incident entities also declare feedSources, which drives the editor's live-detected feed picker via knownFeedSources().

Two conventions worth knowing:

  • Never invent data. If a feed has no expiry, the alert gets endsTs: 0 and the card shows an honest "ongoing" state rather than a fabricated countdown. If severity had to be guessed, it is flagged as inferred so the UI can mark it with a tilde.
  • Normalize in the adapter, not the UI. Provider quirks stop at the adapter boundary.

Adding a provider ​

  1. Add src/adapters/<name>.ts exporting an AlertAdapter — a detector plus a parser producing WeatherAlert[].
  2. Register it in src/adapters/index.ts.
  3. Add the provider to the AlertProvider union in src/types.ts.
  4. Add unit tests under tests/ covering a real captured payload.
  5. Document it in Providers, including its severity/certainty fidelity.

Documentation site ​

This site is VitePress, living under docs/.

bash
npm run docs:media    # regenerate the figures into docs/public/img/
npm run docs:dev      # local dev server with hot reload
npm run docs:build    # production build → docs/.vitepress/dist/
npm run docs:preview  # serve the production build

Run docs:media before docs:build on a fresh clone: the figures are not committed, and VitePress fails the build on an image it cannot resolve rather than warning.

Docs figures are generated deterministically from the screenshot harnesses in scripts/ and published straight into the Pages artifact, so docs/public/img/ is gitignored. The only committed figures are the six the README embeds.

docs:media leaves the tracked figures dirty

The scene content is deterministic, but the encoded bytes are not portable across Chromium and ImageMagick versions — so the run rewrites the six storefront figures in place. Run git restore img/ afterwards unless you actually mean to refresh them, which is a release-time job.

The motion capture (scripts/capture-tap-action.js) is gated behind DOCS_MOTION=1 and off by default. It needs both ffmpeg and ffprobe.

Forum figures must fit under 500 px

The community threads embed the same SVGs from Pages, and Discourse cooks every inline image to at most 500 px tall, scaling the width to match. The composer preview does not show it, so a portrait figure looks right until it is posted and then lands as a thumbnail. Each pair in scripts/encode-adaptive-svgs.sh therefore declares the width the thread writes in its image tag (690 full-width, 560 card-width, or - when the threads never inline it), and the script fails the build when a figure would cook taller than 500 at that width. Fix it in the harness: a wider or shorter canvas, tighter padding, or a -map crop like the geometry figures. The site itself clamps nothing.

The editor figure (img/editor-adaptive.svg) is the one exception to "not committed". The editor is nine HA widgets deep, so a stub harness would be a fake; instead scripts/capture-editor.js photographs the real editor in a running Home Assistant (HA_TOKEN=<long-lived token> npm run screenshot:editor, then bash scripts/encode-adaptive-svgs.sh). The Pages workflow has no HA, so the SVG and its light WebP are tracked and refreshed by hand whenever the editor layout changes. It is not part of docs:media.

Deployment is .github/workflows/docs.yml, which builds and publishes to GitHub Pages on every push to main.

Contributing ​

  • Branches: feat/<name>, fix/<name>, chore/<name>.
  • Commits: type(scope): description — feat, fix, docs, refactor, test, chore.
  • main is protected. Everything goes through a PR with passing build, lint, test and HACS validation checks, and only squash merges are allowed.

See CONTRIBUTING.md for the full guidelines, and AGENTS.md for repository conventions in the depth an automated contributor needs.

Released under the MIT License.