From 6b448ba09297119920304a6d0dfc48a1da6863f4 Mon Sep 17 00:00:00 2001 From: Buddy Date: Sat, 6 Jun 2026 08:51:02 -0700 Subject: [PATCH] feat: Initial implementation of leaflet-components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps Leaflet.js as native Web Components. Each element maps 1:1 to a Leaflet object with reactive attribute binding and proper lifecycle cleanup. Key design decisions: - LeafletElement base class handles attribute→option mapping, the leaflet-register bubble event for parent/child wiring, and disconnectedCallback cleanup tracking parent bindings. - LeafletControl base class for map controls (zoom, attribution, scale). - leaflet-map uses a PROPS table to drive observedAttributes, getters/setters, attributeChangedCallback, and map event listeners from a single source of truth. Property types are derived via a mapped type over the table; the mixin-base pattern (TypedBase) makes them visible to TypeScript without an interface merge. - Boolean map options that default to true use disable-* attributes. - Leaflet and its CSS are bundled into dist/index.js via esbuild. - JSR config included for @buddy/leaflet-components. --- .gitignore | 32 + .prettierrc | 7 + CLAUDE.md | 50 + README.md | 222 ++ eslint.config.js | 24 + index.html | 74 + jsr.json | 5 + package-lock.json | 1853 +++++++++++++++++ package.json | 41 + src/components/leaflet-circle-marker.ts | 54 + src/components/leaflet-circle.ts | 54 + src/components/leaflet-control-attribution.ts | 19 + src/components/leaflet-control-scale.ts | 25 + src/components/leaflet-control-zoom.ts | 25 + src/components/leaflet-feature-group.ts | 10 + src/components/leaflet-geojson.ts | 45 + src/components/leaflet-image-overlay.ts | 44 + src/components/leaflet-layer-group.ts | 10 + src/components/leaflet-line.ts | 18 + src/components/leaflet-map.ts | 329 +++ src/components/leaflet-marker.ts | 28 + src/components/leaflet-polygon.ts | 68 + src/components/leaflet-polyline.ts | 68 + src/components/leaflet-popup.ts | 59 + src/components/leaflet-rectangle.ts | 41 + src/components/leaflet-svg-overlay.ts | 31 + src/components/leaflet-tile-layer-wms.ts | 25 + src/components/leaflet-tile-layer.ts | 25 + src/components/leaflet-tooltip.ts | 50 + src/components/leaflet-video-overlay.ts | 45 + src/core/LeafletControl.ts | 36 + src/core/LeafletElement.ts | 141 ++ src/index.ts | 23 + src/types/css.d.ts | 4 + tsconfig.json | 17 + 35 files changed, 3602 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 jsr.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/components/leaflet-circle-marker.ts create mode 100644 src/components/leaflet-circle.ts create mode 100644 src/components/leaflet-control-attribution.ts create mode 100644 src/components/leaflet-control-scale.ts create mode 100644 src/components/leaflet-control-zoom.ts create mode 100644 src/components/leaflet-feature-group.ts create mode 100644 src/components/leaflet-geojson.ts create mode 100644 src/components/leaflet-image-overlay.ts create mode 100644 src/components/leaflet-layer-group.ts create mode 100644 src/components/leaflet-line.ts create mode 100644 src/components/leaflet-map.ts create mode 100644 src/components/leaflet-marker.ts create mode 100644 src/components/leaflet-polygon.ts create mode 100644 src/components/leaflet-polyline.ts create mode 100644 src/components/leaflet-popup.ts create mode 100644 src/components/leaflet-rectangle.ts create mode 100644 src/components/leaflet-svg-overlay.ts create mode 100644 src/components/leaflet-tile-layer-wms.ts create mode 100644 src/components/leaflet-tile-layer.ts create mode 100644 src/components/leaflet-tooltip.ts create mode 100644 src/components/leaflet-video-overlay.ts create mode 100644 src/core/LeafletControl.ts create mode 100644 src/core/LeafletElement.ts create mode 100644 src/index.ts create mode 100644 src/types/css.d.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9db624 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +/.idea/ +/jspm_packages/ +/node_modules/ +/web_modules/ +/dist/ +*.pid +*.pid.lock +*.seed +*.tgz +*.tsbuildinfo +.env +.env.development.local +.env.local +.env.production.local +.env.test.local +.eslintcache +.node_repl_history +.npm +.pnp.* +.pnpm-debug.log* +.stylelintcache +.yarn-integrity +.yarn/build-state.yml +.yarn/cache +.yarn/install-state.gz +.yarn/unplugged +lerna-debug.log* +npm-debug.log* +pids +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +yarn-debug.log* +yarn-error.log* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ca8527e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2 +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..df1a261 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,50 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +npm run build # Type-check + emit .d.ts via tsc, then bundle to dist/index.js via esbuild +npm run lint # ESLint over src/**/*.{ts,js} +npm run format # Prettier formatting +``` + +There are no tests in this project. To preview components locally, open `index.html` in a browser with a dev server that supports TypeScript (e.g. `npx vite`). + +## 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. + +### Core base class: `src/core/LeafletElement.ts` + +`LeafletElement` (extends `HTMLElement`) is the abstract base for all components except `leaflet-map`. Subclasses must implement `createLeafletObject()` returning an `L.Layer`. The base class: + +- Reads `observedAttributes` to auto-populate `this.options` via `initOptions()` on connect, converting kebab-case attribute names to camelCase for Leaflet options. +- Parses attribute values to booleans, numbers, or JSON automatically (`parseAttributeValue`). +- On `attributeChangedCallback`, calls `updateLeafletObject()` which by default invokes the matching Leaflet setter (e.g. `setOpacity`, `setRadius`). Subclasses override this for attributes needing custom logic (e.g. lat/lng pairs). +- Uses a custom `leaflet-register` bubbling event to wire children into parents. When a child connects, it dispatches this event upward; each parent intercepts and either calls `addLayer`, `bindPopup`, or `bindTooltip` depending on the child's type. + +### `leaflet-map`: `src/components/leaflet-map.ts` + +`LeafletMap` does **not** extend `LeafletElement` — it 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)`. + +### Child component pattern + +All other components extend `LeafletElement`. To add a new component: + +1. Extend `LeafletElement`, implement `createLeafletObject()`. +2. Declare `static get observedAttributes()` listing HTML attributes (kebab-case). +3. Override `updateLeafletObject()` 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 `` 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`**: a passthrough container; children register themselves into it via the standard bubble mechanism. + +### Output + +`tsc` compiles `src/` → `dist/` with `.js` files and `.d.ts` declarations. Imports within the source use `.js` extensions (required for ESM `"moduleResolution": "bundler"`). diff --git a/README.md b/README.md new file mode 100644 index 0000000..d14fece --- /dev/null +++ b/README.md @@ -0,0 +1,222 @@ +# leaflet-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. + +## Installation + +```bash +npm install leaflet-components +``` + +Available on JSR as `@buddy/leaflet-components`. + +## Usage + +Import once to register all custom elements, then use them declaratively in HTML. + +```html + + + + + + + Hello! + Hover me + + + + I am a circle + + + + + + + + + + +``` + +## `leaflet-map` + +The root component. All other components must be children of ``. + +```html + +``` + +### View state + +These attributes stay in sync with the map as the user interacts with it — panning updates `lat`/`lng`, zooming updates `zoom`. + +| Attribute | Default | Description | +|---|---|---| +| `lat` | `0` | Center latitude | +| `lng` | `0` | Center longitude | +| `zoom` | `2` | Zoom level | +| `min-zoom` | `0` | Minimum zoom level | +| `max-zoom` | — | Maximum zoom level. When unset, Leaflet uses the tile layer's own max zoom. | + +### Interaction + +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? | +|---|---|---| +| `disable-dragging` | Mouse/touch panning | ✓ | +| `disable-scroll-wheel-zoom` | Scroll-wheel zoom | ✓ | +| `disable-double-click-zoom` | Double-click zoom | ✓ | +| `disable-touch-zoom` | Pinch-to-zoom | ✓ | +| `disable-box-zoom` | Shift-drag zoom box | ✓ | +| `disable-keyboard` | Keyboard pan/zoom | ✓ | +| `disable-zoom-control` | Built-in zoom control | — | +| `disable-attribution-control` | Built-in attribution | — | +| `disable-close-popup-on-click` | Close popup on map click | — | +| `disable-track-resize` | Auto-resize on window resize | — | +| `disable-bounce-at-zoom-limits` | Bounce animation at min/max zoom | — | +| `disable-tap-hold` | Long-press context menu (mobile) | — | + +Boolean options that default to `false` are enabled by adding the attribute: + +| Attribute | Description | +|---|---| +| `prefer-canvas` | Render vector layers on Canvas instead of SVG | +| `world-copy-jump` | Pan to the original world copy when crossing the antimeridian | + +### Animation + +| Attribute | Default | Description | +|---|---|---| +| `disable-zoom-animation` | — | Disable CSS zoom animation | +| `disable-fade-animation` | — | Disable tile fade-in | +| `disable-marker-zoom-animation` | — | Disable marker zoom animation | +| `zoom-animation-threshold` | `4` | Max zoom delta for animated zoom | + +### Inertia & panning + +| Attribute | Default | Description | +|---|---|---| +| `disable-inertia` | — | Disable inertial panning | +| `inertia-deceleration` | `3000` | Deceleration rate (px/s²) | +| `inertia-max-speed` | `Infinity` | Maximum inertia speed (px/s) | +| `ease-linearity` | `0.2` | Pan easing linearity | + +### Zoom behaviour + +| Attribute | Default | Description | +|---|---|---| +| `zoom-snap` | `1` | Zoom snapping interval; `0` for continuous zoom | +| `zoom-delta` | `1` | Zoom step per keyboard/button press | +| `max-bounds-viscosity` | `0` | How much the bounds resist panning past them (`0`–`1`) | + +### Scroll wheel + +| Attribute | Default | Description | +|---|---|---| +| `wheel-debounce-time` | `40` | Debounce delay for wheel events (ms) | +| `wheel-px-per-zoom-level` | `60` | Pixels of scroll per zoom level | + +### Keyboard & touch + +| Attribute | Default | Description | +|---|---|---| +| `keyboard-pan-delta` | `80` | Pan distance per key press (px) | +| `tap-tolerance` | `15` | Max touch movement to trigger a tap (px) | + +### Rendering + +| Attribute | Default | Description | +|---|---|---| +| `transform-3d-limit` | `8388608` | Max CSS `translate3d` component value before a layer reset | + +### Accessing the underlying map + +```js +const el = document.querySelector('leaflet-map'); +el.map; // L.Map instance (undefined before connected) +el.zoom; // current zoom (reads live from map, falls back to attribute) +el.scrollWheelZoom = false; // disable at runtime +``` + +All `leaflet-map` properties are two-way: reading returns the live map value; writing updates both the attribute and the map. + +--- + +## Other components + +### Tile layers + +| Element | Key attributes | +|---|---| +| `leaflet-tile-layer` | `url`, `attribution`, `min-zoom`, `max-zoom`, `opacity`, `z-index` | +| `leaflet-tile-layer-wms` | `url`, `layers`, `styles`, `format`, `transparent`, `version` | + +### Vector layers + +Path style attributes (`color`, `weight`, `opacity`, `fill`, `fill-color`, `fill-opacity`, `dash-array`, `line-cap`, `line-join`) are shared by all vector layers. + +| Element | Additional attributes | Notes | +|---|---|---| +| `leaflet-circle` | `lat`, `lng`, `radius` (meters) | | +| `leaflet-circle-marker` | `lat`, `lng`, `radius` (pixels) | | +| `leaflet-polyline` | — | Vertices via `` children | +| `leaflet-polygon` | — | Vertices via `` children | +| `leaflet-rectangle` | `bounds` (JSON) | | +| `leaflet-line` | `lat`, `lng` | Vertex only — not rendered directly | + +### Markers & overlays + +| Element | Key attributes | +|---|---| +| `leaflet-marker` | `lat`, `lng`, `title`, `alt`, `draggable`, `opacity`, `z-index-offset` | +| `leaflet-image-overlay` | `url`, `bounds` (JSON), `opacity`, `alt`, `interactive` | +| `leaflet-video-overlay` | `url`, `bounds` (JSON), `opacity`, `loop`, `autoplay`, `muted` | +| `leaflet-svg-overlay` | `bounds` (JSON), `opacity`, `interactive` — wrap an `` element | + +### Groups & GeoJSON + +| Element | Key attributes | +|---|---| +| `leaflet-layer-group` | — | +| `leaflet-feature-group` | — | +| `leaflet-geojson` | `data` (GeoJSON string), plus path style attributes | + +### UI layers + +Content is set via `innerHTML`, not attributes. + +| Element | Key attributes | +|---|---| +| `leaflet-popup` | `lat`, `lng`, `max-width`, `min-width`, `auto-pan`, `close-button`, `auto-close` | +| `leaflet-tooltip` | `lat`, `lng`, `direction`, `permanent`, `sticky`, `opacity` | + +### Controls + +| Element | Key attributes | +|---|---| +| `leaflet-control-zoom` | `position`, `zoom-in-text`, `zoom-out-text` | +| `leaflet-control-attribution` | `position`, `prefix` | +| `leaflet-control-scale` | `position`, `max-width`, `metric`, `imperial` | + +## Nesting rules + +- **Popup / tooltip as child of a layer** → bound via `bindPopup` / `bindTooltip`. +- **Layer as child of a group** → added via `addLayer`. +- **Any layer as child of `leaflet-map`** → added to the map directly. +- **`leaflet-polygon` / `leaflet-polyline`** take their coordinates from `` children, not from attributes. + +## Development + +```bash +npm run build # type-check + emit .d.ts via tsc, then bundle to dist/index.js via esbuild +npm run lint # ESLint +npm run format # Prettier +npx vite # open index.html for a live demo +``` diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..db00b85 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,24 @@ +import tsParser from '@typescript-eslint/parser'; +import tsPlugin from '@typescript-eslint/eslint-plugin'; +import prettierPlugin from 'eslint-plugin-prettier'; +import prettierConfig from 'eslint-config-prettier'; + +export default [ + { + files: ['src/**/*.ts'], + languageOptions: { + parser: tsParser, + ecmaVersion: 2020, + sourceType: 'module', + }, + plugins: { + '@typescript-eslint': tsPlugin, + prettier: prettierPlugin, + }, + rules: { + ...tsPlugin.configs.recommended.rules, + ...prettierConfig.rules, + 'prettier/prettier': 'error', + }, + }, +]; diff --git a/index.html b/index.html new file mode 100644 index 0000000..a897bb7 --- /dev/null +++ b/index.html @@ -0,0 +1,74 @@ + + + + + + Leaflet Components Demo + + + +

Leaflet Web Components

+ + + + + + + Hello world!
I am a popup.
+ I am a tooltip +
+ + + I am a circle. + + + + + + + I am a polygon. + + + + + Part of a feature group + + + + + + + + +
+ + + + + diff --git a/jsr.json b/jsr.json new file mode 100644 index 0000000..ea485e2 --- /dev/null +++ b/jsr.json @@ -0,0 +1,5 @@ +{ + "name": "@buddy/leaflet-components", + "version": "0.1.0", + "exports": "./src/index.ts" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a1b3033 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1853 @@ +{ + "name": "leaflet-components", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "leaflet-components", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "leaflet": "1.9.4" + }, + "devDependencies": { + "@types/leaflet": "1.9.21", + "@typescript-eslint/eslint-plugin": "8.60.1", + "@typescript-eslint/parser": "8.60.1", + "esbuild": "^0.28.0", + "eslint": "10.4.1", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.6", + "prettier": "3.8.3", + "typescript": "6.0.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", + "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/type-utils": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.60.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", + "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", + "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.1", + "@typescript-eslint/types": "^8.60.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", + "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", + "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", + "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", + "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", + "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.60.1", + "@typescript-eslint/tsconfig-utils": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", + "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", + "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", + "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..96ac872 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "leaflet-components", + "version": "0.1.0", + "description": "LeafletJS as Web Components", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "rm -rf dist && tsc --emitDeclarationOnly && esbuild src/index.ts --bundle --format=esm --loader:.css=text --outfile=dist/index.js", + "lint": "eslint 'src/**/*.{ts,js}'", + "format": "prettier --write 'src/**/*.{ts,js,json,md}'", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "leaflet", + "web-components", + "maps" + ], + "author": "buddy", + "license": "MIT", + "type": "module", + "dependencies": {}, + "devDependencies": { + "@types/leaflet": "1.9.21", + "leaflet": "1.9.4", + "@typescript-eslint/eslint-plugin": "8.60.1", + "@typescript-eslint/parser": "8.60.1", + "esbuild": "^0.28.0", + "eslint": "10.4.1", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.6", + "prettier": "3.8.3", + "typescript": "6.0.3" + } +} diff --git a/src/components/leaflet-circle-marker.ts b/src/components/leaflet-circle-marker.ts new file mode 100644 index 0000000..53bd8f1 --- /dev/null +++ b/src/components/leaflet-circle-marker.ts @@ -0,0 +1,54 @@ +import { Layer, CircleMarker } from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletCircleMarker extends LeafletElement { + static get observedAttributes() { + return [ + 'lat', + 'lng', + 'radius', + 'color', + 'weight', + 'opacity', + 'fill', + 'fill-color', + 'fill-opacity', + ]; + } + + protected createLeafletObject(): Layer { + const lat = parseFloat(this.getAttribute('lat') || '0'); + const lng = parseFloat(this.getAttribute('lng') || '0'); + return new CircleMarker([lat, lng], this.options); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (this.leafletObject instanceof CircleMarker) { + if (property === 'lat' || property === 'lng') { + const lat = parseFloat(this.getAttribute('lat') || '0'); + const lng = parseFloat(this.getAttribute('lng') || '0'); + this.leafletObject.setLatLng([lat, lng]); + } else if ( + [ + 'color', + 'weight', + 'opacity', + 'fill', + 'fillColor', + 'fillOpacity', + 'stroke', + 'dashArray', + 'dashOffset', + 'lineCap', + 'lineJoin', + ].includes(property) + ) { + this.leafletObject.setStyle({ [property]: value }); + } else { + super.updateLeafletObject(property, value); + } + } + } +} + +customElements.define('leaflet-circle-marker', LeafletCircleMarker); diff --git a/src/components/leaflet-circle.ts b/src/components/leaflet-circle.ts new file mode 100644 index 0000000..0b34f08 --- /dev/null +++ b/src/components/leaflet-circle.ts @@ -0,0 +1,54 @@ +import { Circle, Layer } from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletCircle extends LeafletElement { + static get observedAttributes() { + return [ + 'lat', + 'lng', + 'radius', + 'color', + 'weight', + 'opacity', + 'fill', + 'fill-color', + 'fill-opacity', + ]; + } + + protected createLeafletObject(): Layer { + const lat = parseFloat(this.getAttribute('lat') || '0'); + const lng = parseFloat(this.getAttribute('lng') || '0'); + return new Circle([lat, lng], this.options); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (this.leafletObject instanceof Circle) { + if (property === 'lat' || property === 'lng') { + const lat = parseFloat(this.getAttribute('lat') || '0'); + const lng = parseFloat(this.getAttribute('lng') || '0'); + this.leafletObject.setLatLng([lat, lng]); + } else if ( + [ + 'color', + 'weight', + 'opacity', + 'fill', + 'fillColor', + 'fillOpacity', + 'stroke', + 'dashArray', + 'dashOffset', + 'lineCap', + 'lineJoin', + ].includes(property) + ) { + this.leafletObject.setStyle({ [property]: value }); + } else { + super.updateLeafletObject(property, value); + } + } + } +} + +customElements.define('leaflet-circle', LeafletCircle); diff --git a/src/components/leaflet-control-attribution.ts b/src/components/leaflet-control-attribution.ts new file mode 100644 index 0000000..dd102da --- /dev/null +++ b/src/components/leaflet-control-attribution.ts @@ -0,0 +1,19 @@ +import { Control, ControlPosition } from 'leaflet'; +import { LeafletControl } from '../core/LeafletControl.js'; + +export class LeafletControlAttribution extends LeafletControl { + static get observedAttributes() { + return ['position', 'prefix']; + } + + protected createControl(): Control { + const options: Control.AttributionOptions = {}; + const position = this.getAttribute('position') as ControlPosition | null; + const prefix = this.getAttribute('prefix'); + if (position) options.position = position; + if (prefix !== null) options.prefix = prefix; + return new Control.Attribution(options); + } +} + +customElements.define('leaflet-control-attribution', LeafletControlAttribution); diff --git a/src/components/leaflet-control-scale.ts b/src/components/leaflet-control-scale.ts new file mode 100644 index 0000000..0190d20 --- /dev/null +++ b/src/components/leaflet-control-scale.ts @@ -0,0 +1,25 @@ +import { Control, ControlPosition } from 'leaflet'; +import { LeafletControl } from '../core/LeafletControl.js'; + +export class LeafletControlScale extends LeafletControl { + static get observedAttributes() { + return ['position', 'max-width', 'metric', 'imperial', 'update-when-idle']; + } + + protected createControl(): Control { + const options: Control.ScaleOptions = {}; + const position = this.getAttribute('position') as ControlPosition | null; + const maxWidth = this.getAttribute('max-width'); + const metric = this.getAttribute('metric'); + const imperial = this.getAttribute('imperial'); + const updateWhenIdle = this.getAttribute('update-when-idle'); + if (position) options.position = position; + if (maxWidth !== null) options.maxWidth = parseInt(maxWidth, 10); + if (metric !== null) options.metric = metric !== 'false'; + if (imperial !== null) options.imperial = imperial !== 'false'; + if (updateWhenIdle !== null) options.updateWhenIdle = updateWhenIdle !== 'false'; + return new Control.Scale(options); + } +} + +customElements.define('leaflet-control-scale', LeafletControlScale); diff --git a/src/components/leaflet-control-zoom.ts b/src/components/leaflet-control-zoom.ts new file mode 100644 index 0000000..adf3b09 --- /dev/null +++ b/src/components/leaflet-control-zoom.ts @@ -0,0 +1,25 @@ +import { Control, ControlPosition } from 'leaflet'; +import { LeafletControl } from '../core/LeafletControl.js'; + +export class LeafletControlZoom extends LeafletControl { + static get observedAttributes() { + return ['position', 'zoom-in-text', 'zoom-in-title', 'zoom-out-text', 'zoom-out-title']; + } + + protected createControl(): Control { + const options: Control.ZoomOptions = {}; + const position = this.getAttribute('position') as ControlPosition | null; + const zoomInText = this.getAttribute('zoom-in-text'); + const zoomInTitle = this.getAttribute('zoom-in-title'); + const zoomOutText = this.getAttribute('zoom-out-text'); + const zoomOutTitle = this.getAttribute('zoom-out-title'); + if (position) options.position = position; + if (zoomInText !== null) options.zoomInText = zoomInText; + if (zoomInTitle !== null) options.zoomInTitle = zoomInTitle; + if (zoomOutText !== null) options.zoomOutText = zoomOutText; + if (zoomOutTitle !== null) options.zoomOutTitle = zoomOutTitle; + return new Control.Zoom(options); + } +} + +customElements.define('leaflet-control-zoom', LeafletControlZoom); diff --git a/src/components/leaflet-feature-group.ts b/src/components/leaflet-feature-group.ts new file mode 100644 index 0000000..21d2009 --- /dev/null +++ b/src/components/leaflet-feature-group.ts @@ -0,0 +1,10 @@ +import { FeatureGroup, Layer } from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletFeatureGroup extends LeafletElement { + protected createLeafletObject(): Layer { + return new FeatureGroup([], this.options); + } +} + +customElements.define('leaflet-feature-group', LeafletFeatureGroup); diff --git a/src/components/leaflet-geojson.ts b/src/components/leaflet-geojson.ts new file mode 100644 index 0000000..a25cbc0 --- /dev/null +++ b/src/components/leaflet-geojson.ts @@ -0,0 +1,45 @@ +import { GeoJSON, PathOptions, Layer } from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletGeoJSON extends LeafletElement { + static get observedAttributes() { + return [ + 'data', + 'stroke', + 'color', + 'weight', + 'opacity', + 'line-cap', + 'line-join', + 'dash-array', + 'dash-offset', + 'fill', + 'fill-color', + 'fill-opacity', + 'fill-rule', + ]; + } + + protected createLeafletObject(): Layer { + const raw = this.getAttribute('data'); + const data = raw ? JSON.parse(raw) : undefined; + const styleOpts = Object.fromEntries( + Object.entries(this.options).filter(([k]) => k !== 'data'), + ); + return new GeoJSON(data, { style: styleOpts as PathOptions }); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (!(this.leafletObject instanceof GeoJSON)) return; + if (property === 'data') { + this.leafletObject.clearLayers(); + if (value) { + this.leafletObject.addData(value as Parameters[0]); + } + } else { + this.leafletObject.setStyle({ [property]: value } as PathOptions); + } + } +} + +customElements.define('leaflet-geojson', LeafletGeoJSON); diff --git a/src/components/leaflet-image-overlay.ts b/src/components/leaflet-image-overlay.ts new file mode 100644 index 0000000..204102a --- /dev/null +++ b/src/components/leaflet-image-overlay.ts @@ -0,0 +1,44 @@ +import { + ImageOverlay, + LatLngBounds, + LatLngBoundsExpression, + LatLngExpression, + Layer, +} from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletImageOverlay extends LeafletElement { + static get observedAttributes() { + return [ + 'url', + 'bounds', + 'opacity', + 'alt', + 'interactive', + 'cross-origin', + 'error-overlay-url', + 'z-index', + 'className', + ]; + } + + protected createLeafletObject(): Layer { + const url = this.getAttribute('url') || ''; + const bounds = this.options.bounds as LatLngBoundsExpression; + return new ImageOverlay(url, bounds, this.options); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (this.leafletObject instanceof ImageOverlay) { + if (property === 'url') { + this.leafletObject.setUrl(value as string); + } else if (property === 'bounds') { + this.leafletObject.setBounds(new LatLngBounds(value as LatLngExpression[])); + } else { + super.updateLeafletObject(property, value); + } + } + } +} + +customElements.define('leaflet-image-overlay', LeafletImageOverlay); diff --git a/src/components/leaflet-layer-group.ts b/src/components/leaflet-layer-group.ts new file mode 100644 index 0000000..5e80ddf --- /dev/null +++ b/src/components/leaflet-layer-group.ts @@ -0,0 +1,10 @@ +import { LayerGroup, Layer } from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletLayerGroup extends LeafletElement { + protected createLeafletObject(): Layer { + return new LayerGroup([], this.options); + } +} + +customElements.define('leaflet-layer-group', LeafletLayerGroup); diff --git a/src/components/leaflet-line.ts b/src/components/leaflet-line.ts new file mode 100644 index 0000000..4df61ab --- /dev/null +++ b/src/components/leaflet-line.ts @@ -0,0 +1,18 @@ +export class LeafletLine extends HTMLElement { + static get observedAttributes() { + return ['lat', 'lng']; + } + + get latlng(): [number, number] { + return [ + parseFloat(this.getAttribute('lat') || '0'), + parseFloat(this.getAttribute('lng') || '0'), + ]; + } + + attributeChangedCallback() { + this.dispatchEvent(new CustomEvent('line-updated', { bubbles: true })); + } +} + +customElements.define('leaflet-line', LeafletLine); diff --git a/src/components/leaflet-map.ts b/src/components/leaflet-map.ts new file mode 100644 index 0000000..81e5231 --- /dev/null +++ b/src/components/leaflet-map.ts @@ -0,0 +1,329 @@ +import { Map as LMap, MapOptions } from 'leaflet'; +import leafletCSS from 'leaflet/dist/leaflet.css'; +import { LeafletRegisterEvent } from '../core/LeafletElement.js'; + +// ── Prop types ───────────────────────────────────────────────────────────── + +type NumProp = { + kind: 'num'; + attr: string; + default: number; + viewState?: true; + mapGet?: (m: LMap) => number | undefined; + mapSet?: (m: LMap, v: number) => void; + event?: string; +}; + +type BoolOffProp = { + kind: 'bool-off'; + attr: string; + mapSet?: (m: LMap, enabled: boolean) => void; +}; + +type BoolOnProp = { + kind: 'bool-on'; + attr: string; +}; + +type PropDef = NumProp | BoolOffProp | BoolOnProp; + +// ── Property table ───────────────────────────────────────────────────────── + +const PROPS = { + // View state — excluded from #buildOptions, initialised via setView() + lat: { + kind: 'num', + attr: 'lat', + default: 0, + viewState: true, + event: 'moveend', + mapGet: (m: LMap) => m.getCenter()?.lat, + mapSet: (m: LMap, v: number) => m.setView([v, m.getCenter()?.lng ?? 0], m.getZoom()), + }, + lng: { + kind: 'num', + attr: 'lng', + default: 0, + viewState: true, + event: 'moveend', + mapGet: (m: LMap) => m.getCenter()?.lng, + mapSet: (m: LMap, v: number) => m.setView([m.getCenter()?.lat ?? 0, v], m.getZoom()), + }, + zoom: { + kind: 'num', + attr: 'zoom', + default: 2, + viewState: true, + event: 'zoomend', + mapGet: (m: LMap) => m.getZoom(), + mapSet: (m: LMap, v: number) => m.setZoom(v), + }, + + // Live numeric options — have Leaflet setters + minZoom: { + kind: 'num', + attr: 'min-zoom', + default: 0, + mapGet: (m: LMap) => m.getMinZoom(), + mapSet: (m: LMap, v: number) => m.setMinZoom(v), + }, + maxZoom: { + kind: 'num', + attr: 'max-zoom', + default: Infinity, + mapGet: (m: LMap) => m.getMaxZoom(), + mapSet: (m: LMap, v: number) => m.setMaxZoom(v), + }, + + // Constructor-only numeric options + zoomSnap: { kind: 'num', attr: 'zoom-snap', default: 1 }, + zoomDelta: { kind: 'num', attr: 'zoom-delta', default: 1 }, + keyboardPanDelta: { kind: 'num', attr: 'keyboard-pan-delta', default: 80 }, + wheelDebounceTime: { kind: 'num', attr: 'wheel-debounce-time', default: 40 }, + wheelPxPerZoomLevel: { kind: 'num', attr: 'wheel-px-per-zoom-level', default: 60 }, + inertiaDeceleration: { kind: 'num', attr: 'inertia-deceleration', default: 3000 }, + inertiaMaxSpeed: { kind: 'num', attr: 'inertia-max-speed', default: Infinity }, + easeLinearity: { kind: 'num', attr: 'ease-linearity', default: 0.2 }, + maxBoundsViscosity: { kind: 'num', attr: 'max-bounds-viscosity', default: 0 }, + tapTolerance: { kind: 'num', attr: 'tap-tolerance', default: 15 }, + zoomAnimationThreshold: { kind: 'num', attr: 'zoom-animation-threshold', default: 4 }, + transform3DLimit: { kind: 'num', attr: 'transform-3d-limit', default: 8388608 }, + + // Boolean defaults-true → disable-* attribute; handler-based options have live mapSet + scrollWheelZoom: { + kind: 'bool-off', + attr: 'disable-scroll-wheel-zoom', + mapSet: (m: LMap, v: boolean) => (v ? m.scrollWheelZoom.enable() : m.scrollWheelZoom.disable()), + }, + dragging: { + kind: 'bool-off', + attr: 'disable-dragging', + mapSet: (m: LMap, v: boolean) => (v ? m.dragging.enable() : m.dragging.disable()), + }, + touchZoom: { + kind: 'bool-off', + attr: 'disable-touch-zoom', + mapSet: (m: LMap, v: boolean) => (v ? m.touchZoom.enable() : m.touchZoom.disable()), + }, + doubleClickZoom: { + kind: 'bool-off', + attr: 'disable-double-click-zoom', + mapSet: (m: LMap, v: boolean) => (v ? m.doubleClickZoom.enable() : m.doubleClickZoom.disable()), + }, + boxZoom: { + kind: 'bool-off', + attr: 'disable-box-zoom', + mapSet: (m: LMap, v: boolean) => (v ? m.boxZoom.enable() : m.boxZoom.disable()), + }, + keyboard: { + kind: 'bool-off', + attr: 'disable-keyboard', + mapSet: (m: LMap, v: boolean) => (v ? m.keyboard.enable() : m.keyboard.disable()), + }, + closePopupOnClick: { kind: 'bool-off', attr: 'disable-close-popup-on-click' }, + trackResize: { kind: 'bool-off', attr: 'disable-track-resize' }, + zoomControl: { kind: 'bool-off', attr: 'disable-zoom-control' }, + attributionControl: { kind: 'bool-off', attr: 'disable-attribution-control' }, + inertia: { kind: 'bool-off', attr: 'disable-inertia' }, + zoomAnimation: { kind: 'bool-off', attr: 'disable-zoom-animation' }, + fadeAnimation: { kind: 'bool-off', attr: 'disable-fade-animation' }, + markerZoomAnimation: { kind: 'bool-off', attr: 'disable-marker-zoom-animation' }, + bounceAtZoomLimits: { kind: 'bool-off', attr: 'disable-bounce-at-zoom-limits' }, + tapHold: { kind: 'bool-off', attr: 'disable-tap-hold' }, + + // Boolean defaults-false → normal attribute + preferCanvas: { kind: 'bool-on', attr: 'prefer-canvas' }, + worldCopyJump: { kind: 'bool-on', attr: 'world-copy-jump' }, +} satisfies Record; + +type PropName = keyof typeof PROPS; + +const ATTR_TO_PROP = new globalThis.Map( + (Object.entries(PROPS) as [PropName, PropDef][]).map(([name, spec]) => [spec.attr, name]), +); + +// ── Element ──────────────────────────────────────────────────────────────── + +// Derive property types directly from the PROPS table so adding a prop to the +// table automatically makes it part of the LeafletMap instance type. +type PropTypes = { + [K in keyof typeof PROPS]: (typeof PROPS)[K] extends { kind: 'num' } ? number : boolean; +}; + +// Cast HTMLElement to a typed base whose instances include PropTypes. This is +// the mixin pattern — TypeScript sees LeafletMap as having all prop types, the +// runtime still extends HTMLElement, and no interface merge is required. +const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes; + +export class LeafletMap extends TypedBase { + #map?: LMap; + #container!: HTMLDivElement; + #syncing = false; + #mapEventHandlers = new globalThis.Map void>(); + + static get observedAttributes(): string[] { + return Object.values(PROPS).map((s) => s.attr); + } + + // Generates a getter/setter on the prototype for every entry in PROPS. + // The functions are defined inside the class body so they can access private fields. + static { + for (const [propName, spec] of Object.entries(PROPS) as [PropName, PropDef][]) { + if (spec.kind === 'num') { + Object.defineProperty(LeafletMap.prototype, propName, { + get(this: LeafletMap) { + const val = spec.mapGet && this.#map ? spec.mapGet(this.#map) : undefined; + return val !== undefined ? val : Number(this.getAttribute(spec.attr) ?? spec.default); + }, + set(this: LeafletMap, v: number) { + if (spec.event) { + this.#syncAttr(spec.attr, String(v)); + } else { + this.setAttribute(spec.attr, String(v)); + } + }, + configurable: true, + enumerable: true, + }); + } else if (spec.kind === 'bool-off') { + Object.defineProperty(LeafletMap.prototype, propName, { + get(this: LeafletMap) { + return !this.hasAttribute(spec.attr); + }, + set(this: LeafletMap, v: boolean) { + this.toggleAttribute(spec.attr, !v); + }, + configurable: true, + enumerable: true, + }); + } else { + Object.defineProperty(LeafletMap.prototype, propName, { + get(this: LeafletMap) { + return this.hasAttribute(spec.attr); + }, + set(this: LeafletMap, v: boolean) { + this.toggleAttribute(spec.attr, v); + }, + configurable: true, + enumerable: true, + }); + } + } + } + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.#container = document.createElement('div'); + this.#container.style.width = '100%'; + this.#container.style.height = '100%'; + this.shadowRoot!.appendChild(this.#container); + const style = document.createElement('style'); + style.textContent = `:host { display: block; width: 100%; height: 400px; }\n${leafletCSS}`; + this.shadowRoot!.appendChild(style); + } + + connectedCallback() { + // Read view-state from attributes directly — the getters call getCenter()/getZoom() + // which throw if invoked before setView(), so we can't use them here. + const lat = Number(this.getAttribute('lat') ?? 0); + const lng = Number(this.getAttribute('lng') ?? 0); + const zoom = Number(this.getAttribute('zoom') ?? 2); + + // Assign #map only after setView so the getters' `this.#map` guard is + // equivalent to "map is ready" — getCenter()/getZoom() throw before setView. + const map = new LMap(this.#container, this.#buildOptions()); + map.setView([lat, lng], zoom); + this.#map = map; + + // Register one handler per unique map event, updating all props that share it + const groups = new globalThis.Map(); + for (const spec of Object.values(PROPS) as PropDef[]) { + if (spec.kind === 'num' && spec.event && spec.mapGet) { + const g = groups.get(spec.event) ?? []; + g.push(spec); + groups.set(spec.event, g); + } + } + for (const [event, specs] of groups) { + const handler = () => { + for (const s of specs) { + if (this.#map && s.mapGet) { + const v = s.mapGet(this.#map); + if (v !== undefined) this.#syncAttr(s.attr, String(v)); + } + } + }; + this.#mapEventHandlers.set(event, handler); + this.#map.on(event, handler); + } + + this.addEventListener( + 'leaflet-register', + this.#handleLeafletRegister as EventListenerOrEventListenerObject, + ); + } + + disconnectedCallback() { + this.removeEventListener( + 'leaflet-register', + this.#handleLeafletRegister as EventListenerOrEventListenerObject, + ); + if (!this.#map) return; + for (const [event, handler] of this.#mapEventHandlers) { + this.#map.off(event, handler); + } + this.#mapEventHandlers.clear(); + this.#map.remove(); + this.#map = undefined; + } + + attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) { + if (oldValue === newValue || !this.#map || this.#syncing) return; + const propName = ATTR_TO_PROP.get(name); + if (!propName) return; + const spec = PROPS[propName] as PropDef; + if (spec.kind === 'num') { + if (spec.mapSet && newValue !== null) spec.mapSet(this.#map, Number(newValue)); + } else if (spec.kind === 'bool-off') { + spec.mapSet?.(this.#map, newValue === null); + } + // bool-on: constructor-only, no live update + } + + #syncAttr(name: string, value: string) { + if (this.#syncing || this.getAttribute(name) === value) return; + this.#syncing = true; + this.setAttribute(name, value); + this.#syncing = false; + } + + #buildOptions(): MapOptions { + const o: Record = {}; + for (const [propName, spec] of Object.entries(PROPS) as [PropName, PropDef][]) { + if (spec.kind === 'num') { + if (spec.viewState) continue; + const v = this.getAttribute(spec.attr); + if (v !== null) o[propName] = Number(v); + } else if (spec.kind === 'bool-off') { + if (this.hasAttribute(spec.attr)) o[propName] = false; + } else { + if (this.hasAttribute(spec.attr)) o[propName] = true; + } + } + return o as MapOptions; + } + + #handleLeafletRegister = (e: LeafletRegisterEvent) => { + e.stopPropagation(); + if (this.#map && e.detail.leafletObject) { + e.detail.leafletObject.addTo(this.#map); + } + }; + + get map(): LMap | undefined { + return this.#map; + } +} + +customElements.define('leaflet-map', LeafletMap); diff --git a/src/components/leaflet-marker.ts b/src/components/leaflet-marker.ts new file mode 100644 index 0000000..46cda03 --- /dev/null +++ b/src/components/leaflet-marker.ts @@ -0,0 +1,28 @@ +import L from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletMarker extends LeafletElement { + static get observedAttributes() { + return ['lat', 'lng', 'title', 'alt', 'draggable', 'opacity', 'z-index-offset']; + } + + protected createLeafletObject(): L.Layer { + const lat = parseFloat(this.getAttribute('lat') || '0'); + const lng = parseFloat(this.getAttribute('lng') || '0'); + return new L.Marker([lat, lng], this.options); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (this.leafletObject instanceof L.Marker) { + if (property === 'lat' || property === 'lng') { + const lat = parseFloat(this.getAttribute('lat') || '0'); + const lng = parseFloat(this.getAttribute('lng') || '0'); + this.leafletObject.setLatLng([lat, lng]); + } else { + super.updateLeafletObject(property, value); + } + } + } +} + +customElements.define('leaflet-marker', LeafletMarker); diff --git a/src/components/leaflet-polygon.ts b/src/components/leaflet-polygon.ts new file mode 100644 index 0000000..1e7c819 --- /dev/null +++ b/src/components/leaflet-polygon.ts @@ -0,0 +1,68 @@ +import L from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; +import { LeafletLine } from './leaflet-line.js'; + +export class LeafletPolygon extends LeafletElement { + private _observer?: MutationObserver; + static get observedAttributes() { + return ['color', 'weight', 'opacity', 'fill', 'fill-color', 'fill-opacity']; + } + + protected createLeafletObject(): L.Layer { + const coords = this.getCoords(); + return new L.Polygon(coords, this.options); + } + + private getCoords(): [number, number][] { + const lines = Array.from(this.querySelectorAll('leaflet-line')) as LeafletLine[]; + return lines.map((line) => line.latlng); + } + + connectedCallback() { + super.connectedCallback(); + this.addEventListener('line-updated', () => { + if (this.leafletObject instanceof L.Polygon) { + this.leafletObject.setLatLngs(this.getCoords()); + } + }); + + this._observer = new MutationObserver(() => { + if (this.leafletObject instanceof L.Polygon) { + this.leafletObject.setLatLngs(this.getCoords()); + } + }); + this._observer.observe(this, { childList: true }); + } + + disconnectedCallback() { + this._observer?.disconnect(); + this._observer = undefined; + super.disconnectedCallback(); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (this.leafletObject instanceof L.Polygon) { + if ( + [ + 'color', + 'weight', + 'opacity', + 'fill', + 'fillColor', + 'fillOpacity', + 'stroke', + 'dashArray', + 'dashOffset', + 'lineCap', + 'lineJoin', + ].includes(property) + ) { + this.leafletObject.setStyle({ [property]: value }); + } else { + super.updateLeafletObject(property, value); + } + } + } +} + +customElements.define('leaflet-polygon', LeafletPolygon); diff --git a/src/components/leaflet-polyline.ts b/src/components/leaflet-polyline.ts new file mode 100644 index 0000000..c26ccc2 --- /dev/null +++ b/src/components/leaflet-polyline.ts @@ -0,0 +1,68 @@ +import L from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; +import { LeafletLine } from './leaflet-line.js'; + +export class LeafletPolyline extends LeafletElement { + private _observer?: MutationObserver; + static get observedAttributes() { + return ['color', 'weight', 'opacity', 'fill', 'fill-color', 'fill-opacity']; + } + + protected createLeafletObject(): L.Layer { + const coords = this.getCoords(); + return new L.Polyline(coords, this.options); + } + + private getCoords(): [number, number][] { + const lines = Array.from(this.querySelectorAll('leaflet-line')) as LeafletLine[]; + return lines.map((line) => line.latlng); + } + + connectedCallback() { + super.connectedCallback(); + this.addEventListener('line-updated', () => { + if (this.leafletObject instanceof L.Polyline) { + this.leafletObject.setLatLngs(this.getCoords()); + } + }); + + this._observer = new MutationObserver(() => { + if (this.leafletObject instanceof L.Polyline) { + this.leafletObject.setLatLngs(this.getCoords()); + } + }); + this._observer.observe(this, { childList: true }); + } + + disconnectedCallback() { + this._observer?.disconnect(); + this._observer = undefined; + super.disconnectedCallback(); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (this.leafletObject instanceof L.Polyline) { + if ( + [ + 'color', + 'weight', + 'opacity', + 'fill', + 'fillColor', + 'fillOpacity', + 'stroke', + 'dashArray', + 'dashOffset', + 'lineCap', + 'lineJoin', + ].includes(property) + ) { + this.leafletObject.setStyle({ [property]: value }); + } else { + super.updateLeafletObject(property, value); + } + } + } +} + +customElements.define('leaflet-polyline', LeafletPolyline); diff --git a/src/components/leaflet-popup.ts b/src/components/leaflet-popup.ts new file mode 100644 index 0000000..1625f18 --- /dev/null +++ b/src/components/leaflet-popup.ts @@ -0,0 +1,59 @@ +import L from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletPopup extends LeafletElement { + private _observer?: MutationObserver; + static get observedAttributes() { + return [ + 'lat', + 'lng', + 'max-width', + 'min-width', + 'max-height', + 'auto-pan', + 'close-button', + 'auto-close', + ]; + } + + protected createLeafletObject(): L.Layer { + const lat = this.getAttribute('lat'); + const lng = this.getAttribute('lng'); + const options = { ...this.options, content: this.innerHTML }; + const popup = new L.Popup(options); + if (lat && lng) { + popup.setLatLng([parseFloat(lat), parseFloat(lng)]); + } + return popup; + } + + connectedCallback() { + super.connectedCallback(); + this._observer = new MutationObserver(() => { + if (this.leafletObject instanceof L.Popup) { + this.leafletObject.setContent(this.innerHTML); + } + }); + this._observer.observe(this, { childList: true, characterData: true, subtree: true }); + } + + disconnectedCallback() { + this._observer?.disconnect(); + this._observer = undefined; + super.disconnectedCallback(); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (this.leafletObject instanceof L.Popup) { + if (property === 'lat' || property === 'lng') { + const lat = parseFloat(this.getAttribute('lat') || '0'); + const lng = parseFloat(this.getAttribute('lng') || '0'); + this.leafletObject.setLatLng([lat, lng]); + } else { + super.updateLeafletObject(property, value); + } + } + } +} + +customElements.define('leaflet-popup', LeafletPopup); diff --git a/src/components/leaflet-rectangle.ts b/src/components/leaflet-rectangle.ts new file mode 100644 index 0000000..1daa39d --- /dev/null +++ b/src/components/leaflet-rectangle.ts @@ -0,0 +1,41 @@ +import L from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletRectangle extends LeafletElement { + static get observedAttributes() { + return ['bounds', 'color', 'weight', 'opacity', 'fill', 'fill-color', 'fill-opacity']; + } + + protected createLeafletObject(): L.Layer { + const bounds = this.options.bounds as L.LatLngBoundsExpression; + return new L.Rectangle(bounds, this.options); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (this.leafletObject instanceof L.Rectangle) { + if (property === 'bounds') { + this.leafletObject.setBounds(value as L.LatLngBoundsExpression); + } else if ( + [ + 'color', + 'weight', + 'opacity', + 'fill', + 'fillColor', + 'fillOpacity', + 'stroke', + 'dashArray', + 'dashOffset', + 'lineCap', + 'lineJoin', + ].includes(property) + ) { + this.leafletObject.setStyle({ [property]: value }); + } else { + super.updateLeafletObject(property, value); + } + } + } +} + +customElements.define('leaflet-rectangle', LeafletRectangle); diff --git a/src/components/leaflet-svg-overlay.ts b/src/components/leaflet-svg-overlay.ts new file mode 100644 index 0000000..b8fafda --- /dev/null +++ b/src/components/leaflet-svg-overlay.ts @@ -0,0 +1,31 @@ +import L from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletSVGOverlay extends LeafletElement { + static get observedAttributes() { + return ['bounds', 'opacity', 'alt', 'interactive', 'cross-origin', 'z-index', 'className']; + } + + protected createLeafletObject(): L.Layer { + const svg = this.querySelector('svg'); + const bounds = this.options.bounds as L.LatLngBoundsExpression; + if (!svg) { + // Create a dummy SVG if none provided + const dummy = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + return new L.SVGOverlay(dummy, bounds, this.options); + } + return new L.SVGOverlay(svg, bounds, this.options); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (this.leafletObject instanceof L.SVGOverlay) { + if (property === 'bounds') { + this.leafletObject.setBounds(new L.LatLngBounds(value as L.LatLngExpression[])); + } else { + super.updateLeafletObject(property, value); + } + } + } +} + +customElements.define('leaflet-svg-overlay', LeafletSVGOverlay); diff --git a/src/components/leaflet-tile-layer-wms.ts b/src/components/leaflet-tile-layer-wms.ts new file mode 100644 index 0000000..fca2982 --- /dev/null +++ b/src/components/leaflet-tile-layer-wms.ts @@ -0,0 +1,25 @@ +import L from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletTileLayerWMS extends LeafletElement { + static get observedAttributes() { + return ['url', 'layers', 'styles', 'format', 'transparent', 'version', 'crs', 'uppercase']; + } + + protected createLeafletObject(): L.Layer { + const url = this.getAttribute('url') || ''; + return new L.TileLayer.WMS(url, this.options as L.WMSOptions); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (this.leafletObject instanceof L.TileLayer.WMS) { + if (property === 'url') { + this.leafletObject.setUrl(value as string); + } else { + super.updateLeafletObject(property, value); + } + } + } +} + +customElements.define('leaflet-tile-layer-wms', LeafletTileLayerWMS); diff --git a/src/components/leaflet-tile-layer.ts b/src/components/leaflet-tile-layer.ts new file mode 100644 index 0000000..4cfc2e7 --- /dev/null +++ b/src/components/leaflet-tile-layer.ts @@ -0,0 +1,25 @@ +import L from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletTileLayer extends LeafletElement { + static get observedAttributes() { + return ['url', 'attribution', 'min-zoom', 'max-zoom', 'opacity', 'z-index']; + } + + protected createLeafletObject(): L.Layer { + const url = this.getAttribute('url') || ''; + return new L.TileLayer(url, this.options); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (this.leafletObject instanceof L.TileLayer) { + if (property === 'url') { + this.leafletObject.setUrl(value as string); + } else { + super.updateLeafletObject(property, value); + } + } + } +} + +customElements.define('leaflet-tile-layer', LeafletTileLayer); diff --git a/src/components/leaflet-tooltip.ts b/src/components/leaflet-tooltip.ts new file mode 100644 index 0000000..1f34be2 --- /dev/null +++ b/src/components/leaflet-tooltip.ts @@ -0,0 +1,50 @@ +import L from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletTooltip extends LeafletElement { + private _observer?: MutationObserver; + static get observedAttributes() { + return ['lat', 'lng', 'pane', 'offset', 'direction', 'permanent', 'sticky', 'opacity']; + } + + protected createLeafletObject(): L.Layer { + const lat = this.getAttribute('lat'); + const lng = this.getAttribute('lng'); + const options = { ...this.options, content: this.innerHTML }; + const tooltip = new L.Tooltip(options); + if (lat && lng) { + tooltip.setLatLng([parseFloat(lat), parseFloat(lng)]); + } + return tooltip; + } + + connectedCallback() { + super.connectedCallback(); + this._observer = new MutationObserver(() => { + if (this.leafletObject instanceof L.Tooltip) { + this.leafletObject.setContent(this.innerHTML); + } + }); + this._observer.observe(this, { childList: true, characterData: true, subtree: true }); + } + + disconnectedCallback() { + this._observer?.disconnect(); + this._observer = undefined; + super.disconnectedCallback(); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (this.leafletObject instanceof L.Tooltip) { + if (property === 'lat' || property === 'lng') { + const lat = parseFloat(this.getAttribute('lat') || '0'); + const lng = parseFloat(this.getAttribute('lng') || '0'); + this.leafletObject.setLatLng([lat, lng]); + } else { + super.updateLeafletObject(property, value); + } + } + } +} + +customElements.define('leaflet-tooltip', LeafletTooltip); diff --git a/src/components/leaflet-video-overlay.ts b/src/components/leaflet-video-overlay.ts new file mode 100644 index 0000000..1132642 --- /dev/null +++ b/src/components/leaflet-video-overlay.ts @@ -0,0 +1,45 @@ +import L from 'leaflet'; +import { LeafletElement } from '../core/LeafletElement.js'; + +export class LeafletVideoOverlay extends LeafletElement { + static get observedAttributes() { + return [ + 'url', + 'bounds', + 'opacity', + 'alt', + 'interactive', + 'cross-origin', + 'loop', + 'autoplay', + 'muted', + 'playsinline', + ]; + } + + protected createLeafletObject(): L.Layer { + const url = this.getAttribute('url') || ''; + const bounds = this.options.bounds as L.LatLngBoundsExpression; + return new L.VideoOverlay(url, bounds, this.options); + } + + protected updateLeafletObject(property: string, value: unknown) { + if (this.leafletObject instanceof L.VideoOverlay) { + if (property === 'url') { + this.leafletObject.setUrl(value as string); + } else if (property === 'bounds') { + this.leafletObject.setBounds(new L.LatLngBounds(value as L.LatLngExpression[])); + } else { + super.updateLeafletObject(property, value); + } + } + } + + getElement(): HTMLVideoElement | undefined { + return this.leafletObject instanceof L.VideoOverlay + ? this.leafletObject.getElement() + : undefined; + } +} + +customElements.define('leaflet-video-overlay', LeafletVideoOverlay); diff --git a/src/core/LeafletControl.ts b/src/core/LeafletControl.ts new file mode 100644 index 0000000..1e65e75 --- /dev/null +++ b/src/core/LeafletControl.ts @@ -0,0 +1,36 @@ +import { Control } from 'leaflet'; + +export abstract class LeafletControl extends HTMLElement { + protected control?: Control; + + static get observedAttributes(): string[] { + return []; + } + + connectedCallback() { + this.control = this.createControl(); + this.register(); + } + + disconnectedCallback() { + this.control?.remove(); + this.control = undefined; + } + + protected abstract createControl(): Control; + + protected register() { + if (this.control) { + this.dispatchEvent( + new CustomEvent('leaflet-register', { + detail: { + leafletObject: this.control, + element: this, + }, + bubbles: true, + composed: true, + }), + ); + } + } +} diff --git a/src/core/LeafletElement.ts b/src/core/LeafletElement.ts new file mode 100644 index 0000000..6eb51d1 --- /dev/null +++ b/src/core/LeafletElement.ts @@ -0,0 +1,141 @@ +import { Layer, Control, Popup, Tooltip, LayerGroup } from 'leaflet'; + +export interface LeafletRegisterEvent extends CustomEvent { + detail: { + leafletObject: Layer | Control; + element: HTMLElement; + }; +} + +export abstract class LeafletElement extends HTMLElement { + protected leafletObject?: Layer; + protected options: Record = {}; + protected _parentLayer?: Layer; + protected _parentBindingType?: 'popup' | 'tooltip' | 'layer'; + private _registerHandler?: EventListener; + + static get observedAttributes(): string[] { + return []; + } + + connectedCallback() { + this.initOptions(); + this.leafletObject = this.createLeafletObject(); + + this._registerHandler = ((e: LeafletRegisterEvent) => { + if (this.leafletObject) { + const childObj = e.detail.leafletObject; + const childEl = e.detail.element; + if (childObj instanceof Popup) { + e.stopPropagation(); + this.leafletObject.bindPopup(childObj); + if (childEl instanceof LeafletElement) { + childEl._parentLayer = this.leafletObject; + childEl._parentBindingType = 'popup'; + } + } else if (childObj instanceof Tooltip) { + e.stopPropagation(); + this.leafletObject.bindTooltip(childObj); + if (childEl instanceof LeafletElement) { + childEl._parentLayer = this.leafletObject; + childEl._parentBindingType = 'tooltip'; + } + } else if (childObj instanceof Layer && 'addLayer' in this.leafletObject) { + e.stopPropagation(); + (this.leafletObject as LayerGroup).addLayer(childObj); + if (childEl instanceof LeafletElement) { + childEl._parentLayer = this.leafletObject; + childEl._parentBindingType = 'layer'; + } + } + } + }) as EventListener; + this.addEventListener('leaflet-register', this._registerHandler); + + this.register(); + } + + disconnectedCallback() { + if (this._registerHandler) { + this.removeEventListener('leaflet-register', this._registerHandler); + this._registerHandler = undefined; + } + if (!this.leafletObject) return; + if (this._parentBindingType === 'popup' && this._parentLayer) { + this._parentLayer.unbindPopup(); + } else if (this._parentBindingType === 'tooltip' && this._parentLayer) { + this._parentLayer.unbindTooltip(); + } else if ( + this._parentBindingType === 'layer' && + this._parentLayer && + 'removeLayer' in this._parentLayer + ) { + (this._parentLayer as LayerGroup).removeLayer(this.leafletObject); + } else { + this.leafletObject.remove(); + } + this._parentLayer = undefined; + this._parentBindingType = undefined; + this.leafletObject = undefined; + } + + protected initOptions() { + const observed = (this.constructor as typeof LeafletElement).observedAttributes; + observed.forEach((attr) => { + const val = this.getAttribute(attr); + if (val !== null) { + this.options[camelCase(attr)] = parseAttributeValue(val); + } + }); + } + + attributeChangedCallback(name: string, oldValue: string, newValue: string) { + if (oldValue === newValue) return; + const propertyName = camelCase(name); + const parsedValue = parseAttributeValue(newValue); + this.options[propertyName] = parsedValue; + + if (this.leafletObject) { + this.updateLeafletObject(propertyName, parsedValue); + } + } + + protected abstract createLeafletObject(): Layer; + + protected updateLeafletObject(property: string, value: unknown) { + const setter = `set${property.charAt(0).toUpperCase()}${property.slice(1)}`; + const obj = this.leafletObject as unknown as Record; + if (obj && typeof obj[setter] === 'function') { + (obj[setter] as (val: unknown) => void)(value); + } + } + + protected register() { + if (!this.leafletObject) return; + const event = new CustomEvent('leaflet-register', { + detail: { + leafletObject: this.leafletObject, + element: this, + }, + bubbles: true, + composed: true, + }); + this.dispatchEvent(event); + } +} + +function camelCase(str: string): string { + return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); +} + +function parseAttributeValue(value: string): unknown { + if (value === 'true') return true; + if (value === 'false') return false; + const num = Number(value); + if (!isNaN(num) && value !== '') return num; + try { + return JSON.parse(value); + } catch { + return value; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..2cb74cf --- /dev/null +++ b/src/index.ts @@ -0,0 +1,23 @@ +export * from './core/LeafletElement.js'; +export * from './core/LeafletControl.js'; +export * from './components/leaflet-map.js'; +export * from './components/leaflet-marker.js'; +export * from './components/leaflet-circle.js'; +export * from './components/leaflet-circle-marker.js'; +export * from './components/leaflet-line.js'; +export * from './components/leaflet-polygon.js'; +export * from './components/leaflet-polyline.js'; +export * from './components/leaflet-rectangle.js'; +export * from './components/leaflet-tile-layer.js'; +export * from './components/leaflet-tile-layer-wms.js'; +export * from './components/leaflet-image-overlay.js'; +export * from './components/leaflet-video-overlay.js'; +export * from './components/leaflet-svg-overlay.js'; +export * from './components/leaflet-layer-group.js'; +export * from './components/leaflet-feature-group.js'; +export * from './components/leaflet-geojson.js'; +export * from './components/leaflet-control-zoom.js'; +export * from './components/leaflet-control-attribution.js'; +export * from './components/leaflet-control-scale.js'; +export * from './components/leaflet-popup.js'; +export * from './components/leaflet-tooltip.js'; diff --git a/src/types/css.d.ts b/src/types/css.d.ts new file mode 100644 index 0000000..31f07ea --- /dev/null +++ b/src/types/css.d.ts @@ -0,0 +1,4 @@ +declare module '*.css' { + const content: string; + export default content; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0053a9d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + }, + "include": ["src/**/*"] +}