@aiaiai-pt/design-system 0.4.4 → 0.5.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.
@@ -0,0 +1,139 @@
1
+ <!--
2
+ @component MapDisplay
3
+
4
+ Read-only map showing a point or polygon. OpenLayers with configurable tiles.
5
+ Consumes --map-* tokens from components.css.
6
+
7
+ @example Point
8
+ <MapDisplay center={[-9.14, 38.74]} zoom={14} marker={[-9.14, 38.74]} />
9
+
10
+ @example Styled tiles
11
+ <MapDisplay center={[-9.14, 38.74]} tileSource={{ type: 'stadia', layer: 'stamen_toner_lite' }} />
12
+ -->
13
+ <script>
14
+ import { fromLonLat } from 'ol/proj.js';
15
+ import { createTileLayer, createMapStyles, watchTheme, renderMapError } from './map-utils.js';
16
+
17
+ let {
18
+ /** @type {[number, number]} — [longitude, latitude] WGS84 */
19
+ center = [0, 0],
20
+ /** @type {number} */
21
+ zoom = 12,
22
+ /** @type {[number, number] | undefined} — [lon, lat] for single marker */
23
+ marker = undefined,
24
+ /** @type {number[][] | undefined} — polygon coords [[lon,lat], ...] */
25
+ polygon = undefined,
26
+ /** @type {import('./map-utils.js').TileSourceConfig} */
27
+ tileSource = { type: 'osm' },
28
+ /** @type {string} */
29
+ height = '100%',
30
+ /** @type {string} */
31
+ class: className = '',
32
+ ...rest
33
+ } = $props();
34
+
35
+ /** @type {HTMLElement | undefined} */
36
+ let container = $state();
37
+
38
+ $effect(() => {
39
+ if (!container) return;
40
+
41
+ let disposed = false;
42
+ /** @type {import('ol/Map.js').default | undefined} */
43
+ let map;
44
+ /** @type {(() => void) | undefined} */
45
+ let disposeTheme;
46
+
47
+ (async () => { try {
48
+ const [
49
+ { default: OlMap },
50
+ { default: View },
51
+ { default: VectorLayer },
52
+ { default: VectorSource },
53
+ { default: Feature },
54
+ { default: Point },
55
+ { default: Polygon },
56
+ ] = await Promise.all([
57
+ import('ol/Map.js'),
58
+ import('ol/View.js'),
59
+ import('ol/layer/Vector.js'),
60
+ import('ol/source/Vector.js'),
61
+ import('ol/Feature.js'),
62
+ import('ol/geom/Point.js'),
63
+ import('ol/geom/Polygon.js'),
64
+ ]);
65
+
66
+ if (disposed) return;
67
+
68
+ const [tileLayer, styles] = await Promise.all([
69
+ createTileLayer(tileSource),
70
+ createMapStyles(container),
71
+ ]);
72
+ if (disposed) return;
73
+
74
+ /** @type {Feature[]} */
75
+ const features = [];
76
+
77
+ if (marker) {
78
+ features.push(new Feature({ geometry: new Point(fromLonLat(marker)) }));
79
+ }
80
+
81
+ if (polygon) {
82
+ features.push(new Feature({
83
+ geometry: new Polygon([polygon.map(c => fromLonLat(c))]),
84
+ }));
85
+ }
86
+
87
+ const vectorLayer = new VectorLayer({
88
+ source: new VectorSource({ features }),
89
+ style: (feature) => {
90
+ const type = feature.getGeometry()?.getType();
91
+ return type === 'Point' ? styles.marker : styles.polygon;
92
+ },
93
+ });
94
+
95
+ map = new OlMap({
96
+ target: container,
97
+ layers: [tileLayer, vectorLayer],
98
+ view: new View({
99
+ center: fromLonLat(center),
100
+ zoom,
101
+ }),
102
+ controls: [],
103
+ });
104
+
105
+ disposeTheme = watchTheme(() => {
106
+ styles.refresh();
107
+ vectorLayer.getSource()?.changed();
108
+ });
109
+ } catch (err) { renderMapError(container, 'MapDisplay', /** @type {Error} */ (err)); } })();
110
+
111
+ return () => {
112
+ disposed = true;
113
+ disposeTheme?.();
114
+ map?.setTarget(undefined);
115
+ };
116
+ });
117
+ </script>
118
+
119
+ <div
120
+ bind:this={container}
121
+ class="map-display {className}"
122
+ style:height
123
+ role="img"
124
+ aria-label="Map"
125
+ {...rest}
126
+ ></div>
127
+
128
+ <style>
129
+ .map-display {
130
+ width: 100%;
131
+ border: var(--map-border);
132
+ border-radius: var(--map-radius);
133
+ overflow: hidden;
134
+ }
135
+
136
+ .map-display :global(.ol-viewport) {
137
+ border-radius: inherit;
138
+ }
139
+ </style>
@@ -0,0 +1,35 @@
1
+ export default MapDisplay;
2
+ type MapDisplay = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * MapDisplay
8
+ *
9
+ * Read-only map showing a point or polygon. OpenLayers with configurable tiles.
10
+ * Consumes --map-* tokens from components.css.
11
+ *
12
+ * @example Point
13
+ * <MapDisplay center={[-9.14, 38.74]} zoom={14} marker={[-9.14, 38.74]} />
14
+ *
15
+ * @example Styled tiles
16
+ * <MapDisplay center={[-9.14, 38.74]} tileSource={{ type: 'stadia', layer: 'stamen_toner_lite' }} />
17
+ */
18
+ declare const MapDisplay: import("svelte").Component<{
19
+ center?: any[];
20
+ zoom?: number;
21
+ marker?: any;
22
+ polygon?: any;
23
+ tileSource?: Record<string, any>;
24
+ height?: string;
25
+ class?: string;
26
+ } & Record<string, any>, {}, "">;
27
+ type $$ComponentProps = {
28
+ center?: any[];
29
+ zoom?: number;
30
+ marker?: any;
31
+ polygon?: any;
32
+ tileSource?: Record<string, any>;
33
+ height?: string;
34
+ class?: string;
35
+ } & Record<string, any>;
@@ -0,0 +1,164 @@
1
+ <!--
2
+ @component MapHeatmap
3
+
4
+ Density visualization using OpenLayers built-in Heatmap layer.
5
+ Gradient derived from DS semantic color tokens by default.
6
+ Consumes --map-* tokens from components.css.
7
+
8
+ OL Heatmap intensity is density-driven: overlapping point quads
9
+ accumulate alpha. blur/radius are screen pixels (not map units).
10
+ Keep radius >= blur for peak intensity (blurSlope = radius/blur).
11
+
12
+ @example
13
+ <MapHeatmap
14
+ points={[{ lon: -9.14, lat: 38.74, weight: 5 }]}
15
+ radius={25}
16
+ blur={15}
17
+ />
18
+
19
+ @example Custom gradient
20
+ <MapHeatmap points={data} gradient={['#fef9ee', '#fef3c7', '#f59e0b', '#dc2626']} />
21
+ -->
22
+ <script>
23
+ import { fromLonLat } from 'ol/proj.js';
24
+ import { createTileLayer, getHeatmapGradient, watchTheme, renderMapError } from './map-utils.js';
25
+
26
+ let {
27
+ /** @type {{ lon: number, lat: number, weight?: number }[]} */
28
+ points = [],
29
+ /** @type {[number, number]} — fallback center if no points [lon, lat] */
30
+ center = [0, 0],
31
+ /** @type {number} — fallback zoom if no points */
32
+ zoom = 6,
33
+ /** @type {number} — point radius in screen pixels. Keep >= blur for peak intensity. */
34
+ radius = 25,
35
+ /** @type {number} — blur falloff in screen pixels */
36
+ blur = 15,
37
+ /** @type {string[] | undefined} — custom gradient overrides tokens */
38
+ gradient = undefined,
39
+ /** @type {import('./map-utils.js').TileSourceConfig} */
40
+ tileSource = { type: 'osm' },
41
+ /** @type {number} — max zoom when auto-fitting to points extent */
42
+ maxZoom = 17,
43
+ /** @type {string} */
44
+ height = '100%',
45
+ /** @type {string} */
46
+ class: className = '',
47
+ ...rest
48
+ } = $props();
49
+
50
+ /** @type {HTMLElement | undefined} */
51
+ let container = $state();
52
+
53
+ $effect(() => {
54
+ if (!container) return;
55
+
56
+ let disposed = false;
57
+ /** @type {import('ol/Map.js').default | undefined} */
58
+ let map;
59
+ /** @type {(() => void) | undefined} */
60
+ let disposeTheme;
61
+
62
+ (async () => { try {
63
+ const [
64
+ { default: OlMap },
65
+ { default: View },
66
+ { default: VectorSource },
67
+ { default: Heatmap },
68
+ { default: Feature },
69
+ { default: Point },
70
+ ] = await Promise.all([
71
+ import('ol/Map.js'),
72
+ import('ol/View.js'),
73
+ import('ol/source/Vector.js'),
74
+ import('ol/layer/Heatmap.js'),
75
+ import('ol/Feature.js'),
76
+ import('ol/geom/Point.js'),
77
+ ]);
78
+
79
+ if (disposed) return;
80
+
81
+ const tileLayer = await createTileLayer(tileSource);
82
+ if (disposed) return;
83
+
84
+ // Non-linear weight normalization: sqrt lifts low values so they're
85
+ // visible while preserving relative ordering. OL expects 0-1.
86
+ const maxWeight = Math.max(...points.map(p => p.weight ?? 1), 1);
87
+
88
+ const features = points.map(p => {
89
+ const f = new Feature({ geometry: new Point(fromLonLat([p.lon, p.lat])) });
90
+ f.set('weight', Math.sqrt((p.weight ?? 1) / maxWeight));
91
+ return f;
92
+ });
93
+
94
+ const vectorSource = new VectorSource({ features });
95
+
96
+ const resolvedGradient = gradient ?? getHeatmapGradient(container);
97
+
98
+ const heatmapLayer = new Heatmap({
99
+ source: vectorSource,
100
+ blur,
101
+ radius,
102
+ gradient: resolvedGradient,
103
+ weight: (/** @type {import('ol/Feature.js').default} */ feature) =>
104
+ feature.get('weight') ?? 0,
105
+ });
106
+
107
+ map = new OlMap({
108
+ target: container,
109
+ layers: [tileLayer, heatmapLayer],
110
+ view: new View({
111
+ center: fromLonLat(center),
112
+ zoom,
113
+ }),
114
+ });
115
+
116
+ // Auto-fit view to points extent
117
+ if (points.length > 0) {
118
+ const extent = vectorSource.getExtent();
119
+ if (extent && isFinite(extent[0])) {
120
+ map.getView().fit(extent, {
121
+ padding: [50, 50, 50, 50],
122
+ maxZoom,
123
+ });
124
+ }
125
+ }
126
+
127
+ // Theme reactivity: re-read gradient tokens on theme change
128
+ if (!gradient) {
129
+ disposeTheme = watchTheme(() => {
130
+ heatmapLayer.setGradient(getHeatmapGradient(container));
131
+ map?.render();
132
+ });
133
+ }
134
+ } catch (err) { renderMapError(container, 'MapHeatmap', /** @type {Error} */ (err)); } })();
135
+
136
+ return () => {
137
+ disposed = true;
138
+ disposeTheme?.();
139
+ map?.setTarget(undefined);
140
+ };
141
+ });
142
+ </script>
143
+
144
+ <div
145
+ bind:this={container}
146
+ class="map-heatmap {className}"
147
+ style:height
148
+ role="img"
149
+ aria-label="Heatmap"
150
+ {...rest}
151
+ ></div>
152
+
153
+ <style>
154
+ .map-heatmap {
155
+ width: 100%;
156
+ border: var(--map-border);
157
+ border-radius: var(--map-radius);
158
+ overflow: hidden;
159
+ }
160
+
161
+ .map-heatmap :global(.ol-viewport) {
162
+ border-radius: inherit;
163
+ }
164
+ </style>
@@ -0,0 +1,50 @@
1
+ export default MapHeatmap;
2
+ type MapHeatmap = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * MapHeatmap
8
+ *
9
+ * Density visualization using OpenLayers built-in Heatmap layer.
10
+ * Gradient derived from DS semantic color tokens by default.
11
+ * Consumes --map-* tokens from components.css.
12
+ *
13
+ * OL Heatmap intensity is density-driven: overlapping point quads
14
+ * accumulate alpha. blur/radius are screen pixels (not map units).
15
+ * Keep radius >= blur for peak intensity (blurSlope = radius/blur).
16
+ *
17
+ * @example
18
+ * <MapHeatmap
19
+ * points={[{ lon: -9.14, lat: 38.74, weight: 5 }]}
20
+ * radius={25}
21
+ * blur={15}
22
+ * />
23
+ *
24
+ * @example Custom gradient
25
+ * <MapHeatmap points={data} gradient={['#fef9ee', '#fef3c7', '#f59e0b', '#dc2626']} />
26
+ */
27
+ declare const MapHeatmap: import("svelte").Component<{
28
+ points?: any[];
29
+ center?: any[];
30
+ zoom?: number;
31
+ radius?: number;
32
+ blur?: number;
33
+ gradient?: any;
34
+ tileSource?: Record<string, any>;
35
+ maxZoom?: number;
36
+ height?: string;
37
+ class?: string;
38
+ } & Record<string, any>, {}, "">;
39
+ type $$ComponentProps = {
40
+ points?: any[];
41
+ center?: any[];
42
+ zoom?: number;
43
+ radius?: number;
44
+ blur?: number;
45
+ gradient?: any;
46
+ tileSource?: Record<string, any>;
47
+ maxZoom?: number;
48
+ height?: string;
49
+ class?: string;
50
+ } & Record<string, any>;
@@ -0,0 +1,243 @@
1
+ <!--
2
+ @component MapPicker
3
+
4
+ Interactive map for selecting a point or drawing a polygon.
5
+ OpenLayers with configurable tiles and Draw interaction.
6
+ Draw sketch styled with DS tokens (not OL blue default).
7
+ Consumes --map-* tokens from components.css.
8
+
9
+ @example Point selection
10
+ <MapPicker mode="point" onchange={(coords) => console.log(coords)} />
11
+
12
+ @example Polygon drawing
13
+ <MapPicker mode="polygon" onchange={(coords) => console.log(coords)} />
14
+ -->
15
+ <script module>
16
+ let _mappickerUid = 0;
17
+ </script>
18
+
19
+ <script>
20
+ import { fromLonLat, toLonLat } from 'ol/proj.js';
21
+ import { createTileLayer, createMapStyles, watchTheme, renderMapError } from './map-utils.js';
22
+
23
+ let {
24
+ /** @type {'point' | 'polygon'} */
25
+ mode = 'point',
26
+ /** @type {[number, number]} — initial center [lon, lat] */
27
+ center = [0, 0],
28
+ /** @type {number} */
29
+ zoom = 12,
30
+ /** @type {[number, number] | undefined} — current point value [lon, lat] */
31
+ value = $bindable(undefined),
32
+ /** @type {string | undefined} */
33
+ label = undefined,
34
+ /** @type {string | undefined} */
35
+ help = undefined,
36
+ /** @type {string | undefined} */
37
+ error = undefined,
38
+ /** @type {boolean} */
39
+ disabled = false,
40
+ /** @type {import('./map-utils.js').TileSourceConfig} */
41
+ tileSource = { type: 'osm' },
42
+ /** @type {((coords: [number, number] | number[][]) => void) | undefined} */
43
+ onchange = undefined,
44
+ /** @type {string} */
45
+ height = '100%',
46
+ /** @type {string | undefined} */
47
+ id = undefined,
48
+ /** @type {string} */
49
+ class: className = '',
50
+ ...rest
51
+ } = $props();
52
+
53
+ const fallbackId = `mappicker-${_mappickerUid++}`;
54
+ const pickerId = $derived(id ?? fallbackId);
55
+ const hintId = $derived(`${pickerId}-hint`);
56
+ const hasHint = $derived(!!error || !!help);
57
+
58
+ /** @type {HTMLElement | undefined} */
59
+ let container = $state();
60
+
61
+ $effect(() => {
62
+ if (!container || disabled) return;
63
+
64
+ let disposed = false;
65
+ /** @type {import('ol/Map.js').default | undefined} */
66
+ let map;
67
+ /** @type {(() => void) | undefined} */
68
+ let disposeTheme;
69
+
70
+ (async () => { try {
71
+ const [
72
+ { default: OlMap },
73
+ { default: View },
74
+ { default: VectorLayer },
75
+ { default: VectorSource },
76
+ { default: Feature },
77
+ { default: Point },
78
+ { default: Draw },
79
+ ] = await Promise.all([
80
+ import('ol/Map.js'),
81
+ import('ol/View.js'),
82
+ import('ol/layer/Vector.js'),
83
+ import('ol/source/Vector.js'),
84
+ import('ol/Feature.js'),
85
+ import('ol/geom/Point.js'),
86
+ import('ol/interaction/Draw.js'),
87
+ ]);
88
+
89
+ if (disposed) return;
90
+
91
+ const [tileLayer, styles] = await Promise.all([
92
+ createTileLayer(tileSource),
93
+ createMapStyles(container),
94
+ ]);
95
+ if (disposed) return;
96
+
97
+ const vectorSource = new VectorSource();
98
+
99
+ if (value && mode === 'point') {
100
+ vectorSource.addFeature(new Feature({ geometry: new Point(fromLonLat(value)) }));
101
+ }
102
+
103
+ const vectorLayer = new VectorLayer({
104
+ source: vectorSource,
105
+ style: (feature) => {
106
+ const type = feature.getGeometry()?.getType();
107
+ return type === 'Point' ? styles.marker : styles.polygon;
108
+ },
109
+ });
110
+
111
+ // Composite draw style: marker for vertices + polygon for fill/stroke
112
+ const drawStyle = [
113
+ styles.polygon,
114
+ styles.marker,
115
+ ];
116
+
117
+ const drawType = mode === 'point' ? 'Point' : 'Polygon';
118
+ const drawInteraction = new Draw({
119
+ source: vectorSource,
120
+ type: drawType,
121
+ style: drawStyle,
122
+ });
123
+
124
+ drawInteraction.on('drawstart', () => {
125
+ vectorSource.clear();
126
+ });
127
+
128
+ drawInteraction.on('drawend', (evt) => {
129
+ const geom = evt.feature.getGeometry();
130
+ if (!geom) return;
131
+
132
+ if (mode === 'point') {
133
+ const coords = /** @type {import('ol/geom/Point.js').default} */ (geom).getCoordinates();
134
+ const wgs84 = /** @type {[number, number]} */ (toLonLat(coords));
135
+ value = wgs84;
136
+ onchange?.(wgs84);
137
+ } else {
138
+ const coords = /** @type {import('ol/geom/Polygon.js').default} */ (geom).getCoordinates()[0];
139
+ const wgs84 = coords.map(c => /** @type {[number, number]} */ (toLonLat(c)));
140
+ onchange?.(wgs84);
141
+ }
142
+ });
143
+
144
+ const initialCenter = value && mode === 'point' ? fromLonLat(value) : fromLonLat(center);
145
+
146
+ map = new OlMap({
147
+ target: container,
148
+ layers: [tileLayer, vectorLayer],
149
+ view: new View({
150
+ center: initialCenter,
151
+ zoom,
152
+ }),
153
+ });
154
+
155
+ map.addInteraction(drawInteraction);
156
+
157
+ disposeTheme = watchTheme(() => {
158
+ styles.refresh();
159
+ vectorSource.changed();
160
+ });
161
+ } catch (err) { renderMapError(container, 'MapPicker', /** @type {Error} */ (err)); } })();
162
+
163
+ return () => {
164
+ disposed = true;
165
+ disposeTheme?.();
166
+ map?.setTarget(undefined);
167
+ };
168
+ });
169
+ </script>
170
+
171
+ <div class="map-picker {className}" {...rest}>
172
+ {#if label}
173
+ <label class="map-picker-label" for={pickerId}>{label}</label>
174
+ {/if}
175
+
176
+ <div
177
+ bind:this={container}
178
+ id={pickerId}
179
+ class="map-picker-canvas"
180
+ class:map-picker-error={!!error}
181
+ class:map-picker-disabled={disabled}
182
+ style:height
183
+ role="application"
184
+ aria-label={label ?? 'Map picker'}
185
+ aria-describedby={hasHint ? hintId : undefined}
186
+ ></div>
187
+
188
+ {#if error}
189
+ <span id={hintId} class="map-picker-error-text" role="alert">{error}</span>
190
+ {:else if help}
191
+ <span id={hintId} class="map-picker-help">{help}</span>
192
+ {/if}
193
+ </div>
194
+
195
+ <style>
196
+ .map-picker {
197
+ display: flex;
198
+ flex-direction: column;
199
+ gap: var(--input-label-gap);
200
+ width: 100%;
201
+ }
202
+
203
+ .map-picker-label {
204
+ font-family: var(--input-label-font);
205
+ font-size: var(--input-label-size);
206
+ letter-spacing: var(--input-label-tracking);
207
+ color: var(--input-label-color);
208
+ }
209
+
210
+ .map-picker-canvas {
211
+ width: 100%;
212
+ border: var(--map-border);
213
+ border-radius: var(--map-radius);
214
+ overflow: hidden;
215
+ cursor: crosshair;
216
+ }
217
+
218
+ .map-picker-canvas :global(.ol-viewport) {
219
+ border-radius: inherit;
220
+ }
221
+
222
+ .map-picker-canvas.map-picker-error {
223
+ border-color: var(--input-error-border-color);
224
+ }
225
+
226
+ .map-picker-canvas.map-picker-disabled {
227
+ opacity: 0.5;
228
+ pointer-events: none;
229
+ cursor: not-allowed;
230
+ }
231
+
232
+ .map-picker-help {
233
+ font-family: var(--input-help-font);
234
+ font-size: var(--input-help-size);
235
+ color: var(--input-help-color);
236
+ }
237
+
238
+ .map-picker-error-text {
239
+ font-family: var(--input-help-font);
240
+ font-size: var(--input-help-size);
241
+ color: var(--input-error-text);
242
+ }
243
+ </style>
@@ -0,0 +1,49 @@
1
+ export default MapPicker;
2
+ type MapPicker = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * MapPicker
8
+ *
9
+ * Interactive map for selecting a point or drawing a polygon.
10
+ * OpenLayers with configurable tiles and Draw interaction.
11
+ * Draw sketch styled with DS tokens (not OL blue default).
12
+ * Consumes --map-* tokens from components.css.
13
+ *
14
+ * @example Point selection
15
+ * <MapPicker mode="point" onchange={(coords) => console.log(coords)} />
16
+ *
17
+ * @example Polygon drawing
18
+ * <MapPicker mode="polygon" onchange={(coords) => console.log(coords)} />
19
+ */
20
+ declare const MapPicker: import("svelte").Component<{
21
+ mode?: string;
22
+ center?: any[];
23
+ zoom?: number;
24
+ value?: any;
25
+ label?: any;
26
+ help?: any;
27
+ error?: any;
28
+ disabled?: boolean;
29
+ tileSource?: Record<string, any>;
30
+ onchange?: any;
31
+ height?: string;
32
+ id?: any;
33
+ class?: string;
34
+ } & Record<string, any>, {}, "value">;
35
+ type $$ComponentProps = {
36
+ mode?: string;
37
+ center?: any[];
38
+ zoom?: number;
39
+ value?: any;
40
+ label?: any;
41
+ help?: any;
42
+ error?: any;
43
+ disabled?: boolean;
44
+ tileSource?: Record<string, any>;
45
+ onchange?: any;
46
+ height?: string;
47
+ id?: any;
48
+ class?: string;
49
+ } & Record<string, any>;