@aiaiai-pt/design-system 0.17.1 → 0.19.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.
@@ -1,49 +1,90 @@
1
1
  <!--
2
2
  @component FilterBar
3
3
 
4
- Horizontal bar of filter chips with active-filter state and clear-all action.
5
- Accepts a declarative config of filters or renders children directly.
6
- Consumes --filter-chip-* and --filter-bar-* tokens from components.css.
4
+ Filter controls over a feed. Two modes:
7
5
 
8
- @example Declarative (common case)
6
+ **Facet mode** (set `facets` and/or `searchable`) — a row of labelled
7
+ dropdowns + an optional search box, the shape a browse/list/map surface
8
+ uses for its declared facets. Options are supplied by the consumer (it
9
+ owns the data source); FilterBar is presentational. A misconfigured /
10
+ undeclared facet is surfaced LOUDLY via `error` — never a silent dead
11
+ control. Consumes --filter-bar-* tokens.
12
+
13
+ • **Chip mode** (default — `filters` and/or `children`) — horizontal bar of
14
+ filter chips with active-filter state and clear-all. Consumes
15
+ --filter-chip-* and --filter-bar-* tokens.
16
+
17
+ @example Facet mode (browse/list/map)
18
+ <FilterBar
19
+ facets={[
20
+ { key: 'status', label: 'Status', value: '', options: [
21
+ { value: '', label: 'All' },
22
+ { value: 'open', label: 'Open' },
23
+ ]},
24
+ ]}
25
+ searchable
26
+ searchPlaceholder="Search"
27
+ onfacetchange={(key, value) => refetch(key, value)}
28
+ onsearch={(term) => refetch('search', term)}
29
+ />
30
+
31
+ @example Chip mode (declarative)
9
32
  <FilterBar
10
33
  filters={[
11
34
  { key: 'status', label: 'Status', type: 'toggle', options: [
12
35
  { value: 'active', label: 'Active' },
13
36
  { value: 'draft', label: 'Draft' },
14
- { value: 'error', label: 'Error' },
15
37
  ]},
16
38
  ]}
17
39
  bind:value={activeFilters}
18
40
  onchange={handleFilterChange}
19
41
  />
20
-
21
- @example Composable (custom rendering)
22
- <FilterBar bind:value={activeFilters} onchange={handleFilterChange}>
23
- {#snippet children(toggle, isActive)}
24
- <FilterBar.Chip active={isActive('status', 'active')} onclick={() => toggle('status', 'active')}>
25
- Active
26
- </FilterBar.Chip>
27
- {/snippet}
28
- </FilterBar>
29
42
  -->
30
43
  <script module>
31
44
  /**
32
45
  * @typedef {{ value: string, label: string, icon?: import('svelte').Component }} FilterOption
33
46
  * @typedef {{ key: string, label: string, type: 'toggle' | 'select' | 'multi-select', options?: FilterOption[], defaultValue?: any }} FilterDef
47
+ * @typedef {{ value: string, label: string, disabled?: boolean }} FacetOption
48
+ * @typedef {{ key: string, label: string, value?: string, options: FacetOption[] }} FacetControl
34
49
  */
35
50
  </script>
36
51
 
37
52
  <script>
53
+ import Select from './Select.svelte';
54
+ import SearchInput from './SearchInput.svelte';
55
+ import Alert from './Alert.svelte';
56
+
38
57
  let {
39
- /** @type {FilterDef[]} Declarative filter definitions */
58
+ // ─── Facet mode ───
59
+ /** @type {FacetControl[]} Labelled dropdown controls (facet mode). */
60
+ facets = [],
61
+ /** @type {boolean} Render a search box (facet mode). */
62
+ searchable = false,
63
+ /** @type {string} */
64
+ searchPlaceholder = 'Search',
65
+ /** @type {string} */
66
+ searchValue = '',
67
+ /** @type {boolean} Spinner on the search box / aria-busy on the bar. */
68
+ busy = false,
69
+ /** @type {string | undefined} Loud-fail message for misconfigured facets. */
70
+ error = undefined,
71
+ /** @type {((key: string, value: string) => void) | undefined} */
72
+ onfacetchange = undefined,
73
+ /** @type {((value: string) => void) | undefined} */
74
+ onsearch = undefined,
75
+ /** @type {(() => void) | undefined} Fires when the search box is cleared. */
76
+ onsearchclear = undefined,
77
+
78
+ // ─── Chip mode ───
79
+ /** @type {FilterDef[]} Declarative chip definitions */
40
80
  filters = [],
41
- /** @type {Record<string, any>} Active filter values (bindable) */
81
+ /** @type {Record<string, any>} Active chip values (bindable) */
42
82
  value = $bindable({}),
43
- /** @type {((value: Record<string, any>) => void) | undefined} Fires when any filter changes */
83
+ /** @type {((value: Record<string, any>) => void) | undefined} Fires when any chip changes */
44
84
  onchange = undefined,
45
85
  /** @type {(() => void) | undefined} Fires when "Clear all" is clicked */
46
86
  onclear = undefined,
87
+
47
88
  /** @type {string} */
48
89
  class: className = '',
49
90
  /** @type {import('svelte').Snippet | undefined} */
@@ -51,6 +92,10 @@
51
92
  ...rest
52
93
  } = $props();
53
94
 
95
+ // Facet mode wins whenever facet controls or search are configured; chip
96
+ // mode is the default (back-compat with the original FilterBar contract).
97
+ const isFacetMode = $derived(facets.length > 0 || searchable);
98
+
54
99
  const activeCount = $derived(
55
100
  Object.values(value).filter(v => {
56
101
  if (Array.isArray(v)) return v.length > 0;
@@ -61,7 +106,7 @@
61
106
  const showClearAll = $derived(activeCount >= 2);
62
107
 
63
108
  /**
64
- * Toggle a filter value
109
+ * Toggle a filter value (chip mode)
65
110
  * @param {string} key
66
111
  * @param {string} optionValue
67
112
  */
@@ -90,7 +135,7 @@
90
135
  }
91
136
 
92
137
  /**
93
- * Check if a filter option is active
138
+ * Check if a filter option is active (chip mode)
94
139
  * @param {string} key
95
140
  * @param {string} optionValue
96
141
  * @returns {boolean}
@@ -106,48 +151,82 @@
106
151
  onchange?.({});
107
152
  onclear?.();
108
153
  }
109
-
110
154
  </script>
111
155
 
112
- <div
113
- class="filter-bar {className}"
114
- role="group"
115
- aria-label="Filters"
116
- {...rest}
117
- >
118
- {#if children}
119
- {@render children(toggle, isActive)}
120
- {:else}
121
- <div class="filter-bar-chips">
122
- {#each filters as filter}
123
- {#if filter.options}
124
- {#each filter.options as option}
125
- {@const active = isActive(filter.key, option.value)}
126
- <button
127
- class="filter-chip"
128
- class:filter-chip-active={active}
129
- onclick={() => toggle(filter.key, option.value)}
130
- aria-pressed={active}
131
- type="button"
132
- >
133
- {option.label}
134
- </button>
135
- {/each}
136
- {/if}
137
- {/each}
138
- </div>
139
-
140
- {#if showClearAll}
141
- <button
142
- class="filter-bar-clear"
143
- onclick={clearAll}
144
- type="button"
145
- >
146
- Clear all
147
- </button>
148
- {/if}
156
+ {#if isFacetMode}
157
+ <!-- Loud-fail (never a silent dead control): a configured facet the surface
158
+ does not declare can't filter — surface it as an operator fix. -->
159
+ {#if error}
160
+ <Alert variant="error" data-testid="filter-bar-error" class="filter-bar-alert">
161
+ {error}
162
+ </Alert>
149
163
  {/if}
150
- </div>
164
+ <div
165
+ class="filter-bar filter-bar-facets {className}"
166
+ role="group"
167
+ aria-label="Filters"
168
+ aria-busy={busy}
169
+ {...rest}
170
+ >
171
+ {#if searchable}
172
+ <SearchInput
173
+ placeholder={searchPlaceholder}
174
+ value={searchValue}
175
+ loading={busy}
176
+ onsearch={(v) => onsearch?.((v ?? '').trim())}
177
+ onclear={() => onsearchclear?.()}
178
+ />
179
+ {/if}
180
+ {#each facets as facet (facet.key)}
181
+ <Select
182
+ label={facet.label}
183
+ options={facet.options}
184
+ value={facet.value ?? ''}
185
+ onchange={(v) => onfacetchange?.(facet.key, v)}
186
+ />
187
+ {/each}
188
+ </div>
189
+ {:else}
190
+ <div
191
+ class="filter-bar {className}"
192
+ role="group"
193
+ aria-label="Filters"
194
+ {...rest}
195
+ >
196
+ {#if children}
197
+ {@render children(toggle, isActive)}
198
+ {:else}
199
+ <div class="filter-bar-chips">
200
+ {#each filters as filter}
201
+ {#if filter.options}
202
+ {#each filter.options as option}
203
+ {@const active = isActive(filter.key, option.value)}
204
+ <button
205
+ class="filter-chip"
206
+ class:filter-chip-active={active}
207
+ onclick={() => toggle(filter.key, option.value)}
208
+ aria-pressed={active}
209
+ type="button"
210
+ >
211
+ {option.label}
212
+ </button>
213
+ {/each}
214
+ {/if}
215
+ {/each}
216
+ </div>
217
+
218
+ {#if showClearAll}
219
+ <button
220
+ class="filter-bar-clear"
221
+ onclick={clearAll}
222
+ type="button"
223
+ >
224
+ Clear all
225
+ </button>
226
+ {/if}
227
+ {/if}
228
+ </div>
229
+ {/if}
151
230
 
152
231
  <style>
153
232
  .filter-bar {
@@ -157,6 +236,16 @@
157
236
  flex-wrap: wrap;
158
237
  }
159
238
 
239
+ /* Facet mode: labelled dropdowns + search align to their baselines. */
240
+ .filter-bar-facets {
241
+ align-items: flex-end;
242
+ gap: var(--space-sm);
243
+ }
244
+
245
+ .filter-bar-alert {
246
+ margin-bottom: var(--space-sm);
247
+ }
248
+
160
249
  .filter-bar-chips {
161
250
  display: flex;
162
251
  align-items: center;
@@ -11,6 +11,17 @@ export type FilterDef = {
11
11
  options?: FilterOption[];
12
12
  defaultValue?: any;
13
13
  };
14
+ export type FacetOption = {
15
+ value: string;
16
+ label: string;
17
+ disabled?: boolean;
18
+ };
19
+ export type FacetControl = {
20
+ key: string;
21
+ label: string;
22
+ value?: string;
23
+ options: FacetOption[];
24
+ };
14
25
  type FilterBar = {
15
26
  $on?(type: string, callback: (e: any) => void): () => void;
16
27
  $set?(props: Partial<$$ComponentProps>): void;
@@ -18,33 +29,55 @@ type FilterBar = {
18
29
  /**
19
30
  * FilterBar
20
31
  *
21
- * Horizontal bar of filter chips with active-filter state and clear-all action.
22
- * Accepts a declarative config of filters or renders children directly.
23
- * Consumes --filter-chip-* and --filter-bar-* tokens from components.css.
32
+ * Filter controls over a feed. Two modes:
33
+ *
34
+ * **Facet mode** (set `facets` and/or `searchable`) a row of labelled
35
+ * dropdowns + an optional search box, the shape a browse/list/map surface
36
+ * uses for its declared facets. Options are supplied by the consumer (it
37
+ * owns the data source); FilterBar is presentational. A misconfigured /
38
+ * undeclared facet is surfaced LOUDLY via `error` — never a silent dead
39
+ * control. Consumes --filter-bar-* tokens.
24
40
  *
25
- * @example Declarative (common case)
41
+ * **Chip mode** (default — `filters` and/or `children`) — horizontal bar of
42
+ * filter chips with active-filter state and clear-all. Consumes
43
+ * --filter-chip-* and --filter-bar-* tokens.
44
+ *
45
+ * @example Facet mode (browse/list/map)
46
+ * <FilterBar
47
+ * facets={[
48
+ * { key: 'status', label: 'Status', value: '', options: [
49
+ * { value: '', label: 'All' },
50
+ * { value: 'open', label: 'Open' },
51
+ * ]},
52
+ * ]}
53
+ * searchable
54
+ * searchPlaceholder="Search"
55
+ * onfacetchange={(key, value) => refetch(key, value)}
56
+ * onsearch={(term) => refetch('search', term)}
57
+ * />
58
+ *
59
+ * @example Chip mode (declarative)
26
60
  * <FilterBar
27
61
  * filters={[
28
62
  * { key: 'status', label: 'Status', type: 'toggle', options: [
29
63
  * { value: 'active', label: 'Active' },
30
64
  * { value: 'draft', label: 'Draft' },
31
- * { value: 'error', label: 'Error' },
32
65
  * ]},
33
66
  * ]}
34
67
  * bind:value={activeFilters}
35
68
  * onchange={handleFilterChange}
36
69
  * />
37
- *
38
- * @example Composable (custom rendering)
39
- * <FilterBar bind:value={activeFilters} onchange={handleFilterChange}>
40
- * {#snippet children(toggle, isActive)}
41
- * <FilterBar.Chip active={isActive('status', 'active')} onclick={() => toggle('status', 'active')}>
42
- * Active
43
- * </FilterBar.Chip>
44
- * {/snippet}
45
- * </FilterBar>
46
70
  */
47
71
  declare const FilterBar: import("svelte").Component<{
72
+ facets?: any[];
73
+ searchable?: boolean;
74
+ searchPlaceholder?: string;
75
+ searchValue?: string;
76
+ busy?: boolean;
77
+ error?: any;
78
+ onfacetchange?: any;
79
+ onsearch?: any;
80
+ onsearchclear?: any;
48
81
  filters?: any[];
49
82
  value?: Record<string, any>;
50
83
  onchange?: any;
@@ -53,6 +86,15 @@ declare const FilterBar: import("svelte").Component<{
53
86
  children?: any;
54
87
  } & Record<string, any>, {}, "value">;
55
88
  type $$ComponentProps = {
89
+ facets?: any[];
90
+ searchable?: boolean;
91
+ searchPlaceholder?: string;
92
+ searchValue?: string;
93
+ busy?: boolean;
94
+ error?: any;
95
+ onfacetchange?: any;
96
+ onsearch?: any;
97
+ onsearchclear?: any;
56
98
  filters?: any[];
57
99
  value?: Record<string, any>;
58
100
  onchange?: any;
@@ -0,0 +1,37 @@
1
+ <!--
2
+ @component LinkHighlightToggle
3
+
4
+ Citizen "underline links" preference (#244 C7 accessibility pack). A single
5
+ on/off switch: "on" forces a visible underline on every in-content link, so
6
+ links are distinguishable by more than colour (WCAG 1.4.1 Use of Colour /
7
+ Selo item 6 — hyperlinks should not rely on colour alone).
8
+
9
+ Presentational, mirrors the scheme/motion controls: it reports the new value
10
+ via `onchange`; the consumer persists it (cookie) and applies the
11
+ `:root[data-link-highlight="on"]` layer (tokens/semantic.css) server-side, so
12
+ SSR paints it with no flash. The label is a prop for i18n (default English).
13
+
14
+ @example
15
+ <LinkHighlightToggle on={prefs.linkHighlight} onchange={(v) => setLinkHighlight(v)} />
16
+ -->
17
+ <script>
18
+ import Toggle from "./Toggle.svelte";
19
+
20
+ let {
21
+ /** @type {boolean} Whether link-underlining is currently on. */
22
+ on = false,
23
+ /** @type {((on: boolean) => void) | undefined} */
24
+ onchange = undefined,
25
+ /** @type {string} Accessible label (i18n). */
26
+ label = "Underline links",
27
+ ...rest
28
+ } = $props();
29
+ </script>
30
+
31
+ <Toggle
32
+ checked={on}
33
+ {label}
34
+ {onchange}
35
+ data-testid="link-highlight-toggle"
36
+ {...rest}
37
+ />
@@ -0,0 +1,31 @@
1
+ export default LinkHighlightToggle;
2
+ type LinkHighlightToggle = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * LinkHighlightToggle
8
+ *
9
+ * Citizen "underline links" preference (#244 C7 accessibility pack). A single
10
+ * on/off switch: "on" forces a visible underline on every in-content link, so
11
+ * links are distinguishable by more than colour (WCAG 1.4.1 Use of Colour /
12
+ * Selo item 6 — hyperlinks should not rely on colour alone).
13
+ *
14
+ * Presentational, mirrors the scheme/motion controls: it reports the new value
15
+ * via `onchange`; the consumer persists it (cookie) and applies the
16
+ * `:root[data-link-highlight="on"]` layer (tokens/semantic.css) server-side, so
17
+ * SSR paints it with no flash. The label is a prop for i18n (default English).
18
+ *
19
+ * @example
20
+ * <LinkHighlightToggle on={prefs.linkHighlight} onchange={(v) => setLinkHighlight(v)} />
21
+ */
22
+ declare const LinkHighlightToggle: import("svelte").Component<{
23
+ on?: boolean;
24
+ onchange?: any;
25
+ label?: string;
26
+ } & Record<string, any>, {}, "">;
27
+ type $$ComponentProps = {
28
+ on?: boolean;
29
+ onchange?: any;
30
+ label?: string;
31
+ } & Record<string, any>;
@@ -39,6 +39,14 @@
39
39
  layers = [],
40
40
  /** @type {((marker: { id: string, lon: number, lat: number, label?: string }) => void) | undefined} */
41
41
  onclick = undefined,
42
+ /** @type {import('svelte').Snippet<[any, () => void]> | undefined} —
43
+ * click-popup content. When provided, clicking a single marker (or a
44
+ * stacked pile) opens an anchored OL Overlay rendering this snippet with
45
+ * `(markerData, close)` — the "ficha resumo" intermediate step before the
46
+ * consumer's detail link — INSTEAD of firing `onclick`. Omit for the
47
+ * legacy click-to-navigate behaviour. Compose with the DS `MapPopup` for
48
+ * the card chrome (the original `let:popup` contract, now a snippet). */
49
+ popup = undefined,
42
50
  /** @type {string} */
43
51
  height = '100%',
44
52
  /** @type {string} */
@@ -48,6 +56,18 @@
48
56
 
49
57
  /** @type {HTMLElement | undefined} */
50
58
  let container = $state();
59
+ /** @type {HTMLElement | undefined} — the popup overlay's element (Svelte
60
+ * renders the snippet into it; OL positions it). */
61
+ let popupEl = $state();
62
+ /** @type {any} — the marker data of the open popup, or null. */
63
+ let selected = $state(null);
64
+ /** @type {any} — popup OL Overlay ref, set at mount. */
65
+ let _popupOverlay;
66
+
67
+ function closePopup() {
68
+ selected = null;
69
+ _popupOverlay?.setPosition(undefined);
70
+ }
51
71
 
52
72
  // Hoisted references for reactive effects
53
73
  /** @type {import('ol/Map.js').default | undefined} */
@@ -211,6 +231,22 @@
211
231
  stopEvent: false,
212
232
  });
213
233
 
234
+ // Click-popup overlay (the "ficha resumo"): anchored above the marker.
235
+ // `stopEvent: true` (OL default) keeps it in the interactive overlay
236
+ // pane so the link/close button inside the popup snippet are clickable.
237
+ // Its element is the Svelte-rendered `popupEl`; Svelte owns the content,
238
+ // OL owns the position. Only created when a `popup` snippet is supplied.
239
+ const overlays = [tooltipOverlay];
240
+ if (popup && popupEl) {
241
+ _popupOverlay = new Overlay({
242
+ element: popupEl,
243
+ positioning: 'bottom-center',
244
+ offset: [0, -18],
245
+ stopEvent: true,
246
+ });
247
+ overlays.push(_popupOverlay);
248
+ }
249
+
214
250
  const viewCenter = center
215
251
  ? fromLonLat(center)
216
252
  : markers.length > 0
@@ -225,7 +261,7 @@
225
261
  // The `layers` overlays are NOT built here — the reactive owner
226
262
  // effect above inserts them between the tiles and the cluster layer.
227
263
  layers: [tileLayer, clusterLayer],
228
- overlays: [tooltipOverlay],
264
+ overlays,
229
265
  view: new View({
230
266
  center: viewCenter,
231
267
  zoom,
@@ -267,16 +303,32 @@
267
303
  }
268
304
  });
269
305
 
306
+ // `popup` opens the anchored ficha-resumo overlay; otherwise `onclick`
307
+ // navigates (legacy). A single marker (or a stacked pile that can't split
308
+ // by zoom) "selects"; a spread cluster zooms in.
309
+ const selectMarker = (data, coords) => {
310
+ if (!data) return;
311
+ if (popup) {
312
+ selected = data;
313
+ if (coords) _popupOverlay?.setPosition(coords);
314
+ } else {
315
+ onclick?.(data);
316
+ }
317
+ };
318
+
270
319
  // Click handler
271
- if (onclick) {
320
+ if (onclick || popup) {
272
321
  map.on('click', (evt) => {
273
322
  const feature = map?.forEachFeatureAtPixel(evt.pixel, f => f);
274
- if (!feature) return;
323
+ // Clicking empty map dismisses an open popup (no-op without one).
324
+ if (!feature) { closePopup(); return; }
275
325
 
276
326
  const clustered = feature.get('features');
277
327
  if (clustered?.length === 1) {
278
- const data = clustered[0].get('markerData');
279
- if (data) onclick(data);
328
+ selectMarker(
329
+ clustered[0].get('markerData'),
330
+ (/** @type {any} */ (feature.getGeometry()))?.getCoordinates(),
331
+ );
280
332
  } else if (clustered?.length > 1) {
281
333
  // A cluster of (near-)IDENTICAL coordinates can never split by
282
334
  // zooming (stacked reports at one point are common in civic
@@ -286,8 +338,10 @@
286
338
  const lats = coords.map((c) => c[1]);
287
339
  const spread = Math.max(...lons) - Math.min(...lons) + (Math.max(...lats) - Math.min(...lats));
288
340
  if (spread < 1) { // metres in web-mercator units — a stacked pile
289
- const data = clustered[0].get('markerData');
290
- if (data) onclick(data);
341
+ selectMarker(
342
+ clustered[0].get('markerData'),
343
+ (/** @type {any} */ (feature.getGeometry()))?.getCoordinates(),
344
+ );
291
345
  return;
292
346
  }
293
347
  const view = map?.getView();
@@ -307,9 +361,16 @@
307
361
  });
308
362
  } catch (err) { renderMapError(container, 'MapCluster', /** @type {Error} */ (err)); } })();
309
363
 
364
+ // Escape closes an open popup (keyboard dismissal).
365
+ const onKeydown = (/** @type {KeyboardEvent} */ e) => {
366
+ if (e.key === 'Escape') closePopup();
367
+ };
368
+ if (typeof window !== 'undefined') window.addEventListener('keydown', onKeydown);
369
+
310
370
  return () => {
311
371
  disposed = true;
312
372
  disposeTheme?.();
373
+ if (typeof window !== 'undefined') window.removeEventListener('keydown', onKeydown);
313
374
  map?.setTarget(undefined);
314
375
  };
315
376
  });
@@ -324,6 +385,15 @@
324
385
  {...rest}
325
386
  ></div>
326
387
 
388
+ <!-- Popup overlay element. Always in the DOM (OL moves it into the overlay
389
+ pane + positions it); Svelte owns its content. The snippet receives the
390
+ selected marker + a `close` callback (the original `let:popup` contract). -->
391
+ {#if popup}
392
+ <div bind:this={popupEl} class="map-cluster-popup">
393
+ {#if selected}{@render popup(selected, closePopup)}{/if}
394
+ </div>
395
+ {/if}
396
+
327
397
  <style>
328
398
  .map-cluster {
329
399
  width: 100%;
@@ -336,4 +406,11 @@
336
406
  .map-cluster :global(.ol-viewport) {
337
407
  border-radius: inherit;
338
408
  }
409
+
410
+ /* Overlay element wrapper — OL owns its position (inline transform); this
411
+ just lifts it above the map controls. The card chrome is the consumer's
412
+ MapPopup. */
413
+ .map-cluster-popup {
414
+ z-index: 2;
415
+ }
339
416
  </style>
@@ -25,6 +25,7 @@ declare const MapCluster: import("svelte").Component<{
25
25
  tileSource?: Record<string, any>;
26
26
  layers?: any[];
27
27
  onclick?: any;
28
+ popup?: any;
28
29
  height?: string;
29
30
  class?: string;
30
31
  } & Record<string, any>, {}, "">;
@@ -36,6 +37,7 @@ type $$ComponentProps = {
36
37
  tileSource?: Record<string, any>;
37
38
  layers?: any[];
38
39
  onclick?: any;
40
+ popup?: any;
39
41
  height?: string;
40
42
  class?: string;
41
43
  } & Record<string, any>;