@maplibre-yaml/core 0.2.2 → 0.3.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.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 mariogiampieri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -25,23 +25,32 @@ The simplest way to use maplibre-yaml is with the `<ml-map>` web component:
25
25
  <style>
26
26
  ml-map { display: block; height: 400px; }
27
27
  </style>
28
+ <!-- Tell the browser where to find maplibre-gl (kept external, not bundled) -->
29
+ <script type="importmap">
30
+ { "imports": { "maplibre-gl": "https://esm.sh/maplibre-gl@^4" } }
31
+ </script>
32
+ <!-- Register the <ml-map> web component -->
33
+ <script
34
+ type="module"
35
+ src="https://unpkg.com/@maplibre-yaml/core/register.js"
36
+ ></script>
28
37
  </head>
29
38
  <body>
30
39
  <ml-map src="/map.yaml"></ml-map>
31
- <script type="module">
32
- import '@maplibre-yaml/core/register';
33
- </script>
34
40
  </body>
35
41
  </html>
36
42
  ```
37
43
 
44
+ With a bundler (Vite, Webpack, etc.) skip the import map and CDN script — `import '@maplibre-yaml/core/register'` in your entry point instead.
45
+
38
46
  ### JavaScript API
39
47
 
40
48
  ```typescript
41
- import { parseYAMLConfig, MapRenderer } from '@maplibre-yaml/core';
49
+ import { YAMLParser, MapRenderer } from '@maplibre-yaml/core';
42
50
 
43
51
  const yaml = `
44
52
  type: map
53
+ id: my-map
45
54
  config:
46
55
  center: [-122.4, 37.8]
47
56
  zoom: 12
@@ -57,11 +66,22 @@ layers:
57
66
  circle-color: "#3b82f6"
58
67
  `;
59
68
 
60
- const config = parseYAMLConfig(yaml);
69
+ const mapBlock = YAMLParser.parseMapBlock(yaml);
61
70
  const container = document.getElementById('map');
62
- const renderer = new MapRenderer(container, config);
71
+ const renderer = new MapRenderer(
72
+ container,
73
+ mapBlock.config,
74
+ mapBlock.layers,
75
+ {
76
+ onLoad: () => console.log('Map loaded'),
77
+ onError: (error) => console.error(error),
78
+ },
79
+ mapBlock.sources
80
+ );
63
81
  ```
64
82
 
83
+ Use `YAMLParser.parseMapBlock` for single `type: map` documents. `parseYAMLConfig` parses full root documents (a `pages:` array of pages and blocks) and will reject a bare map block.
84
+
65
85
  ## Entry Points
66
86
 
67
87
  The package provides multiple entry points for different use cases:
@@ -197,6 +217,8 @@ layers:
197
217
 
198
218
  ### Interactive Popups
199
219
 
220
+ Popup content is a list of HTML elements, where each element maps a tag name to an array of static (`str`) or dynamic (`property`) content items:
221
+
200
222
  ```yaml
201
223
  layers:
202
224
  - id: locations
@@ -206,12 +228,16 @@ layers:
206
228
  url: https://example.com/locations.geojson
207
229
  layout:
208
230
  icon-image: marker
209
- interactions:
210
- - type: click
231
+ interactive:
232
+ hover:
233
+ cursor: pointer
234
+ click:
211
235
  popup:
212
- content: |
213
- <h3>{{name}}</h3>
214
- <p>{{description}}</p>
236
+ - h3:
237
+ - property: name
238
+ else: "Unknown"
239
+ - p:
240
+ - property: description
215
241
  ```
216
242
 
217
243
  ## API Reference
@@ -229,12 +255,13 @@ const result = safeParseYAMLConfig(yamlString);
229
255
  if (result.success) {
230
256
  console.log(result.data);
231
257
  } else {
232
- console.error(result.error);
258
+ console.error(result.errors);
233
259
  }
234
260
 
235
- // Using the class
236
- const parser = new YAMLParser();
237
- const config = parser.parse(yamlString);
261
+ // Single-block documents (all methods are static)
262
+ const mapBlock = YAMLParser.parseMapBlock(mapYaml);
263
+ const story = YAMLParser.parseScrollytellingBlock(storyYaml);
264
+ const safe = YAMLParser.safeParseMapBlock(mapYaml);
238
265
  ```
239
266
 
240
267
  ### Renderer
@@ -242,7 +269,8 @@ const config = parser.parse(yamlString);
242
269
  ```typescript
243
270
  import { MapRenderer } from '@maplibre-yaml/core';
244
271
 
245
- const renderer = new MapRenderer(container, config, {
272
+ // new MapRenderer(container, config, layers?, options?, sources?)
273
+ const renderer = new MapRenderer(container, config, layers, {
246
274
  onLoad: () => console.log('Map loaded'),
247
275
  onError: (error) => console.error(error),
248
276
  });
@@ -1,6 +1,6 @@
1
1
  export { MLMap, registerMLMap } from '../register.js';
2
2
  import 'maplibre-gl';
3
- import '../map-renderer-SjO3KQmx.js';
3
+ import '../map-renderer-4FF3EmBO.js';
4
4
  import '../page.schema-Cad2FFqh.js';
5
5
  import 'zod';
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { parse } from 'yaml';
2
2
  import { z, ZodError } from 'zod';
3
- import maplibregl2 from 'maplibre-gl';
3
+ import * as maplibre from 'maplibre-gl';
4
4
 
5
5
  // @maplibre-yaml/core - Declarative web maps with YAML
6
6
 
@@ -154,8 +154,30 @@ var GeoJSONSourceSchema = z.object({
154
154
  generateId: z.boolean().optional(),
155
155
  promoteId: z.union([z.string(), z.record(z.string())]).optional(),
156
156
  attribution: z.string().optional()
157
- }).passthrough().refine((data) => data.url || data.data || data.prefetchedData, {
158
- message: 'GeoJSON source requires at least one of: url, data, or prefetchedData. Use "url" to fetch from an endpoint, "data" for inline GeoJSON, or "prefetchedData" for build-time fetched data.'
157
+ }).passthrough().superRefine((d, ctx) => {
158
+ if (!d.url && !d.data && !d.prefetchedData) {
159
+ ctx.addIssue({
160
+ code: z.ZodIssueCode.custom,
161
+ message: 'GeoJSON source requires at least one of: url, data, or prefetchedData. Use "url" to fetch from an endpoint, "data" for inline GeoJSON, or "prefetchedData" for build-time fetched data.'
162
+ });
163
+ return;
164
+ }
165
+ if (typeof d.data === "string" && /^(\.\.?|\/?src)\//.test(d.data)) {
166
+ ctx.addIssue({
167
+ code: z.ZodIssueCode.custom,
168
+ path: ["data"],
169
+ // Recommends `data: "/data/..."` rather than `url:` because the
170
+ // `url:` field is schema-validated with z.string().url(), which
171
+ // requires a fully-qualified URL (`https://...`) and rejects
172
+ // root-relative paths. MapLibre treats a string in `data:` as a
173
+ // URL and fetches it correctly. Until url: relaxes to accept
174
+ // root-relative paths (see follow-up), `data: "/path"` is the
175
+ // working pattern for files served from public/.
176
+ message: `GeoJSON source.data must be an inline GeoJSON object or a URL. "${d.data}" is a local source-directory path that won't resolve at runtime. Move the file to public/ and reference the public-served URL:
177
+ data: "/data/<filename>.geojson"
178
+ (\`url:\` requires a fully-qualified https:// URL and won't accept root-relative paths; use \`data:\` for runtime URLs to public assets.)`
179
+ });
180
+ }
159
181
  });
160
182
  var VectorSourceSchema = z.object({
161
183
  type: z.literal("vector").describe("Source type"),
@@ -1022,6 +1044,95 @@ var YAMLParser = class {
1022
1044
  };
1023
1045
  }
1024
1046
  }
1047
+ /**
1048
+ * Detect the document type of a YAML string and validate it against the
1049
+ * matching schema
1050
+ *
1051
+ * @param yaml - YAML string to parse (a map block, a scrollytelling block, or a root document)
1052
+ * @returns Discriminated result with the detected block type and the corresponding safeParse result
1053
+ *
1054
+ * @remarks
1055
+ * Dispatches on the document's top-level `type:` field:
1056
+ *
1057
+ * - `type: map` → {@link safeParseMapBlock}
1058
+ * - `type: scrollytelling` → {@link safeParseScrollytellingBlock}
1059
+ * - no `type:` but a `pages:` key → {@link safeParse} (root document)
1060
+ *
1061
+ * Any other `type:` value produces a failure result listing the valid
1062
+ * values, as does a document that has neither `type:` nor `pages:`.
1063
+ * This method never throws.
1064
+ *
1065
+ * @example
1066
+ * ```typescript
1067
+ * const { blockType, result } = YAMLParser.safeParseAny(yamlString);
1068
+ * if (!result.success) {
1069
+ * result.errors.forEach(err => console.error(`${err.path}: ${err.message}`));
1070
+ * } else if (blockType === 'map') {
1071
+ * renderMap(result.data);
1072
+ * }
1073
+ * ```
1074
+ */
1075
+ static safeParseAny(yaml) {
1076
+ let parsed;
1077
+ try {
1078
+ parsed = parse(yaml);
1079
+ } catch (error) {
1080
+ return {
1081
+ blockType: "unknown",
1082
+ result: {
1083
+ success: false,
1084
+ errors: [
1085
+ {
1086
+ path: "",
1087
+ message: `YAML syntax error: ${error instanceof Error ? error.message : String(error)}`
1088
+ }
1089
+ ]
1090
+ }
1091
+ };
1092
+ }
1093
+ const doc = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
1094
+ const type = doc?.type;
1095
+ if (type === "map") {
1096
+ return { blockType: "map", result: this.safeParseMapBlock(yaml) };
1097
+ }
1098
+ if (type === "scrollytelling") {
1099
+ return {
1100
+ blockType: "scrollytelling",
1101
+ result: this.safeParseScrollytellingBlock(yaml)
1102
+ };
1103
+ }
1104
+ if (type !== void 0) {
1105
+ return {
1106
+ blockType: "unknown",
1107
+ result: {
1108
+ success: false,
1109
+ errors: [
1110
+ {
1111
+ path: "type",
1112
+ message: `Unknown block type: ${JSON.stringify(
1113
+ type
1114
+ )}. Expected one of: map, scrollytelling. Root documents omit "type" and use a top-level "pages:" array instead.`
1115
+ }
1116
+ ]
1117
+ }
1118
+ };
1119
+ }
1120
+ if (doc !== null && "pages" in doc) {
1121
+ return { blockType: "root", result: this.safeParse(yaml) };
1122
+ }
1123
+ return {
1124
+ blockType: "unknown",
1125
+ result: {
1126
+ success: false,
1127
+ errors: [
1128
+ {
1129
+ path: "",
1130
+ message: 'Unable to determine document type. Expected a block with "type: map" or "type: scrollytelling", or a root document with a top-level "pages:" array.'
1131
+ }
1132
+ ]
1133
+ }
1134
+ };
1135
+ }
1025
1136
  /**
1026
1137
  * Resolve $ref references to global layers and sources
1027
1138
  *
@@ -1187,6 +1298,14 @@ var YAMLParser = class {
1187
1298
  };
1188
1299
  YAMLParser.parse.bind(YAMLParser);
1189
1300
  YAMLParser.safeParse.bind(YAMLParser);
1301
+ YAMLParser.safeParseAny.bind(YAMLParser);
1302
+ var gl = maplibre.default ?? maplibre;
1303
+ var Map2 = gl.Map;
1304
+ var Popup = gl.Popup;
1305
+ var NavigationControl = gl.NavigationControl;
1306
+ var GeolocateControl = gl.GeolocateControl;
1307
+ var ScaleControl = gl.ScaleControl;
1308
+ var FullscreenControl = gl.FullscreenControl;
1190
1309
 
1191
1310
  // src/data/memory-cache.ts
1192
1311
  var MemoryCache = class _MemoryCache {
@@ -1712,9 +1831,17 @@ var DataFetcher = class _DataFetcher {
1712
1831
  * Perform the actual HTTP fetch
1713
1832
  */
1714
1833
  async performFetch(url, options) {
1715
- const controller = options.signal ? new AbortController() : new AbortController();
1716
- if (options.signal) {
1717
- options.signal.addEventListener("abort", () => controller.abort());
1834
+ const controller = new AbortController();
1835
+ const externalSignal = options.signal;
1836
+ const onExternalAbort = () => controller.abort(externalSignal?.reason);
1837
+ if (externalSignal) {
1838
+ if (externalSignal.aborted) {
1839
+ controller.abort(externalSignal.reason);
1840
+ } else {
1841
+ externalSignal.addEventListener("abort", onExternalAbort, {
1842
+ once: true
1843
+ });
1844
+ }
1718
1845
  }
1719
1846
  const timeoutId = setTimeout(() => {
1720
1847
  controller.abort();
@@ -1772,6 +1899,7 @@ var DataFetcher = class _DataFetcher {
1772
1899
  return data;
1773
1900
  } finally {
1774
1901
  clearTimeout(timeoutId);
1902
+ externalSignal?.removeEventListener("abort", onExternalAbort);
1775
1903
  this.activeRequests.delete(url);
1776
1904
  }
1777
1905
  }
@@ -3553,9 +3681,6 @@ var LayerManager = class {
3553
3681
  loadingManager;
3554
3682
  sourceData;
3555
3683
  layerToSource;
3556
- // Legacy support (deprecated)
3557
- refreshIntervals;
3558
- abortControllers;
3559
3684
  constructor(map, callbacks) {
3560
3685
  this.map = map;
3561
3686
  this.callbacks = callbacks || {};
@@ -3566,8 +3691,6 @@ var LayerManager = class {
3566
3691
  this.loadingManager = new LoadingManager({ showUI: false });
3567
3692
  this.sourceData = /* @__PURE__ */ new Map();
3568
3693
  this.layerToSource = /* @__PURE__ */ new Map();
3569
- this.refreshIntervals = /* @__PURE__ */ new Map();
3570
- this.abortControllers = /* @__PURE__ */ new Map();
3571
3694
  }
3572
3695
  async addLayer(layer) {
3573
3696
  const isSourceRef = typeof layer.source === "string";
@@ -3818,12 +3941,6 @@ var LayerManager = class {
3818
3941
  this.pollingManager.stop(layerId);
3819
3942
  this.streamManager.disconnect(layerId);
3820
3943
  this.loadingManager.hideLoading(layerId);
3821
- this.stopRefreshInterval(layerId);
3822
- const controller = this.abortControllers.get(layerId);
3823
- if (controller) {
3824
- controller.abort();
3825
- this.abortControllers.delete(layerId);
3826
- }
3827
3944
  if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
3828
3945
  const sourceId = this.layerToSource.get(layerId) || `${layerId}-source`;
3829
3946
  const isInlineSource = sourceId === `${layerId}-source`;
@@ -3846,61 +3963,12 @@ var LayerManager = class {
3846
3963
  const source = this.map.getSource(sourceId);
3847
3964
  if (source && source.setData) source.setData(data);
3848
3965
  }
3849
- /**
3850
- * @deprecated Legacy refresh method - use PollingManager instead
3851
- */
3852
- startRefreshInterval(layer) {
3853
- if (typeof layer.source !== "object" || layer.source === null) {
3854
- return;
3855
- }
3856
- const sourceObj = layer.source;
3857
- if (sourceObj.type !== "geojson" || !sourceObj.url || !sourceObj.refreshInterval) {
3858
- return;
3859
- }
3860
- const geojsonSource = layer.source;
3861
- const interval = setInterval(async () => {
3862
- const sourceId = `${layer.id}-source`;
3863
- try {
3864
- const cacheEnabled = geojsonSource.cache?.enabled ?? true;
3865
- const cacheTTL = geojsonSource.cache?.ttl;
3866
- const result = await this.dataFetcher.fetch(geojsonSource.url, {
3867
- skipCache: !cacheEnabled,
3868
- ttl: cacheTTL
3869
- });
3870
- const data = result.data;
3871
- this.sourceData.set(sourceId, data);
3872
- const source = this.map.getSource(sourceId);
3873
- if (source?.setData) {
3874
- source.setData(data);
3875
- }
3876
- this.callbacks.onDataLoaded?.(layer.id, data.features.length);
3877
- } catch (error) {
3878
- this.callbacks.onDataError?.(layer.id, error);
3879
- }
3880
- }, geojsonSource.refreshInterval);
3881
- this.refreshIntervals.set(layer.id, interval);
3882
- }
3883
- stopRefreshInterval(layerId) {
3884
- const interval = this.refreshIntervals.get(layerId);
3885
- if (interval) {
3886
- clearInterval(interval);
3887
- this.refreshIntervals.delete(layerId);
3888
- }
3889
- }
3890
- clearAllIntervals() {
3891
- for (const interval of this.refreshIntervals.values())
3892
- clearInterval(interval);
3893
- this.refreshIntervals.clear();
3894
- }
3895
3966
  destroy() {
3896
3967
  this.pollingManager.destroy();
3897
3968
  this.streamManager.destroy();
3898
3969
  this.loadingManager.destroy();
3899
3970
  this.sourceData.clear();
3900
3971
  this.layerToSource.clear();
3901
- this.clearAllIntervals();
3902
- for (const controller of this.abortControllers.values()) controller.abort();
3903
- this.abortControllers.clear();
3904
3972
  }
3905
3973
  };
3906
3974
 
@@ -4037,7 +4105,7 @@ var EventHandler = class {
4037
4105
  showPopup(content, feature, lngLat) {
4038
4106
  this.activePopup?.remove();
4039
4107
  const html = this.popupBuilder.build(content, feature.properties);
4040
- this.activePopup = new maplibregl2.Popup().setLngLat(lngLat).setHTML(html).addTo(this.map);
4108
+ this.activePopup = new Popup().setLngLat(lngLat).setHTML(html).addTo(this.map);
4041
4109
  }
4042
4110
  /**
4043
4111
  * Detach events for a layer
@@ -4127,6 +4195,8 @@ var LegendBuilder = class {
4127
4195
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
4128
4196
  }
4129
4197
  };
4198
+
4199
+ // src/renderer/controls-manager.ts
4130
4200
  var ControlsManager = class {
4131
4201
  map;
4132
4202
  addedControls;
@@ -4142,14 +4212,14 @@ var ControlsManager = class {
4142
4212
  if (config.navigation) {
4143
4213
  const options = typeof config.navigation === "object" ? config.navigation : {};
4144
4214
  const position = options.position || "top-right";
4145
- const control = new maplibregl2.NavigationControl();
4215
+ const control = new NavigationControl();
4146
4216
  this.map.addControl(control, position);
4147
4217
  this.addedControls.push(control);
4148
4218
  }
4149
4219
  if (config.geolocate) {
4150
4220
  const options = typeof config.geolocate === "object" ? config.geolocate : {};
4151
4221
  const position = options.position || "top-right";
4152
- const control = new maplibregl2.GeolocateControl({
4222
+ const control = new GeolocateControl({
4153
4223
  positionOptions: { enableHighAccuracy: true },
4154
4224
  trackUserLocation: true
4155
4225
  });
@@ -4159,14 +4229,14 @@ var ControlsManager = class {
4159
4229
  if (config.scale) {
4160
4230
  const options = typeof config.scale === "object" ? config.scale : {};
4161
4231
  const position = options.position || "bottom-left";
4162
- const control = new maplibregl2.ScaleControl();
4232
+ const control = new ScaleControl();
4163
4233
  this.map.addControl(control, position);
4164
4234
  this.addedControls.push(control);
4165
4235
  }
4166
4236
  if (config.fullscreen) {
4167
4237
  const options = typeof config.fullscreen === "object" ? config.fullscreen : {};
4168
4238
  const position = options.position || "top-right";
4169
- const control = new maplibregl2.FullscreenControl();
4239
+ const control = new FullscreenControl();
4170
4240
  this.map.addControl(control, position);
4171
4241
  this.addedControls.push(control);
4172
4242
  }
@@ -4191,10 +4261,18 @@ var MapRenderer = class {
4191
4261
  controlsManager;
4192
4262
  eventListeners;
4193
4263
  isLoaded;
4264
+ containerEl;
4265
+ controlsAdded;
4266
+ legendBuilt;
4267
+ autoLegendContainer;
4194
4268
  constructor(container, config, layers = [], options = {}, sources) {
4195
4269
  this.eventListeners = /* @__PURE__ */ new Map();
4196
4270
  this.isLoaded = false;
4197
- this.map = new maplibregl2.Map({
4271
+ this.containerEl = typeof container === "string" ? document.getElementById(container) : container;
4272
+ this.controlsAdded = false;
4273
+ this.legendBuilt = false;
4274
+ this.autoLegendContainer = null;
4275
+ this.map = new Map2({
4198
4276
  ...config,
4199
4277
  container: typeof container === "string" ? container : container,
4200
4278
  style: config.mapStyle,
@@ -4226,6 +4304,12 @@ var MapRenderer = class {
4226
4304
  }
4227
4305
  }
4228
4306
  }
4307
+ if (options.controls && !this.controlsAdded) {
4308
+ this.addControls(options.controls);
4309
+ }
4310
+ if (options.legend && !this.legendBuilt) {
4311
+ this.buildLegend(this.createLegendContainer(options.legend), layers, options.legend);
4312
+ }
4229
4313
  Promise.all(layers.map((layer) => this.addLayer(layer))).then(() => {
4230
4314
  this.emit("load", void 0);
4231
4315
  options.onLoad?.();
@@ -4279,16 +4363,43 @@ var MapRenderer = class {
4279
4363
  }
4280
4364
  /**
4281
4365
  * Add controls to the map
4366
+ *
4367
+ * @remarks
4368
+ * Called automatically on map load when a `controls:` config was passed via
4369
+ * {@link MapRendererOptions}. Calling it manually marks controls as added so
4370
+ * the automatic invocation is skipped (no double-add).
4282
4371
  */
4283
4372
  addControls(config) {
4373
+ this.controlsAdded = true;
4284
4374
  this.controlsManager.addControls(config);
4285
4375
  }
4286
4376
  /**
4287
4377
  * Build legend in container
4378
+ *
4379
+ * @remarks
4380
+ * Called automatically on map load when a `legend:` config was passed via
4381
+ * {@link MapRendererOptions}. Calling it manually marks the legend as built
4382
+ * so the automatic invocation is skipped (no double-build).
4288
4383
  */
4289
4384
  buildLegend(container, layers, config) {
4385
+ this.legendBuilt = true;
4290
4386
  this.legendBuilder.build(container, layers, config);
4291
4387
  }
4388
+ /**
4389
+ * Create a positioned container inside the map element for the auto legend
4390
+ */
4391
+ createLegendContainer(config) {
4392
+ const el = document.createElement("div");
4393
+ el.className = "ml-map-legend";
4394
+ const position = config.position ?? "top-left";
4395
+ el.style.position = "absolute";
4396
+ el.style.zIndex = "1";
4397
+ el.style[position.includes("top") ? "top" : "bottom"] = "10px";
4398
+ el.style[position.includes("left") ? "left" : "right"] = "10px";
4399
+ (this.containerEl ?? this.map.getContainer()).appendChild(el);
4400
+ this.autoLegendContainer = el;
4401
+ return el;
4402
+ }
4292
4403
  /**
4293
4404
  * Get the legend builder instance
4294
4405
  */
@@ -4331,6 +4442,8 @@ var MapRenderer = class {
4331
4442
  this.eventHandler.destroy();
4332
4443
  this.layerManager.destroy();
4333
4444
  this.controlsManager.removeAllControls();
4445
+ this.autoLegendContainer?.remove();
4446
+ this.autoLegendContainer = null;
4334
4447
  this.eventListeners.clear();
4335
4448
  this.map.remove();
4336
4449
  }
@@ -4514,6 +4627,15 @@ var MLMap = class extends HTMLElement {
4514
4627
  * Render the map with the given configuration
4515
4628
  */
4516
4629
  renderMap(mapBlock) {
4630
+ if (!mapBlock.config?.mapStyle) {
4631
+ this.handleError([
4632
+ {
4633
+ path: "config.mapStyle",
4634
+ message: 'mapStyle is required for standalone maps. Add it to your config, for example: mapStyle: "https://demotiles.maplibre.org/style.json" (inheriting a defaultMapStyle is a feature of the Astro builders, not the standalone <ml-map> element).'
4635
+ }
4636
+ ]);
4637
+ return;
4638
+ }
4517
4639
  if (this.renderer) {
4518
4640
  this.renderer.destroy();
4519
4641
  this.renderer = null;
@@ -4525,8 +4647,10 @@ var MLMap = class extends HTMLElement {
4525
4647
  }
4526
4648
  this.appendChild(this.mapContainer);
4527
4649
  try {
4528
- const { config, sources, layers = [] } = mapBlock;
4650
+ const { config, sources, layers = [], controls, legend } = mapBlock;
4529
4651
  this.renderer = new MapRenderer(this.mapContainer, config, layers, {
4652
+ controls,
4653
+ legend,
4530
4654
  onLoad: () => {
4531
4655
  },
4532
4656
  onError: (error) => {