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.
@ -24,7 +24,7 @@ Formatting runs on `oxfmt` (oxc's Prettier-compatible formatter), configured in
### 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.
- `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:
- **`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/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/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` / `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(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.
- 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.
- `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... */ } = { ... };
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.
### `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
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`).
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).
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, `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).
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).
### 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-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`
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.
`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.
### 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):
`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.
[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
```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
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
// Registers all components
import 'leaflet-components';
import 'leaflet-web-components';
// Register only one component (defines the <leaflet-marker> tag)
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
// 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
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
@ -37,7 +39,7 @@ Import once to register all custom elements, then use them declaratively in HTML
@ -603,20 +605,34 @@ This library's own components are built from a small toolkit — a mixin, some p
### 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.
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.)
```ts
import { MyClusterGroup, type MyClusterGroupOptions } from 'some-leaflet-plugin';
import { WithProps, bool, num } from 'leaflet-components';
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>`.
### 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
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
import { WithProps, pathProps, positional, json } from 'leaflet-components';
`Positional<T, Obj>` is the exported alias for a `positional()` prop's type
(`PropDef<T,Obj>& { option: false }`).
### 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).
@ -674,16 +713,17 @@ import {
type LeafletAddEventListener,
type LeafletRemoveEventListener,
type PathEvents,
} from 'leaflet-components';
} from 'leaflet-web-components';
class MyShapeLayer extends WithProps({/* ... */}) {
// Base = WithProps(PROPS), as in the previous example
`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
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
`<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
import { emitIconChanged } from 'leaflet-components';
import { emitIconChanged } from 'leaflet-web-components';