commit 6b448ba09297119920304a6d0dfc48a1da6863f4 Author: Buddy Date: Sat Jun 6 08:51:02 2026 -0700 feat: Initial implementation of leaflet-components 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. 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/**/*"] +}