chore: prep 0.1.0 for npm + JSR publish

npm name `leaflet-components` is taken by another author, so the npm package
is renamed to `leaflet-web-components`; JSR stays `@buddy/leaflet-components`.
Adds the MIT `LICENSE` file (was declared but missing).

Entry split so JSR can publish with no slow types:

- `src/index.ts` is the shared/JSR entry and no longer carries any
  `declare global`.
- `src/core/globals.ts` (new) holds the `HTMLElementTagNameMap` /
  `HTMLElementEventMap` augmentations; imported only by the new npm entry
  `src/index.npm.ts` and by `test/setup.ts`, and listed in `jsr.json`'s
  `publish.exclude` so it never enters JSR's module graph.
- `package.json` `.`/`main`/`module`/`types` now point at `dist/index.npm.*`.

Every element file switches from `class extends WithProps({...})` to a
`const PROPS` (with an explicit type) plus
`const Base: LeafletElementConstructor<TheClass, typeof PROPS> = WithProps(PROPS)`,
which is what clears JSR's `unsupported-super-class-expr` /
`missing-explicit-type` errors. Adds a `Positional<T, Obj>` alias in
`props.ts` and explicit object types on the `shared-props.ts` fragments to
keep those annotations short. Empty group tables use `Record<never, never>`.

`npx jsr publish --dry-run` now reports "Success" with zero slow-type
errors; `npm run typecheck`, `lint`, `test` (76) and `build` all pass.

CLAUDE.md, docs/ and README.md updated for the rename, the entry split, and
the `const PROPS` / `const Base` pattern.
main
Buddy 2 weeks ago
parent f9c07a263d
commit b7f7a43113

@ -24,7 +24,7 @@ Formatting runs on `oxfmt` (oxc's Prettier-compatible formatter), configured in
### Testing ### Testing
Tests run under Vitest + jsdom (`test/**/*.test.ts`), with a single setup file (`test/setup.ts`) that stubs `ResizeObserver` -- jsdom doesn't implement it, and `leaflet-map.ts` constructs one unconditionally. Real Leaflet objects work fine under jsdom for everything this library actually needs to verify (option/attribute wiring, event forwarding); no browser is required. Two things worth knowing: Tests run under Vitest + jsdom (`test/**/*.test.ts`), with a single setup file (`test/setup.ts`) that stubs `ResizeObserver` (jsdom doesn't implement it, and `leaflet-map.ts` constructs one unconditionally) and imports `src/core/globals.ts` so the ambient `HTMLElementTagNameMap` / `HTMLElementEventMap` augmentations are in scope for the whole test program (tests import element modules directly, not the npm entry that would otherwise pull them in). Real Leaflet objects work fine under jsdom for everything this library actually needs to verify (option/attribute wiring, event forwarding); no browser is required. Two things worth knowing:
- Some Leaflet DOM state (the `<img>`/`<video>` element behind an overlay, a marker's `dragging` handler) is only created in `onAdd()`, i.e. once the layer is actually added to a real map -- tests touching that state append through a `<leaflet-map>` rather than standalone. - Some Leaflet DOM state (the `<img>`/`<video>` element behind an overlay, a marker's `dragging` handler) is only created in `onAdd()`, i.e. once the layer is actually added to a real map -- tests touching that state append through a `<leaflet-map>` rather than standalone.
- `test/core/with-props.test.ts` covers the `WithProps` mixin itself against a fake Leaflet-like class (no real Leaflet needed for most of it) -- the one exception is child registration (popup/tooltip/layer binding), which needs real `Popup`/`Tooltip`/`Layer` instances because `#onChildRegister` discriminates by `instanceof`, not duck typing. - `test/core/with-props.test.ts` covers the `WithProps` mixin itself against a fake Leaflet-like class (no real Leaflet needed for most of it) -- the one exception is child registration (popup/tooltip/layer binding), which needs real `Popup`/`Tooltip`/`Layer` instances because `#onChildRegister` discriminates by `instanceof`, not duck typing.
@ -42,14 +42,20 @@ This library wraps [Leaflet.js](https://leafletjs.com/) as native Web Components
Every component is split across two files with the same basename: Every component is split across two files with the same basename:
- **`src/elements/leaflet-foo.ts`** — `export default class LeafletFooElement extends WithProps(PROPS) {...}`. The class only; **no `customElements.define`**, no side effects. `src/elements/index.ts` is a barrel re-exporting all of them by name (`LeafletFooElement`), order-free because nothing here registers a tag. - **`src/elements/leaflet-foo.ts`** — a `const PROPS`, a `const Base = WithProps(PROPS)` (explicitly typed — see below), then `export default class LeafletFooElement extends Base {...}`. The class only; **no `customElements.define`**, no side effects. `src/elements/index.ts` is a barrel re-exporting all of them by name (`LeafletFooElement`), order-free because nothing here registers a tag.
- **`src/components/leaflet-foo.ts`** — three lines: `import LeafletFooElement from '../elements/leaflet-foo.ts'`, `customElements.define('leaflet-foo', LeafletFooElement)`, `export { LeafletFooElement }`. Importing this module (or `src/index.ts`) is what actually registers the tag. - **`src/components/leaflet-foo.ts`** — three lines: `import LeafletFooElement from '../elements/leaflet-foo.ts'`, `customElements.define('leaflet-foo', LeafletFooElement)`, `export { LeafletFooElement }`. Importing this module (or `src/index.ts` / `src/index.npm.ts`) is what actually registers the tag.
Most consumers just import `src/index.ts` (or a `components/*` module) and get the tags defined. The `elements/*` classes exist for plugin authors who want to subclass an element or register it under a different tag name without triggering the built-in `define`. Package subpath exports (`leaflet-components/elements`, `leaflet-components/elements/leaflet-foo.js`, `leaflet-components/components/leaflet-foo.js`) map straight onto `dist/`. Most consumers just import the package root (or a `components/*` module) and get the tags defined. The `elements/*` classes exist for plugin authors who want to subclass an element or register it under a different tag name without triggering the built-in `define`. Package subpath exports (`leaflet-web-components/elements`, `.../elements/leaflet-foo.js`, `.../components/leaflet-foo.js`) map straight onto `dist/`.
### Two package entry points: `src/index.ts` vs `src/index.npm.ts`
- **`src/index.ts`** is the shared entry: every element class, every helper, and the load-bearing `import './components/*.ts'` side effects. **No `declare global`.** This is what `jsr.json` publishes as `.`.
- **`src/index.npm.ts`** is the npm entry — `import './core/globals.ts'; export * from './index.ts';`. `package.json`'s `.`/`main`/`module`/`types` all point at its build (`dist/index.npm.*`).
- **`src/core/globals.ts`** holds the two ambient `declare global` blocks (`HTMLElementTagNameMap`, `HTMLElementEventMap`). It is imported **only** by `src/index.npm.ts` and `test/setup.ts`, and is listed in `jsr.json`'s `publish.exclude`, so it never enters JSR's module graph — JSR's "no slow types" check rejects `declare global` anywhere it can reach. npm consumers get the augmentations automatically through the package root; JSR (`jsr:@buddy/leaflet-components`) consumers do not, and add their own tag-map/event-map typing if they want it. The whole `src/` tree still sees the augmentations at build/typecheck time because `globals.ts` is under `tsconfig.json`'s `include`.
### `WithProps` mixin: `src/core/with-props.ts` ### `WithProps` mixin: `src/core/with-props.ts`
`WithProps(PROPS, options?)` is a mixin factory: it always extends `HTMLElement` internally (there's no separate base-class parameter) and returns a constructor. Each element class (in `src/elements/`) defines a `PROPS` table (a const record mapping property names to `PropDef` descriptors from `src/core/props.ts` — kebab-cased automatically for the attribute name) and extends `WithProps(PROPS)`. The mixin handles: `WithProps(PROPS, options?)` is a mixin factory: it always extends `HTMLElement` internally (there's no separate base-class parameter) and returns a constructor typed `LeafletElementConstructor<TObj, TProps>`. Each element class (in `src/elements/`) defines a `PROPS` table (a const record mapping property names to `PropDef` descriptors from `src/core/props.ts` — kebab-cased automatically for the attribute name) and extends it. The mixin handles:
- `static observedAttributes`, derived from the PROPS table keys. - `static observedAttributes`, derived from the PROPS table keys.
- Property getters/setters on the prototype (one `Object.defineProperty` per prop) that read the live Leaflet value when the prop defines a `get`, falling back to the attribute, then the default; setting encodes the value onto the attribute. - Property getters/setters on the prototype (one `Object.defineProperty` per prop) that read the live Leaflet value when the prop defines a `get`, falling back to the attribute, then the default; setting encodes the value onto the attribute.
@ -58,40 +64,62 @@ Most consumers just import `src/index.ts` (or a `components/*` module) and get t
- Every Leaflet event the created object fires is re-emitted on the element as `leaflet:<type>` (e.g. `leaflet:zoomend`, `leaflet:dragend`), with `detail` matching exactly what a real Leaflet `.on()` listener would receive (`type`/`target`/`sourceTarget` plus the event's own fields). This is generic and automatic: the private `#forwardEvents()` wraps the object's own `fire()` method, so no per-component or per-event-type registration is needed. Not bubbling — Leaflet already propagates layer events up to the map, so `<leaflet-map>` would otherwise see each one twice. - Every Leaflet event the created object fires is re-emitted on the element as `leaflet:<type>` (e.g. `leaflet:zoomend`, `leaflet:dragend`), with `detail` matching exactly what a real Leaflet `.on()` listener would receive (`type`/`target`/`sourceTarget` plus the event's own fields). This is generic and automatic: the private `#forwardEvents()` wraps the object's own `fire()` method, so no per-component or per-event-type registration is needed. Not bubbling — Leaflet already propagates layer events up to the map, so `<leaflet-map>` would otherwise see each one twice.
- `options.attach` controls how the element joins the component tree: `'children'` (default) registers with its parent and adopts registering descendants as layers/popups/tooltips; `'self'` (popups, tooltips, controls) registers with its parent but manages no children; `'none'` (the map, icons) does neither. - `options.attach` controls how the element joins the component tree: `'children'` (default) registers with its parent and adopts registering descendants as layers/popups/tooltips; `'self'` (popups, tooltips, controls) registers with its parent but manages no children; `'none'` (the map, icons) does neither.
#### The `const PROPS` / `const Base` shape (JSR "no slow types")
An element file must not write `class Foo extends WithProps({ ... })` directly. JSR's fast-check rejects a call expression as a superclass ("super class expression too complex") and rejects any leaked type it can't resolve without full inference. So every element file does:
```ts
const PROPS: { lat: PropDef<number, LMap>; /* ...one line per prop... */ } = { ... };
const Base: LeafletElementConstructor<TheLeafletClass, typeof PROPS> = WithProps(PROPS /*, opts */);
export default class LeafletFooElement extends Base { ... }
```
- **`PROPS` needs an explicit type annotation.** Spell out `{ key: PropDef<T, Obj>; ... }`, using `typeof pathProps` / `typeof latLngProps` / `typeof tileLayerProps` in an intersection for the shared fragments (which now carry their own explicit types in `shared-props.ts`), and `Positional<T, Obj>` (alias in `props.ts` for `PropDef<T, Obj> & { option: false }`) for `positional()` props. `as const` alone is only enough when **every** value is a plain identifier reference (e.g. `leaflet-circle`, `leaflet-tile-layer`, `leaflet-polygon`); any inline `num(…)` / `str(…)` / `positional(…)` in the table forces the full annotation. Empty tables use `Record<never, never>` (not `Record<string, never>`, which would impose a `never` index signature on the subclass).
- **`Base` needs `: LeafletElementConstructor<TheLeafletClass, typeof PROPS>`** — without it JSR can't resolve the base's type either. `TheLeafletClass` is whatever `createLeafletObject` returns; the subclass still redeclares `declare readonly leafletObject?: X` to narrow it.
TypeScript typing for the above is opt-in per component, not part of `WithProps` itself — see "Typing a component's events" below. TypeScript typing for the above is opt-in per component, not part of `WithProps` itself — see "Typing a component's events" below.
### `leaflet-map`: `src/elements/leaflet-map.ts` ### `leaflet-map`: `src/elements/leaflet-map.ts`
`LeafletMapElement` extends `WithProps(PROPS, { attach: 'none' })` like everything else, but builds its own Shadow DOM root in `connectedCallback` instead of relying on `createLeafletObject` alone, and terminates all bubbling `leaflet-register` events by calling `layer.addTo(this.map)` since it's the root of the component tree. Uses a `ResizeObserver` on the host element to call `map.invalidateSize()` automatically. `src/components/leaflet-map.ts` is the usual three-line `import` + `define` + re-export. `LeafletMapElement` extends a `Base = WithProps(PROPS, { attach: 'none' })` like everything else, but builds its own Shadow DOM root in `connectedCallback` instead of relying on `createLeafletObject` alone, and terminates all bubbling `leaflet-register` events by calling `layer.addTo(this.map)` since it's the root of the component tree. Uses a `ResizeObserver` on the host element to call `map.invalidateSize()` automatically. `src/components/leaflet-map.ts` is the usual three-line `import` + `define` + re-export.
### Child component pattern ### Child component pattern
All non-map element classes extend `WithProps(PROPS, options?)`. To add a new component: All non-map element classes extend a `Base = WithProps(PROPS, options?)`. To add a new component:
1. In `src/elements/leaflet-foo.ts`, define a `PROPS = {...}` const table mapping property names to `PropDef` entries (reuse fragments from `src/core/shared-props.ts` where they fit — `pathProps`, `latLngProps`, `tileLayerProps`, `urlProp`). 1. In `src/elements/leaflet-foo.ts`, define a `const PROPS` table mapping property names to `PropDef` entries (reuse fragments from `src/core/shared-props.ts` where they fit — `pathProps`, `latLngProps`, `tileLayerProps`, `urlProp`), **with an explicit type annotation** (see "The `const PROPS` / `const Base` shape" above).
2. In the same file, `export default class LeafletFooElement extends WithProps(PROPS)` implementing `createLeafletObject(options): L.Layer`, and `declare readonly leafletObject?: TheLeafletClass;` (the mixin's own inference of the object type from the PROPS table alone isn't reliable enough to skip this). 2. In the same file, `const Base: LeafletElementConstructor<TheLeafletClass, typeof PROPS> = WithProps(PROPS /*, opts */);` then `export default class LeafletFooElement extends Base` implementing `createLeafletObject(options): L.Layer`, and `declare readonly leafletObject?: TheLeafletClass;` (the mixin's own inference of the object type from the PROPS table alone isn't reliable enough to skip this).
3. Provide a `set` function on individual `PropDef`s only where the default setter-based dispatch won't work (common for coordinate pairs — see `latLngProps` in shared-props.ts). 3. Provide a `set` function on individual `PropDef`s only where the default setter-based dispatch won't work (common for coordinate pairs — see `latLngProps` in shared-props.ts).
4. Add `src/components/leaflet-foo.ts`: `import LeafletFooElement from '../elements/leaflet-foo.ts';` then `customElements.define('leaflet-foo', LeafletFooElement);` then `export { LeafletFooElement };`. 4. Add `src/components/leaflet-foo.ts`: `import LeafletFooElement from '../elements/leaflet-foo.ts';` then `customElements.define('leaflet-foo', LeafletFooElement);` then `export { LeafletFooElement };`.
5. Add `LeafletFooElement` to the `src/elements/index.ts` barrel (order-free), add a side-effect `import './components/leaflet-foo.ts';` to `src/index.ts`, and add the tag to the `HTMLElementTagNameMap` augmentation at the bottom of `src/index.ts` (which imports the class names from `./elements/index.ts` — a `export *` doesn't bind them locally). **The order of the `./components/*` imports in `src/index.ts` is load-bearing**, not cosmetic: if the new component listens for a bubbling announcement from some other tag (a `leaflet-register`-style child, or something like `leaflet-line-sync`), its `import` must come _before_ that other tag's. `customElements.define()` upgrades every matching element already in the document immediately, so on a real static-HTML page, whichever tag gets defined first wins the race — a child tag defined before its listening parent will fire its one-shot connect-time announcement into a parent that doesn't exist yet, and that announcement is gone for good. See the comment at the top of `index.ts` for the full ordering and worked example. (The `elements/*` modules and the `elements/index.ts` barrel carry no `define`, so their order never matters.) 5. Add `LeafletFooElement` to the `src/elements/index.ts` barrel (order-free), add a side-effect `import './components/leaflet-foo.ts';` to `src/index.ts`, and add the tag to the `HTMLElementTagNameMap` augmentation in `src/core/globals.ts` (npm-only; imports the class names from `../elements/index.ts`). **The order of the `./components/*` imports in `src/index.ts` is load-bearing**, not cosmetic: if the new component listens for a bubbling announcement from some other tag (a `leaflet-register`-style child, or something like `leaflet-line-sync`), its `import` must come _before_ that other tag's. `customElements.define()` upgrades every matching element already in the document immediately, so on a real static-HTML page, whichever tag gets defined first wins the race — a child tag defined before its listening parent will fire its one-shot connect-time announcement into a parent that doesn't exist yet, and that announcement is gone for good. See the comment at the top of `index.ts` for the full ordering and worked example. (The `elements/*` modules and the `elements/index.ts` barrel carry no `define`, so their order never matters.)
6. If the component fires meaningful Leaflet events (beyond nothing), compose or reuse an event map in `src/core/event-types.ts` and add `declare addEventListener: LeafletAddEventListener<TheEvents>;` / `declare removeEventListener: LeafletRemoveEventListener<TheEvents>;` (see "Typing a component's events" below). 6. If the component fires meaningful Leaflet events (beyond nothing), compose or reuse an event map in `src/core/event-types.ts` and add `declare addEventListener: LeafletAddEventListener<TheEvents>;` / `declare removeEventListener: LeafletRemoveEventListener<TheEvents>;` (see "Typing a component's events" below).
### Special cases ### Special cases
- **`leaflet-polygon`** / **`leaflet-polyline`** take their vertices from `<leaflet-line>` children rather than an attribute. This is purely event-driven, not a lookup: `<leaflet-line>` fires `leaflet-line-sync` (on connect and on every lat/lng change) and `leaflet-line-remove` (on disconnect) on itself, each carrying its own position (`src/core/register.ts`); the polygon/polyline never reads a child's property or queries the DOM for them. `src/core/vertex-tracker.ts`'s `VertexTracker` (shared by both) turns that event stream into an ordered coordinate list, inserting a newly-registered vertex at its actual document position via `compareDocumentPosition` rather than assuming registration order matches DOM order. One non-obvious wrinkle `<leaflet-line>` has to work around: `disconnectedCallback` fires _after_ a node is already detached from its parent, so a bubbling dispatch from the node itself has nowhere to go on removal — it caches `parentNode` in `connectedCallback` and dispatches `leaflet-line-remove` from that cached reference instead. - **`leaflet-polygon`** / **`leaflet-polyline`** take their vertices from `<leaflet-line>` children rather than an attribute. This is purely event-driven, not a lookup: `<leaflet-line>` fires `leaflet-line-sync` (on connect and on every lat/lng change) and `leaflet-line-remove` (on disconnect) on itself, each carrying its own position (`src/core/register.ts`); the polygon/polyline never reads a child's property or queries the DOM for them. `src/core/vertex-tracker.ts`'s `VertexTracker` (shared by both) turns that event stream into an ordered coordinate list, inserting a newly-registered vertex at its actual document position via `compareDocumentPosition` rather than assuming registration order matches DOM order. One non-obvious wrinkle `<leaflet-line>` has to work around: `disconnectedCallback` fires _after_ a node is already detached from its parent, so a bubbling dispatch from the node itself has nowhere to go on removal — it caches `parentNode` in `connectedCallback` and dispatches `leaflet-line-remove` from that cached reference instead.
- **`leaflet-popup`** / **`leaflet-tooltip`**: content comes from `innerHTML`, not attributes. `leaflet-popup` watches for DOM mutations to keep Leaflet in sync. - **`leaflet-popup`** / **`leaflet-tooltip`**: content comes from `innerHTML`, not attributes. `leaflet-popup` watches for DOM mutations to keep Leaflet in sync.
- **`leaflet-layer-group`** / **`leaflet-feature-group`**: passthrough containers built with `WithProps({})` (an empty props table) — they have no options of their own, but still get the standard lifecycle, child registration, and `leaflet:` event forwarding for free. Children register themselves into them via the standard bubble mechanism. - **`leaflet-layer-group`** / **`leaflet-feature-group`**: passthrough containers built from an empty props table (`const PROPS: Record<never, never> = {}`) — they have no options of their own, but still get the standard lifecycle, child registration, and `leaflet:` event forwarding for free. Children register themselves into them via the standard bubble mechanism.
### Typing a component's events: `src/core/event-types.ts` ### Typing a component's events: `src/core/event-types.ts`
Two separate TypeScript augmentations make the components usable from TS, neither of which costs anything at runtime: Two separate TypeScript augmentations make the components usable from TS, neither of which costs anything at runtime:
- **`HTMLElementTagNameMap`** (bottom of `src/index.ts`) maps every `leaflet-*` tag to its `LeafletFooElement` class, so `document.createElement`/`querySelector` infer the right type. Since `export *` doesn't bind the names locally, the tag map needs its own `import type { ... } from './elements/index.ts'`. - **`HTMLElementTagNameMap`** (in `src/core/globals.ts`, alongside the `HTMLElementEventMap` augmentation for the internal bubbling events) maps every `leaflet-*` tag to its `LeafletFooElement` class, so `document.createElement`/`querySelector` infer the right type. `globals.ts` is npm-only (imported by `src/index.npm.ts` and `test/setup.ts`, excluded from JSR) — see "Two package entry points" above.
- **Per-component `addEventListener`/`removeEventListener`** typing for `leaflet:<name>` events. `event-types.ts` defines small reusable event-name → Leaflet-payload-type fragments (`MouseEvents`, `PopupBindEvents`, `DragEvents`, etc. — mirroring how `shared-props.ts` shares prop fragments), composed per family (`PathEvents`, `MarkerEvents`, `MapEvents`, `TileLayerEvents`, `DivOverlayLayerEvents`, `GroupEvents`). A component applies its family with `declare addEventListener: LeafletAddEventListener<TheEvents>;` (and the `Remove` counterpart), the same `declare`-to-narrow-without-runtime-code idiom as `declare readonly leafletObject?: X`. Components that don't fire meaningful custom events (controls, icons, `leaflet-line`) skip this and keep the default `HTMLElementEventMap` typing. - **Per-component `addEventListener`/`removeEventListener`** typing for `leaflet:<name>` events. `event-types.ts` defines small reusable event-name → Leaflet-payload-type fragments (`MouseEvents`, `PopupBindEvents`, `DragEvents`, etc. — mirroring how `shared-props.ts` shares prop fragments), composed per family (`PathEvents`, `MarkerEvents`, `MapEvents`, `TileLayerEvents`, `DivOverlayLayerEvents`, `GroupEvents`). A component applies its family with `declare addEventListener: LeafletAddEventListener<TheEvents>;` (and the `Remove` counterpart), the same `declare`-to-narrow-without-runtime-code idiom as `declare readonly leafletObject?: X`. Components that don't fire meaningful custom events (controls, icons, `leaflet-line`) skip this and keep the default `HTMLElementEventMap` typing.
`LeafletAddEventListener`/`LeafletRemoveEventListener` (in `with-props.ts`) deliberately have no generic `(type: string, ...)` fallback overload, unlike the real DOM API — a fallback would silently accept any unrecognized `leaflet:*` name too, which defeats the point of typing this at all. The cost is that a genuinely dynamic (non-literal) event name string needs a cast. `LeafletAddEventListener`/`LeafletRemoveEventListener` (in `with-props.ts`) deliberately have no generic `(type: string, ...)` fallback overload, unlike the real DOM API — a fallback would silently accept any unrecognized `leaflet:*` name too, which defeats the point of typing this at all. The cost is that a genuinely dynamic (non-literal) event name string needs a cast.
If you add or change what events a component fires, keep `#forwardEvents`'s `detail` shape (`with-props.ts`) in mind: it mirrors Leaflet's own `Evented#fire` merge (original data plus `type`/`target`/`sourceTarget`) so the event-types.ts payload types stay honest — don't type an event's `detail` against a Leaflet interface it wouldn't actually match at runtime. If you add or change what events a component fires, keep `#forwardEvents`'s `detail` shape (`with-props.ts`) in mind: it mirrors Leaflet's own `Evented#fire` merge (original data plus `type`/`target`/`sourceTarget`) so the event-types.ts payload types stay honest — don't type an event's `detail` against a Leaflet interface it wouldn't actually match at runtime.
### Output ### Output & dual publish
`tsc` compiles `src/``dist/` as individual ESM modules (`.js` + `.d.ts` + `.d.ts.map`) — no bundling step, so `dist/elements/` and `dist/components/` mirror `src/`. There is no CJS or UMD build. Leaflet is always external (never bundled). Imports within source use `.ts` extensions; `rewriteRelativeImportExtensions` in tsconfig strips them to `.js` in the emitted `.js` (the `.d.ts` keep `.ts` specifiers, which TS resolves to the sibling `.d.ts`).
Two registries, different entry files (both pinned to the same version — keep them in step):
| Registry | Package name | Entry | Ships |
| -------- | --------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| npm | `leaflet-web-components` | `package.json` `.`/`main`/`module`/`types` → `dist/index.npm.*` | compiled `dist/` + `README.md` + `LICENSE` (`files` field); `prepublishOnly` runs `build` |
| JSR | `@buddy/leaflet-components` | `jsr.json` `exports``./src/index.ts` (+ `./elements`) | TypeScript source directly, minus `jsr.json`'s `publish.exclude` (which drops `src/index.npm.ts`, `src/core/globals.ts`, `test/`, `docs/`, `dist/`, configs) |
`tsc` compiles `src/``dist/` as individual ESM modules (`.js` + `.d.ts` + `.d.ts.map`) — no bundling step, so `dist/elements/` and `dist/components/` mirror `src/`. Consumers import `dist/index.js` (or, via the `package.json` subpath exports, `leaflet-components/elements`, `leaflet-components/elements/leaflet-foo.js`, or `leaflet-components/components/leaflet-foo.js`) directly; there is no CJS or UMD build. Leaflet is always external (never bundled). Imports within source use `.ts` extensions; `rewriteRelativeImportExtensions` in tsconfig strips them to `.js` in the tsc output (`tsconfig.json:16`). The npm entry (`src/index.npm.ts`) adds the `declare global` augmentations via `src/core/globals.ts`; the JSR entry (`src/index.ts`) can't, because JSR's "no slow types" check forbids `declare global` in its published graph. That constraint is also why every element file uses the explicitly-typed `const PROPS` / `const Base` shape (see the `WithProps` section). `npx jsr publish --dry-run` is the authoritative check; it must print "Success" with no slow-type errors.

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Buddy Sandidge
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,35 +1,37 @@
# leaflet-components # leaflet-web-components
[Leaflet.js](https://leafletjs.com/) as native Web Components (Custom Elements). Each HTML element maps 1:1 to a Leaflet object with reactive attribute binding. [Leaflet.js](https://leafletjs.com/) as native Web Components (Custom Elements). Each HTML element maps 1:1 to a Leaflet object with reactive attribute binding.
## Installation ## Installation
```bash ```bash
npm install leaflet-components npm install leaflet-web-components leaflet
``` ```
Also published to JSR as [`@buddy/leaflet-components`](https://jsr.io/@buddy/leaflet-components) (`deno add jsr:@buddy/leaflet-components`). `leaflet` is always a peer dependency you install yourself; there is no CommonJS or UMD build. The examples below use the npm name — substitute `@buddy/leaflet-components` if you pull from JSR.
## Import options ## Import options
The package is ESM-only — `tsc` emits `dist/` as individual modules, one per component, with no bundling step. Deep imports work for registering only what you use. The package is ESM-only — `tsc` emits `dist/` as individual modules, one per component, with no bundling step. Deep imports work for registering only what you use.
```js ```js
// Registers all components // Registers all components
import 'leaflet-components'; import 'leaflet-web-components';
// Register only one component (defines the <leaflet-marker> tag) // Register only one component (defines the <leaflet-marker> tag)
import 'leaflet-components/components/leaflet-marker.js'; import 'leaflet-web-components/components/leaflet-marker.js';
``` ```
Each component is two modules with the same basename: `components/leaflet-marker.js` is the side-effecting one that calls `customElements.define()`, and `elements/leaflet-marker.js` is just the class (`default` export, **no** `define`). Import from `elements/` when you want to subclass a component or register it under a different tag name: Each component is two modules with the same basename: `components/leaflet-marker.js` is the side-effecting one that calls `customElements.define()`, and `elements/leaflet-marker.js` is just the class (`default` export, **no** `define`). Import from `elements/` when you want to subclass a component or register it under a different tag name:
```js ```js
// The class only — nothing is registered // The class only — nothing is registered
import LeafletMarkerElement from 'leaflet-components/elements/leaflet-marker.js'; import LeafletMarkerElement from 'leaflet-web-components/elements/leaflet-marker.js';
// …or the whole set, by name // …or the whole set, by name
import { LeafletMarkerElement, LeafletCircleElement } from 'leaflet-components/elements'; import { LeafletMarkerElement, LeafletCircleElement } from 'leaflet-web-components/elements';
``` ```
`leaflet` is always an external dependency — you must install it yourself. There is no CommonJS or UMD build. On npm the package root resolves to a build that also installs the ambient TypeScript augmentations (`HTMLElementTagNameMap` so `document.querySelector('leaflet-map')` is typed, and the custom-event map). The JSR build omits those — JSR's type rules disallow global augmentation — so from JSR you add your own if you want them.
## Usage ## Usage
@ -37,7 +39,7 @@ Import once to register all custom elements, then use them declaratively in HTML
```html ```html
<script type="module"> <script type="module">
import 'leaflet-components'; import 'leaflet-web-components';
</script> </script>
<leaflet-map lat="51.505" lng="-0.09" zoom="13" style="height:400px"> <leaflet-map lat="51.505" lng="-0.09" zoom="13" style="height:400px">
@ -603,20 +605,34 @@ This library's own components are built from a small toolkit — a mixin, some p
### The pattern ### The pattern
1. Describe your attributes as a `PROPS` table, then extend `WithProps(PROPS, options?)`. 1. Describe your attributes as a `const PROPS` table, build a base class with `const Base = WithProps(PROPS, options?)`, then `extends Base`.
2. Implement `createLeafletObject(options)`, returning whatever Leaflet object your plugin provides. 2. Implement `createLeafletObject(options)`, returning whatever Leaflet object your plugin provides.
3. `declare readonly leafletObject?: TheType;``WithProps`'s own inference of the object type from the `PROPS` table alone isn't reliable enough to skip this (every component in this package does it). 3. `declare readonly leafletObject?: TheType;``WithProps`'s own inference of the object type from the `PROPS` table alone isn't reliable enough to skip this (every component in this package does it).
4. Call `customElements.define('my-plugin-layer', MyPluginLayer)`. (If you want to let _your_ consumers subclass or re-register, mirror this package's split: put the class in its own module as a `default` export and keep the `define()` call in a separate side-effecting module.) 4. Call `customElements.define('my-plugin-layer', MyPluginLayer)`. (If you want to let _your_ consumers subclass or re-register, mirror this package's split: put the class in its own module as a `default` export and keep the `define()` call in a separate side-effecting module.)
```ts ```ts
import { MyClusterGroup, type MyClusterGroupOptions } from 'some-leaflet-plugin'; import { MyClusterGroup, type MyClusterGroupOptions } from 'some-leaflet-plugin';
import { WithProps, bool, num } from 'leaflet-components'; import {
WithProps,
class MyClusterLayer extends WithProps({ bool,
num,
type PropDef,
type LeafletElementConstructor,
} from 'leaflet-web-components';
const PROPS: {
radius: PropDef<number>;
disableClusteringAtZoom: PropDef<number>;
spiderfy: PropDef<boolean>;
} = {
radius: num(80), radius: num(80),
disableClusteringAtZoom: num(18), disableClusteringAtZoom: num(18),
spiderfy: bool(true), spiderfy: bool(true),
}) { };
const Base: LeafletElementConstructor<MyClusterGroup, typeof PROPS> = WithProps(PROPS);
class MyClusterLayer extends Base {
declare readonly leafletObject?: MyClusterGroup; declare readonly leafletObject?: MyClusterGroup;
createLeafletObject(options: MyClusterGroupOptions): MyClusterGroup { createLeafletObject(options: MyClusterGroupOptions): MyClusterGroup {
@ -627,6 +643,13 @@ class MyClusterLayer extends WithProps({
customElements.define('my-cluster-layer', MyClusterLayer); customElements.define('my-cluster-layer', MyClusterLayer);
``` ```
The two `const`s with explicit type annotations — rather than
`class X extends WithProps({ ... })` inline — are what lets this package
publish to JSR, whose type checker rejects a call expression as a superclass
and any public type it can't resolve without full inference. If you're only
consuming the package in an app (not publishing your component to JSR), the
inline `extends WithProps({ ... })` form works too.
That's it — no other integration point is needed. `WithProps` handles `observedAttributes`, attribute↔property sync, building the options object from attributes on connect, dispatching attribute changes to the matching Leaflet setter (or a `set` you provide per prop, for anything a setter can't handle), and re-emitting every event your object fires as `leaflet:<type>`. That's it — no other integration point is needed. `WithProps` handles `observedAttributes`, attribute↔property sync, building the options object from attributes on connect, dispatching attribute changes to the matching Leaflet setter (or a `set` you provide per prop, for anything a setter can't handle), and re-emitting every event your object fires as `leaflet:<type>`.
### Attribute builders (`PROPS` table entries) ### Attribute builders (`PROPS` table entries)
@ -645,19 +668,35 @@ Each builder's `opts` can include `set(obj, value, el)` (called on attribute cha
### Reusing the built-in fragments ### Reusing the built-in fragments
Common attribute groups are already factored out and exported: `pathProps` (Leaflet `Path` styling — color, weight, dashes, fill, etc.), `latLngProps` (a synced lat/lng pair, for anything positioned on the map), `tileLayerProps` (the base `GridLayer`/`TileLayer` options), and `urlProp`. A custom vector layer plugin, for instance, can just spread `pathProps` in instead of redeclaring styling attributes: Common attribute groups are already factored out and exported: `pathProps` (Leaflet `Path` styling — color, weight, dashes, fill, etc.), `latLngProps` (a synced lat/lng pair, for anything positioned on the map), `tileLayerProps` (the base `GridLayer`/`TileLayer` options), and `urlProp`. Each is exported with an explicit object type, so a `PROPS` annotation can pull it in with `typeof`. A custom vector layer plugin, for instance, can just spread `pathProps` in instead of redeclaring styling attributes:
```ts ```ts
import { WithProps, pathProps, positional, json } from 'leaflet-components'; import {
WithProps,
class MyShapeLayer extends WithProps({ pathProps,
positional,
json,
type Positional,
type LeafletElementConstructor,
} from 'leaflet-web-components';
const PROPS: typeof pathProps & {
data: Positional<MyData | null>;
} = {
...pathProps, ...pathProps,
data: positional(json(null)), data: positional(json<MyData | null>(null)),
}) { };
const Base: LeafletElementConstructor<MyShape, typeof PROPS> = WithProps(PROPS);
class MyShapeLayer extends Base {
/* ... */ /* ... */
} }
``` ```
`Positional<T, Obj>` is the exported alias for a `positional()` prop's type
(`PropDef<T, Obj> & { option: false }`).
### Nesting into the tree ### Nesting into the tree
By default (`attach: 'children'`, the default when you omit `options`), your component registers itself with its nearest ancestor and accepts registering descendants as child layers/popups/tooltips. Use `attach: 'self'` for something that has a parent but manages no children of its own (a popup, a tooltip, a control); use `attach: 'none'` for something that's neither (an icon). By default (`attach: 'children'`, the default when you omit `options`), your component registers itself with its nearest ancestor and accepts registering descendants as child layers/popups/tooltips. Use `attach: 'self'` for something that has a parent but manages no children of its own (a popup, a tooltip, a control); use `attach: 'none'` for something that's neither (an icon).
@ -674,16 +713,17 @@ import {
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
type PathEvents, type PathEvents,
} from 'leaflet-components'; } from 'leaflet-web-components';
class MyShapeLayer extends WithProps({/* ... */}) { // Base = WithProps(PROPS), as in the previous example
class MyShapeLayer extends Base {
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;
declare removeEventListener: LeafletRemoveEventListener<PathEvents>; declare removeEventListener: LeafletRemoveEventListener<PathEvents>;
/* ... */ /* ... */
} }
``` ```
`event-types.ts`'s fragments (`MouseEvents`, `PopupBindEvents`, `DragEvents`, `TileEvents`, ...) and their compositions (`PathEvents`, `MarkerEvents`, `TileLayerEvents`, `GroupEvents`, `MapEvents`, ...) are all exported for reuse; compose your own if your plugin fires events none of them cover. If you also want `document.createElement('my-cluster-layer')` to infer your class, augment `HTMLElementTagNameMap` the same way this package does for its own tags: `event-types.ts`'s fragments (`MouseEvents`, `PopupBindEvents`, `DragEvents`, `TileEvents`, ...) and their compositions (`PathEvents`, `MarkerEvents`, `TileLayerEvents`, `GroupEvents`, `MapEvents`, ...) are all exported for reuse; compose your own if your plugin fires events none of them cover. If you also want `document.createElement('my-cluster-layer')` to infer your class, augment `HTMLElementTagNameMap`:
```ts ```ts
declare global { declare global {
@ -693,12 +733,14 @@ declare global {
} }
``` ```
This package keeps its own version of that augmentation in a dedicated module (`src/core/globals.ts`) that its npm entry imports and its JSR entry doesn't — JSR disallows `declare global`. If you publish your plugin to JSR, do the same: keep the augmentation out of your JSR entry's module graph.
### Custom marker icons ### Custom marker icons
`<leaflet-marker>` listens for a generic `icon-changed` event and calls `setIcon()` with whatever `detail.icon` holds — it doesn't check which component fired it. A component wrapping a plugin that provides its own icon (e.g. a themed marker icon set) just needs to call `emitIconChanged`: `<leaflet-marker>` listens for a generic `icon-changed` event and calls `setIcon()` with whatever `detail.icon` holds — it doesn't check which component fired it. A component wrapping a plugin that provides its own icon (e.g. a themed marker icon set) just needs to call `emitIconChanged`:
```ts ```ts
import { emitIconChanged } from 'leaflet-components'; import { emitIconChanged } from 'leaflet-web-components';
// after building or updating your icon: // after building or updating your icon:
emitIconChanged(this, myPluginIcon); emitIconChanged(this, myPluginIcon);

@ -8,8 +8,9 @@ element owns that object for its connected lifetime, holds the only reference
to it, and exposes it as `element.leafletObject`. to it, and exposes it as `element.leafletObject`.
Nothing about the wrapping is per-object hand-written plumbing. An element Nothing about the wrapping is per-object hand-written plumbing. An element
class is small (often 3060 lines): a table of property descriptors, a file is small: an explicitly-typed `const PROPS` table of property
`createLeafletObject()` that calls one Leaflet constructor, and a couple of descriptors, a `const Base = WithProps(PROPS)`, then a class with a
`createLeafletObject()` that calls one Leaflet constructor and a couple of
`declare` lines for types. Everything else — attributes, option building, `declare` lines for types. Everything else — attributes, option building,
two-way sync, event forwarding, tree membership — comes from the `WithProps` two-way sync, event forwarding, tree membership — comes from the `WithProps`
mixin. mixin.
@ -19,29 +20,48 @@ mixin.
Each component is two files sharing a basename: Each component is two files sharing a basename:
- **`src/elements/leaflet-foo.ts`** exports `default class LeafletFooElement - **`src/elements/leaflet-foo.ts`** exports `default class LeafletFooElement
extends WithProps(PROPS)` — the class alone, no side effects. Import it (or extends Base` (where `const Base = WithProps(PROPS)`) — the class alone, no
the `src/elements/index.ts` barrel, which re-exports every class by name) side effects. Import it (or the `src/elements/index.ts` barrel, which
to get the constructor **without** registering a tag; useful for re-exports every class by name) to get the constructor **without**
subclassing or defining it under a different name. registering a tag; useful for subclassing or defining it under a different
name.
- **`src/components/leaflet-foo.ts`** is three lines: import the class, - **`src/components/leaflet-foo.ts`** is three lines: import the class,
`customElements.define('leaflet-foo', LeafletFooElement)`, re-export it. `customElements.define('leaflet-foo', LeafletFooElement)`, re-export it.
Importing this module (or `src/index.ts`, which imports all of them in a Importing this module (or `src/index.ts` / `src/index.npm.ts`, which import
load-bearing order — see [06](./06-load-order.md)) is what registers the all of them in a load-bearing order — see [06](./06-load-order.md)) is what
tag. registers the tag.
`package.json` maps `leaflet-components/elements`, `package.json` maps `leaflet-web-components/elements`,
`leaflet-components/elements/leaflet-foo.js` and `.../elements/leaflet-foo.js` and `.../components/leaflet-foo.js` onto the
`leaflet-components/components/leaflet-foo.js` onto the matching `dist/` matching `dist/` files.
files.
### Two package entry points
`src/index.ts` is the shared entry (all classes + the load-bearing
`./components/*` side effects, **no `declare global`**) and is what `jsr.json`
publishes. `src/index.npm.ts` is the npm entry: it re-exports `./index.ts`
**and** imports `src/core/globals.ts`, which carries the two ambient
`declare global` blocks. `globals.ts` is imported only by the npm entry (and
`test/setup.ts`) and is excluded from the JSR tarball, because JSR's "no slow
types" check rejects `declare global` anywhere in its module graph. See
[07](./07-tooling-and-build.md).
## The `WithProps` mixin ## The `WithProps` mixin
`src/core/with-props.ts`. `WithProps(PROPS, options?)` is a mixin **factory**: `src/core/with-props.ts`. `WithProps(PROPS, options?)` is a mixin **factory**:
it always extends `HTMLElement` internally (there is no base-class parameter) it always extends `HTMLElement` internally (there is no base-class parameter)
and returns a constructor. An element class does: and returns a constructor typed `LeafletElementConstructor<TObj, TProps>`. An
element file does:
```ts ```ts
export default class LeafletMarkerElement extends WithProps(PROPS) { const PROPS: typeof latLngProps & {
title: PropDef<string, Marker>;
/* ...one line per prop... */
} = { ...latLngProps, title: str('', { set: /* ... */ }), /* ... */ };
const Base: LeafletElementConstructor<Marker, typeof PROPS> = WithProps(PROPS);
export default class LeafletMarkerElement extends Base {
declare readonly leafletObject?: Marker; declare readonly leafletObject?: Marker;
createLeafletObject(options: MarkerOptions): Marker { createLeafletObject(options: MarkerOptions): Marker {
return new Marker([this.lat, this.lng], options); return new Marker([this.lat, this.lng], options);
@ -53,6 +73,21 @@ export default class LeafletMarkerElement extends WithProps(PROPS) {
(see [02](./02-props-and-attributes.md)). From that table alone the mixin (see [02](./02-props-and-attributes.md)). From that table alone the mixin
derives everything below. derives everything below.
### Why `const PROPS` / `const Base`, both explicitly typed
`class Foo extends WithProps({ ... })` is not allowed: JSR's fast-check
rejects a call expression as a superclass, and rejects any leaked type it
can't resolve without full type inference. So each file hoists the table to
`const PROPS` **with an explicit type annotation**, then
`const Base: LeafletElementConstructor<TheLeafletClass, typeof PROPS> =
WithProps(PROPS)`, then `extends Base`. The shared fragments in
`shared-props.ts` (`pathProps`, `latLngProps`, `tileLayerProps`, `urlProp`)
carry their own explicit types so a `PROPS` annotation can reference them via
`typeof`; `Positional<T, Obj>` (in `props.ts`) is the alias for a
`positional()` prop's type. `as const` with no annotation works only when
every table value is a bare identifier reference. This is a JSR constraint,
not a TypeScript one — see [07](./07-tooling-and-build.md).
### What the generated class does ### What the generated class does
- **`static observedAttributes`** — the kebab-cased attribute name of every - **`static observedAttributes`** — the kebab-cased attribute name of every
@ -102,10 +137,10 @@ derives everything below.
The prop table drives code generation (`observedAttributes`, the accessor The prop table drives code generation (`observedAttributes`, the accessor
descriptors) that has to exist on the class _before_ any instance. A factory descriptors) that has to exist on the class _before_ any instance. A factory
that closes over the resolved table and defines accessors on that closes over the resolved table and defines accessors on
`Class.prototype` is the natural shape for that. `WithProps({})` — an empty `Class.prototype` is the natural shape for that. An empty table
table — is still a useful base: `leaflet-layer-group` and (`const PROPS: Record<never, never> = {}`) is still a useful base:
`leaflet-feature-group` use it to get lifecycle, child registration and event `leaflet-layer-group` and `leaflet-feature-group` use it to get lifecycle,
forwarding with no options of their own. child registration and event forwarding with no options of their own.
### Duck typing, deliberately ### Duck typing, deliberately
@ -117,8 +152,9 @@ anything about the concrete class.
## `leaflet-map` is special ## `leaflet-map` is special
`src/elements/leaflet-map.ts`. `LeafletMapElement` still extends `src/elements/leaflet-map.ts`. `LeafletMapElement` still extends a
`WithProps(PROPS)` with `attach: 'none'`, but additionally: `Base = WithProps(PROPS, { attach: 'none' })` like every other element, but
additionally:
- builds its own **Shadow DOM** in `connectedCallback` (a `<div>` container - builds its own **Shadow DOM** in `connectedCallback` (a `<div>` container
for Leaflet, a `<style>` for `:host`, and a `<link>` to Leaflet's CSS), for Leaflet, a `<style>` for `:host`, and a `<link>` to Leaflet's CSS),

@ -32,11 +32,19 @@ Instead of writing `decode`/`encode` by hand, components call a factory:
| `disabled(opts?)` | the inverse of `bool(true)`: attribute named `disable-<kebab>`, `<leaflet-map disable-dragging>` reads as `dragging === false` | | `disabled(opts?)` | the inverse of `bool(true)`: attribute named `disable-<kebab>`, `<leaflet-map disable-dragging>` reads as `dragging === false` |
| `json<T>(default, opts?)` | `JSON.parse``JSON.stringify` — bounds, icon sizes/anchors, GeoJSON data | | `json<T>(default, opts?)` | `JSON.parse``JSON.stringify` — bounds, icon sizes/anchors, GeoJSON data |
`positional(def)` wraps any of the above to set `option: false`. `positional(def)` wraps any of the above to set `option: false`. Its result
type is aliased as `Positional<T, Obj>` (= `PropDef<T, Obj> & { option:
false }`) in `props.ts`, for use in the explicit `const PROPS` annotations
each element file needs (see [01](./01-architecture.md) and
[07](./07-tooling-and-build.md)).
## Shared fragments ## Shared fragments
`src/core/shared-props.ts` — spread these into a `PROPS` table: `src/core/shared-props.ts` — spread these into a `PROPS` table. Each is
exported with an **explicit object type** (not just `as const`), so a
`PROPS` annotation can pull it in via `typeof pathProps` / `typeof
latLngProps` / `typeof tileLayerProps` without tripping JSR's slow-type
check:
- **`latLngProps`** (`lat`, `lng`) — the coordinate pair, shared by marker, - **`latLngProps`** (`lat`, `lng`) — the coordinate pair, shared by marker,
circle, circle-marker, popup, tooltip. Both are `positional` (passed to the circle, circle-marker, popup, tooltip. Both are `positional` (passed to the

@ -63,14 +63,17 @@ A component narrows its listener types with a `declare` field — the same
zero-runtime idiom as `declare readonly leafletObject?: Marker`: zero-runtime idiom as `declare readonly leafletObject?: Marker`:
```ts ```ts
class LeafletCircleElement extends WithProps(PROPS) { class LeafletCircleElement extends Base {
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;
declare removeEventListener: LeafletRemoveEventListener<PathEvents>; declare removeEventListener: LeafletRemoveEventListener<PathEvents>;
} }
``` ```
Components that fire nothing meaningful (controls, icons, `leaflet-line`) Components that fire nothing meaningful (controls, icons, `leaflet-line`)
skip this and keep the default `HTMLElementEventMap` typing. skip this and keep the default `HTMLElementEventMap` typing — which, for the
internal bubbling events (`leaflet-register`, `icon-changed`,
`leaflet-line-sync`, …), is itself an augmentation living in
`src/core/globals.ts` (npm-only; see [07](./07-tooling-and-build.md)).
### Why no string fallback overload ### Why no string fallback overload

@ -65,8 +65,11 @@ reference instead (`emitLineRemove(from, el)` in `register.ts`).
## `leaflet-layer-group` / `leaflet-feature-group` — passthrough ## `leaflet-layer-group` / `leaflet-feature-group` — passthrough
`WithProps({})` — an empty prop table (`new LayerGroup([])` / `const PROPS: Record<never, never> = {}` — an empty prop table
`new FeatureGroup([])`). No options of their own, but they still get the full (`new LayerGroup([])` / `new FeatureGroup([])`). `Record<never, never>`, not
`Record<string, never>`: the latter would put a `never` string-index
signature on the generated instance type that the subclass's own members
can't satisfy. No options of their own, but they still get the full
lifecycle, child registration (`attach: 'children'`), and `leaflet:` event lifecycle, child registration (`attach: 'children'`), and `leaflet:` event
forwarding. A descendant layer's `leaflet-register` is claimed by the mixin's forwarding. A descendant layer's `leaflet-register` is claimed by the mixin's
own `#onChildRegister` (`Layer` + parent has `addLayer``obj.addLayer`), own `#onChildRegister` (`Layer` + parent has `addLayer``obj.addLayer`),

@ -60,10 +60,13 @@ their order is free.
## The `HTMLElementTagNameMap` augmentation ## The `HTMLElementTagNameMap` augmentation
At the bottom of `src/index.ts`. Because `export *` doesn't bind the Lives in `src/core/globals.ts` (not `src/index.ts`), together with the
re-exported names locally, the tag map needs its **own** `HTMLElementEventMap` augmentation for the internal bubbling events. That
`import type { … } from './elements/index.ts'`. That import is type-only, module is imported only by the npm entry `src/index.npm.ts` and by
erased at build, backs no `define()` — its order doesn't matter. `test/setup.ts`, and is excluded from the JSR tarball — JSR's "no slow types"
check forbids `declare global`. Its `import type { … } from
'../elements/index.ts'` is type-only, erased at build, backs no `define()`
order doesn't matter. See [07](./07-tooling-and-build.md).
## The test that guards this ## The test that guards this

@ -3,11 +3,11 @@
## Scripts ## Scripts
| `npm run …` | Does | | `npm run …` | Does |
| --------------------- | -------------------------------------------------------- | | --------------------- | ----------------------------------------------------------------------- |
| `build` | `rm -rf dist && tsc --outDir dist` | | `build` | `rm -rf dist && tsc --outDir dist` |
| `typecheck` | `tsc --noEmit`, then `tsc -p tsconfig.test.json` | | `typecheck` | `tsc --noEmit`, then `tsc -p tsconfig.test.json` |
| `lint` | `oxlint src test` | | `lint` | `oxlint src test` |
| `format` | `oxfmt '*.md' 'src/**/*.{ts,js,json,md}' 'test/**/*.ts'` | | `format` | `oxfmt '*.md' 'docs/**/*.md' 'src/**/*.{ts,js,json,md}' 'test/**/*.ts'` |
| `test` / `test:watch` | `vitest run` / `vitest` | | `test` / `test:watch` | `vitest run` / `vitest` |
## TypeScript 7 ## TypeScript 7
@ -23,7 +23,10 @@ setup below. `tsconfig.json` highlights:
- `strict`, `isolatedModules`, `skipLibCheck` - `strict`, `isolatedModules`, `skipLibCheck`
- `include: ["src/**/*"]` only — `test/**` never reaches `dist/`. The - `include: ["src/**/*"]` only — `test/**` never reaches `dist/`. The
root-level `oxfmt.config.ts` is outside this glob too, so it's never root-level `oxfmt.config.ts` is outside this glob too, so it's never
compiled. compiled. Note this glob **does** pull in `src/index.npm.ts` and
`src/core/globals.ts`, so the whole `src/` tree sees the `declare global`
augmentations at build/typecheck time even though the JSR entry doesn't
import them (see "Two registries" below).
## Linting: oxlint (not ESLint) ## Linting: oxlint (not ESLint)
@ -75,7 +78,11 @@ export default defineConfig({
- **`test/setup.ts`** stubs `ResizeObserver` — jsdom doesn't implement it and - **`test/setup.ts`** stubs `ResizeObserver` — jsdom doesn't implement it and
`leaflet-map.ts` constructs one unconditionally, so without the stub even `leaflet-map.ts` constructs one unconditionally, so without the stub even
_importing_ the component throws. Nothing here depends on it firing. _importing_ the component throws. Nothing here depends on it firing. It also
`import`s `src/core/globals.ts` so the ambient `HTMLElementTagNameMap` /
`HTMLElementEventMap` augmentations are in scope for `tsconfig.test.json`
(tests import element modules directly, bypassing the npm entry that would
otherwise supply them).
- **Real Leaflet objects work under jsdom** for everything this library - **Real Leaflet objects work under jsdom** for everything this library
verifies: option/attribute wiring, event forwarding, child binding. No verifies: option/attribute wiring, event forwarding, child binding. No
browser needed. browser needed.
@ -98,24 +105,72 @@ export default defineConfig({
`.d.ts` + `.d.ts.map` per source file, no bundling step. `dist/elements/` and `.d.ts` + `.d.ts.map` per source file, no bundling step. `dist/elements/` and
`dist/components/` mirror the `src/` split one-to-one. `dist/components/` mirror the `src/` split one-to-one.
- Consumers import `dist/index.js` (or any single module) directly. - Consumers import the package root (`dist/index.npm.js` on npm,
`src/index.ts` on JSR — see "Publishing" below) or any single module
directly.
- **No CJS, no UMD** build. - **No CJS, no UMD** build.
- **Leaflet is always external** — never bundled. It's a `dependencies` entry - **Leaflet is always external** — never bundled. It's a `dependencies` entry
and a bare `import` in the output. and a bare `import` in the output.
- `package.json` `exports`: `.``dist/index.js` (+ types); - `package.json` `exports`: `.``dist/index.npm.js` (+ types — this is the
npm entry, `src/index.npm.ts` compiled; see below);
`./elements``dist/elements/index.js` (the class barrel, no `define`); `./elements``dist/elements/index.js` (the class barrel, no `define`);
`./elements/*.js` and `./components/*.js` → the matching `dist/` module; `./elements/*.js` and `./components/*.js` → the matching `dist/` module;
`./dist/*` still there for deep imports. `./dist/*` still there for deep imports.
- `sideEffects: true` — the `components/*` modules (and `index.js`) call - `sideEffects: true` — the `components/*` modules (and both `index*.js`
`customElements.define()` at import time, so a bundler must not tree-shake entries) call `customElements.define()` at import time, so a bundler must
them away. The `elements/*` modules have no side effect. not tree-shake them away. The `elements/*` modules have no side effect.
## Publishing to two registries ## Publishing to two registries
| Registry | Entry | Ships | | Registry | Package name | Entry | Ships |
| -------- | -------------------------------------------------------- | ----------------------------------------------------------------------------- | | -------- | --------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| npm | `package.json` `main`/`module`/`types` → `dist/…` | compiled `dist/` + `README.md` (`files` field); `prepublishOnly` runs `build` | | npm | `leaflet-web-components` | `package.json` `.`/`main`/`module`/`types` → `dist/index.npm.*` | compiled `dist/` + `README.md` + `LICENSE` (`files` field); `prepublishOnly` runs `build` |
| JSR | `jsr.json` `exports``./src/index.ts` (+ `./elements`) | TypeScript source directly (JSR compiles per-consumer) | | JSR | `@buddy/leaflet-components` | `jsr.json` `exports``./src/index.ts` (+ `./elements`) | TypeScript source directly (JSR compiles per-consumer), minus `jsr.json`'s `publish.exclude` |
Both are version `0.1.0`; keep them in step. JSR publishes `src/` with its Both are pinned to the same version; keep them in step. JSR publishes `src/`
`.ts` import extensions intact, which JSR supports natively. with its `.ts` import extensions intact, which JSR supports natively.
### The npm/JSR entry split
The two registries resolve **different entry files**:
- **`src/index.ts`** (JSR's `.`, and the shared base) — every element class,
every helper, the load-bearing `import './components/*.ts'` side effects.
**No `declare global`.**
- **`src/index.npm.ts`** (npm's `.`) — `import './core/globals.ts'; export *
from './index.ts';`. Compiled to `dist/index.npm.js`.
- **`src/core/globals.ts`** — the two ambient `declare global` blocks
(`HTMLElementTagNameMap`, `HTMLElementEventMap`). Imported only by
`src/index.npm.ts` and `test/setup.ts`; listed in `jsr.json`'s
`publish.exclude` so it's not even uploaded to JSR.
**Why:** JSR's "no slow types" check (below) rejects `declare global` anywhere
in the module graph reachable from `jsr.json`'s `exports`. Keeping the
augmentations in a module that graph never reaches lets JSR publish with full
fast types while npm consumers still get `querySelector('leaflet-map')` /
`addEventListener('leaflet-register', …)` typing for free through the package
root. JSR (`jsr:@buddy/leaflet-components`) consumers don't get the ambient
augmentations and supply their own if they want them.
### JSR "no slow types"
JSR statically analyses the public API of what it publishes and refuses
constructs it can't resolve without a full `tsc` run. For this package that
means:
- **No `declare global`** anywhere reachable from `jsr.json`'s `exports`
handled by the entry split above.
- **No call expression as a superclass.** `class Foo extends WithProps({…})`
is rejected; every element file uses `const Base = WithProps(PROPS)` then
`extends Base` instead.
- **Explicit types on anything that leaks into the public API.** `const PROPS`
gets a written-out `{ key: PropDef<…>; … }` annotation (or `as const` when
every value is a bare reference); `const Base` gets
`: LeafletElementConstructor<TheLeafletClass, typeof PROPS>`; the shared
fragments in `shared-props.ts` carry their own object-type annotations; the
`Positional<T, Obj>` alias in `props.ts` exists to keep those annotations
short.
`npx jsr publish --dry-run` runs the full check offline and is the gate:
it must print **"Success"** with zero slow-type errors before publishing.
Pass `--allow-dirty` to run it against an uncommitted tree.

@ -1,8 +1,24 @@
{ {
"name": "@buddy/leaflet-components", "name": "@buddy/leaflet-components",
"version": "0.1.0", "version": "0.1.0",
"license": "MIT",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./elements": "./src/elements/index.ts" "./elements": "./src/elements/index.ts"
},
"publish": {
"exclude": [
"src/index.npm.ts",
"src/core/globals.ts",
"test/",
"docs/",
"demos/",
"dist/",
"index.html",
"*.config.ts",
"tsconfig*.json",
".idea/",
".claude/"
]
} }
} }

@ -1,14 +1,14 @@
{ {
"name": "leaflet-components", "name": "leaflet-web-components",
"version": "0.1.0", "version": "0.1.0",
"description": "LeafletJS as Web Components", "description": "LeafletJS as Web Components",
"main": "dist/index.js", "main": "dist/index.npm.js",
"module": "dist/index.js", "module": "dist/index.npm.js",
"types": "dist/index.d.ts", "types": "dist/index.npm.d.ts",
"exports": { "exports": {
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.npm.d.ts",
"import": "./dist/index.js" "import": "./dist/index.npm.js"
}, },
"./elements": { "./elements": {
"types": "./dist/elements/index.d.ts", "types": "./dist/elements/index.d.ts",
@ -28,7 +28,8 @@
"sideEffects": true, "sideEffects": true,
"files": [ "files": [
"dist", "dist",
"README.md" "README.md",
"LICENSE"
], ],
"scripts": { "scripts": {
"build": "rm -rf dist && tsc --outDir dist", "build": "rm -rf dist && tsc --outDir dist",

@ -0,0 +1,81 @@
// Ambient DOM augmentations: the `leaflet-*` tag -> element class map, and the
// custom bubbling events the registration protocol fires. Kept in this
// standalone module -- imported only by `src/index.npm.ts`, never by
// `src/index.ts` or anything in `jsr.json`'s export graph -- because JSR's
// "no slow types" check rejects `declare global` anywhere it can reach. npm
// consumers get these automatically via the package's `.` entry; the whole
// `src/` tree also sees them at build/typecheck time because this file is
// under `tsconfig.json`'s `include`. See docs/ for the split rationale.
import type {
LeafletCRSChangedEvent,
LeafletIconChangedEvent,
LeafletLayerEvent,
LeafletLineRemoveEvent,
LeafletLineSyncEvent,
LeafletRegisterEvent,
} from './register.ts';
import type {
LeafletCircleElement,
LeafletCircleMarkerElement,
LeafletControlAttributionElement,
LeafletControlLayersElement,
LeafletControlScaleElement,
LeafletControlZoomElement,
LeafletDivIconElement,
LeafletFeatureGroupElement,
LeafletGeoJSONElement,
LeafletIconElement,
LeafletImageOverlayElement,
LeafletLayerGroupElement,
LeafletLineElement,
LeafletMapElement,
LeafletMarkerElement,
LeafletPolygonElement,
LeafletPolylineElement,
LeafletPopupElement,
LeafletRectangleElement,
LeafletSVGOverlayElement,
LeafletTileLayerElement,
LeafletTileLayerWMSElement,
LeafletTooltipElement,
LeafletVideoOverlayElement,
} from '../elements/index.ts';
declare global {
interface HTMLElementEventMap {
'leaflet-register': LeafletRegisterEvent;
'leaflet-add-layer': LeafletLayerEvent;
'leaflet-remove-layer': LeafletLayerEvent;
'icon-changed': LeafletIconChangedEvent;
'leaflet-line-sync': LeafletLineSyncEvent;
'leaflet-line-remove': LeafletLineRemoveEvent;
'leaflet-crs-changed': LeafletCRSChangedEvent;
}
interface HTMLElementTagNameMap {
'leaflet-map': LeafletMapElement;
'leaflet-marker': LeafletMarkerElement;
'leaflet-circle': LeafletCircleElement;
'leaflet-circle-marker': LeafletCircleMarkerElement;
'leaflet-line': LeafletLineElement;
'leaflet-polygon': LeafletPolygonElement;
'leaflet-polyline': LeafletPolylineElement;
'leaflet-rectangle': LeafletRectangleElement;
'leaflet-tile-layer': LeafletTileLayerElement;
'leaflet-tile-layer-wms': LeafletTileLayerWMSElement;
'leaflet-image-overlay': LeafletImageOverlayElement;
'leaflet-video-overlay': LeafletVideoOverlayElement;
'leaflet-svg-overlay': LeafletSVGOverlayElement;
'leaflet-layer-group': LeafletLayerGroupElement;
'leaflet-feature-group': LeafletFeatureGroupElement;
'leaflet-geojson': LeafletGeoJSONElement;
'leaflet-control-layers': LeafletControlLayersElement;
'leaflet-control-zoom': LeafletControlZoomElement;
'leaflet-control-attribution': LeafletControlAttributionElement;
'leaflet-control-scale': LeafletControlScaleElement;
'leaflet-popup': LeafletPopupElement;
'leaflet-tooltip': LeafletTooltipElement;
'leaflet-icon': LeafletIconElement;
'leaflet-div-icon': LeafletDivIconElement;
}
}

@ -53,6 +53,12 @@ export type PropOptionValues<T> = Partial<{
[K in keyof T as T[K] extends { option: false } ? never : K]: PropValue<T[K]>; [K in keyof T as T[K] extends { option: false } ? never : K]: PropValue<T[K]>;
}>; }>;
// The type `positional()` produces: a PropDef flagged so #buildOptions skips
// it. Spelled out as an alias so element PROPS tables can be given the explicit
// type annotations JSR's "no slow types" check requires without repeating the
// intersection everywhere.
export type Positional<T, TObj = unknown> = PropDef<T, TObj> & { option: false };
// Marks a prop the Leaflet constructor takes as an argument, so it is left out // Marks a prop the Leaflet constructor takes as an argument, so it is left out
// of the options object handed to createLeafletObject(). // of the options object handed to createLeafletObject().
export function positional<T extends PropDef>(def: T): T & { option: false } { export function positional<T extends PropDef>(def: T): T & { option: false } {

@ -30,17 +30,9 @@ export type LeafletLineRemoveEvent = CustomEvent<{ element: HTMLElement }>;
// own default, mirroring icon-changed's `icon: null`. // own default, mirroring icon-changed's `icon: null`.
export type LeafletCRSChangedEvent = CustomEvent<{ crs: CRS | null }>; export type LeafletCRSChangedEvent = CustomEvent<{ crs: CRS | null }>;
declare global { // The `HTMLElementEventMap` augmentation for these event names lives in
interface HTMLElementEventMap { // `./globals.ts` (npm-only), kept out of this module so JSR -- which rejects
'leaflet-register': LeafletRegisterEvent; // `declare global` in its published graph -- can still publish it.
'leaflet-add-layer': LeafletLayerEvent;
'leaflet-remove-layer': LeafletLayerEvent;
'icon-changed': LeafletIconChangedEvent;
'leaflet-line-sync': LeafletLineSyncEvent;
'leaflet-line-remove': LeafletLineRemoveEvent;
'leaflet-crs-changed': LeafletCRSChangedEvent;
}
}
export function emitIconChanged(el: HTMLElement, icon: Icon | DivIcon | null | undefined) { export function emitIconChanged(el: HTMLElement, icon: Icon | DivIcon | null | undefined) {
el.dispatchEvent( el.dispatchEvent(

@ -56,7 +56,7 @@ export function style<T>(key: keyof PathOptions): (obj: Styleable, value: T) =>
// The source url. Passed positionally by every Leaflet constructor that takes // The source url. Passed positionally by every Leaflet constructor that takes
// one, and ignored when blank so clearing the attribute can't request nothing. // one, and ignored when blank so clearing the attribute can't request nothing.
// No `get`: none of TileLayer/ImageOverlay/VideoOverlay expose a getUrl(). // No `get`: none of TileLayer/ImageOverlay/VideoOverlay expose a getUrl().
export const urlProp = positional( export const urlProp: PropDef<string, Sourced> & { option: false } = positional(
str<Sourced>('', { str<Sourced>('', {
set(obj, value) { set(obj, value) {
if (value) obj.setUrl(value); if (value) obj.setUrl(value);
@ -69,7 +69,10 @@ export const urlProp = positional(
// the other's current value, and both are written back whenever the object // the other's current value, and both are written back whenever the object
// moves -- which is what keeps the attributes current while a marker is // moves -- which is what keeps the attributes current while a marker is
// dragged. Shared by marker, circle, circle-marker, popup and tooltip. // dragged. Shared by marker, circle, circle-marker, popup and tooltip.
export const latLngProps = { export const latLngProps: {
lat: PropDef<number, Positioned> & { option: false };
lng: PropDef<number, Positioned> & { option: false };
} = {
lat: positional( lat: positional(
num<Positioned>(0, { num<Positioned>(0, {
event: 'move', event: 'move',
@ -88,11 +91,28 @@ export const latLngProps = {
}, },
}), }),
), ),
} as const; };
// The style options every Path accepts. Defaults match Leaflet's own, so an // The style options every Path accepts. Defaults match Leaflet's own, so an
// absent attribute and an unset option mean the same thing. // absent attribute and an unset option mean the same thing.
export const pathProps = { export const pathProps: {
stroke: PropDef<boolean, Styleable>;
color: PropDef<string, Styleable>;
weight: PropDef<number, Styleable>;
opacity: PropDef<number, Styleable>;
lineCap: PropDef<LineCapShape, Styleable>;
lineJoin: PropDef<LineJoinShape, Styleable>;
dashArray: PropDef<string, Styleable>;
dashOffset: PropDef<string, Styleable>;
fill: PropDef<boolean, Styleable>;
fillColor: PropDef<string, Styleable>;
fillOpacity: PropDef<number, Styleable>;
fillRule: PropDef<FillRule, Styleable>;
className: PropDef<string>;
interactive: PropDef<boolean>;
bubblingMouseEvents: PropDef<boolean>;
pane: PropDef<string>;
} = {
stroke: bool<Styleable>(true, { set: style('stroke') }), stroke: bool<Styleable>(true, { set: style('stroke') }),
color: str<Styleable>('#3388ff', { set: style('color') }), color: str<Styleable>('#3388ff', { set: style('color') }),
weight: num<Styleable>(3, { set: style('weight') }), weight: num<Styleable>(3, { set: style('weight') }),
@ -111,14 +131,39 @@ export const pathProps = {
interactive: bool(true), interactive: bool(true),
bubblingMouseEvents: bool(true), bubblingMouseEvents: bool(true),
pane: str('overlay'), pane: str('overlay'),
} as const; };
// The GridLayer/TileLayer options every tile source accepts, shared by // The GridLayer/TileLayer options every tile source accepts, shared by
// leaflet-tile-layer and leaflet-tile-layer-wms (WMSOptions extends // leaflet-tile-layer and leaflet-tile-layer-wms (WMSOptions extends
// TileLayerOptions). Most are constructor-only -- Leaflet exposes no setter // TileLayerOptions). Most are constructor-only -- Leaflet exposes no setter
// for them -- so changing the attribute after creation has no effect, same as // for them -- so changing the attribute after creation has no effect, same as
// leaflet-map's zoomSnap. // leaflet-map's zoomSnap.
export const tileLayerProps = { export const tileLayerProps: {
attribution: PropDef<string>;
minZoom: PropDef<number>;
maxZoom: PropDef<number>;
opacity: PropDef<number>;
zIndex: PropDef<number>;
subdomains: PropDef<string>;
tms: PropDef<boolean>;
zoomOffset: PropDef<number>;
zoomReverse: PropDef<boolean>;
detectRetina: PropDef<boolean>;
crossOrigin: PropDef<CrossOrigin>;
referrerPolicy: PropDef<ReferrerPolicy | undefined>;
errorTileUrl: PropDef<string>;
tileSize: PropDef<number>;
noWrap: PropDef<boolean>;
bounds: PropDef<LatLngBoundsExpression>;
className: PropDef<string>;
minNativeZoom: PropDef<number>;
maxNativeZoom: PropDef<number>;
keepBuffer: PropDef<number>;
updateWhenIdle: PropDef<boolean>;
updateWhenZooming: PropDef<boolean>;
updateInterval: PropDef<number>;
pane: PropDef<string>;
} = {
attribution: str(), attribution: str(),
minZoom: num(0), minZoom: num(0),
maxZoom: num(18), maxZoom: num(18),
@ -150,4 +195,4 @@ export const tileLayerProps = {
updateWhenZooming: bool(true), updateWhenZooming: bool(true),
updateInterval: num(200), updateInterval: num(200),
pane: str('tilePane'), pane: str('tilePane'),
} as const; };

@ -1,18 +1,25 @@
import { CircleMarker, type CircleMarkerOptions } from 'leaflet'; import { CircleMarker, type CircleMarkerOptions } from 'leaflet';
import { num } from '../core/props.ts'; import { num, type PropDef } from '../core/props.ts';
import { latLngProps, pathProps } from '../core/shared-props.ts'; import { latLngProps, pathProps } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { PathEvents } from '../core/event-types.ts'; import type { PathEvents } from '../core/event-types.ts';
export default class LeafletCircleMarkerElement extends WithProps({ const PROPS: typeof latLngProps &
typeof pathProps & {
radius: PropDef<number, CircleMarker>;
} = {
...latLngProps, ...latLngProps,
radius: num<CircleMarker>(10, { get: (obj) => obj.getRadius() }), radius: num<CircleMarker>(10, { get: (obj) => obj.getRadius() }),
...pathProps, ...pathProps,
}) { };
const Base: LeafletElementConstructor<CircleMarker, typeof PROPS> = WithProps(PROPS);
export default class LeafletCircleMarkerElement extends Base {
declare readonly leafletObject?: CircleMarker; declare readonly leafletObject?: CircleMarker;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;
declare removeEventListener: LeafletRemoveEventListener<PathEvents>; declare removeEventListener: LeafletRemoveEventListener<PathEvents>;

@ -1,20 +1,27 @@
import { Circle, type CircleOptions } from 'leaflet'; import { Circle, type CircleOptions } from 'leaflet';
import { num, positional } from '../core/props.ts'; import { num, positional, type PropDef } from '../core/props.ts';
import { latLngProps, pathProps } from '../core/shared-props.ts'; import { latLngProps, pathProps } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { PathEvents } from '../core/event-types.ts'; import type { PathEvents } from '../core/event-types.ts';
export default class LeafletCircleElement extends WithProps({ // Leaflet has no default radius -- it throws without one -- so unlike every
// other option, this one is always passed, from the default when unset.
const radiusProp: PropDef<number, Circle> & { option: false } = positional(
num<Circle>(1000, { get: (obj) => obj.getRadius() }),
);
const PROPS = {
...latLngProps, ...latLngProps,
...pathProps, ...pathProps,
// Leaflet has no default radius -- it throws without one -- so unlike every radius: radiusProp,
// other option, this one is always passed, from the default when unset. } as const;
radius: positional(num<Circle>(1000, { get: (obj) => obj.getRadius() })), const Base: LeafletElementConstructor<Circle, typeof PROPS> = WithProps(PROPS);
}) {
export default class LeafletCircleElement extends Base {
declare readonly leafletObject?: Circle; declare readonly leafletObject?: Circle;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;
declare removeEventListener: LeafletRemoveEventListener<PathEvents>; declare removeEventListener: LeafletRemoveEventListener<PathEvents>;

@ -1,14 +1,19 @@
import { Control, type ControlPosition } from 'leaflet'; import { Control, type ControlPosition } from 'leaflet';
import { choice, str } from '../core/props.ts'; import { choice, str, type PropDef } from '../core/props.ts';
import { WithProps } from '../core/with-props.ts'; import { WithProps, type LeafletElementConstructor } from '../core/with-props.ts';
export default class LeafletControlAttributionElement extends WithProps( const PROPS: {
{ position: PropDef<ControlPosition>;
prefix: PropDef<string>;
} = {
position: choice<ControlPosition>('bottomright'), position: choice<ControlPosition>('bottomright'),
prefix: str(), prefix: str(),
}, };
{ attach: 'self' }, const Base: LeafletElementConstructor<Control.Attribution, typeof PROPS> = WithProps(PROPS, {
) { attach: 'self',
});
export default class LeafletControlAttributionElement extends Base {
declare readonly leafletObject?: Control.Attribution; declare readonly leafletObject?: Control.Attribution;
createLeafletObject(options: Control.AttributionOptions): Control.Attribution { createLeafletObject(options: Control.AttributionOptions): Control.Attribution {

@ -1,18 +1,26 @@
import { Control, type ControlPosition } from 'leaflet'; import { Control, type ControlPosition } from 'leaflet';
import { bool, choice } from '../core/props.ts'; import { bool, choice, type PropDef } from '../core/props.ts';
import { WithProps } from '../core/with-props.ts'; import { WithProps, type LeafletElementConstructor } from '../core/with-props.ts';
import type { LeafletRegisterEvent } from '../core/register.ts'; import type { LeafletRegisterEvent } from '../core/register.ts';
export default class LeafletControlLayersElement extends WithProps( const PROPS: {
{ position: PropDef<ControlPosition>;
collapsed: PropDef<boolean>;
autoZIndex: PropDef<boolean>;
hideSingleBase: PropDef<boolean>;
sortLayers: PropDef<boolean>;
} = {
position: choice<ControlPosition>('topright'), position: choice<ControlPosition>('topright'),
collapsed: bool(true), collapsed: bool(true),
autoZIndex: bool(true), autoZIndex: bool(true),
hideSingleBase: bool(), hideSingleBase: bool(),
sortLayers: bool(), sortLayers: bool(),
}, };
{ attach: 'self' }, const Base: LeafletElementConstructor<Control.Layers, typeof PROPS> = WithProps(PROPS, {
) { attach: 'self',
});
export default class LeafletControlLayersElement extends Base {
declare readonly leafletObject?: Control.Layers; declare readonly leafletObject?: Control.Layers;
createLeafletObject(options: Control.LayersOptions): Control.Layers { createLeafletObject(options: Control.LayersOptions): Control.Layers {

@ -1,17 +1,25 @@
import { Control, type ControlPosition } from 'leaflet'; import { Control, type ControlPosition } from 'leaflet';
import { bool, choice, num } from '../core/props.ts'; import { bool, choice, num, type PropDef } from '../core/props.ts';
import { WithProps } from '../core/with-props.ts'; import { WithProps, type LeafletElementConstructor } from '../core/with-props.ts';
export default class LeafletControlScaleElement extends WithProps( const PROPS: {
{ position: PropDef<ControlPosition>;
maxWidth: PropDef<number>;
metric: PropDef<boolean>;
imperial: PropDef<boolean>;
updateWhenIdle: PropDef<boolean>;
} = {
position: choice<ControlPosition>('bottomleft'), position: choice<ControlPosition>('bottomleft'),
maxWidth: num(100), maxWidth: num(100),
metric: bool(true), metric: bool(true),
imperial: bool(true), imperial: bool(true),
updateWhenIdle: bool(), updateWhenIdle: bool(),
}, };
{ attach: 'self' }, const Base: LeafletElementConstructor<Control.Scale, typeof PROPS> = WithProps(PROPS, {
) { attach: 'self',
});
export default class LeafletControlScaleElement extends Base {
declare readonly leafletObject?: Control.Scale; declare readonly leafletObject?: Control.Scale;
createLeafletObject(options: Control.ScaleOptions): Control.Scale { createLeafletObject(options: Control.ScaleOptions): Control.Scale {

@ -1,19 +1,27 @@
import { Control, type ControlPosition } from 'leaflet'; import { Control, type ControlPosition } from 'leaflet';
import { choice, str } from '../core/props.ts'; import { choice, str, type PropDef } from '../core/props.ts';
import { WithProps } from '../core/with-props.ts'; import { WithProps, type LeafletElementConstructor } from '../core/with-props.ts';
// The button labels default to Leaflet's own markup, which hides the glyph // The button labels default to Leaflet's own markup, which hides the glyph
// from screen readers in favour of the title. // from screen readers in favour of the title.
export default class LeafletControlZoomElement extends WithProps( const PROPS: {
{ position: PropDef<ControlPosition>;
zoomInText: PropDef<string>;
zoomInTitle: PropDef<string>;
zoomOutText: PropDef<string>;
zoomOutTitle: PropDef<string>;
} = {
position: choice<ControlPosition>('topleft'), position: choice<ControlPosition>('topleft'),
zoomInText: str('<span aria-hidden="true">+</span>'), zoomInText: str('<span aria-hidden="true">+</span>'),
zoomInTitle: str('Zoom in'), zoomInTitle: str('Zoom in'),
zoomOutText: str('<span aria-hidden="true"></span>'), zoomOutText: str('<span aria-hidden="true"></span>'),
zoomOutTitle: str('Zoom out'), zoomOutTitle: str('Zoom out'),
}, };
{ attach: 'self' }, const Base: LeafletElementConstructor<Control.Zoom, typeof PROPS> = WithProps(PROPS, {
) { attach: 'self',
});
export default class LeafletControlZoomElement extends Base {
declare readonly leafletObject?: Control.Zoom; declare readonly leafletObject?: Control.Zoom;
createLeafletObject(options: Control.ZoomOptions): Control.Zoom { createLeafletObject(options: Control.ZoomOptions): Control.Zoom {

@ -1,12 +1,19 @@
import { DivIcon, type DivIconOptions, type PointExpression } from 'leaflet'; import { DivIcon, type DivIconOptions, type PointExpression } from 'leaflet';
import { json, str } from '../core/props.ts'; import { json, str, type PropDef } from '../core/props.ts';
import { emitIconChanged } from '../core/register.ts'; import { emitIconChanged } from '../core/register.ts';
import { WithProps } from '../core/with-props.ts'; import { WithProps, type LeafletElementConstructor } from '../core/with-props.ts';
// Like leaflet-icon, rebuilt on every change -- including changes to the // Like leaflet-icon, rebuilt on every change -- including changes to the
// markup, which is what the icon renders when `html` isn't set. // markup, which is what the icon renders when `html` isn't set.
export default class LeafletDivIconElement extends WithProps( const PROPS: {
{ iconSize: PropDef<PointExpression>;
iconAnchor: PropDef<PointExpression>;
popupAnchor: PropDef<PointExpression>;
tooltipAnchor: PropDef<PointExpression>;
className: PropDef<string>;
html: PropDef<string>;
bgPos: PropDef<PointExpression>;
} = {
iconSize: json<PointExpression>([0, 0]), iconSize: json<PointExpression>([0, 0]),
iconAnchor: json<PointExpression>([0, 0]), iconAnchor: json<PointExpression>([0, 0]),
popupAnchor: json<PointExpression>([0, 0]), popupAnchor: json<PointExpression>([0, 0]),
@ -14,9 +21,13 @@ export default class LeafletDivIconElement extends WithProps(
className: str('leaflet-div-icon'), className: str('leaflet-div-icon'),
html: str(), html: str(),
bgPos: json<PointExpression>([0, 0]), bgPos: json<PointExpression>([0, 0]),
}, };
{ attach: 'none', recreate: true }, const Base: LeafletElementConstructor<DivIcon, typeof PROPS> = WithProps(PROPS, {
) { attach: 'none',
recreate: true,
});
export default class LeafletDivIconElement extends Base {
declare readonly leafletObject?: DivIcon; declare readonly leafletObject?: DivIcon;
#observer?: MutationObserver; #observer?: MutationObserver;

@ -2,12 +2,16 @@ import { FeatureGroup } from 'leaflet';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { GroupEvents } from '../core/event-types.ts'; import type { GroupEvents } from '../core/event-types.ts';
const PROPS: Record<never, never> = {};
const Base: LeafletElementConstructor<FeatureGroup, typeof PROPS> = WithProps(PROPS);
// Like leaflet-layer-group, but its children share events and a bounding box. // Like leaflet-layer-group, but its children share events and a bounding box.
export default class LeafletFeatureGroupElement extends WithProps({}) { export default class LeafletFeatureGroupElement extends Base {
declare readonly leafletObject?: FeatureGroup; declare readonly leafletObject?: FeatureGroup;
declare addEventListener: LeafletAddEventListener<GroupEvents>; declare addEventListener: LeafletAddEventListener<GroupEvents>;
declare removeEventListener: LeafletRemoveEventListener<GroupEvents>; declare removeEventListener: LeafletRemoveEventListener<GroupEvents>;

@ -1,15 +1,18 @@
import { GeoJSON, type PathOptions } from 'leaflet'; import { GeoJSON, type PathOptions } from 'leaflet';
import type { GeoJsonObject } from 'geojson'; import type { GeoJsonObject } from 'geojson';
import { json, positional } from '../core/props.ts'; import { json, positional, type Positional } from '../core/props.ts';
import { pathProps } from '../core/shared-props.ts'; import { pathProps } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { GroupEvents } from '../core/event-types.ts'; import type { GroupEvents } from '../core/event-types.ts';
export default class LeafletGeoJSONElement extends WithProps({ const PROPS: {
data: Positional<GeoJsonObject | null, GeoJSON>;
} & typeof pathProps = {
data: positional( data: positional(
json<GeoJsonObject | null>(null, { json<GeoJsonObject | null>(null, {
set(obj: GeoJSON, value) { set(obj: GeoJSON, value) {
@ -19,7 +22,10 @@ export default class LeafletGeoJSONElement extends WithProps({
}), }),
), ),
...pathProps, ...pathProps,
}) { };
const Base: LeafletElementConstructor<GeoJSON, typeof PROPS> = WithProps(PROPS);
export default class LeafletGeoJSONElement extends Base {
declare readonly leafletObject?: GeoJSON; declare readonly leafletObject?: GeoJSON;
declare addEventListener: LeafletAddEventListener<GroupEvents>; declare addEventListener: LeafletAddEventListener<GroupEvents>;
declare removeEventListener: LeafletRemoveEventListener<GroupEvents>; declare removeEventListener: LeafletRemoveEventListener<GroupEvents>;

@ -1,9 +1,21 @@
import { Icon, type IconOptions, type PointExpression } from 'leaflet'; import { Icon, type IconOptions, type PointExpression } from 'leaflet';
import { json, str } from '../core/props.ts'; import { json, str, type PropDef } from '../core/props.ts';
import { emitIconChanged } from '../core/register.ts'; import { emitIconChanged } from '../core/register.ts';
import { WithProps } from '../core/with-props.ts'; import { WithProps, type LeafletElementConstructor } from '../core/with-props.ts';
const PROPS = { const PROPS: {
iconUrl: PropDef<string>;
iconRetinaUrl: PropDef<string>;
iconSize: PropDef<PointExpression>;
iconAnchor: PropDef<PointExpression>;
popupAnchor: PropDef<PointExpression>;
tooltipAnchor: PropDef<PointExpression>;
shadowUrl: PropDef<string>;
shadowRetinaUrl: PropDef<string>;
shadowSize: PropDef<PointExpression>;
shadowAnchor: PropDef<PointExpression>;
className: PropDef<string>;
} = {
iconUrl: str(), iconUrl: str(),
iconRetinaUrl: str(), iconRetinaUrl: str(),
iconSize: json<PointExpression>([0, 0]), iconSize: json<PointExpression>([0, 0]),
@ -15,14 +27,16 @@ const PROPS = {
shadowSize: json<PointExpression>([0, 0]), shadowSize: json<PointExpression>([0, 0]),
shadowAnchor: json<PointExpression>([0, 0]), shadowAnchor: json<PointExpression>([0, 0]),
className: str(), className: str(),
} as const; };
// An Icon has no setters, so every attribute change builds a new one and the // An Icon has no setters, so every attribute change builds a new one and the
// parent marker is told to swap it in. // parent marker is told to swap it in.
export default class LeafletIconElement extends WithProps(PROPS, { const Base: LeafletElementConstructor<Icon, typeof PROPS> = WithProps(PROPS, {
attach: 'none', attach: 'none',
recreate: true, recreate: true,
}) { });
export default class LeafletIconElement extends Base {
declare readonly leafletObject?: Icon; declare readonly leafletObject?: Icon;
createLeafletObject(options: Partial<IconOptions>): Icon | undefined { createLeafletObject(options: Partial<IconOptions>): Icon | undefined {

@ -4,16 +4,36 @@ import {
type ImageOverlayOptions, type ImageOverlayOptions,
type LatLngBoundsExpression, type LatLngBoundsExpression,
} from 'leaflet'; } from 'leaflet';
import { bool, choice, json, num, positional, str } from '../core/props.ts'; import {
import { getBounds, urlProp } from '../core/shared-props.ts'; bool,
choice,
json,
num,
positional,
str,
type PropDef,
type Positional,
} from '../core/props.ts';
import { getBounds, urlProp, type Sourced } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { PathEvents } from '../core/event-types.ts'; import type { PathEvents } from '../core/event-types.ts';
export default class LeafletImageOverlayElement extends WithProps({ const PROPS: {
url: PropDef<string, Sourced> & { option: false };
bounds: Positional<LatLngBoundsExpression, ImageOverlay>;
opacity: PropDef<number>;
alt: PropDef<string, ImageOverlay>;
interactive: PropDef<boolean>;
crossOrigin: PropDef<CrossOrigin>;
errorOverlayUrl: PropDef<string>;
zIndex: PropDef<number>;
className: PropDef<string>;
} = {
url: urlProp, url: urlProp,
bounds: positional(json<LatLngBoundsExpression, ImageOverlay>([], { get: getBounds })), bounds: positional(json<LatLngBoundsExpression, ImageOverlay>([], { get: getBounds })),
opacity: num(1.0), opacity: num(1.0),
@ -28,7 +48,10 @@ export default class LeafletImageOverlayElement extends WithProps({
errorOverlayUrl: str(), errorOverlayUrl: str(),
zIndex: num(), zIndex: num(),
className: str(), className: str(),
}) { };
const Base: LeafletElementConstructor<ImageOverlay, typeof PROPS> = WithProps(PROPS);
export default class LeafletImageOverlayElement extends Base {
declare readonly leafletObject?: ImageOverlay; declare readonly leafletObject?: ImageOverlay;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;
declare removeEventListener: LeafletRemoveEventListener<PathEvents>; declare removeEventListener: LeafletRemoveEventListener<PathEvents>;

@ -2,13 +2,17 @@ import { LayerGroup } from 'leaflet';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { GroupEvents } from '../core/event-types.ts'; import type { GroupEvents } from '../core/event-types.ts';
const PROPS: Record<never, never> = {};
const Base: LeafletElementConstructor<LayerGroup, typeof PROPS> = WithProps(PROPS);
// A passthrough container: it has no options of its own, and children add // A passthrough container: it has no options of its own, and children add
// themselves to it through the standard registration bubble. // themselves to it through the standard registration bubble.
export default class LeafletLayerGroupElement extends WithProps({}) { export default class LeafletLayerGroupElement extends Base {
declare readonly leafletObject?: LayerGroup; declare readonly leafletObject?: LayerGroup;
declare addEventListener: LeafletAddEventListener<GroupEvents>; declare addEventListener: LeafletAddEventListener<GroupEvents>;
declare removeEventListener: LeafletRemoveEventListener<GroupEvents>; declare removeEventListener: LeafletRemoveEventListener<GroupEvents>;

@ -1,7 +1,7 @@
import { emitLineRemove, emitLineSync } from '../core/register.ts'; import { emitLineRemove, emitLineSync } from '../core/register.ts';
export default class LeafletLineElement extends HTMLElement { export default class LeafletLineElement extends HTMLElement {
static get observedAttributes() { static get observedAttributes(): string[] {
return ['lat', 'lng']; return ['lat', 'lng'];
} }

@ -1,15 +1,25 @@
import { Icon, Map as LMap, type MapOptions } from 'leaflet'; import { Icon, Map as LMap, type MapOptions, version } from 'leaflet';
import { bool, disabled, num, positional, str } from '../core/props.ts'; import {
bool,
disabled,
num,
positional,
str,
type PropDef,
type Positional,
} from '../core/props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { LeafletLayerEvent, LeafletRegisterEvent } from '../core/register.ts'; import type { LeafletLayerEvent, LeafletRegisterEvent } from '../core/register.ts';
import type { MapEvents } from '../core/event-types.ts'; import type { MapEvents } from '../core/event-types.ts';
const DEFAULT_CSS_URL = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; const DEFAULT_CSS_URL = `https://unpkg.com/leaflet@${version}/dist/leaflet.css`;
const DEFAULT_CSS_INTEGRITY = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY='; const DEFAULT_CSS_INTEGRITY =
version === '1.9.4' ? 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=' : '';
interface CssHost extends HTMLElement { interface CssHost extends HTMLElement {
applyCss(): void; applyCss(): void;
@ -21,10 +31,46 @@ function relinkCss(_map: LMap, _value: string, el: HTMLElement): void {
(el as CssHost).applyCss(); (el as CssHost).applyCss();
} }
// The root of the component tree. Every other component bubbles a const PROPS: {
// `leaflet-register` event up to here, which is where it stops. lat: Positional<number, LMap>;
export default class LeafletMapElement extends WithProps( lng: Positional<number, LMap>;
{ zoom: Positional<number, LMap>;
minZoom: PropDef<number, LMap>;
maxZoom: PropDef<number, LMap>;
zoomSnap: PropDef<number>;
zoomDelta: PropDef<number>;
keyboardPanDelta: PropDef<number>;
wheelDebounceTime: PropDef<number>;
wheelPxPerZoomLevel: PropDef<number>;
inertiaDeceleration: PropDef<number>;
inertiaMaxSpeed: PropDef<number>;
easeLinearity: PropDef<number>;
maxBoundsViscosity: PropDef<number>;
tapTolerance: PropDef<number>;
zoomAnimationThreshold: PropDef<number>;
transform3DLimit: PropDef<number>;
scrollWheelZoom: PropDef<boolean, LMap>;
dragging: PropDef<boolean, LMap>;
touchZoom: PropDef<boolean, LMap>;
doubleClickZoom: PropDef<boolean, LMap>;
boxZoom: PropDef<boolean, LMap>;
keyboard: PropDef<boolean, LMap>;
closePopupOnClick: PropDef<boolean>;
trackResize: PropDef<boolean>;
zoomControl: PropDef<boolean>;
attributionControl: PropDef<boolean>;
inertia: PropDef<boolean>;
zoomAnimation: PropDef<boolean>;
fadeAnimation: PropDef<boolean>;
markerZoomAnimation: PropDef<boolean>;
bounceAtZoomLimits: PropDef<boolean>;
tapHold: PropDef<boolean>;
preferCanvas: PropDef<boolean>;
worldCopyJump: PropDef<boolean>;
cssUrl: Positional<string>;
cssIntegrity: Positional<string>;
cssCrossorigin: Positional<string>;
} = {
// View state. Not constructor options -- the map is positioned with setView // View state. Not constructor options -- the map is positioned with setView
// once it exists -- and written back whenever the user pans or zooms. // once it exists -- and written back whenever the user pans or zooms.
lat: positional( lat: positional(
@ -122,9 +168,12 @@ export default class LeafletMapElement extends WithProps(
cssUrl: positional(str(DEFAULT_CSS_URL, { set: relinkCss })), cssUrl: positional(str(DEFAULT_CSS_URL, { set: relinkCss })),
cssIntegrity: positional(str(DEFAULT_CSS_INTEGRITY, { set: relinkCss })), cssIntegrity: positional(str(DEFAULT_CSS_INTEGRITY, { set: relinkCss })),
cssCrossorigin: positional(str('', { set: relinkCss })), cssCrossorigin: positional(str('', { set: relinkCss })),
}, };
{ attach: 'none' }, const Base: LeafletElementConstructor<LMap, typeof PROPS> = WithProps(PROPS, { attach: 'none' });
) {
// The root of the component tree. Every other component bubbles a
// `leaflet-register` event up to here, which is where it stops.
export default class LeafletMapElement extends Base {
declare readonly leafletObject?: LMap; declare readonly leafletObject?: LMap;
declare addEventListener: LeafletAddEventListener<MapEvents>; declare addEventListener: LeafletAddEventListener<MapEvents>;
declare removeEventListener: LeafletRemoveEventListener<MapEvents>; declare removeEventListener: LeafletRemoveEventListener<MapEvents>;

@ -1,15 +1,22 @@
import { Icon, Marker, type MarkerOptions } from 'leaflet'; import { Icon, Marker, type MarkerOptions } from 'leaflet';
import { bool, num, str } from '../core/props.ts'; import { bool, num, str, type PropDef } from '../core/props.ts';
import { latLngProps } from '../core/shared-props.ts'; import { latLngProps } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { LeafletIconChangedEvent } from '../core/register.ts'; import type { LeafletIconChangedEvent } from '../core/register.ts';
import type { MarkerEvents } from '../core/event-types.ts'; import type { MarkerEvents } from '../core/event-types.ts';
const PROPS = { const PROPS: typeof latLngProps & {
title: PropDef<string, Marker>;
alt: PropDef<string, Marker>;
draggable: PropDef<boolean, Marker>;
opacity: PropDef<number>;
zIndexOffset: PropDef<number>;
} = {
...latLngProps, ...latLngProps,
// title and alt end up on the <img> Leaflet renders, so live updates go there. // title and alt end up on the <img> Leaflet renders, so live updates go there.
title: str('', { title: str('', {
@ -32,9 +39,10 @@ const PROPS = {
}), }),
opacity: num(1.0), opacity: num(1.0),
zIndexOffset: num(), zIndexOffset: num(),
} as const; };
const Base: LeafletElementConstructor<Marker, typeof PROPS> = WithProps(PROPS);
export default class LeafletMarkerElement extends WithProps(PROPS) { export default class LeafletMarkerElement extends Base {
declare readonly leafletObject?: Marker; declare readonly leafletObject?: Marker;
declare addEventListener: LeafletAddEventListener<MarkerEvents>; declare addEventListener: LeafletAddEventListener<MarkerEvents>;
declare removeEventListener: LeafletRemoveEventListener<MarkerEvents>; declare removeEventListener: LeafletRemoveEventListener<MarkerEvents>;

@ -3,13 +3,17 @@ import { pathProps } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { PathEvents } from '../core/event-types.ts'; import type { PathEvents } from '../core/event-types.ts';
import type { LeafletLineRemoveEvent, LeafletLineSyncEvent } from '../core/register.ts'; import type { LeafletLineRemoveEvent, LeafletLineSyncEvent } from '../core/register.ts';
import { VertexTracker } from '../core/vertex-tracker.ts'; import { VertexTracker } from '../core/vertex-tracker.ts';
export default class LeafletPolygonElement extends WithProps({ ...pathProps }) { const PROPS = { ...pathProps } as const;
const Base: LeafletElementConstructor<Polygon, typeof PROPS> = WithProps(PROPS);
export default class LeafletPolygonElement extends Base {
declare readonly leafletObject?: Polygon; declare readonly leafletObject?: Polygon;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;
declare removeEventListener: LeafletRemoveEventListener<PathEvents>; declare removeEventListener: LeafletRemoveEventListener<PathEvents>;

@ -1,22 +1,30 @@
import { Polyline, type PolylineOptions } from 'leaflet'; import { Polyline, type PolylineOptions } from 'leaflet';
import { bool, num } from '../core/props.ts'; import { bool, num, type PropDef } from '../core/props.ts';
import { pathProps, style } from '../core/shared-props.ts'; import { pathProps, style, type Styleable } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { PathEvents } from '../core/event-types.ts'; import type { PathEvents } from '../core/event-types.ts';
import type { LeafletLineRemoveEvent, LeafletLineSyncEvent } from '../core/register.ts'; import type { LeafletLineRemoveEvent, LeafletLineSyncEvent } from '../core/register.ts';
import { VertexTracker } from '../core/vertex-tracker.ts'; import { VertexTracker } from '../core/vertex-tracker.ts';
export default class LeafletPolylineElement extends WithProps({ const PROPS: typeof pathProps & {
fill: PropDef<boolean, Styleable>;
smoothFactor: PropDef<number>;
noClip: PropDef<boolean>;
} = {
...pathProps, ...pathProps,
// Unlike closed shapes, a polyline is unfilled by default. // Unlike closed shapes, a polyline is unfilled by default.
fill: bool(false, { set: style('fill') }), fill: bool(false, { set: style('fill') }),
smoothFactor: num(1.0), smoothFactor: num(1.0),
noClip: bool(), noClip: bool(),
}) { };
const Base: LeafletElementConstructor<Polyline, typeof PROPS> = WithProps(PROPS);
export default class LeafletPolylineElement extends Base {
declare readonly leafletObject?: Polyline; declare readonly leafletObject?: Polyline;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;
declare removeEventListener: LeafletRemoveEventListener<PathEvents>; declare removeEventListener: LeafletRemoveEventListener<PathEvents>;

@ -1,15 +1,22 @@
import { Popup, type PopupOptions } from 'leaflet'; import { Popup, type PopupOptions } from 'leaflet';
import { bool, num } from '../core/props.ts'; import { bool, num, type PropDef } from '../core/props.ts';
import { latLngProps } from '../core/shared-props.ts'; import { latLngProps } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { DivOverlayLayerEvents } from '../core/event-types.ts'; import type { DivOverlayLayerEvents } from '../core/event-types.ts';
export default class LeafletPopupElement extends WithProps( const PROPS: typeof latLngProps & {
{ maxWidth: PropDef<number>;
minWidth: PropDef<number>;
maxHeight: PropDef<number>;
autoPan: PropDef<boolean>;
closeButton: PropDef<boolean>;
autoClose: PropDef<boolean>;
} = {
...latLngProps, ...latLngProps,
maxWidth: num(300), maxWidth: num(300),
minWidth: num(50), minWidth: num(50),
@ -17,9 +24,10 @@ export default class LeafletPopupElement extends WithProps(
autoPan: bool(true), autoPan: bool(true),
closeButton: bool(true), closeButton: bool(true),
autoClose: bool(true), autoClose: bool(true),
}, };
{ attach: 'self' }, const Base: LeafletElementConstructor<Popup, typeof PROPS> = WithProps(PROPS, { attach: 'self' });
) {
export default class LeafletPopupElement extends Base {
declare readonly leafletObject?: Popup; declare readonly leafletObject?: Popup;
declare addEventListener: LeafletAddEventListener<DivOverlayLayerEvents>; declare addEventListener: LeafletAddEventListener<DivOverlayLayerEvents>;
declare removeEventListener: LeafletRemoveEventListener<DivOverlayLayerEvents>; declare removeEventListener: LeafletRemoveEventListener<DivOverlayLayerEvents>;

@ -1,17 +1,23 @@
import { Rectangle, type LatLngBoundsExpression, type PolylineOptions } from 'leaflet'; import { Rectangle, type LatLngBoundsExpression, type PolylineOptions } from 'leaflet';
import { json, positional } from '../core/props.ts'; import { json, positional, type Positional } from '../core/props.ts';
import { pathProps, getBounds } from '../core/shared-props.ts'; import { pathProps, getBounds } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { PathEvents } from '../core/event-types.ts'; import type { PathEvents } from '../core/event-types.ts';
export default class LeafletRectangleElement extends WithProps({ const PROPS: {
bounds: Positional<LatLngBoundsExpression, Rectangle>;
} & typeof pathProps = {
bounds: positional(json<LatLngBoundsExpression, Rectangle>([], { get: getBounds })), bounds: positional(json<LatLngBoundsExpression, Rectangle>([], { get: getBounds })),
...pathProps, ...pathProps,
}) { };
const Base: LeafletElementConstructor<Rectangle, typeof PROPS> = WithProps(PROPS);
export default class LeafletRectangleElement extends Base {
declare readonly leafletObject?: Rectangle; declare readonly leafletObject?: Rectangle;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;
declare removeEventListener: LeafletRemoveEventListener<PathEvents>; declare removeEventListener: LeafletRemoveEventListener<PathEvents>;

@ -4,23 +4,43 @@ import {
type ImageOverlayOptions, type ImageOverlayOptions,
type LatLngBoundsExpression, type LatLngBoundsExpression,
} from 'leaflet'; } from 'leaflet';
import { bool, choice, json, num, positional, str } from '../core/props.ts'; import {
bool,
choice,
json,
num,
positional,
str,
type PropDef,
type Positional,
} from '../core/props.ts';
import { getBounds } from '../core/shared-props.ts'; import { getBounds } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { PathEvents } from '../core/event-types.ts'; import type { PathEvents } from '../core/event-types.ts';
export default class LeafletSVGOverlayElement extends WithProps({ const PROPS: {
bounds: Positional<LatLngBoundsExpression, SVGOverlay>;
opacity: PropDef<number>;
interactive: PropDef<boolean>;
crossOrigin: PropDef<CrossOrigin>;
zIndex: PropDef<number>;
className: PropDef<string>;
} = {
bounds: positional(json<LatLngBoundsExpression, SVGOverlay>([], { get: getBounds })), bounds: positional(json<LatLngBoundsExpression, SVGOverlay>([], { get: getBounds })),
opacity: num(1.0), opacity: num(1.0),
interactive: bool(), interactive: bool(),
crossOrigin: choice<CrossOrigin>(''), crossOrigin: choice<CrossOrigin>(''),
zIndex: num(), zIndex: num(),
className: str(), className: str(),
}) { };
const Base: LeafletElementConstructor<SVGOverlay, typeof PROPS> = WithProps(PROPS);
export default class LeafletSVGOverlayElement extends Base {
declare readonly leafletObject?: SVGOverlay; declare readonly leafletObject?: SVGOverlay;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;
declare removeEventListener: LeafletRemoveEventListener<PathEvents>; declare removeEventListener: LeafletRemoveEventListener<PathEvents>;

@ -1,9 +1,10 @@
import { CRS, TileLayer, type WMSOptions, type WMSParams } from 'leaflet'; import { CRS, TileLayer, type WMSOptions, type WMSParams } from 'leaflet';
import { bool, str, type PropDef } from '../core/props.ts'; import { bool, str, type PropDef, type Positional } from '../core/props.ts';
import { tileLayerProps, urlProp } from '../core/shared-props.ts'; import { tileLayerProps, urlProp, type Sourced } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { TileLayerEvents } from '../core/event-types.ts'; import type { TileLayerEvents } from '../core/event-types.ts';
@ -39,7 +40,17 @@ const crsProp: PropDef<CRS> = {
: ((Object.keys(NAMED_CRS) as CrsName[]).find((name) => NAMED_CRS[name] === value) ?? null), : ((Object.keys(NAMED_CRS) as CrsName[]).find((name) => NAMED_CRS[name] === value) ?? null),
}; };
export default class LeafletTileLayerWMSElement extends WithProps({ const PROPS: {
url: Positional<string, Sourced>;
} & typeof tileLayerProps & {
layers: PropDef<string, TileLayer.WMS>;
styles: PropDef<string, TileLayer.WMS>;
format: PropDef<string, TileLayer.WMS>;
transparent: PropDef<boolean, TileLayer.WMS>;
version: PropDef<string, TileLayer.WMS>;
uppercase: PropDef<boolean>;
crs: PropDef<CRS>;
} = {
url: urlProp, url: urlProp,
...tileLayerProps, ...tileLayerProps,
layers: str('', { set: param('layers') }), layers: str('', { set: param('layers') }),
@ -49,7 +60,10 @@ export default class LeafletTileLayerWMSElement extends WithProps({
version: str('1.1.1', { set: param('version') }), version: str('1.1.1', { set: param('version') }),
uppercase: bool(), uppercase: bool(),
crs: crsProp, crs: crsProp,
}) { };
const Base: LeafletElementConstructor<TileLayer.WMS, typeof PROPS> = WithProps(PROPS);
export default class LeafletTileLayerWMSElement extends Base {
declare readonly leafletObject?: TileLayer.WMS; declare readonly leafletObject?: TileLayer.WMS;
declare addEventListener: LeafletAddEventListener<TileLayerEvents>; declare addEventListener: LeafletAddEventListener<TileLayerEvents>;
declare removeEventListener: LeafletRemoveEventListener<TileLayerEvents>; declare removeEventListener: LeafletRemoveEventListener<TileLayerEvents>;

@ -3,14 +3,18 @@ import { tileLayerProps, urlProp } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { TileLayerEvents } from '../core/event-types.ts'; import type { TileLayerEvents } from '../core/event-types.ts';
export default class LeafletTileLayerElement extends WithProps({ const PROPS = {
url: urlProp, url: urlProp,
...tileLayerProps, ...tileLayerProps,
}) { } as const;
const Base: LeafletElementConstructor<TileLayer, typeof PROPS> = WithProps(PROPS);
export default class LeafletTileLayerElement extends Base {
declare readonly leafletObject?: TileLayer; declare readonly leafletObject?: TileLayer;
declare addEventListener: LeafletAddEventListener<TileLayerEvents>; declare addEventListener: LeafletAddEventListener<TileLayerEvents>;
declare removeEventListener: LeafletRemoveEventListener<TileLayerEvents>; declare removeEventListener: LeafletRemoveEventListener<TileLayerEvents>;

@ -1,15 +1,22 @@
import { Tooltip, type Direction, type PointExpression, type TooltipOptions } from 'leaflet'; import { Tooltip, type Direction, type PointExpression, type TooltipOptions } from 'leaflet';
import { bool, choice, json, num, str } from '../core/props.ts'; import { bool, choice, json, num, str, type PropDef } from '../core/props.ts';
import { latLngProps } from '../core/shared-props.ts'; import { latLngProps } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { DivOverlayLayerEvents } from '../core/event-types.ts'; import type { DivOverlayLayerEvents } from '../core/event-types.ts';
export default class LeafletTooltipElement extends WithProps( const PROPS: typeof latLngProps & {
{ pane: PropDef<string>;
offset: PropDef<PointExpression>;
direction: PropDef<Direction>;
permanent: PropDef<boolean>;
sticky: PropDef<boolean>;
opacity: PropDef<number>;
} = {
...latLngProps, ...latLngProps,
pane: str(), pane: str(),
offset: json<PointExpression>([0, 0]), offset: json<PointExpression>([0, 0]),
@ -17,9 +24,10 @@ export default class LeafletTooltipElement extends WithProps(
permanent: bool(), permanent: bool(),
sticky: bool(), sticky: bool(),
opacity: num(1.0), opacity: num(1.0),
}, };
{ attach: 'self' }, const Base: LeafletElementConstructor<Tooltip, typeof PROPS> = WithProps(PROPS, { attach: 'self' });
) {
export default class LeafletTooltipElement extends Base {
declare readonly leafletObject?: Tooltip; declare readonly leafletObject?: Tooltip;
declare addEventListener: LeafletAddEventListener<DivOverlayLayerEvents>; declare addEventListener: LeafletAddEventListener<DivOverlayLayerEvents>;
declare removeEventListener: LeafletRemoveEventListener<DivOverlayLayerEvents>; declare removeEventListener: LeafletRemoveEventListener<DivOverlayLayerEvents>;

@ -4,11 +4,21 @@ import {
type LatLngBoundsExpression, type LatLngBoundsExpression,
type VideoOverlayOptions, type VideoOverlayOptions,
} from 'leaflet'; } from 'leaflet';
import { bool, choice, json, num, positional, str } from '../core/props.ts'; import {
import { getBounds, urlProp } from '../core/shared-props.ts'; bool,
choice,
json,
num,
positional,
str,
type PropDef,
type Positional,
} from '../core/props.ts';
import { getBounds, urlProp, type Sourced } from '../core/shared-props.ts';
import { import {
WithProps, WithProps,
type LeafletAddEventListener, type LeafletAddEventListener,
type LeafletElementConstructor,
type LeafletRemoveEventListener, type LeafletRemoveEventListener,
} from '../core/with-props.ts'; } from '../core/with-props.ts';
import type { PathEvents } from '../core/event-types.ts'; import type { PathEvents } from '../core/event-types.ts';
@ -24,7 +34,22 @@ function media(
}; };
} }
export default class LeafletVideoOverlayElement extends WithProps({ const PROPS: {
url: Positional<string, Sourced>;
bounds: Positional<LatLngBoundsExpression, VideoOverlay>;
opacity: PropDef<number>;
alt: PropDef<string>;
interactive: PropDef<boolean>;
crossOrigin: PropDef<CrossOrigin>;
loop: PropDef<boolean, VideoOverlay>;
autoplay: PropDef<boolean, VideoOverlay>;
muted: PropDef<boolean, VideoOverlay>;
playsInline: PropDef<boolean, VideoOverlay>;
zIndex: PropDef<number>;
className: PropDef<string>;
keepAspectRatio: PropDef<boolean>;
errorOverlayUrl: PropDef<string>;
} = {
url: urlProp, url: urlProp,
bounds: positional(json<LatLngBoundsExpression, VideoOverlay>([], { get: getBounds })), bounds: positional(json<LatLngBoundsExpression, VideoOverlay>([], { get: getBounds })),
opacity: num(1.0), opacity: num(1.0),
@ -39,7 +64,10 @@ export default class LeafletVideoOverlayElement extends WithProps({
className: str(), className: str(),
keepAspectRatio: bool(true), keepAspectRatio: bool(true),
errorOverlayUrl: str(), errorOverlayUrl: str(),
}) { };
const Base: LeafletElementConstructor<VideoOverlay, typeof PROPS> = WithProps(PROPS);
export default class LeafletVideoOverlayElement extends Base {
declare readonly leafletObject?: VideoOverlay; declare readonly leafletObject?: VideoOverlay;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;
declare removeEventListener: LeafletRemoveEventListener<PathEvents>; declare removeEventListener: LeafletRemoveEventListener<PathEvents>;

@ -0,0 +1,15 @@
// npm package entry point.
//
// Identical to `./index.ts` (every element class, every helper, and the
// side-effect `customElements.define` calls) but additionally installs the
// ambient DOM augmentations from `./core/globals.ts` -- the `leaflet-*` tag
// map and the custom-event map -- so `document.querySelector('leaflet-map')`
// and `el.addEventListener('leaflet-register', ...)` are typed out of the box.
//
// `./index.ts` is kept free of that import because it is the module JSR
// publishes, and JSR's "no slow types" rule forbids `declare global` anywhere
// in the published graph. JSR consumers import `./index.ts` and can pull in
// `leaflet-components/elements` etc.; they just don't get the ambient globals.
import './core/globals.ts';
export * from './index.ts';

@ -67,61 +67,8 @@ import './components/leaflet-tooltip.ts';
import './components/leaflet-icon.ts'; import './components/leaflet-icon.ts';
import './components/leaflet-div-icon.ts'; import './components/leaflet-div-icon.ts';
// `export * from './elements/index.ts'` above re-exports these names but // The `leaflet-*` tag map (HTMLElementTagNameMap) and custom-event map
// doesn't bind them locally, so the tag map needs its own type-only import. // (HTMLElementEventMap) augmentations live in `./core/globals.ts`, pulled in
// Erased at build, backs no `define()` -- order doesn't matter. // by the npm entry `./index.npm.ts`. They are deliberately kept out of this
import type { // module: JSR publishes `./index.ts` and rejects `declare global` anywhere in
LeafletMapElement, // its export graph. See `./core/globals.ts` and docs/ for the split.
LeafletMarkerElement,
LeafletCircleElement,
LeafletCircleMarkerElement,
LeafletLineElement,
LeafletPolygonElement,
LeafletPolylineElement,
LeafletRectangleElement,
LeafletTileLayerElement,
LeafletTileLayerWMSElement,
LeafletImageOverlayElement,
LeafletVideoOverlayElement,
LeafletSVGOverlayElement,
LeafletLayerGroupElement,
LeafletFeatureGroupElement,
LeafletGeoJSONElement,
LeafletControlLayersElement,
LeafletControlZoomElement,
LeafletControlAttributionElement,
LeafletControlScaleElement,
LeafletPopupElement,
LeafletTooltipElement,
LeafletIconElement,
LeafletDivIconElement,
} from './elements/index.ts';
declare global {
interface HTMLElementTagNameMap {
'leaflet-map': LeafletMapElement;
'leaflet-marker': LeafletMarkerElement;
'leaflet-circle': LeafletCircleElement;
'leaflet-circle-marker': LeafletCircleMarkerElement;
'leaflet-line': LeafletLineElement;
'leaflet-polygon': LeafletPolygonElement;
'leaflet-polyline': LeafletPolylineElement;
'leaflet-rectangle': LeafletRectangleElement;
'leaflet-tile-layer': LeafletTileLayerElement;
'leaflet-tile-layer-wms': LeafletTileLayerWMSElement;
'leaflet-image-overlay': LeafletImageOverlayElement;
'leaflet-video-overlay': LeafletVideoOverlayElement;
'leaflet-svg-overlay': LeafletSVGOverlayElement;
'leaflet-layer-group': LeafletLayerGroupElement;
'leaflet-feature-group': LeafletFeatureGroupElement;
'leaflet-geojson': LeafletGeoJSONElement;
'leaflet-control-layers': LeafletControlLayersElement;
'leaflet-control-zoom': LeafletControlZoomElement;
'leaflet-control-attribution': LeafletControlAttributionElement;
'leaflet-control-scale': LeafletControlScaleElement;
'leaflet-popup': LeafletPopupElement;
'leaflet-tooltip': LeafletTooltipElement;
'leaflet-icon': LeafletIconElement;
'leaflet-div-icon': LeafletDivIconElement;
}
}

@ -1,3 +1,10 @@
// Install the ambient DOM augmentations (leaflet-* tag map, custom-event map)
// for the whole test program. In the shipped package these come in through the
// npm entry (src/index.npm.ts); the tests import element modules directly, so
// they need this explicit pull-in to type `addEventListener('leaflet-register',
// ...)` and `querySelector('leaflet-map')`.
import '../src/core/globals.ts';
// jsdom has no ResizeObserver. leaflet-map only uses it to call // jsdom has no ResizeObserver. leaflet-map only uses it to call
// invalidateSize() on resize, which no test here depends on incidentally -- // invalidateSize() on resize, which no test here depends on incidentally --
// without a stub, just importing the component throws. // without a stub, just importing the component throws.

Loading…
Cancel
Save