docs: bring README and CLAUDE.md up to date

README.md:
- Import options: drop CJS/UMD/minified-bundle mentions, none exist since
  the build simplified to ESM-only.
- New Events + TypeScript sections documenting the leaflet:<type> event
  forwarding mechanism and the HTMLElementTagNameMap / per-component
  addEventListener typing added this session.
- leaflet-tile-layer: 6 -> 23 attributes (full tileLayerProps), fixed
  z-index default (0 -> 1).
- leaflet-tile-layer-wms: now documents that it inherits all tile-layer
  attributes (previously listed none, matching the bug fixed earlier) +
  the new crs attribute.
- leaflet-video-overlay: added the 4 attributes added this session.
- leaflet-polyline: fixed a wrong "excludes fill*" claim and added
  smooth-factor/no-clip.
- leaflet-geojson: was documenting a stale hand-picked subset of path
  attributes (with stroke mis-described as a color); now says all
  path-style attributes apply, matching the dedup against pathProps.
- Path style table: added stroke/interactive/bubbling-mouse-events/
  class-name/pane, fixed fill's default.
- Added leaflet-icon/leaflet-div-icon sections (existed but were
  undocumented).
- Nesting rules and Development section brought in line with reality.

CLAUDE.md: the WithProps mixin description referenced an API from before
this session that no longer exists (WithProps(Base, PROPS), definePropAccessors,
initOptions(), updateLeafletObject()) and claimed LeafletMap extends
HTMLElement directly when it extends WithProps(...) like everything else.
Rewrote to match the real internals, fixed the child-component-pattern
steps, and documented how to type a new component's events.

Also added '*.md' to the format script's glob -- root markdown files
weren't covered, so this formatting could have silently drifted again.
main
Buddy 4 weeks ago
parent 0419ab781d
commit 255d10499b

@ -32,27 +32,31 @@ This library wraps [Leaflet.js](https://leafletjs.com/) as native Web Components
### `WithProps` mixin: `src/core/with-props.ts` ### `WithProps` mixin: `src/core/with-props.ts`
The `WithProps(Base, PROPS)` mixin factory replaces the old `LeafletElement` base class. Each component defines a `PROPS` table (a const record mapping kebab-case attribute names to `PropDef` descriptors), then extends `WithProps(HTMLElement, 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. Each component 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:
- `observedAttributes` getter derived from the PROPS table keys. - `static observedAttributes`, derived from the PROPS table keys.
- `definePropAccessors` — property getters/setters on the prototype that sync attributes. - 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.
- On connect, `initOptions()` builds a Leaflet options object from current attributes + PROPS defaults, then calls `createLeafletObject()`. - On connect, the private `#buildOptions()` builds a Leaflet options object from current attributes + PROPS defaults (skipping any prop marked `positional()`), then `createLeafletObject()` is called with it.
- On attribute change, `updateLeafletObject()` by default dispatches to the matching Leaflet setter (e.g. `setOpacity`, `setRadius`). Components override this for custom attribute handling (e.g. lat/lng pairs). - On attribute change, `attributeChangedCallback` dispatches to a prop's own `set` function if it has one, otherwise the matching Leaflet setter (e.g. `setOpacity`, `setRadius`) if the object has one — otherwise it's a silent no-op (many options are constructor-only). `options.recreate` switches this to rebuilding the whole object instead (for objects Leaflet gives no in-place mutation, like icons).
- Every Leaflet event the created object fires is re-emitted on the element as `leaflet:<type>` (e.g. `leaflet:zoomend`, `leaflet:dragend`), carrying the original Leaflet event object as `event.detail`. This is generic and automatic: `WithProps` 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.
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/components/leaflet-map.ts` ### `leaflet-map`: `src/components/leaflet-map.ts`
`LeafletMap` extends `HTMLElement` directly and uses Shadow DOM. It is the root of the component tree and terminates all bubbling `leaflet-register` events by calling `layer.addTo(this.map)`. Uses a `ResizeObserver` on the host element to call `map.invalidateSize()` automatically. `LeafletMap` 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.
### Child component pattern ### Child component pattern
All non-map components extend `WithProps(HTMLElement, PROPS)`. To add a new component: All non-map components extend `WithProps(PROPS, options?)`. To add a new component:
1. Define a `PROPS = {...}` const table mapping kebab-case attributes to `PropDef` entries. 1. 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. Declare `class LeafletFoo extends WithProps(HTMLElement, PROPS)` implementing `createLeafletObject(): L.Layer`. 2. Declare `class LeafletFoo 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).
3. Override `updateLeafletObject(name, val)` only if the default setter-based update won't work (common for coordinate pairs). 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. Call `customElements.define('leaflet-foo', LeafletFoo)` at the bottom. 4. Call `customElements.define('leaflet-foo', LeafletFoo)` at the bottom.
5. Export from `src/index.ts`. 5. Export from `src/index.ts`, and add the tag to the `HTMLElementTagNameMap` augmentation at the bottom of that file (needs its own `import type` — a re-export doesn't bind the name locally).
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
@ -60,6 +64,17 @@ All non-map components extend `WithProps(HTMLElement, PROPS)`. To add a new comp
- **`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 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.
### 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 class, so `document.createElement`/`querySelector` infer the right type. Since `export { X } from '...'` re-exports don't bind `X` locally, the tag map needs its own `import type` of each class alongside the re-export.
- **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
`tsc` compiles `src/``dist/` as individual ESM modules (`.js` + `.d.ts` + `.d.ts.map`) — no bundling step. Consumers import `dist/index.js` (or any individual module) 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`). `tsc` compiles `src/``dist/` as individual ESM modules (`.js` + `.d.ts` + `.d.ts.map`) — no bundling step. Consumers import `dist/index.js` (or any individual module) 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`).

@ -10,26 +10,17 @@ npm install leaflet-components
## Import options ## Import options
The package ships multiple bundle formats and supports deep imports for tree-shaking. 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
// Default ESM bundle (recommended) — registers all components // Registers all components
import 'leaflet-components'; import 'leaflet-components';
// Minified ESM bundle
import 'leaflet-components/dist/index.min.js';
// CommonJS
const lc = require('leaflet-components');
// Deep import — register only one component // Deep import — register only one component
import 'leaflet-components/dist/components/leaflet-marker.js'; import 'leaflet-components/dist/components/leaflet-marker.js';
// UMD (for script tags, expects Leaflet as window.L)
// <script src="node_modules/leaflet-components/dist/index.umd.js"></script>
``` ```
`leaflet` is always an external dependency — you must install it yourself. `leaflet` is always an external dependency — you must install it yourself. There is no CommonJS or UMD build.
## Usage ## Usage
@ -51,7 +42,14 @@ Import once to register all custom elements, then use them declaratively in HTML
<leaflet-tooltip>Hover me</leaflet-tooltip> <leaflet-tooltip>Hover me</leaflet-tooltip>
</leaflet-marker> </leaflet-marker>
<leaflet-circle lat="51.508" lng="-0.11" radius="500" color="red" fill-color="#f03" fill-opacity="0.5"> <leaflet-circle
lat="51.508"
lng="-0.11"
radius="500"
color="red"
fill-color="#f03"
fill-opacity="0.5"
>
<leaflet-popup>I am a circle</leaflet-popup> <leaflet-popup>I am a circle</leaflet-popup>
</leaflet-circle> </leaflet-circle>
@ -70,7 +68,7 @@ Import once to register all custom elements, then use them declaratively in HTML
The root component. All other components must be children of `<leaflet-map>`. The root component. All other components must be children of `<leaflet-map>`.
```html ```html
<leaflet-map lat="51.505" lng="-0.09" zoom="13" disable-scroll-wheel-zoom> <leaflet-map lat="51.505" lng="-0.09" zoom="13" disable-scroll-wheel-zoom></leaflet-map>
``` ```
### View state ### View state
@ -78,7 +76,7 @@ The root component. All other components must be children of `<leaflet-map>`.
These attributes stay in sync with the map as the user interacts with it — panning updates `lat`/`lng`, zooming updates `zoom`. These attributes stay in sync with the map as the user interacts with it — panning updates `lat`/`lng`, zooming updates `zoom`.
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ---------- | ------- | --------------------------------------------------------------------------- |
| `lat` | `0` | Center latitude | | `lat` | `0` | Center latitude |
| `lng` | `0` | Center longitude | | `lng` | `0` | Center longitude |
| `zoom` | `2` | Zoom level | | `zoom` | `2` | Zoom level |
@ -90,7 +88,7 @@ These attributes stay in sync with the map as the user interacts with it — pan
Boolean options that default to `true` are controlled by the presence of a `disable-*` attribute. Handler-based options can be toggled at any time; the rest only apply at construction. Boolean options that default to `true` are controlled by the presence of a `disable-*` attribute. Handler-based options can be toggled at any time; the rest only apply at construction.
| Attribute | Controls | Live? | | Attribute | Controls | Live? |
|---|---|---| | ------------------------------- | -------------------------------- | ----- |
| `disable-dragging` | Mouse/touch panning | ✓ | | `disable-dragging` | Mouse/touch panning | ✓ |
| `disable-scroll-wheel-zoom` | Scroll-wheel zoom | ✓ | | `disable-scroll-wheel-zoom` | Scroll-wheel zoom | ✓ |
| `disable-double-click-zoom` | Double-click zoom | ✓ | | `disable-double-click-zoom` | Double-click zoom | ✓ |
@ -107,14 +105,14 @@ Boolean options that default to `true` are controlled by the presence of a `disa
Boolean options that default to `false` are enabled by adding the attribute: Boolean options that default to `false` are enabled by adding the attribute:
| Attribute | Description | | Attribute | Description |
|---|---| | ----------------- | ------------------------------------------------------------- |
| `prefer-canvas` | Render vector layers on Canvas instead of SVG | | `prefer-canvas` | Render vector layers on Canvas instead of SVG |
| `world-copy-jump` | Pan to the original world copy when crossing the antimeridian | | `world-copy-jump` | Pan to the original world copy when crossing the antimeridian |
### Animation ### Animation
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ------------------------------- | ------- | -------------------------------- |
| `disable-zoom-animation` | — | Disable CSS zoom animation | | `disable-zoom-animation` | — | Disable CSS zoom animation |
| `disable-fade-animation` | — | Disable tile fade-in | | `disable-fade-animation` | — | Disable tile fade-in |
| `disable-marker-zoom-animation` | — | Disable marker zoom animation | | `disable-marker-zoom-animation` | — | Disable marker zoom animation |
@ -123,7 +121,7 @@ Boolean options that default to `false` are enabled by adding the attribute:
### Inertia & panning ### Inertia & panning
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ---------------------- | ---------- | ---------------------------- |
| `disable-inertia` | — | Disable inertial panning | | `disable-inertia` | — | Disable inertial panning |
| `inertia-deceleration` | `3000` | Deceleration rate (px/s²) | | `inertia-deceleration` | `3000` | Deceleration rate (px/s²) |
| `inertia-max-speed` | `Infinity` | Maximum inertia speed (px/s) | | `inertia-max-speed` | `Infinity` | Maximum inertia speed (px/s) |
@ -132,7 +130,7 @@ Boolean options that default to `false` are enabled by adding the attribute:
### Zoom behaviour ### Zoom behaviour
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ---------------------- | ------- | ------------------------------------------------------ |
| `zoom-snap` | `1` | Zoom snapping interval; `0` for continuous zoom | | `zoom-snap` | `1` | Zoom snapping interval; `0` for continuous zoom |
| `zoom-delta` | `1` | Zoom step per keyboard/button press | | `zoom-delta` | `1` | Zoom step per keyboard/button press |
| `max-bounds-viscosity` | `0` | How much the bounds resist panning past them (`0``1`) | | `max-bounds-viscosity` | `0` | How much the bounds resist panning past them (`0``1`) |
@ -140,21 +138,21 @@ Boolean options that default to `false` are enabled by adding the attribute:
### Scroll wheel ### Scroll wheel
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ------------------------- | ------- | ------------------------------------ |
| `wheel-debounce-time` | `40` | Debounce delay for wheel events (ms) | | `wheel-debounce-time` | `40` | Debounce delay for wheel events (ms) |
| `wheel-px-per-zoom-level` | `60` | Pixels of scroll per zoom level | | `wheel-px-per-zoom-level` | `60` | Pixels of scroll per zoom level |
### Keyboard & touch ### Keyboard & touch
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | -------------------- | ------- | ---------------------------------------- |
| `keyboard-pan-delta` | `80` | Pan distance per key press (px) | | `keyboard-pan-delta` | `80` | Pan distance per key press (px) |
| `tap-tolerance` | `15` | Max touch movement to trigger a tap (px) | | `tap-tolerance` | `15` | Max touch movement to trigger a tap (px) |
### Rendering ### Rendering
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | -------------------- | --------- | ---------------------------------------------------------- |
| `transform-3d-limit` | `8388608` | Max CSS `translate3d` component value before a layer reset | | `transform-3d-limit` | `8388608` | Max CSS `translate3d` component value before a layer reset |
### Accessing the underlying map ### Accessing the underlying map
@ -187,7 +185,7 @@ Returns `undefined` while the element isn't connected to the DOM.
Leaflet's stylesheet is loaded from a CDN via a `<link>` in the shadow DOM. Marker icon paths are derived from the CSS URL automatically. Leaflet's stylesheet is loaded from a CDN via a `<link>` in the shadow DOM. Marker icon paths are derived from the CSS URL automatically.
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ----------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `css-url` | `https://unpkg.com/leaflet@1.9.4/dist/leaflet.css` | URL for Leaflet CSS | | `css-url` | `https://unpkg.com/leaflet@1.9.4/dist/leaflet.css` | URL for Leaflet CSS |
| `css-integrity` | `sha256-...` | SRI hash (required with default URL; when `css-url` is custom, no default integrity is used unless explicitly set) | | `css-integrity` | `sha256-...` | SRI hash (required with default URL; when `css-url` is custom, no default integrity is used unless explicitly set) |
| `css-crossorigin` | _(auto)_ | `"anonymous"` when integrity is active, otherwise absent. Set explicitly to override. | | `css-crossorigin` | _(auto)_ | `"anonymous"` when integrity is active, otherwise absent. Set explicitly to override. |
@ -196,6 +194,47 @@ Leaflet's stylesheet is loaded from a CDN via a `<link>` in the shadow DOM. Mark
A `ResizeObserver` on the host element automatically calls `map.invalidateSize()` whenever the element's dimensions change — from CSS classes, inline styles, attribute changes, or parent layout. A `ResizeObserver` on the host element automatically calls `map.invalidateSize()` whenever the element's dimensions change — from CSS classes, inline styles, attribute changes, or parent layout.
## Events
Every event the underlying Leaflet object fires is re-emitted on the element as a `leaflet:<type>` DOM event — `zoomend` becomes `leaflet:zoomend`, `popupopen` becomes `leaflet:popupopen`, and so on. This is generic and automatic for every component; nothing needs to be configured per event.
```js
const map = document.querySelector('leaflet-map');
map.addEventListener('leaflet:moveend', (e) => {
console.log(e.detail.target.getCenter());
});
const marker = document.querySelector('leaflet-marker');
marker.addEventListener('leaflet:dragend', (e) => {
console.log('dragged', e.detail.distance, 'px');
});
```
`event.detail` is the same object a native Leaflet `.on()` listener receives — it always carries `type`, `target`, and `sourceTarget` in addition to whatever fields that particular event adds (`latlng` for mouse events, `popup` for `popupopen`/`popupclose`, `distance` for `dragend`, etc.). These events don't bubble: Leaflet already propagates layer events up to the map internally, so a bubbling DOM event would make `<leaflet-map>` see each one twice.
Which events a component can fire depends on the Leaflet class it wraps — see [Leaflet's own event reference](https://leafletjs.com/reference.html) for the full list per class (Map, Layer, Marker, Path, Popup, TileLayer, etc.).
## TypeScript
`leaflet-*` elements are typed. `document.createElement`/`querySelector` infer the right component class for every tag:
```ts
const map = document.querySelector('leaflet-map'); // LeafletMap | null
```
`addEventListener` is typed per component against the real Leaflet event payload for events that component actually fires — ordinary DOM events (`click`, etc.) still work normally, and an event name the component doesn't fire is a compile error:
```ts
marker.addEventListener('leaflet:dragend', (e) => {
e.detail.distance; // number — DragEndEvent
});
// Type error: baselayerchange is only fired by <leaflet-map>, not <leaflet-marker>.
marker.addEventListener('leaflet:baselayerchange', () => {});
```
There's no generic fallback overload for `leaflet:*` names — that's deliberate, so a typo or a wrong event name doesn't silently type-check. A genuinely dynamic (non-literal) event name string needs a cast.
--- ---
## Components ## Components
@ -203,18 +242,41 @@ A `ResizeObserver` on the host element automatically calls `map.invalidateSize()
### `<leaflet-tile-layer>` ### `<leaflet-tile-layer>`
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | --------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `url` | `''` | Tile URL template (`https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`) | | `url` | `''` | Tile URL template (`https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`) |
| `attribution` | `''` | Attribution text | | `attribution` | `''` | Attribution text |
| `min-zoom` | `0` | Minimum zoom level | | `min-zoom` | `0` | Minimum zoom level |
| `max-zoom` | `18` | Maximum zoom level | | `max-zoom` | `18` | Maximum zoom level |
| `opacity` | `1.0` | Tile layer opacity | | `opacity` | `1.0` | Tile layer opacity |
| `z-index` | `0` | Z-index | | `z-index` | `1` | Z-index |
| `subdomains` | `'abc'` | Subdomains for `{s}` in the URL template |
| `tms` | — | Use TMS tile coordinate scheme |
| `zoom-offset` | `0` | Offset added to the zoom level when requesting tiles |
| `zoom-reverse` | — | Reverse the zoom level when requesting tiles |
| `detect-retina` | — | Request higher-resolution tiles on retina displays |
| `cross-origin` | `''` | CORS setting for tile images |
| `referrer-policy` | — | `referrerPolicy` for tile image requests |
| `error-tile-url` | `''` | Fallback tile image on load error |
| `tile-size` | `256` | Tile size in pixels |
| `no-wrap` | — | Don't wrap tiles horizontally across the antimeridian |
| `bounds` | `''` | Restrict tile loading to this bounding box, as JSON: `[[south,west],[north,east]]` |
| `class-name` | `''` | CSS class for tile images |
| `min-native-zoom` | — | Lowest zoom level the tile source natively supports |
| `max-native-zoom` | — | Highest zoom level the tile source natively supports |
| `keep-buffer` | `2` | Extra rows/columns of tiles to keep loaded outside the viewport |
| `update-when-idle` | — | Update tiles only when the map stops moving |
| `update-when-zooming` | `true` (toggle with `update-when-zooming="false"`) | Update tiles continuously while zooming |
| `update-interval` | `200` | Throttle between tile updates while panning (ms) |
| `pane` | `'tilePane'` | Map pane name |
Most of these apply only at construction — Leaflet exposes no setter for them, so changing the attribute afterward has no effect. `min-zoom`, `max-zoom`, `opacity`, and `z-index` are live.
### `<leaflet-tile-layer-wms>` ### `<leaflet-tile-layer-wms>`
All `<leaflet-tile-layer>` attributes above apply (a WMS layer is a tile layer), plus:
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ------------- | -------------- | -------------------------------------------------------------------------------------------- |
| `url` | `''` | WMS service URL | | `url` | `''` | WMS service URL |
| `layers` | `''` | Comma-separated layer names | | `layers` | `''` | Comma-separated layer names |
| `styles` | `''` | Comma-separated style names | | `styles` | `''` | Comma-separated style names |
@ -222,11 +284,12 @@ A `ResizeObserver` on the host element automatically calls `map.invalidateSize()
| `transparent` | — | Request transparent tiles | | `transparent` | — | Request transparent tiles |
| `version` | `'1.1.1'` | WMS version | | `version` | `'1.1.1'` | WMS version |
| `uppercase` | — | Use uppercase WMS parameter names | | `uppercase` | — | Use uppercase WMS parameter names |
| `crs` | — | Coordinate reference system by name: `'EPSG3857'`, `'EPSG4326'`, `'EPSG3395'`, or `'Simple'` |
### `<leaflet-marker>` ### `<leaflet-marker>`
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ---------------- | ------- | ----------------------------- |
| `lat` | `0` | Latitude | | `lat` | `0` | Latitude |
| `lng` | `0` | Longitude | | `lng` | `0` | Longitude |
| `title` | `''` | Tooltip text on hover | | `title` | `''` | Tooltip text on hover |
@ -235,12 +298,46 @@ A `ResizeObserver` on the host element automatically calls `map.invalidateSize()
| `opacity` | `1.0` | Marker opacity | | `opacity` | `1.0` | Marker opacity |
| `z-index-offset` | `0` | Z-index offset | | `z-index-offset` | `0` | Z-index offset |
A `<leaflet-icon>` or `<leaflet-div-icon>` child sets the marker's icon (see below); without one, Leaflet's default icon is used.
### `<leaflet-icon>`
Sets the marker icon to an image. Not rendered itself — swapped into a parent `<leaflet-marker>` via `setIcon()` whenever an attribute changes. Produces no icon (Leaflet's default is used instead) until `icon-url` is set.
| Attribute | Default | Description |
| ------------------- | ------- | --------------------------------------------------------------------- |
| `icon-url` | `''` | Icon image URL (required) |
| `icon-retina-url` | `''` | Higher-resolution icon image URL |
| `icon-size` | — | Icon size as JSON: `[width, height]` |
| `icon-anchor` | — | Point of the icon aligned to the marker's location, as JSON: `[x, y]` |
| `popup-anchor` | — | Popup anchor point relative to `icon-anchor`, as JSON: `[x, y]` |
| `tooltip-anchor` | — | Tooltip anchor point relative to `icon-anchor`, as JSON: `[x, y]` |
| `shadow-url` | `''` | Shadow image URL |
| `shadow-retina-url` | `''` | Higher-resolution shadow image URL |
| `shadow-size` | — | Shadow size as JSON: `[width, height]` |
| `shadow-anchor` | — | Shadow anchor point, as JSON: `[x, y]` |
| `class-name` | `''` | CSS class for the icon image |
### `<leaflet-div-icon>`
Sets the marker icon to an HTML element instead of an image. Content comes from `innerHTML` (or the `html` attribute if no markup is given) — either way it's rebuilt on every change, the same as `<leaflet-icon>`.
| Attribute | Default | Description |
| ---------------- | -------------------- | --------------------------------------------------------------------- |
| `icon-size` | — | Icon size as JSON: `[width, height]` |
| `icon-anchor` | — | Point of the icon aligned to the marker's location, as JSON: `[x, y]` |
| `popup-anchor` | — | Popup anchor point relative to `icon-anchor`, as JSON: `[x, y]` |
| `tooltip-anchor` | — | Tooltip anchor point relative to `icon-anchor`, as JSON: `[x, y]` |
| `class-name` | `'leaflet-div-icon'` | CSS class for the icon element |
| `html` | `''` | HTML content, if not using `innerHTML` |
| `bg-pos` | — | Background position offset, as JSON: `[x, y]` |
### `<leaflet-circle>` ### `<leaflet-circle>`
All [path style](#path-style) attributes apply. All [path style](#path-style) attributes apply.
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | --------- | ------- | ---------------- |
| `lat` | `0` | Center latitude | | `lat` | `0` | Center latitude |
| `lng` | `0` | Center longitude | | `lng` | `0` | Center longitude |
| `radius` | `1000` | Radius in meters | | `radius` | `1000` | Radius in meters |
@ -250,20 +347,19 @@ All [path style](#path-style) attributes apply.
All [path style](#path-style) attributes apply. All [path style](#path-style) attributes apply.
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | --------- | ------- | ---------------- |
| `lat` | `0` | Center latitude | | `lat` | `0` | Center latitude |
| `lng` | `0` | Center longitude | | `lng` | `0` | Center longitude |
| `radius` | `10` | Radius in pixels | | `radius` | `10` | Radius in pixels |
### `<leaflet-polyline>` ### `<leaflet-polyline>`
Coordinates are taken from child `<leaflet-line>` elements, not from attributes. All [path style](#path-style) attributes apply (excluding `fill*` — polylines don't fill). Coordinates are taken from child `<leaflet-line>` elements, not from attributes. All [path style](#path-style) attributes apply, except `fill` defaults to unfilled (a polyline isn't a closed shape, but it can still be filled explicitly).
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | --------------- | ------- | ------------------------------------------------------------------------------- |
| `color` | `'#3388ff'` | Stroke color | | `smooth-factor` | `1.0` | Simplification factor applied while panning/zooming — higher is more simplified |
| `weight` | `3` | Stroke width (px) | | `no-clip` | — | Disable polyline clipping |
| `opacity` | `1.0` | Stroke opacity |
### `<leaflet-polygon>` ### `<leaflet-polygon>`
@ -274,7 +370,7 @@ Coordinates are taken from child `<leaflet-line>` elements, not from attributes.
All [path style](#path-style) attributes apply. All [path style](#path-style) attributes apply.
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | --------- | ------- | --------------------------------------------------- |
| `bounds` | `''` | Bounding box as JSON: `[[south,west],[north,east]]` | | `bounds` | `''` | Bounding box as JSON: `[[south,west],[north,east]]` |
### `<leaflet-line>` ### `<leaflet-line>`
@ -282,14 +378,14 @@ All [path style](#path-style) attributes apply.
Vertex helper — not rendered directly. Used as a child of `<leaflet-polyline>` or `<leaflet-polygon>`. Vertex helper — not rendered directly. Used as a child of `<leaflet-polyline>` or `<leaflet-polygon>`.
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | --------- | ------- | ----------- |
| `lat` | `0` | Latitude | | `lat` | `0` | Latitude |
| `lng` | `0` | Longitude | | `lng` | `0` | Longitude |
### `<leaflet-image-overlay>` ### `<leaflet-image-overlay>`
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ------------------- | ------- | --------------------------------------------------- |
| `url` | `''` | Image URL | | `url` | `''` | Image URL |
| `bounds` | `''` | Bounding box as JSON: `[[south,west],[north,east]]` | | `bounds` | `''` | Bounding box as JSON: `[[south,west],[north,east]]` |
| `opacity` | `1.0` | Overlay opacity | | `opacity` | `1.0` | Overlay opacity |
@ -303,7 +399,7 @@ Vertex helper — not rendered directly. Used as a child of `<leaflet-polyline>`
### `<leaflet-video-overlay>` ### `<leaflet-video-overlay>`
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ------------------- | ------------------------------------------------ | --------------------------------------------------- |
| `url` | `''` | Video URL | | `url` | `''` | Video URL |
| `bounds` | `''` | Bounding box as JSON: `[[south,west],[north,east]]` | | `bounds` | `''` | Bounding box as JSON: `[[south,west],[north,east]]` |
| `opacity` | `1.0` | Overlay opacity | | `opacity` | `1.0` | Overlay opacity |
@ -314,13 +410,17 @@ Vertex helper — not rendered directly. Used as a child of `<leaflet-polyline>`
| `autoplay` | — | Start playing automatically | | `autoplay` | — | Start playing automatically |
| `muted` | — | Mute audio | | `muted` | — | Mute audio |
| `playsinline` | — | Play inline (mobile) | | `playsinline` | — | Play inline (mobile) |
| `z-index` | `0` | Z-index |
| `class-name` | `''` | CSS class for the video element |
| `keep-aspect-ratio` | `true` (toggle with `keep-aspect-ratio="false"`) | Preserve the video's aspect ratio within its bounds |
| `error-overlay-url` | `''` | Fallback image on load error |
### `<leaflet-svg-overlay>` ### `<leaflet-svg-overlay>`
Content comes from an inline `<svg>` child element. No `url` attribute. Content comes from an inline `<svg>` child element. No `url` attribute.
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | -------------- | ------- | --------------------------------------------------- |
| `bounds` | `''` | Bounding box as JSON: `[[south,west],[north,east]]` | | `bounds` | `''` | Bounding box as JSON: `[[south,west],[north,east]]` |
| `opacity` | `1.0` | Overlay opacity | | `opacity` | `1.0` | Overlay opacity |
| `interactive` | — | Receive mouse/touch events | | `interactive` | — | Receive mouse/touch events |
@ -338,24 +438,18 @@ Passthrough container with Leaflet's `FeatureGroup` (supports `getBounds()`, sty
### `<leaflet-geojson>` ### `<leaflet-geojson>`
All [path style](#path-style) attributes apply. All [path style](#path-style) attributes apply — each feature GeoJSON builds gets them as its style.
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | --------- | ------- | -------------- |
| `data` | `''` | GeoJSON string | | `data` | `''` | GeoJSON string |
| `stroke` | `''` | Stroke color (overrides `color`) |
| `line-cap` | `'round'` | Line cap style |
| `line-join` | `'round'` | Line join style |
| `dash-array` | `''` | Dash pattern |
| `dash-offset` | `''` | Dash offset |
| `fill-rule` | `'evenodd'` | Fill rule |
### `<leaflet-popup>` ### `<leaflet-popup>`
Content comes from `innerHTML`, not an attribute. Mutations to innerHTML sync automatically via a `MutationObserver`. Content comes from `innerHTML`, not an attribute. Mutations to innerHTML sync automatically via a `MutationObserver`.
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | -------------- | ------- | ------------------------------------------------------------- |
| `lat` | `0` | Latitude (omit to auto-attach to the parent layer's position) | | `lat` | `0` | Latitude (omit to auto-attach to the parent layer's position) |
| `lng` | `0` | Longitude | | `lng` | `0` | Longitude |
| `max-width` | `300` | Maximum width (px) | | `max-width` | `300` | Maximum width (px) |
@ -370,7 +464,7 @@ Content comes from `innerHTML`, not an attribute. Mutations to innerHTML sync au
Content comes from `innerHTML`, not an attribute. Content comes from `innerHTML`, not an attribute.
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ----------- | -------- | ------------------------------------------------------------------------- |
| `lat` | `0` | Latitude (omit to auto-attach to the parent layer's position) | | `lat` | `0` | Latitude (omit to auto-attach to the parent layer's position) |
| `lng` | `0` | Longitude | | `lng` | `0` | Longitude |
| `pane` | — | Map pane name | | `pane` | — | Map pane name |
@ -383,7 +477,7 @@ Content comes from `innerHTML`, not an attribute.
### `<leaflet-control-zoom>` ### `<leaflet-control-zoom>`
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ---------------- | ------------ | ----------------------- |
| `position` | `'topleft'` | Corner position | | `position` | `'topleft'` | Corner position |
| `zoom-in-text` | `'+'` | Zoom-in button label | | `zoom-in-text` | `'+'` | Zoom-in button label |
| `zoom-in-title` | `'Zoom in'` | Zoom-in button tooltip | | `zoom-in-title` | `'Zoom in'` | Zoom-in button tooltip |
@ -393,14 +487,14 @@ Content comes from `innerHTML`, not an attribute.
### `<leaflet-control-attribution>` ### `<leaflet-control-attribution>`
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ---------- | --------------- | --------------------------- |
| `position` | `'bottomright'` | Corner position | | `position` | `'bottomright'` | Corner position |
| `prefix` | `''` | Text before the attribution | | `prefix` | `''` | Text before the attribution |
### `<leaflet-control-scale>` ### `<leaflet-control-scale>`
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ------------------ | -------------- | ------------------------------------- |
| `position` | `'bottomleft'` | Corner position | | `position` | `'bottomleft'` | Corner position |
| `max-width` | `100` | Maximum width of the scale (px) | | `max-width` | `100` | Maximum width of the scale (px) |
| `metric` | — | Show metric scale (m/km) | | `metric` | — | Show metric scale (m/km) |
@ -422,7 +516,7 @@ The built-in Leaflet layer switcher. Children with `type="base"` appear as radio
``` ```
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ------------------ | ---------------------------------------- | ------------------------------------------------------------ |
| `position` | `'topright'` | Corner position | | `position` | `'topright'` | Corner position |
| `collapsed` | `true` (toggle with `collapsed="false"`) | Collapse into an icon until hovered | | `collapsed` | `true` (toggle with `collapsed="false"`) | Collapse into an icon until hovered |
| `auto-z-index` | `true` | Assign increasing z-indexes to layers | | `auto-z-index` | `true` | Assign increasing z-indexes to layers |
@ -432,7 +526,7 @@ The built-in Leaflet layer switcher. Children with `type="base"` appear as radio
Each child layer inside `<leaflet-control-layers>` may carry: Each child layer inside `<leaflet-control-layers>` may carry:
| Attribute | Description | | Attribute | Description |
|---|---| | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type="base"` | Show as a radio button (base layer). Omit or set `type="overlay"` for a checkbox. | | `type="base"` | Show as a radio button (base layer). Omit or set `type="overlay"` for a checkbox. |
| `name` | Human-readable label displayed in the control. | | `name` | Human-readable label displayed in the control. |
| `active` | Add the layer to the map immediately so it starts visible. Internally, the control dispatches a `leaflet-add-layer` event that the map handles. Layers without `active` are removed from the map before the control renders, so they correctly appear unchecked. | | `active` | Add the layer to the map immediately so it starts visible. Internally, the control dispatches a `leaflet-add-layer` event that the map handles. Layers without `active` are removed from the map before the control renders, so they correctly appear unchecked. |
@ -444,7 +538,8 @@ The control intercepts child `leaflet-register` events and stops propagation —
The following attributes apply to `<leaflet-circle>`, `<leaflet-circle-marker>`, `<leaflet-polyline>`, `<leaflet-polygon>`, `<leaflet-rectangle>`, and `<leaflet-geojson>`. The following attributes apply to `<leaflet-circle>`, `<leaflet-circle-marker>`, `<leaflet-polyline>`, `<leaflet-polygon>`, `<leaflet-rectangle>`, and `<leaflet-geojson>`.
| Attribute | Default | Description | | Attribute | Default | Description |
|---|---|---| | ----------------------- | ---------------------------------------------------- | ------------------------------------------------- |
| `stroke` | `true` (toggle with `stroke="false"`) | Draw the stroke |
| `color` | `'#3388ff'` | Stroke color | | `color` | `'#3388ff'` | Stroke color |
| `weight` | `3` | Stroke width (px) | | `weight` | `3` | Stroke width (px) |
| `opacity` | `1.0` | Stroke opacity | | `opacity` | `1.0` | Stroke opacity |
@ -452,10 +547,16 @@ The following attributes apply to `<leaflet-circle>`, `<leaflet-circle-marker>`,
| `line-join` | `'round'` | Line join: `'miter'`, `'round'`, `'bevel'` | | `line-join` | `'round'` | Line join: `'miter'`, `'round'`, `'bevel'` |
| `dash-array` | `''` | Dash pattern, e.g. `'5, 10'` | | `dash-array` | `''` | Dash pattern, e.g. `'5, 10'` |
| `dash-offset` | `''` | Dash offset | | `dash-offset` | `''` | Dash offset |
| `fill` | — | Enable fill | | `fill` | `true` (toggle with `fill="false"`) | Enable fill |
| `fill-color` | `'#3388ff'` | Fill color | | `fill-color` | `'#3388ff'` | Fill color |
| `fill-opacity` | `0.2` | Fill opacity | | `fill-opacity` | `0.2` | Fill opacity |
| `fill-rule` | `'evenodd'` | Fill rule: `'nonzero'`, `'evenodd'` | | `fill-rule` | `'evenodd'` | Fill rule: `'nonzero'`, `'evenodd'` |
| `interactive` | `true` (toggle with `interactive="false"`) | Receive mouse/touch events |
| `bubbling-mouse-events` | `true` (toggle with `bubbling-mouse-events="false"`) | Let mouse events on this layer also reach the map |
| `class-name` | `''` | CSS class for the path element |
| `pane` | `'overlay'` | Map pane name |
`stroke`, `color`, `weight`, `opacity`, `line-cap`, `line-join`, `dash-array`, `dash-offset`, `fill`, `fill-color`, `fill-opacity`, and `fill-rule` are live — Leaflet has a setter for them (`setStyle`). `interactive`, `bubbling-mouse-events`, `class-name`, and `pane` only apply at construction.
## Nesting rules ## Nesting rules
@ -463,14 +564,17 @@ The following attributes apply to `<leaflet-circle>`, `<leaflet-circle-marker>`,
- **Layer as child of a group** → added via `addLayer`. - **Layer as child of a group** → added via `addLayer`.
- **Any layer as child of `leaflet-map`** → added to the map directly. - **Any layer as child of `leaflet-map`** → added to the map directly.
- **`leaflet-polygon` / `leaflet-polyline`** take their coordinates from `<leaflet-line>` children, not from attributes. - **`leaflet-polygon` / `leaflet-polyline`** take their coordinates from `<leaflet-line>` children, not from attributes.
- **`<leaflet-icon>` / `<leaflet-div-icon>` as child of `<leaflet-marker>`** → swapped in via `setIcon()`.
- **Layer as child of `<leaflet-control-layers>`** → intercepted by the control and registered as a base or overlay entry. The control stops propagation so the layer doesn't reach the map directly. Use `active` to start the layer visible. - **Layer as child of `<leaflet-control-layers>`** → intercepted by the control and registered as a base or overlay entry. The control stops propagation so the layer doesn't reach the map directly. Use `active` to start the layer visible.
## Development ## Development
```bash ```bash
npm run build # tsc emits individual ESM modules; Rollup bundles ESM, CJS + UMD (minified/unminified) npm run build # tsc emits individual ESM modules to dist/ (no bundling)
npm run typecheck # tsc --noEmit npm run typecheck # tsc --noEmit, then tsc -p tsconfig.test.json for test/
npm run lint # ESLint npm run lint # oxlint over src/ and test/
npm run format # Prettier npm run format # Prettier
npm run test # vitest run
npm run test:watch # vitest, watch mode
# serve the project root with any static-file server and open index.html # serve the project root with any static-file server and open index.html
``` ```

@ -22,7 +22,7 @@
"build": "rm -rf dist && tsc --outDir dist", "build": "rm -rf dist && tsc --outDir dist",
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json", "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
"lint": "oxlint src test", "lint": "oxlint src test",
"format": "prettier --write 'src/**/*.{ts,js,json,md}' 'test/**/*.ts'", "format": "prettier --write '*.md' 'src/**/*.{ts,js,json,md}' 'test/**/*.ts'",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"

Loading…
Cancel
Save