You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
leaflet-components/CLAUDE.md

66 lines
5.6 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
npm run build # tsc emits individual ESM modules to dist/ (no bundling)
npm run typecheck # tsc --noEmit, then tsc -p tsconfig.test.json for test/
npm run lint # oxlint over src/ and test/
npm run format # Prettier formatting
npm run test # vitest run
npm run test:watch # vitest, watch mode
```
Linting runs on `oxlint`, not ESLint/`@typescript-eslint`. This project pins `typescript@^7.0.2`, and `@typescript-eslint` has no released version that supports it (peer range caps at `<6.1.0`, and even loading `@typescript-eslint/parser` crashes against TS 7's package shape). `oxlint` has its own parser and doesn't touch the `typescript` package, so it works regardless of TS version — the tradeoff is no type-aware rules (no `no-floating-promises`, `no-unnecessary-condition`, etc.). Revisit once `@typescript-eslint` supports TS 7.
### 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:
- 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/**` is excluded from the main `tsconfig.json` (so test code never ends up in `dist/`) and typechecked separately via `tsconfig.test.json`.
To preview components in a real browser, open `index.html` with any static-file server.
## Architecture
This library wraps [Leaflet.js](https://leafletjs.com/) as native Web Components (Custom Elements). Each HTML element maps 1:1 to a Leaflet object.
### `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:
- `observedAttributes` getter derived from the PROPS table keys.
- `definePropAccessors` — property getters/setters on the prototype that sync attributes.
- On connect, `initOptions()` builds a Leaflet options object from current attributes + PROPS defaults, then calls `createLeafletObject()`.
- 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).
- 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.
### `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.
### Child component pattern
All non-map components extend `WithProps(HTMLElement, PROPS)`. To add a new component:
1. Define a `PROPS = {...}` const table mapping kebab-case attributes to `PropDef` entries.
2. Declare `class LeafletFoo extends WithProps(HTMLElement, PROPS)` implementing `createLeafletObject(): L.Layer`.
3. Override `updateLeafletObject(name, val)` only if the default setter-based update won't work (common for coordinate pairs).
4. Call `customElements.define('leaflet-foo', LeafletFoo)` at the bottom.
5. Export from `src/index.ts`.
### Special cases
- **`leaflet-polygon`** uses `<leaflet-line>` children for vertices. The polygon collects lat/lng from child `leaflet-line` elements rather than having them as direct attributes.
- **`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.
### 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`).