@florasync/leaflet-geokit 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +684 -64
  3. package/dist/django/index.js +2871 -2253
  4. package/dist/django/index.js.map +1 -1
  5. package/dist/leaflet-geokit.es.js +2928 -2325
  6. package/dist/leaflet-geokit.es.js.map +1 -1
  7. package/dist/leaflet-geokit.external.es.js +2636 -0
  8. package/dist/leaflet-geokit.external.es.js.map +1 -0
  9. package/dist/leaflet-geokit.umd.js +69 -16
  10. package/dist/leaflet-geokit.umd.js.map +1 -1
  11. package/dist/preact/index-D8EjMf-1.js +2623 -0
  12. package/dist/preact/index-D8EjMf-1.js.map +1 -0
  13. package/dist/preact/index.js +109 -0
  14. package/dist/preact/index.js.map +1 -0
  15. package/dist/preact-bundled/index.js +9931 -0
  16. package/dist/preact-bundled/index.js.map +1 -0
  17. package/dist/react/index-D8EjMf-1.js +2623 -0
  18. package/dist/react/index-D8EjMf-1.js.map +1 -0
  19. package/dist/react/index.js +109 -0
  20. package/dist/react/index.js.map +1 -0
  21. package/dist/react-bundled/index.js +9931 -0
  22. package/dist/react-bundled/index.js.map +1 -0
  23. package/dist/types/e2e/component.spec.d.ts +1 -0
  24. package/dist/types/e2e/dummy.spec.d.ts +1 -0
  25. package/dist/types/src/components/LeafletDrawMapElement.d.ts +94 -0
  26. package/dist/types/src/django/index.d.ts +35 -0
  27. package/dist/types/src/external.d.ts +4 -0
  28. package/dist/types/src/index.d.ts +4 -0
  29. package/dist/types/src/lib/FeatureStore.d.ts +48 -0
  30. package/dist/types/src/lib/MapController.d.ts +121 -0
  31. package/dist/types/src/lib/draw/L.Draw.Cake.d.ts +9 -0
  32. package/dist/types/src/lib/draw/L.Draw.Move.d.ts +49 -0
  33. package/dist/types/src/lib/draw/toolbar-patch.d.ts +2 -0
  34. package/dist/types/src/lib/layer-cake/CakeBaker.d.ts +12 -0
  35. package/dist/types/src/lib/layer-cake/LayerCakeManager.d.ts +29 -0
  36. package/dist/types/src/lib/layer-cake/bindCakeControls.d.ts +8 -0
  37. package/dist/types/src/lib/layer-cake/ensureCircleEditable.d.ts +2 -0
  38. package/dist/types/src/lib/leaflet-assets.d.ts +23 -0
  39. package/dist/types/src/preact/core.d.ts +36 -0
  40. package/dist/types/src/preact/index.d.ts +8 -0
  41. package/dist/types/src/preact-bundled/index.d.ts +9 -0
  42. package/dist/types/src/react/core.d.ts +36 -0
  43. package/dist/types/src/react/index.d.ts +7 -0
  44. package/dist/types/src/react-bundled/index.d.ts +8 -0
  45. package/dist/types/src/shims/ensure-element.d.ts +2 -0
  46. package/dist/types/src/state/types.d.ts +7 -0
  47. package/dist/types/src/types/events.d.ts +71 -0
  48. package/dist/types/src/types/public.d.ts +108 -0
  49. package/dist/types/src/utils/geodesic.d.ts +8 -0
  50. package/dist/types/src/utils/geojson.d.ts +70 -0
  51. package/dist/types/src/utils/leaflet-guards.d.ts +9 -0
  52. package/dist/types/src/utils/logger.d.ts +12 -0
  53. package/dist/types/src/utils/ruler.d.ts +31 -0
  54. package/dist/types/tests/bbox-more.spec.d.ts +1 -0
  55. package/dist/types/tests/component-api-more.spec.d.ts +1 -0
  56. package/dist/types/tests/component-delegation.spec.d.ts +1 -0
  57. package/dist/types/tests/component-events.spec.d.ts +1 -0
  58. package/dist/types/tests/component-io.spec.d.ts +1 -0
  59. package/dist/types/tests/django-shim.spec.d.ts +1 -0
  60. package/dist/types/tests/draw-cake.spec.d.ts +1 -0
  61. package/dist/types/tests/draw-move.spec.d.ts +1 -0
  62. package/dist/types/tests/eachcoord.spec.d.ts +1 -0
  63. package/dist/types/tests/element.spec.d.ts +1 -0
  64. package/dist/types/tests/ensure-element.spec.d.ts +1 -0
  65. package/dist/types/tests/external-entry.spec.d.ts +1 -0
  66. package/dist/types/tests/featureStore-more.spec.d.ts +1 -0
  67. package/dist/types/tests/featureStore.spec.d.ts +1 -0
  68. package/dist/types/tests/framework-runtime-externalization.spec.d.ts +1 -0
  69. package/dist/types/tests/geodesic.spec.d.ts +1 -0
  70. package/dist/types/tests/geojson-merge.spec.d.ts +1 -0
  71. package/dist/types/tests/geojson-more.spec.d.ts +1 -0
  72. package/dist/types/tests/geojson.spec.d.ts +1 -0
  73. package/dist/types/tests/layer-cake-baker.spec.d.ts +1 -0
  74. package/dist/types/tests/layer-cake-controls.spec.d.ts +1 -0
  75. package/dist/types/tests/layer-cake-editing.spec.d.ts +1 -0
  76. package/dist/types/tests/layer-cake-manager.spec.d.ts +1 -0
  77. package/dist/types/tests/leaflet-assets.spec.d.ts +1 -0
  78. package/dist/types/tests/leaflet-draw-circle-resize-patch.spec.d.ts +1 -0
  79. package/dist/types/tests/leaflet-guards.spec.d.ts +1 -0
  80. package/dist/types/tests/logger-more.spec.d.ts +1 -0
  81. package/dist/types/tests/logger.spec.d.ts +1 -0
  82. package/dist/types/tests/map-controller.spec.d.ts +1 -0
  83. package/dist/types/tests/mapcontroller-merge.spec.d.ts +1 -0
  84. package/dist/types/tests/preact-bundled-shim.spec.d.ts +1 -0
  85. package/dist/types/tests/preact-shim.spec.d.ts +1 -0
  86. package/dist/types/tests/react-bundled-shim.spec.d.ts +1 -0
  87. package/dist/types/tests/react-shim.spec.d.ts +1 -0
  88. package/dist/types/tests/ruler.spec.d.ts +1 -0
  89. package/dist/types/vite.config.d.ts +2 -0
  90. package/dist/types/vite.config.external.d.ts +2 -0
  91. package/dist/types/vite.config.preact-bundled.d.ts +2 -0
  92. package/dist/types/vite.config.preact.d.ts +2 -0
  93. package/dist/types/vite.config.react-bundled.d.ts +2 -0
  94. package/dist/types/vite.config.react.d.ts +2 -0
  95. package/package.json +59 -5
package/README.md CHANGED
@@ -30,7 +30,7 @@ Documentation quick-links
30
30
  - Runtime and architecture overview
31
31
  - Public API (attributes, properties, methods, events)
32
32
  - Usage examples (HTML, ESM, framework integration, recipes)
33
- - Shims and integrations
33
+ - Framework Support
34
34
  - Logging, diagnostics, and troubleshooting
35
35
  - Performance, accessibility, and SSR notes
36
36
  - Roadmap and versioning
@@ -60,8 +60,18 @@ Documentation quick-links
60
60
  Inside this package directory:
61
61
 
62
62
  - npm install
63
- - npm run dev — starts Vite dev server
64
- - Open http://localhost:5173 in the browser (or whatever port VITE is using
63
+ - npm run dev — starts the multi-page dev harness (Vite)
64
+ - Open http://localhost:5173 to access the interactive test suite.
65
+
66
+ ### Dev Harness & Prototyping
67
+
68
+ The dev harness is a multi-page environment that allows you to prototype and test the component in various scenarios:
69
+
70
+ - **Bundled WC**: Standard web component with all dependencies included.
71
+ - **External WC**: Web component using external Leaflet/Draw from a CDN.
72
+ - **Preact / React**: First-class wrappers for Preact and React, testing both bundled and external dependency models.
73
+
74
+ Use the navigation bar at the top of the harness to switch between these environments. This is the best way to verify changes across different framework integrations and dependency models.
65
75
 
66
76
  For consumption from another app:
67
77
 
@@ -69,6 +79,43 @@ For consumption from another app:
69
79
  - `npm install @florasync/leaflet-geokit`
70
80
  - `import "@florasync/leaflet-geokit";` and ensure the element is sized via CSS (it does not self-size)
71
81
 
82
+ ### Bundled vs external Leaflet/Leaflet.draw
83
+
84
+ We ship two entrypoints so you can avoid double-loading Leaflet if your app already includes it.
85
+
86
+ - **Bundled (default)** — includes Leaflet + Leaflet.draw JS/CSS inside the library bundle. Use this when you are _not_ providing Leaflet yourself.
87
+ - Import: `import "@florasync/leaflet-geokit";`
88
+
89
+ - **External (no Leaflet bytes)** — expects Leaflet + Leaflet.draw + CSS to be provided by the host (e.g., your framework already loads them).
90
+ - Import: `import "@florasync/leaflet-geokit/external";`
91
+ - Requires `window.L` with `L.Control.Draw` available, and Leaflet/Draw CSS loaded by you.
92
+ - If external `L` is present but Draw APIs are incomplete, runtime falls back to bundled Leaflet/Draw and logs a warning.
93
+
94
+ Runtime options (advanced)
95
+
96
+ - Programmatic flag: `map.useExternalLeaflet = true` to prefer a provided `window.L` (falls back to bundled if missing). See [LeafletDrawMapElement](src/components/LeafletDrawMapElement.ts:15) and [MapController](src/lib/MapController.ts:41).
97
+ - Programmatic flag: `map.skipLeafletStyles = true` to disable our CSS/icon injection when you own the styles. Styling helpers live in [leaflet-assets](src/lib/leaflet-assets.ts:1).
98
+
99
+ Example: external entrypoint in a bundler app
100
+
101
+ ```ts
102
+ import "leaflet/dist/leaflet.css";
103
+ import "leaflet-draw/dist/leaflet.draw.css";
104
+ import "@florasync/leaflet-geokit/external";
105
+ ```
106
+
107
+ Then mount your element as usual:
108
+
109
+ ```html
110
+ <leaflet-geokit draw-polygon draw-marker></leaflet-geokit>
111
+ ```
112
+
113
+ Why choose external?
114
+
115
+ - Avoid double-loading Leaflet/Leaflet.draw when your host already ships them.
116
+ - Keep bundle size smaller in hosts that vendor Leaflet themselves.
117
+ - Control Leaflet/Draw versions and theming directly.
118
+
72
119
  ---
73
120
 
74
121
  ## Build and compilation
@@ -87,12 +134,36 @@ Scripts (see [package.json](package.json))
87
134
 
88
135
  - npm run dev — Vite dev server
89
136
  - npm run build — type declarations + Vite build
137
+ - npm run build:analyze — generate per-target bundle reports in dist/stats/\*.html
90
138
  - npm run test:unit — Vitest (happy-dom)
91
139
  - npm run typecheck — TypeScript noEmit
92
140
  - npm run lint — ESLint (strict TS rules)
93
141
  - npm run format — Prettier write
94
142
  - npm run test:e2e — Playwright (currently a minimal smoke test under e2e/)
95
143
 
144
+ Optional workflow tooling
145
+
146
+ - Spec Kitty is pre-wired as an optional, non-blocking workflow bootstrap.
147
+ - `npm run spec-kitty:init` — initialize Spec Kitty in-place (requires `spec-kitty` CLI installed on your machine).
148
+ - `npm run spec-kitty:doctor` — validate local Spec Kitty setup (requires `spec-kitty` CLI).
149
+ - Build/test/publish scripts do **not** depend on Spec Kitty.
150
+
151
+ Bundle analysis
152
+
153
+ - The analyzer is opt-in and does not change normal production outputs.
154
+ - Run `npm run build:analyze` to produce one report per target:
155
+ - dist/stats/main.html
156
+ - dist/stats/external.html
157
+ - dist/stats/django.html
158
+ - dist/stats/preact.html
159
+ - dist/stats/preact-bundled.html
160
+ - dist/stats/react.html
161
+ - dist/stats/react-bundled.html
162
+ - Read report metrics as:
163
+ - Raw: uncompressed parsed bundle bytes
164
+ - Gzip: transfer cost with gzip compression
165
+ - Brotli: transfer cost with brotli compression
166
+
96
167
  Browser support
97
168
 
98
169
  - ES2019, evergreen browsers (Chromium, Firefox, Safari modern)
@@ -129,65 +200,504 @@ CSS and assets:
129
200
 
130
201
  ## Public API
131
202
 
132
- Attributes (string/boolean)
133
-
134
- - Map configuration
135
- - latitude (string number): initial map latitude; default 0
136
- - longitude (string number): initial map longitude; default 0
137
- - zoom (string number): initial zoom; default 2
138
- - min-zoom (string number, optional)
139
- - max-zoom (string number, optional)
140
- - tile-url (string): tile URL template (default OSM)
141
- - tile-attribution (string, optional): attribution text
142
- - prefer-canvas (boolean): use Canvas rendering instead of SVG for better performance with large datasets; default true
143
- - Controls (boolean; presence = enabled)
144
- - draw-polygon, draw-polyline, draw-rectangle, draw-circle, draw-layer-cake, draw-marker
145
- - edit-features, delete-features
146
- - Behavior
147
- - read-only (boolean): disables all drawing/editing/removing
148
- - log-level (string): trace | debug | info | warn | error | silent (default debug)
149
- - dev-overlay (boolean): reserved for a runtime overlay (future)
150
-
151
- Properties (runtime)
152
-
153
- - latitude: number
154
- - longitude: number
155
- - zoom: number
156
- - minZoom?: number
157
- - maxZoom?: number
158
- - tileUrl: string
159
- - tileAttribution?: string
160
- - readOnly: boolean
161
- - logLevel: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent'
162
- - devOverlay: boolean
163
- - preferCanvas: boolean
164
-
165
- Methods (Promise-based, invoked on the element instance)
166
-
167
- - getGeoJSON(): returns FeatureCollection of current data
168
- - loadGeoJSON(fc): clears + loads FeatureCollection; does not auto-fit
169
- - clearLayers(): clears map layers and store
170
- - addFeatures(fc): adds features; returns array of assigned ids
171
- - updateFeature(id, feature): replaces a feature in the store (visual sync is progressively enhanced)
172
- - removeFeature(id): removes a feature (and layers with matching id)
173
- - fitBoundsToData(padding?): fits view to data bounds (default padding ~0.05)
174
- - fitBounds(bounds, padding?): fits view to provided [[south, west], [north, east]] bounds with optional padding ratio
175
- - setView(lat, lng, zoom?): sets map view
176
- - loadGeoJSONFromUrl(url): fetches a URL (application/json) and loads; auto-fits
177
- - loadGeoJSONFromText(text): parses JSON text and loads; auto-fits
178
- - exportGeoJSON(): emits 'leaflet-draw:export' with current FeatureCollection and returns it
179
-
180
- Events (CustomEvent with detail)
181
-
182
- - leaflet-draw:ready — { bounds?: [[south, west], [north, east]] }
183
- - leaflet-draw:created — { id: string, layerType: 'polygon'|'polyline'|'rectangle'|'circle'|'marker', geoJSON: Feature }
184
- - leaflet-draw:edited — { ids: string[], geoJSON: FeatureCollection }
185
- - leaflet-draw:deleted — { ids: string[], geoJSON: FeatureCollection }
186
- - leaflet-draw:error — { message: string, cause?: unknown }
187
- - leaflet-draw:export — { geoJSON: FeatureCollection, featureCount: number }
188
- - leaflet-draw:ingest { fc: FeatureCollection, mode: 'load'|'add' } (listener may mutate detail.fc to transform input)
189
-
190
- Event payload types live in [src/types/events.ts](src/types/events.ts). Public API types live in [src/types/public.ts](src/types/public.ts).
203
+ The `<leaflet-geokit>` custom element provides a comprehensive API for embedding interactive maps with drawing capabilities. All functionality is exposed through HTML attributes, JavaScript properties, methods, and events.
204
+
205
+ ### HTML Attributes
206
+
207
+ #### Map Configuration
208
+
209
+ Configure the initial map view and tile layer:
210
+
211
+ - **`latitude`** (string number): Initial map latitude coordinate. Default: `"0"`
212
+ - **`longitude`** (string number): Initial map longitude coordinate. Default: `"0"`
213
+ - **`zoom`** (string number): Initial zoom level. Default: `"2"`
214
+ - **`min-zoom`** (string number, optional): Minimum allowed zoom level
215
+ - **`max-zoom`** (string number, optional): Maximum allowed zoom level
216
+ - **`tile-url`** (string): Tile server URL template with `{z}`, `{x}`, `{y}` placeholders. Default: OpenStreetMap
217
+ - **`tile-attribution`** (string, optional): Attribution text for the tile layer
218
+
219
+ ```html
220
+ <leaflet-geokit
221
+ latitude="39.7392"
222
+ longitude="-104.9903"
223
+ zoom="11"
224
+ min-zoom="8"
225
+ max-zoom="18"
226
+ tile-url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
227
+ tile-attribution="&copy; OpenStreetMap contributors"
228
+ ></leaflet-geokit>
229
+ ```
230
+
231
+ #### Drawing Tools
232
+
233
+ Enable specific drawing tools by adding boolean attributes (presence = enabled):
234
+
235
+ - **`draw-polygon`**: Enable polygon drawing tool
236
+ - **`draw-polyline`**: Enable polyline/line drawing tool
237
+ - **`draw-rectangle`**: Enable rectangle drawing tool
238
+ - **`draw-circle`**: Enable circle drawing tool
239
+ - **`draw-layer-cake`**: Enable Layer Cake tool for creating concentric donut polygons
240
+ - **`draw-marker`**: Enable point marker drawing tool
241
+ - **`draw-move`**: Enable move/translate tool for repositioning existing features (with Save/Cancel confirmation)
242
+ - **`draw-ruler`**: Enable measurement/ruler tool for distances and areas
243
+
244
+ ```html
245
+ <leaflet-geokit
246
+ draw-polygon
247
+ draw-polyline
248
+ draw-rectangle
249
+ draw-circle
250
+ draw-layer-cake
251
+ draw-marker
252
+ draw-move
253
+ draw-ruler
254
+ ></leaflet-geokit>
255
+ ```
256
+
257
+ #### Editing Controls
258
+
259
+ Control editing and deletion of existing features:
260
+
261
+ - **`edit-features`**: Enable editing mode for modifying existing shapes
262
+ - **`delete-features`**: Enable deletion mode for removing shapes
263
+
264
+ ```html
265
+ <leaflet-geokit draw-polygon edit-features delete-features></leaflet-geokit>
266
+ ```
267
+
268
+ #### Behavior Modifiers
269
+
270
+ - **`read-only`**: Disables all drawing, editing, and deleting tools. Map becomes view-only
271
+ - **`polygon-allow-intersection`**: Allows polygons to intersect/overlap during drawing
272
+ - **`prefer-canvas`**: Use Canvas rendering instead of SVG for better performance with large datasets. Default: `true`
273
+
274
+ ```html
275
+ <leaflet-geokit read-only></leaflet-geokit>
276
+ <leaflet-geokit draw-polygon polygon-allow-intersection></leaflet-geokit>
277
+ <leaflet-geokit prefer-canvas="false"></leaflet-geokit>
278
+ ```
279
+
280
+ #### Theming & Styling
281
+
282
+ Customize the visual appearance:
283
+
284
+ - **`theme-url`** (string, optional): External CSS stylesheet URL to inject into Shadow DOM
285
+ - **`themeCss`** (property only): Inline CSS strings for custom styling
286
+
287
+ ```html
288
+ <leaflet-geokit theme-url="/css/custom-map-theme.css"></leaflet-geokit>
289
+ ```
290
+
291
+ #### Debugging & Development
292
+
293
+ - **`log-level`** (string): Control console logging verbosity. Options: `trace`, `debug`, `info`, `warn`, `error`, `silent`. Default: `"debug"`
294
+ - **`dev-overlay`** (boolean): Reserved for future development overlay features
295
+
296
+ ```html
297
+ <leaflet-geokit log-level="info"></leaflet-geokit>
298
+ ```
299
+
300
+ ### JavaScript Properties
301
+
302
+ All attributes have corresponding JavaScript properties for runtime access and modification:
303
+
304
+ ```javascript
305
+ const map = document.querySelector("leaflet-geokit");
306
+
307
+ // Map configuration
308
+ map.latitude = 40.7128; // number
309
+ map.longitude = -74.006; // number
310
+ map.zoom = 12; // number
311
+ map.minZoom = 8; // number | undefined
312
+ map.maxZoom = 18; // number | undefined
313
+ map.tileUrl = "https://..."; // string
314
+ map.tileAttribution = "..."; // string | undefined
315
+
316
+ // Behavior
317
+ map.readOnly = true; // boolean
318
+ map.preferCanvas = false; // boolean
319
+ map.logLevel = "info"; // LogLevel
320
+
321
+ // Theming
322
+ map.themeCss = `
323
+ .leaflet-container { font-family: "Inter", sans-serif; }
324
+ .leaflet-draw-toolbar a { border-radius: 8px; }
325
+ `;
326
+ ```
327
+
328
+ ### Methods
329
+
330
+ All methods return Promises and should be awaited. Invoke on the element instance:
331
+
332
+ #### Data Management
333
+
334
+ **[`getGeoJSON()`](src/components/LeafletDrawMapElement.ts:384)**: Promise<FeatureCollection>
335
+
336
+ - Returns current map data as a GeoJSON FeatureCollection
337
+ - Includes all drawn features with stable IDs
338
+
339
+ ```javascript
340
+ const data = await map.getGeoJSON();
341
+ console.log(`${data.features.length} features on map`);
342
+ ```
343
+
344
+ **[`loadGeoJSON(fc)`](src/components/LeafletDrawMapElement.ts:390)**: Promise<void>
345
+
346
+ - Clears existing data and loads new FeatureCollection
347
+ - Does NOT auto-fit the view (use [`fitBoundsToData()`](src/components/LeafletDrawMapElement.ts:428) separately)
348
+ - Triggers [`leaflet-draw:ingest`](src/types/events.ts:51) event before loading
349
+
350
+ ```javascript
351
+ await map.loadGeoJSON({
352
+ type: "FeatureCollection",
353
+ features: [
354
+ /* ... */
355
+ ],
356
+ });
357
+ ```
358
+
359
+ **[`addFeatures(fc)`](src/components/LeafletDrawMapElement.ts:406)**: Promise<string[]>
360
+
361
+ - Adds features to existing map data (does not clear)
362
+ - Returns array of assigned stable feature IDs
363
+ - Triggers [`leaflet-draw:ingest`](src/types/events.ts:51) event before adding
364
+
365
+ ```javascript
366
+ const ids = await map.addFeatures(newFeatures);
367
+ console.log("Added features with IDs:", ids);
368
+ ```
369
+
370
+ **[`clearLayers()`](src/components/LeafletDrawMapElement.ts:400)**: Promise<void>
371
+
372
+ - Removes all features from map and internal storage
373
+
374
+ ```javascript
375
+ await map.clearLayers();
376
+ ```
377
+
378
+ #### Individual Feature Operations
379
+
380
+ **[`updateFeature(id, feature)`](src/components/LeafletDrawMapElement.ts:416)**: Promise<void>
381
+
382
+ - Replaces an existing feature by ID
383
+ - Visual synchronization is progressively enhanced
384
+
385
+ ```javascript
386
+ await map.updateFeature(featureId, {
387
+ type: "Feature",
388
+ properties: { name: "Updated" },
389
+ geometry: {
390
+ /* ... */
391
+ },
392
+ });
393
+ ```
394
+
395
+ **[`removeFeature(id)`](src/components/LeafletDrawMapElement.ts:422)**: Promise<void>
396
+
397
+ - Removes a feature and its visual representation by ID
398
+
399
+ ```javascript
400
+ await map.removeFeature(featureId);
401
+ ```
402
+
403
+ #### View Control
404
+
405
+ **[`setView(lat, lng, zoom?)`](src/components/LeafletDrawMapElement.ts:448)**: Promise<void>
406
+
407
+ - Sets map center and optionally zoom level
408
+ - Updates element properties to maintain consistency
409
+
410
+ ```javascript
411
+ await map.setView(39.7392, -104.9903, 12);
412
+ ```
413
+
414
+ **[`fitBoundsToData(padding?)`](src/components/LeafletDrawMapElement.ts:428)**: Promise<void>
415
+
416
+ - Automatically fits map view to show all current data
417
+ - Optional padding as ratio of bounds size (default: 0.05 = 5%)
418
+
419
+ ```javascript
420
+ await map.fitBoundsToData(0.1); // 10% padding
421
+ ```
422
+
423
+ **[`fitBounds(bounds, padding?)`](src/components/LeafletDrawMapElement.ts:436)**: Promise<void>
424
+
425
+ - Fits map view to specified bounds
426
+ - Bounds format: `[[south, west], [north, east]]`
427
+ - Optional padding ratio (default: 0.05)
428
+
429
+ ```javascript
430
+ await map.fitBounds(
431
+ [
432
+ [39.6, -105.1], // southwest
433
+ [39.8, -104.8], // northeast
434
+ ],
435
+ 0.05,
436
+ );
437
+ ```
438
+
439
+ #### Data Import Helpers
440
+
441
+ **[`loadGeoJSONFromUrl(url)`](src/components/LeafletDrawMapElement.ts:510)**: Promise<void>
442
+
443
+ - Fetches GeoJSON from URL and loads it
444
+ - Expects `application/json` content type
445
+ - Automatically fits view to loaded data
446
+ - Emits error events on fetch/parse failures
447
+
448
+ ```javascript
449
+ await map.loadGeoJSONFromUrl("/api/geodata.json");
450
+ ```
451
+
452
+ **[`loadGeoJSONFromText(text)`](src/components/LeafletDrawMapElement.ts:533)**: Promise<void>
453
+
454
+ - Parses GeoJSON from text string and loads it
455
+ - Automatically fits view to loaded data
456
+ - Emits error events on parse failures
457
+
458
+ ```javascript
459
+ const text = await file.text();
460
+ await map.loadGeoJSONFromText(text);
461
+ ```
462
+
463
+ **[`exportGeoJSON()`](src/components/LeafletDrawMapElement.ts:459)**: Promise<FeatureCollection>
464
+
465
+ - Exports current data and emits [`leaflet-draw:export`](src/types/events.ts:57) event
466
+ - Returns the FeatureCollection for convenience
467
+ - Useful for triggering export workflows
468
+
469
+ ```javascript
470
+ const exported = await map.exportGeoJSON();
471
+ // Listen for the event to trigger download/save workflows
472
+ ```
473
+
474
+ #### Advanced Features
475
+
476
+ **[`mergePolygons(options?)`](src/components/LeafletDrawMapElement.ts:475)**: Promise<string | null>
477
+
478
+ - Merges all visible polygon features into a single polygon
479
+ - Removes original polygons and creates new merged feature
480
+ - Returns ID of merged feature, or null if no polygons to merge
481
+ - Emits `leaflet-draw:merged` event with merge details
482
+
483
+ ```javascript
484
+ const mergedId = await map.mergePolygons({
485
+ properties: { name: "Merged Area", type: "combined" },
486
+ });
487
+ ```
488
+
489
+ **[`setMeasurementUnits(system)`](src/components/LeafletDrawMapElement.ts:504)**: Promise<void>
490
+
491
+ - Changes measurement system for ruler tool
492
+ - Options: `"metric"` (meters/kilometers) or `"imperial"` (feet/miles)
493
+
494
+ ```javascript
495
+ await map.setMeasurementUnits("imperial");
496
+ ```
497
+
498
+ ### Events
499
+
500
+ The component emits typed CustomEvents that can be listened to for real-time updates. All events include structured `detail` objects.
501
+
502
+ #### Core Lifecycle Events
503
+
504
+ **[`leaflet-draw:ready`](src/types/events.ts:7)**
505
+
506
+ - Fired when map is fully initialized and ready for interaction
507
+ - Detail: `{ bounds?: [[number, number], [number, number]] }`
508
+
509
+ ```javascript
510
+ map.addEventListener("leaflet-draw:ready", (e) => {
511
+ console.log("Map ready!", e.detail.bounds);
512
+ });
513
+ ```
514
+
515
+ **[`leaflet-draw:error`](src/types/events.ts:42)**
516
+
517
+ - Fired when errors occur (fetch failures, parse errors, etc.)
518
+ - Detail: `{ message: string, cause?: unknown }`
519
+
520
+ ```javascript
521
+ map.addEventListener("leaflet-draw:error", (e) => {
522
+ console.error("Map error:", e.detail.message, e.detail.cause);
523
+ });
524
+ ```
525
+
526
+ #### Drawing & Editing Events
527
+
528
+ **[`leaflet-draw:created`](src/types/events.ts:15)**
529
+
530
+ - Fired when user creates a new feature via drawing tools
531
+ - Detail: `{ id: string, layerType: string, geoJSON: Feature }`
532
+ - Layer types: `'polygon'`, `'polyline'`, `'rectangle'`, `'circle'`, `'marker'`
533
+
534
+ ```javascript
535
+ map.addEventListener('leaflet-draw:created', (e) => {
536
+ const { id, layerType, geoJSON } = e.detail;
537
+ console.log(`Created ${layerType} with ID: ${id}`);
538
+
539
+ // Save to your backend
540
+ await saveFeature(id, geoJSON);
541
+ });
542
+ ```
543
+
544
+ **[`leaflet-draw:edited`](src/types/events.ts:25)**
545
+
546
+ - Fired when user modifies existing features via edit tools
547
+ - Detail: `{ ids: string[], geoJSON: FeatureCollection }`
548
+ - Includes all edited feature IDs and current state
549
+
550
+ ```javascript
551
+ map.addEventListener('leaflet-draw:edited', (e) => {
552
+ const { ids, geoJSON } = e.detail;
553
+ console.log(`Edited ${ids.length} features`);
554
+
555
+ // Sync changes to backend
556
+ for (const id of ids) {
557
+ const feature = geoJSON.features.find(f => f.id === id);
558
+ if (feature) await updateFeature(id, feature);
559
+ }
560
+ });
561
+ ```
562
+
563
+ **[`leaflet-draw:deleted`](src/types/events.ts:34)**
564
+
565
+ - Fired when user deletes features via delete tools
566
+ - Detail: `{ ids: string[], geoJSON: FeatureCollection }`
567
+ - `geoJSON` contains remaining features (after deletion)
568
+
569
+ ```javascript
570
+ map.addEventListener('leaflet-draw:deleted', (e) => {
571
+ const { ids } = e.detail;
572
+ console.log(`Deleted features: ${ids.join(', ')}`);
573
+
574
+ // Remove from backend
575
+ for (const id of ids) {
576
+ await deleteFeature(id);
577
+ }
578
+ });
579
+ ```
580
+
581
+ #### Data Flow Events
582
+
583
+ **[`leaflet-draw:ingest`](src/types/events.ts:51)**
584
+
585
+ - Fired BEFORE data is loaded/added to the map
586
+ - Detail: `{ fc: FeatureCollection, mode: 'load' | 'add' }`
587
+ - Listeners can mutate `detail.fc` to transform incoming data
588
+ - Useful for data validation, filtering, or preprocessing
589
+
590
+ ```javascript
591
+ map.addEventListener("leaflet-draw:ingest", (e) => {
592
+ const { fc, mode } = e.detail;
593
+
594
+ // Filter out invalid features
595
+ e.detail.fc.features = fc.features.filter(
596
+ (f) => f.geometry && f.geometry.coordinates.length > 0,
597
+ );
598
+
599
+ // Add default properties
600
+ e.detail.fc.features.forEach((f) => {
601
+ f.properties = {
602
+ ...f.properties,
603
+ imported: true,
604
+ timestamp: Date.now(),
605
+ };
606
+ });
607
+ });
608
+ ```
609
+
610
+ **[`leaflet-draw:export`](src/types/events.ts:57)**
611
+
612
+ - Fired when [`exportGeoJSON()`](src/components/LeafletDrawMapElement.ts:459) method is called
613
+ - Detail: `{ geoJSON: FeatureCollection, featureCount: number }`
614
+ - Use for triggering download workflows
615
+
616
+ ```javascript
617
+ map.addEventListener("leaflet-draw:export", (e) => {
618
+ const { geoJSON, featureCount } = e.detail;
619
+
620
+ // Create download link
621
+ const blob = new Blob([JSON.stringify(geoJSON, null, 2)], {
622
+ type: "application/geo+json",
623
+ });
624
+ const url = URL.createObjectURL(blob);
625
+ const a = document.createElement("a");
626
+ a.href = url;
627
+ a.download = `map-data-${new Date().toISOString()}.geojson`;
628
+ a.click();
629
+ });
630
+ ```
631
+
632
+ #### Extended Tool Events
633
+
634
+ **`leaflet-draw:drawstart`** / **`leaflet-draw:drawstop`**
635
+
636
+ - Fired when drawing mode starts/stops
637
+ - Useful for UI state management
638
+
639
+ **`leaflet-draw:editstart`** / **`leaflet-draw:editstop`**
640
+
641
+ - Fired when edit mode starts/stops
642
+
643
+ **`leaflet-draw:merged`**
644
+
645
+ - Fired after successful polygon merge operation
646
+ - Detail includes merge statistics and result
647
+
648
+ ```javascript
649
+ // Show/hide UI elements based on draw state
650
+ map.addEventListener("leaflet-draw:drawstart", () => {
651
+ document.querySelector("#toolbar").classList.add("drawing-active");
652
+ });
653
+
654
+ map.addEventListener("leaflet-draw:drawstop", () => {
655
+ document.querySelector("#toolbar").classList.remove("drawing-active");
656
+ });
657
+ ```
658
+
659
+ ### Feature ID Management
660
+
661
+ The component guarantees stable, persistent feature IDs that survive editing operations:
662
+
663
+ 1. **Explicit IDs**: If a feature has `feature.id` property, it's preserved
664
+ 2. **Property IDs**: If no `feature.id` but has `properties.id`, that's used
665
+ 3. **Generated IDs**: Otherwise, a UUID is generated and stored in `properties.id`
666
+
667
+ ```javascript
668
+ // Features maintain their IDs through edit cycles
669
+ const data = await map.getGeoJSON();
670
+ data.features.forEach((f) => {
671
+ console.log(`Feature ID: ${f.id}, Properties ID: ${f.properties?.id}`);
672
+ });
673
+ ```
674
+
675
+ ### TypeScript Support
676
+
677
+ Full TypeScript definitions are provided in [`src/types/public.ts`](src/types/public.ts) and [`src/types/events.ts`](src/types/events.ts):
678
+
679
+ ```typescript
680
+ import type {
681
+ LeafletDrawMapElementAPI,
682
+ MeasurementSystem,
683
+ } from "@florasync/leaflet-geokit";
684
+ import type {
685
+ CreatedEventDetail,
686
+ EditedEventDetail,
687
+ } from "@florasync/leaflet-geokit/events";
688
+
689
+ const map = document.querySelector(
690
+ "leaflet-geokit",
691
+ ) as LeafletDrawMapElementAPI;
692
+
693
+ map.addEventListener(
694
+ "leaflet-draw:created",
695
+ (e: CustomEvent<CreatedEventDetail>) => {
696
+ const { id, layerType, geoJSON } = e.detail;
697
+ // Fully typed event handling
698
+ },
699
+ );
700
+ ```
191
701
 
192
702
  ---
193
703
 
@@ -216,6 +726,7 @@ A. Basic HTML, served by Vite (development)
216
726
  draw-circle
217
727
  draw-layer-cake
218
728
  draw-marker
729
+ draw-move
219
730
  edit-features
220
731
  delete-features
221
732
  log-level="debug"
@@ -287,9 +798,95 @@ E. Framework integration (React/Preact/Vue/Svelte)
287
798
 
288
799
  ---
289
800
 
290
- ## Shims and integrations
801
+ ## Framework Support
802
+
803
+ This section is the canonical place to discover first-class framework integrations for this project.
804
+
805
+ Current framework support:
806
+
807
+ - Django: [docs/shims/django.md](docs/shims/django.md)
808
+ - Preact: [docs/shims/preact.md](docs/shims/preact.md)
809
+ - React: [docs/shims/react.md](docs/shims/react.md)
810
+
811
+ ### Django
812
+
813
+ Use the Django shim when your source of truth is a form field (`<textarea>`) and you want map edits synchronized to submitted GeoJSON.
814
+
815
+ - Shim docs: [docs/shims/django.md](docs/shims/django.md)
816
+ - Entrypoint: [src/django/index.ts](src/django/index.ts)
817
+
818
+ Quick import path:
819
+
820
+ ```ts
821
+ import { initDjangoGeokit } from "@florasync/leaflet-geokit/django";
822
+ ```
823
+
824
+ ### Preact
825
+
826
+ Use the Preact wrappers when you want component-level integration in Preact apps. We provide pre-baked wrappers that handle the custom element lifecycle and event synchronization.
827
+
828
+ - **Runtime dependency model**: Preact is consumer-provided (peer dependency), so wrapper bundles stay thin.
829
+ - **Shim docs**: [docs/shims/preact.md](docs/shims/preact.md)
830
+ - **Entrypoints**: [src/preact/index.tsx](src/preact/index.tsx), [src/preact-bundled/index.tsx](src/preact-bundled/index.tsx)
831
+
832
+ #### Preact wrapper (additive Leaflet mode)
291
833
 
292
- - Django widget shim: [docs/shims/django.md](docs/shims/django.md)
834
+ If your Preact app already loads Leaflet + Leaflet.draw, use the Preact shim:
835
+
836
+ ```ts
837
+ import { PreactLeafletGeoKit } from "@florasync/leaflet-geokit/preact";
838
+ ```
839
+
840
+ It mounts `<leaflet-geokit>` in additive mode by default (`use-external-leaflet` + `skip-leaflet-styles`) and syncs GeoJSON via callbacks.
841
+
842
+ ```tsx
843
+ <PreactLeafletGeoKit
844
+ style={{ width: "100%", height: "420px" }}
845
+ attributes={{
846
+ latitude: 39.7392,
847
+ longitude: -104.9903,
848
+ zoom: 11,
849
+ "draw-polygon": true,
850
+ "edit-features": true,
851
+ }}
852
+ onChangeText={(text) => {
853
+ // Persist serialized FeatureCollection
854
+ console.log(text);
855
+ }}
856
+ />
857
+ ```
858
+
859
+ For apps that do **not** preload Leaflet/Leaflet.draw, use the bundled Preact entrypoint:
860
+
861
+ ```ts
862
+ import { PreactLeafletGeoKit } from "@florasync/leaflet-geokit/preact-bundled";
863
+ ```
864
+
865
+ See full Preact shim docs: [docs/shims/preact.md](docs/shims/preact.md)
866
+
867
+ ### React
868
+
869
+ Use the React wrappers when you want component-level integration in React apps. These wrappers provide a first-class React experience for the web component.
870
+
871
+ - **Runtime dependency model**: React/ReactDOM are consumer-provided (peer dependencies), so wrapper bundles stay thin.
872
+ - **Shim docs**: [docs/shims/react.md](docs/shims/react.md)
873
+ - **Entrypoints**: [src/react/index.tsx](src/react/index.tsx), [src/react-bundled/index.tsx](src/react-bundled/index.tsx)
874
+
875
+ #### React wrapper (additive Leaflet mode)
876
+
877
+ If your React app already loads Leaflet + Leaflet.draw, use the React shim:
878
+
879
+ ```ts
880
+ import { ReactLeafletGeoKit } from "@florasync/leaflet-geokit/react";
881
+ ```
882
+
883
+ For apps that do **not** preload Leaflet/Leaflet.draw, use the bundled React entrypoint:
884
+
885
+ ```ts
886
+ import { ReactLeafletGeoKit } from "@florasync/leaflet-geokit/react-bundled";
887
+ ```
888
+
889
+ See full React shim docs: [docs/shims/react.md](docs/shims/react.md)
293
890
 
294
891
  ## Recipes
295
892
 
@@ -330,6 +927,29 @@ el.addEventListener("leaflet-draw:edited", async (e) => {
330
927
  - At runtime, set el.logLevel = 'info' to reduce chatter.
331
928
  - If you need a different sink, wrap the element logic in your app and forward to your own logger (see [src/utils/logger.ts](src/utils/logger.ts)).
332
929
 
930
+ 6. Runtime theming (CSS injection)
931
+
932
+ - Provide a theme stylesheet URL via the theme-url attribute.
933
+ - Provide inline CSS overrides via the themeCss property.
934
+ - Cascade order is: built-in Leaflet styles → theme-url → themeCss.
935
+ - Updates are dynamic; changing theme-url or themeCss updates the Shadow DOM styles in place.
936
+
937
+ ```html
938
+ <leaflet-geokit
939
+ theme-url="/themes/geokit-brand.css"
940
+ draw-polygon
941
+ edit-features
942
+ ></leaflet-geokit>
943
+ ```
944
+
945
+ ```js
946
+ const el = document.querySelector("leaflet-geokit");
947
+ el.themeCss = `
948
+ .leaflet-container { font-family: "Inter", sans-serif; }
949
+ .leaflet-draw-toolbar a { border-radius: 8px; }
950
+ `;
951
+ ```
952
+
333
953
  ---
334
954
 
335
955
  ## Logging and diagnostics