@aiaiai-pt/design-system 0.17.0 → 0.17.1

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.
@@ -73,6 +73,18 @@
73
73
  * in submit modes. */
74
74
  captcha?: Snippet;
75
75
  submitLabel?: string;
76
+ /** Forwarded VERBATIM to every `geo` parameter's MapPicker (the renderer
77
+ * stays generic — boundary/overlay semantics live in the consumer):
78
+ * `boundary` draws the dashed tenant-boundary overlay and arms the
79
+ * out-of-bounds check; `layers` are ordered GeoJSON overlays (unbounded);
80
+ * `onoutofbounds(outside, coords)` fires on every point placement when a
81
+ * boundary is set — NON-blocking, the consumer owns surfacing/gating. */
82
+ boundary?: unknown;
83
+ layers?: unknown[];
84
+ onoutofbounds?: (outside: boolean, coords: [number, number]) => void;
85
+ /** Inline error rendered by the geo MapPicker(s) (e.g. the consumer's
86
+ * out-of-bounds copy) — forwarded as MapPicker's `error`. */
87
+ geoError?: string;
76
88
  }
77
89
 
78
90
  let {
@@ -86,6 +98,10 @@
86
98
  uploadFile = undefined,
87
99
  captcha = undefined,
88
100
  submitLabel = "Submit",
101
+ boundary = undefined,
102
+ layers = [],
103
+ onoutofbounds = undefined,
104
+ geoError = undefined,
89
105
  }: Props = $props();
90
106
 
91
107
  let values = $state<Record<string, unknown>>({});
@@ -451,6 +467,10 @@
451
467
  <MapPicker
452
468
  mode="point"
453
469
  height="20rem"
470
+ {boundary}
471
+ {layers}
472
+ {onoutofbounds}
473
+ error={geoError}
454
474
  label={String(parameter.label ?? key)}
455
475
  center={Array.isArray(parameter.default_value)
456
476
  ? (parameter.default_value as [number, number])
@@ -58,6 +58,18 @@ interface Props {
58
58
  * in submit modes. */
59
59
  captcha?: Snippet;
60
60
  submitLabel?: string;
61
+ /** Forwarded VERBATIM to every `geo` parameter's MapPicker (the renderer
62
+ * stays generic — boundary/overlay semantics live in the consumer):
63
+ * `boundary` draws the dashed tenant-boundary overlay and arms the
64
+ * out-of-bounds check; `layers` are ordered GeoJSON overlays (unbounded);
65
+ * `onoutofbounds(outside, coords)` fires on every point placement when a
66
+ * boundary is set — NON-blocking, the consumer owns surfacing/gating. */
67
+ boundary?: unknown;
68
+ layers?: unknown[];
69
+ onoutofbounds?: (outside: boolean, coords: [number, number]) => void;
70
+ /** Inline error rendered by the geo MapPicker(s) (e.g. the consumer's
71
+ * out-of-bounds copy) — forwarded as MapPicker's `error`. */
72
+ geoError?: string;
61
73
  }
62
74
  declare const ActionFormRenderer: import("svelte").Component<Props, {}, "">;
63
75
  type ActionFormRenderer = ReturnType<typeof ActionFormRenderer>;
@@ -15,7 +15,7 @@
15
15
  <script>
16
16
  import { fromLonLat } from 'ol/proj.js';
17
17
  import { boundingExtent } from 'ol/extent.js';
18
- import { createTileLayer, createMapStyles, watchTheme, renderMapError } from './map-utils.js';
18
+ import { createTileLayer, createMapStyles, createOverlayLayers, watchTheme, renderMapError } from './map-utils.js';
19
19
 
20
20
  let {
21
21
  /** @type {{ id: string, lon: number, lat: number, label?: string, [key: string]: any }[]} */
@@ -31,6 +31,12 @@
31
31
  distance = 40,
32
32
  /** @type {import('./map-utils.js').TileSourceConfig} */
33
33
  tileSource = { type: 'osm' },
34
+ /** @type {import('./map-utils.js').OverlayLayerDef[]} — ordered GeoJSON
35
+ * overlays rendered between the tiles and the cluster layer. Each
36
+ * entry: inline `data` or a `url` (e.g. the platform's
37
+ * `/{app}/public/layers/{id}/features`), optional flat or GeoStyler
38
+ * `style`. Unbounded — render as many as the consumer configures. */
39
+ layers = [],
34
40
  /** @type {((marker: { id: string, lon: number, lat: number, label?: string }) => void) | undefined} */
35
41
  onclick = undefined,
36
42
  /** @type {string} */
@@ -50,11 +56,40 @@
50
56
  let _vectorSource = $state();
51
57
  /** @type {any} — Cluster source */
52
58
  let _clusterSource = $state();
59
+ /** @type {any} — shared style factory (set at mount, consumed by the
60
+ * overlay owner effect) */
61
+ let _styles = $state();
53
62
  /** @type {any} — Feature constructor */
54
63
  let _Feature;
55
64
  /** @type {any} — Point constructor */
56
65
  let _Point;
57
66
 
67
+ // The `layers` overlays have ONE owner: this effect (same contract as
68
+ // MapPicker's boundary). Consumers typically RESOLVE overlay defs
69
+ // asynchronously (layer codes → url defs, after a fetch), so a
70
+ // mount-time-only build silently drops them — the overlays must track
71
+ // `layers` for as long as the map lives. The sequence counter drops
72
+ // stale async builds when the defs change mid-flight.
73
+ /** @type {any[]} */
74
+ let _overlayLayers = [];
75
+ let _overlaySeq = 0;
76
+ $effect(() => {
77
+ const defs = layers;
78
+ const map = _map;
79
+ const styles = _styles;
80
+ if (!map || !styles) return;
81
+ const seq = ++_overlaySeq;
82
+ void (async () => {
83
+ const built = defs?.length ? await createOverlayLayers(defs, styles) : [];
84
+ if (seq !== _overlaySeq || _map !== map) return;
85
+ for (const l of _overlayLayers) map.removeLayer(l);
86
+ _overlayLayers = built;
87
+ // Between the tiles (index 0) and the cluster layer — overlays never
88
+ // cover the markers.
89
+ built.forEach((l, i) => map.getLayers().insertAt(1 + i, l));
90
+ })();
91
+ });
92
+
58
93
  // Reactive: animate when the CALLER's center/zoom props change. Guarded
59
94
  // against the mount-time run — with no explicit `center` the view was
60
95
  // initialised to the markers' extent/mean, and animating to the prop
@@ -135,6 +170,7 @@
135
170
  // Store refs for reactive effects
136
171
  _vectorSource = vectorSource;
137
172
  _clusterSource = clusterSource;
173
+ _styles = styles;
138
174
  _Feature = Feature;
139
175
  _Point = Point;
140
176
 
@@ -186,6 +222,8 @@
186
222
 
187
223
  map = new OlMap({
188
224
  target: container,
225
+ // The `layers` overlays are NOT built here — the reactive owner
226
+ // effect above inserts them between the tiles and the cluster layer.
189
227
  layers: [tileLayer, clusterLayer],
190
228
  overlays: [tooltipOverlay],
191
229
  view: new View({
@@ -23,6 +23,7 @@ declare const MapCluster: import("svelte").Component<{
23
23
  zoom?: number;
24
24
  distance?: number;
25
25
  tileSource?: Record<string, any>;
26
+ layers?: any[];
26
27
  onclick?: any;
27
28
  height?: string;
28
29
  class?: string;
@@ -33,6 +34,7 @@ type $$ComponentProps = {
33
34
  zoom?: number;
34
35
  distance?: number;
35
36
  tileSource?: Record<string, any>;
37
+ layers?: any[];
36
38
  onclick?: any;
37
39
  height?: string;
38
40
  class?: string;
@@ -41,6 +41,38 @@
41
41
  /** @type {HTMLElement | undefined} */
42
42
  let container = $state();
43
43
 
44
+ // Hoisted references for the overlay owner effect
45
+ /** @type {import('ol/Map.js').default | undefined} */
46
+ let _map = $state();
47
+ /** @type {any} — shared style factory (set at mount) */
48
+ let _styles = $state();
49
+
50
+ // The `layers` overlays have ONE owner: this effect (same contract as
51
+ // MapPicker's boundary). Consumers typically RESOLVE overlay defs
52
+ // asynchronously (layer codes → url defs, after a fetch), so a
53
+ // mount-time-only build silently drops them — the overlays must track
54
+ // `layers` for as long as the map lives. The sequence counter drops
55
+ // stale async builds when the defs change mid-flight.
56
+ /** @type {any[]} */
57
+ let _overlayLayers = [];
58
+ let _overlaySeq = 0;
59
+ $effect(() => {
60
+ const defs = layers;
61
+ const map = _map;
62
+ const styles = _styles;
63
+ if (!map || !styles) return;
64
+ const seq = ++_overlaySeq;
65
+ void (async () => {
66
+ const built = defs?.length ? await createOverlayLayers(defs, styles) : [];
67
+ if (seq !== _overlaySeq || _map !== map) return;
68
+ for (const l of _overlayLayers) map.removeLayer(l);
69
+ _overlayLayers = built;
70
+ // Between the tiles (index 0) and the marker/polygon vector layer —
71
+ // overlays never cover the marker.
72
+ built.forEach((l, i) => map.getLayers().insertAt(1 + i, l));
73
+ })();
74
+ });
75
+
44
76
  $effect(() => {
45
77
  if (!container) return;
46
78
 
@@ -77,8 +109,7 @@
77
109
  ]);
78
110
  if (disposed) return;
79
111
 
80
- const overlayLayers = await createOverlayLayers(layers, styles);
81
- if (disposed) return;
112
+ _styles = styles;
82
113
 
83
114
  /** @type {Feature[]} */
84
115
  const features = [];
@@ -103,20 +134,23 @@
103
134
 
104
135
  map = new OlMap({
105
136
  target: container,
106
- layers: [tileLayer, ...overlayLayers, vectorLayer],
137
+ // The `layers` overlays are NOT built here — the reactive owner
138
+ // effect above inserts them between the tiles and the vector layer.
139
+ layers: [tileLayer, vectorLayer],
107
140
  view: new View({
108
141
  center: fromLonLat(center),
109
142
  zoom,
110
143
  }),
111
144
  controls: [],
112
145
  });
146
+ _map = map;
113
147
 
114
148
  disposeTheme = watchTheme(() => {
115
149
  styles.refresh();
116
150
  vectorLayer.getSource()?.changed();
117
151
  // Token-styled overlays (no custom style) re-read via the shared
118
152
  // styles object; poke their sources so OL repaints.
119
- for (const l of overlayLayers) l.getSource()?.changed();
153
+ for (const l of _overlayLayers) l.getSource()?.changed();
120
154
  });
121
155
  } catch (err) { renderMapError(container, 'MapDisplay', /** @type {Error} */ (err)); } })();
122
156
 
@@ -21,7 +21,7 @@
21
21
  -->
22
22
  <script>
23
23
  import { fromLonLat } from 'ol/proj.js';
24
- import { createTileLayer, getHeatmapGradient, watchTheme, renderMapError } from './map-utils.js';
24
+ import { createTileLayer, createMapStyles, createOverlayLayers, getHeatmapGradient, watchTheme, renderMapError } from './map-utils.js';
25
25
 
26
26
  let {
27
27
  /** @type {{ lon: number, lat: number, weight?: number }[]} */
@@ -38,6 +38,12 @@
38
38
  gradient = undefined,
39
39
  /** @type {import('./map-utils.js').TileSourceConfig} */
40
40
  tileSource = { type: 'osm' },
41
+ /** @type {import('./map-utils.js').OverlayLayerDef[]} — ordered GeoJSON
42
+ * overlays rendered between the tiles and the heatmap layer. Each
43
+ * entry: inline `data` or a `url` (e.g. the platform's
44
+ * `/{app}/public/layers/{id}/features`), optional flat or GeoStyler
45
+ * `style`. Unbounded — render as many as the consumer configures. */
46
+ layers = [],
41
47
  /** @type {number} — max zoom when auto-fitting to points extent */
42
48
  maxZoom = 17,
43
49
  /** @type {string} */
@@ -50,6 +56,38 @@
50
56
  /** @type {HTMLElement | undefined} */
51
57
  let container = $state();
52
58
 
59
+ // Hoisted references for the overlay owner effect
60
+ /** @type {import('ol/Map.js').default | undefined} */
61
+ let _map = $state();
62
+ /** @type {any} — shared style factory (set at mount) */
63
+ let _styles = $state();
64
+
65
+ // The `layers` overlays have ONE owner: this effect (same contract as
66
+ // MapPicker's boundary). Consumers typically RESOLVE overlay defs
67
+ // asynchronously (layer codes → url defs, after a fetch), so a
68
+ // mount-time-only build silently drops them — the overlays must track
69
+ // `layers` for as long as the map lives. The sequence counter drops
70
+ // stale async builds when the defs change mid-flight.
71
+ /** @type {any[]} */
72
+ let _overlayLayers = [];
73
+ let _overlaySeq = 0;
74
+ $effect(() => {
75
+ const defs = layers;
76
+ const map = _map;
77
+ const styles = _styles;
78
+ if (!map || !styles) return;
79
+ const seq = ++_overlaySeq;
80
+ void (async () => {
81
+ const built = defs?.length ? await createOverlayLayers(defs, styles) : [];
82
+ if (seq !== _overlaySeq || _map !== map) return;
83
+ for (const l of _overlayLayers) map.removeLayer(l);
84
+ _overlayLayers = built;
85
+ // Between the tiles (index 0) and the heatmap layer — overlays never
86
+ // cover the heat surface.
87
+ built.forEach((l, i) => map.getLayers().insertAt(1 + i, l));
88
+ })();
89
+ });
90
+
53
91
  $effect(() => {
54
92
  if (!container) return;
55
93
 
@@ -81,6 +119,12 @@
81
119
  const tileLayer = await createTileLayer(tileSource);
82
120
  if (disposed) return;
83
121
 
122
+ // Style factory only feeds overlay fallback styles here — the heatmap
123
+ // layer itself styles via gradient tokens.
124
+ const styles = await createMapStyles(container);
125
+ if (disposed) return;
126
+ _styles = styles;
127
+
84
128
  // Non-linear weight normalization: sqrt lifts low values so they're
85
129
  // visible while preserving relative ordering. OL expects 0-1.
86
130
  const maxWeight = Math.max(...points.map(p => p.weight ?? 1), 1);
@@ -106,12 +150,15 @@
106
150
 
107
151
  map = new OlMap({
108
152
  target: container,
153
+ // The `layers` overlays are NOT built here — the reactive owner
154
+ // effect above inserts them between the tiles and the heatmap layer.
109
155
  layers: [tileLayer, heatmapLayer],
110
156
  view: new View({
111
157
  center: fromLonLat(center),
112
158
  zoom,
113
159
  }),
114
160
  });
161
+ _map = map;
115
162
 
116
163
  // Auto-fit view to points extent
117
164
  if (points.length > 0) {
@@ -32,6 +32,7 @@ declare const MapHeatmap: import("svelte").Component<{
32
32
  blur?: number;
33
33
  gradient?: any;
34
34
  tileSource?: Record<string, any>;
35
+ layers?: any[];
35
36
  maxZoom?: number;
36
37
  height?: string;
37
38
  class?: string;
@@ -44,6 +45,7 @@ type $$ComponentProps = {
44
45
  blur?: number;
45
46
  gradient?: any;
46
47
  tileSource?: Record<string, any>;
48
+ layers?: any[];
47
49
  maxZoom?: number;
48
50
  height?: string;
49
51
  class?: string;
@@ -95,6 +95,9 @@
95
95
  let container = $state();
96
96
  /** @type {import('ol/Map.js').default | undefined} */
97
97
  let _map = $state();
98
+ /** @type {any} — shared style factory (set at mount, consumed by the
99
+ * overlay owner effect) */
100
+ let _styles = $state();
98
101
  /** @type {any} — VectorSource for placing markers via search */
99
102
  let _vectorSource;
100
103
  /** @type {any} */
@@ -142,6 +145,56 @@
142
145
  onoutofbounds(!pointInRings(coords, boundaryRings), coords);
143
146
  }
144
147
 
148
+ // The dashed boundary overlay has ONE owner: this effect. Consumers
149
+ // typically FETCH the boundary (it arrives after map init), so a
150
+ // mount-time-only build silently drops it — the overlay must track
151
+ // `boundaryRings` for as long as the map lives. The sequence counter
152
+ // drops stale async builds when the rings change mid-flight.
153
+ /** @type {any} */
154
+ let _boundaryLayer = null;
155
+ let _boundarySeq = 0;
156
+ $effect(() => {
157
+ const rings = boundaryRings;
158
+ const map = _map;
159
+ if (!map || !container) return;
160
+ const seq = ++_boundarySeq;
161
+ void (async () => {
162
+ const layer = rings.length ? await createBoundaryLayer(rings, container) : null;
163
+ if (seq !== _boundarySeq || _map !== map) return;
164
+ if (_boundaryLayer) map.removeLayer(_boundaryLayer);
165
+ _boundaryLayer = layer;
166
+ if (layer) {
167
+ // Just below the pin/draw vector layer — boundary never covers the pin.
168
+ const coll = map.getLayers();
169
+ coll.insertAt(coll.getLength() - 1, layer);
170
+ }
171
+ })();
172
+ });
173
+
174
+ // The `layers` overlays get the same single-owner treatment as the
175
+ // boundary above: consumers typically RESOLVE overlay defs asynchronously
176
+ // (layer codes → url defs, after a fetch), so a mount-time-only build
177
+ // silently drops them.
178
+ /** @type {any[]} */
179
+ let _overlayLayers = [];
180
+ let _overlaySeq = 0;
181
+ $effect(() => {
182
+ const defs = layers;
183
+ const map = _map;
184
+ const styles = _styles;
185
+ if (!map || !styles) return;
186
+ const seq = ++_overlaySeq;
187
+ void (async () => {
188
+ const built = defs?.length ? await createOverlayLayers(defs, styles) : [];
189
+ if (seq !== _overlaySeq || _map !== map) return;
190
+ for (const l of _overlayLayers) map.removeLayer(l);
191
+ _overlayLayers = built;
192
+ // Between the tiles (index 0) and the boundary/pin layers — overlays
193
+ // never cover the pin.
194
+ built.forEach((l, i) => map.getLayers().insertAt(1 + i, l));
195
+ })();
196
+ });
197
+
145
198
  $effect(() => {
146
199
  if (!container || disabled) return;
147
200
 
@@ -178,11 +231,10 @@
178
231
  ]);
179
232
  if (disposed) return;
180
233
 
181
- const [overlayLayers, boundaryLayer] = await Promise.all([
182
- createOverlayLayers(layers, styles),
183
- createBoundaryLayer(boundaryRings, container),
184
- ]);
185
- if (disposed) return;
234
+ // Neither the boundary overlay NOR the `layers` overlays are built
235
+ // here — the reactive owner effects above track their late-arriving
236
+ // props for as long as the map lives.
237
+ _styles = styles;
186
238
 
187
239
  const vectorSource = new VectorSource();
188
240
  _vectorSource = vectorSource;
@@ -240,12 +292,7 @@
240
292
 
241
293
  map = new OlMap({
242
294
  target: container,
243
- layers: [
244
- tileLayer,
245
- ...overlayLayers,
246
- ...(boundaryLayer ? [boundaryLayer] : []),
247
- vectorLayer,
248
- ],
295
+ layers: [tileLayer, vectorLayer],
249
296
  view: new View({
250
297
  center: initialCenter,
251
298
  zoom,
@@ -258,7 +305,7 @@
258
305
  disposeTheme = watchTheme(() => {
259
306
  styles.refresh();
260
307
  vectorSource.changed();
261
- for (const l of overlayLayers) l.getSource()?.changed();
308
+ for (const l of _overlayLayers) l.getSource()?.changed();
262
309
  });
263
310
  } catch (err) { renderMapError(container, 'MapPicker', /** @type {Error} */ (err)); } })();
264
311
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",