Every component is now two modules sharing a basename:
- src/elements/leaflet-foo.ts -- `export default class LeafletFooElement
extends WithProps(...)`, the class only, no customElements.define, no
side effects. src/elements/index.ts is an order-free barrel.
- src/components/leaflet-foo.ts -- three lines: import the class, define
the tag, re-export. Importing this (or src/index.ts) registers the tag.
Plugin authors can now import a class without triggering the built-in
define, to subclass it or register it under a different tag name. Package
subpath exports `leaflet-components/elements`,
`leaflet-components/elements/leaflet-foo.js`, and
`leaflet-components/components/leaflet-foo.js` map onto dist/; jsr.json
gains an `./elements` entry.
The load-bearing ordering moves from the export statements in src/index.ts
to its `./components/*` side-effect imports (the elements barrel carries no
define, so its order is free). Classes are renamed LeafletFoo ->
LeafletFooElement, including in the HTMLElementTagNameMap augmentation.
Docs (CLAUDE.md, docs/, README.md) updated for the new layout.
Replaces the mutable registerCRS() registry (never released beyond this
branch) with a web-components-first design: any custom element, no base
class required, can provide a CRS Leaflet doesn't ship by nesting inside
<leaflet-tile-layer-wms> and firing a bubbling leaflet-crs-changed event
(detail: { crs: CRS | null }, mirroring icon-changed's { icon: null }
pattern for "revert to default"). The plain `crs="EPSG4326"` attribute
stays as a convenience shortcut for the 4 CRSes Leaflet itself ships; a
nested provider takes priority over it when both are present.
This surfaced a real, previously-undiscovered bug in with-props.ts:
recreateLeafletObject() was declared on the public LeafletElement
interface but only ever implemented as a private #recreateLeafletObject()
-- calling it would throw "is not a function" at runtime despite
type-checking cleanly. Fixed by making it public, and it now also
re-dispatches registerWithParent() for attach !== 'none' components,
which it never did before (previously only safe for attach: 'none'
components like icons, since recreate would otherwise rebuild a layer
without ever re-adding it to whatever registered it the first time --
exactly what leaflet-tile-layer-wms needs when a nested provider's crs
arrives after it already constructed itself with the default).
Two augmentations, so TypeScript actually knows about the custom elements:
- HTMLElementTagNameMap (src/index.ts): createElement/querySelector now
infer the exact component class for all 24 tags instead of HTMLElement.
- Per-component addEventListener/removeEventListener overrides, typing
`leaflet:<name>` events against the real Leaflet event payload
(PopupEvent, DragEndEvent, LeafletMouseEvent, etc.) while still accepting
ordinary DOM events normally, and rejecting event names that component
doesn't fire.
src/core/event-types.ts holds reusable event-name -> payload-type
fragments (mirroring shared-props.ts's fragment reuse), composed per
family: MapEvents, MarkerEvents, PathEvents, TileLayerEvents,
DivOverlayLayerEvents, GroupEvents. LeafletAddEventListener<T>/
LeafletRemoveEventListener<T> in with-props.ts are type-only
intersection-of-overloads helpers applied via `declare addEventListener:
...`, the same pattern every component already uses for `declare readonly
leafletObject?: X` -- zero runtime cost. Deliberately no generic `string`
fallback overload: a fallback would silently accept unrecognized
`leaflet:*` names too, defeating the point.
Fixed a real bug found while building this: #forwardEvents was
dispatching the raw pre-merge `data` Leaflet passes to fire(), missing
type/target/sourceTarget that Leaflet's own fire() merges in before
notifying real .on() listeners. Typing `detail` against Leaflet's actual
event interfaces would have been dishonest otherwise, so the merge now
matches Leaflet's own Evented#fire.
Also added 'line-updated' to the existing internal-event HTMLElementEventMap
augmentation in register.ts (needed once addEventListener got overridden
on polygon/polyline, which use it internally) and cleaned up ~35 now-
redundant `as HTMLElement & {...}` casts across the test suite that the
tag name map makes unnecessary.
No test coverage existed before this. Adds vitest + jsdom (2 new
devDependencies) and a suite that runs entirely without a browser: real
Leaflet objects work fine under jsdom for everything this library needs to
verify (option/attribute wiring, event forwarding), given a ResizeObserver
stub in test/setup.ts (jsdom's only real gap here).
- test/core/with-props.test.ts: the WithProps mixin itself, against a fake
Leaflet-like class -- attribute<->setter dispatch, get/attribute/default
fallback order, event-driven attribute sync-back, positional/recreate/
attach modes, and the generic fire()-patch event forwarding. Child
registration (popup/tooltip/layer binding) uses real Popup/Tooltip/Marker
instances since #onChildRegister discriminates by instanceof.
- test/core/props.test.ts: the codec functions in isolation.
- test/components/*.test.ts: grouped smoke tests across all components.
- test/integration.test.ts: full tree wiring (map + tile-layer +
feature-group + marker + popup).
Caught and fixed one real bug along the way: urlProp's "live url getter"
from the last refactor was dead code -- neither ImageOverlay nor
VideoOverlay actually expose getUrl(). Removed it and the now-pointless
getUrl?() from the Sourced interface in shared-props.ts.
tsconfig.test.json keeps test/ out of the tsc build (dist/ stays
test-free) while still typechecking it; oxlint.config.ts now covers test/
too, with max-classes-per-file relaxed there since testing a class
factory means many small one-off element classes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>