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