mertani-web-toolkit 0.1.62 → 0.1.64

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,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { onMount, onDestroy, tick, getContext } from 'svelte';
3
3
  import type { IFieldOption } from './SelectInput.js';
4
- import { Icon } from 'mertani-web-toolkit';
4
+ import { Icon } from '../../../index.js';
5
5
  import './SelectInput.css';
6
6
 
7
7
  interface Props {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,284 @@
1
+ <script lang="ts">
2
+ import { onDestroy, onMount } from 'svelte';
3
+ import type { Map as LMap, LatLngTuple, LatLngBounds, TileLayer, Marker, Layer } from 'leaflet';
4
+ import 'leaflet/dist/leaflet.css';
5
+ import { createMapContext } from '../../utils/mapContext.js';
6
+ import './map.css';
7
+ import Icon from '../Icon/Icon.svelte';
8
+
9
+ type LeafletNamespace = typeof import('leaflet');
10
+
11
+ interface Props {
12
+ center?: LatLngTuple;
13
+ zoom?: number;
14
+ minZoom?: number;
15
+ maxZoom?: number;
16
+ tileUrl?: string;
17
+ tileAttribution?: string;
18
+ scrollWheelZoom?: boolean;
19
+ zoomControl?: boolean;
20
+ autoZoom?: boolean;
21
+ class?: string;
22
+ }
23
+
24
+ let {
25
+ center,
26
+ zoom = 5,
27
+ minZoom,
28
+ maxZoom,
29
+ tileUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
30
+ tileAttribution = '©2025 Developed by <b>Mertani</b>',
31
+ scrollWheelZoom = true,
32
+ autoZoom = true,
33
+ class: className = ''
34
+ }: Props = $props();
35
+
36
+ let el: HTMLDivElement | null = $state(null);
37
+ let map: LMap | null = null;
38
+ let tile: TileLayer | null = null;
39
+ let L: LeafletNamespace | null = null;
40
+ const mapStore = createMapContext();
41
+
42
+ function calculateBounds(): LatLngBounds | null {
43
+ if (!map || !L) return null;
44
+
45
+ const bounds = L.latLngBounds([]);
46
+ let hasBounds = false;
47
+
48
+ map.eachLayer((layer: Layer) => {
49
+ if (!L) return;
50
+
51
+ // Skip tile layers
52
+ if (layer instanceof L.TileLayer) return;
53
+
54
+ // Handle MarkerClusterGroup - get all child markers
55
+ if (
56
+ (layer as any).getAllChildMarkers &&
57
+ typeof (layer as any).getAllChildMarkers === 'function'
58
+ ) {
59
+ const clusterGroup = layer as any;
60
+ const markers = clusterGroup.getAllChildMarkers() as Marker[];
61
+ markers.forEach((marker: Marker) => {
62
+ const latlng = marker.getLatLng();
63
+ if (latlng) {
64
+ bounds.extend(latlng);
65
+ hasBounds = true;
66
+ }
67
+ });
68
+ }
69
+ // Handle individual markers
70
+ else if ('getLatLng' in layer && typeof layer.getLatLng === 'function') {
71
+ const latlng = (layer as Marker).getLatLng();
72
+ if (latlng) {
73
+ bounds.extend(latlng);
74
+ hasBounds = true;
75
+ }
76
+ }
77
+ // Handle layers with bounds (GeoJSON, TopoJSON, etc.)
78
+ else if ('getBounds' in layer && typeof layer.getBounds === 'function') {
79
+ const layerBounds = layer.getBounds() as LatLngBounds;
80
+ if (layerBounds && layerBounds.isValid()) {
81
+ bounds.extend(layerBounds);
82
+ hasBounds = true;
83
+ }
84
+ }
85
+ });
86
+
87
+ return hasBounds && bounds.isValid() ? bounds : null;
88
+ }
89
+
90
+ function autoZoomToMarkers() {
91
+ if (!map || !L || !autoZoom) return;
92
+
93
+ setTimeout(() => {
94
+ if (!map || !L) return;
95
+
96
+ const bounds = calculateBounds();
97
+ if (bounds) {
98
+ map.fitBounds(bounds, { padding: [30, 30] });
99
+ } else if (center) {
100
+ map.setView(center, zoom);
101
+ } else {
102
+ map.setView([-2.5489, 118.0149] as LatLngTuple, zoom);
103
+ }
104
+ }, 100);
105
+ }
106
+
107
+ async function init() {
108
+ if (!el) return;
109
+
110
+ const leafletMod = await import('leaflet');
111
+ L = (leafletMod as any).default ?? leafletMod;
112
+
113
+ if (!L) return;
114
+
115
+ map = L.map(el, {
116
+ zoomControl: false,
117
+ zoomAnimation: true,
118
+ fadeAnimation: true,
119
+ markerZoomAnimation: true,
120
+ scrollWheelZoom,
121
+ minZoom,
122
+ maxZoom
123
+ });
124
+
125
+ L.control.zoom({ position: 'bottomright' }).addTo(map);
126
+ tile = L.tileLayer(tileUrl, { attribution: tileAttribution });
127
+ tile.addTo(map);
128
+
129
+ if (center) {
130
+ map.setView(center, zoom);
131
+ } else {
132
+ map.setView([-2.5489, 118.0149] as LatLngTuple, zoom);
133
+ }
134
+
135
+ mapStore.set(map as any);
136
+
137
+ requestAnimationFrame(() => map?.invalidateSize());
138
+ setTimeout(() => map?.invalidateSize(), 50);
139
+
140
+ if (autoZoom) {
141
+ setTimeout(() => autoZoomToMarkers(), 200);
142
+ }
143
+ }
144
+
145
+ function updateTileLayer() {
146
+ if (!map || !L) return;
147
+
148
+ if (tile) {
149
+ map.removeLayer(tile);
150
+ }
151
+
152
+ tile = L.tileLayer(tileUrl, { attribution: tileAttribution });
153
+ tile.addTo(map);
154
+ }
155
+
156
+ $effect(() => {
157
+ if (autoZoom && map && L) {
158
+ autoZoomToMarkers();
159
+ }
160
+ });
161
+
162
+ export function zoomToAll() {
163
+ if (!map || !L) return;
164
+
165
+ setTimeout(() => {
166
+ if (!map || !L) return;
167
+
168
+ const bounds = calculateBounds();
169
+
170
+ if (bounds && bounds.isValid()) {
171
+ map.flyToBounds(bounds, {
172
+ padding: [50, 50],
173
+ maxZoom: 18
174
+ });
175
+ } else if (center) {
176
+ map.flyTo(center, zoom);
177
+ } else {
178
+ map.flyTo([-2.5489, 118.0149] as LatLngTuple, zoom);
179
+ }
180
+ }, 100);
181
+ }
182
+
183
+ $effect(() => {
184
+ if (!map || autoZoom) return;
185
+ if (center) {
186
+ map.setView(center, zoom, { animate: false });
187
+ }
188
+ });
189
+
190
+ $effect(() => {
191
+ tileUrl;
192
+ tileAttribution;
193
+ updateTileLayer();
194
+ });
195
+
196
+ $effect(() => {
197
+ if (!map || !L) return;
198
+ updateTileLayer();
199
+ });
200
+
201
+ onMount(() => {
202
+ init();
203
+ });
204
+
205
+ onDestroy(() => {
206
+ try {
207
+ mapStore.set(null);
208
+ tile?.remove?.();
209
+ map?.remove?.();
210
+ } finally {
211
+ tile = null;
212
+ map = null;
213
+ }
214
+ });
215
+ </script>
216
+
217
+ <div class={`mertani-map ${className}`} bind:this={el} role="application" aria-label="Map">
218
+ <slot />
219
+ <button class="crosshair-btn" onclick={zoomToAll} type="button" aria-label="Zoom to all">
220
+ <div class="info-icon">
221
+ <Icon name="bs-crosshair" />
222
+ </div>
223
+ </button>
224
+ </div>
225
+
226
+ <style>
227
+ .mertani-map {
228
+ position: relative;
229
+ width: 100%;
230
+ height: 100%;
231
+ min-height: 280px;
232
+ }
233
+
234
+ .crosshair-btn {
235
+ position: absolute;
236
+ bottom: 24px;
237
+ left: 24px;
238
+ z-index: 1001;
239
+ background-color: color-mix(in srgb, var(--color-bg-surface), transparent 20%) !important;
240
+ backdrop-filter: blur(2px);
241
+ padding: 4px;
242
+ border-radius: 8px;
243
+ border: none;
244
+ cursor: pointer;
245
+ transition: background-color 0.2s;
246
+ }
247
+
248
+ .crosshair-btn:hover {
249
+ background-color: color-mix(in srgb, var(--color-bg-surface), transparent 5%) !important;
250
+ backdrop-filter: blur(2px);
251
+ }
252
+
253
+ .crosshair-btn .info-icon {
254
+ padding: 8px;
255
+ border-radius: 6px;
256
+ display: flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ color: var(--color-bg-act-primary);
260
+ }
261
+
262
+ .crosshair-btn .info-icon :global(svg) {
263
+ width: 24px;
264
+ height: 24px;
265
+ }
266
+
267
+ @media (max-width: 768px) {
268
+ .crosshair-btn {
269
+ bottom: 12px;
270
+ left: 12px;
271
+ padding: 3px;
272
+ border-radius: 6px;
273
+ }
274
+
275
+ .crosshair-btn .info-icon {
276
+ padding: 6px;
277
+ }
278
+
279
+ .crosshair-btn :global(svg) {
280
+ width: 20px !important;
281
+ height: 20px !important;
282
+ }
283
+ }
284
+ </style>
@@ -0,0 +1,44 @@
1
+ import type { LatLngTuple } from 'leaflet';
2
+ import 'leaflet/dist/leaflet.css';
3
+ import './map.css';
4
+ interface Props {
5
+ center?: LatLngTuple;
6
+ zoom?: number;
7
+ minZoom?: number;
8
+ maxZoom?: number;
9
+ tileUrl?: string;
10
+ tileAttribution?: string;
11
+ scrollWheelZoom?: boolean;
12
+ zoomControl?: boolean;
13
+ autoZoom?: boolean;
14
+ class?: string;
15
+ }
16
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
17
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
18
+ $$bindings?: Bindings;
19
+ } & Exports;
20
+ (internal: unknown, props: Props & {
21
+ $$events?: Events;
22
+ $$slots?: Slots;
23
+ }): Exports & {
24
+ $set?: any;
25
+ $on?: any;
26
+ };
27
+ z_$$bindings?: Bindings;
28
+ }
29
+ type $$__sveltets_2_PropsWithChildren<Props, Slots> = Props & (Slots extends {
30
+ default: any;
31
+ } ? Props extends Record<string, never> ? any : {
32
+ children?: any;
33
+ } : {});
34
+ declare const Map: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<Props, {
35
+ default: {};
36
+ }>, {
37
+ [evt: string]: CustomEvent<any>;
38
+ }, {
39
+ default: {};
40
+ }, {
41
+ zoomToAll: () => void;
42
+ }, "">;
43
+ type Map = InstanceType<typeof Map>;
44
+ export default Map;
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from 'svelte';
3
+ import { useMapContext } from '../../utils/mapContext.js';
4
+
5
+ let {
6
+ data,
7
+ style,
8
+ onEachFeature
9
+ }: {
10
+ data: any; // GeoJSON Feature/FeatureCollection
11
+ style?: any;
12
+ onEachFeature?: (feature: any, layer: any) => void;
13
+ } = $props();
14
+
15
+ const mapStore = useMapContext();
16
+ let map: any = null;
17
+ let layer: any = null;
18
+ let unsub: (() => void) | null = null;
19
+
20
+ async function rebuild() {
21
+ if (!map) return;
22
+ if (layer) {
23
+ layer.remove?.();
24
+ layer = null;
25
+ }
26
+ if (!data) return;
27
+
28
+ const leafletMod = await import('leaflet');
29
+ const L = (leafletMod as any).default ?? leafletMod;
30
+
31
+ layer = L.geoJSON(data, { style, onEachFeature });
32
+ layer.addTo(map);
33
+ }
34
+
35
+ $effect(() => {
36
+ data;
37
+ style;
38
+ onEachFeature;
39
+ rebuild();
40
+ });
41
+
42
+ unsub = (mapStore as any).subscribe((m: any) => {
43
+ map = m;
44
+ if (!map && layer) {
45
+ layer.remove?.();
46
+ layer = null;
47
+ }
48
+ if (map) rebuild();
49
+ });
50
+
51
+ onDestroy(() => {
52
+ try {
53
+ unsub?.();
54
+ } finally {
55
+ unsub = null;
56
+ layer?.remove?.();
57
+ layer = null;
58
+ }
59
+ });
60
+ </script>
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ data: any;
3
+ style?: any;
4
+ onEachFeature?: (feature: any, layer: any) => void;
5
+ };
6
+ declare const MapGeoJSON: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type MapGeoJSON = ReturnType<typeof MapGeoJSON>;
8
+ export default MapGeoJSON;