@smartnet360/svelte-components 0.0.87 → 0.0.89

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.
Files changed (34) hide show
  1. package/dist/map-v3/core/components/MapStoreBridge.svelte +25 -0
  2. package/dist/map-v3/core/components/MapStoreBridge.svelte.d.ts +8 -0
  3. package/dist/map-v3/core/index.d.ts +1 -0
  4. package/dist/map-v3/core/index.js +1 -0
  5. package/dist/map-v3/demo/DemoMap.svelte +10 -0
  6. package/dist/map-v3/features/cells/components/CellFilterControl.svelte +24 -6
  7. package/dist/map-v3/features/cells/components/CellFilterControl.svelte.d.ts +3 -0
  8. package/dist/map-v3/features/cells/constants/statusStyles.d.ts +14 -0
  9. package/dist/map-v3/features/cells/constants/statusStyles.js +49 -0
  10. package/dist/map-v3/features/cells/constants.d.ts +1 -0
  11. package/dist/map-v3/features/cells/constants.js +54 -13
  12. package/dist/map-v3/features/cells/layers/CellLabelsLayer.svelte +7 -2
  13. package/dist/map-v3/features/cells/layers/CellsLayer.svelte +28 -12
  14. package/dist/map-v3/features/cells/logic/geometry.d.ts +1 -1
  15. package/dist/map-v3/features/cells/logic/geometry.js +26 -5
  16. package/dist/map-v3/features/repeaters/components/RepeaterFilterControl.svelte +14 -0
  17. package/dist/map-v3/features/repeaters/layers/RepeaterLabelsLayer.svelte +57 -26
  18. package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte +309 -0
  19. package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte.d.ts +20 -0
  20. package/dist/map-v3/features/selection/index.d.ts +9 -0
  21. package/dist/map-v3/features/selection/index.js +10 -0
  22. package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte +209 -0
  23. package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte.d.ts +13 -0
  24. package/dist/map-v3/features/selection/stores/selection.store.svelte.d.ts +84 -0
  25. package/dist/map-v3/features/selection/stores/selection.store.svelte.js +174 -0
  26. package/dist/map-v3/features/selection/types.d.ts +25 -0
  27. package/dist/map-v3/features/selection/types.js +4 -0
  28. package/dist/map-v3/features/sites/components/SiteFilterControl.svelte +14 -0
  29. package/dist/map-v3/features/sites/layers/SiteLabelsLayer.svelte +57 -26
  30. package/dist/map-v3/index.d.ts +1 -0
  31. package/dist/map-v3/index.js +2 -0
  32. package/dist/map-v3/shared/controls/MapControl.svelte +37 -10
  33. package/dist/map-v3/shared/controls/MapControl.svelte.d.ts +2 -0
  34. package/package.json +1 -1
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Feature Selection Store - Svelte 5 Runes Implementation
3
+ *
4
+ * Manages selection of map features (cells, sites) with click detection.
5
+ */
6
+ export class FeatureSelectionStore {
7
+ selectedFeatures = $state([]);
8
+ map = $state(null);
9
+ selectionMode = $state(false);
10
+ idProperty = $state('siteId');
11
+ queryLayers = $state([
12
+ 'cells-layer',
13
+ 'sites-layer'
14
+ ]);
15
+ clickHandler = null;
16
+ onSelectionChange;
17
+ /**
18
+ * Initialize the store with a map instance
19
+ */
20
+ setMap(mapInstance) {
21
+ this.map = mapInstance;
22
+ this.setupClickHandler();
23
+ }
24
+ /**
25
+ * Set which property to use as the ID
26
+ */
27
+ setIdProperty(property) {
28
+ this.idProperty = property;
29
+ }
30
+ /**
31
+ * Set which layers to query for features
32
+ */
33
+ setQueryLayers(layers) {
34
+ this.queryLayers = layers;
35
+ }
36
+ /**
37
+ * Register callback for selection changes
38
+ */
39
+ setSelectionChangeCallback(callback) {
40
+ this.onSelectionChange = callback;
41
+ }
42
+ /**
43
+ * Setup global click handler for any feature
44
+ */
45
+ setupClickHandler() {
46
+ if (!this.map)
47
+ return;
48
+ this.clickHandler = (e) => {
49
+ if (!this.selectionMode)
50
+ return;
51
+ // Query specific layers for features
52
+ const features = this.map.queryRenderedFeatures(e.point, {
53
+ layers: this.queryLayers
54
+ });
55
+ if (features && features.length > 0) {
56
+ // Get the topmost feature with an id
57
+ for (const feature of features) {
58
+ // Use the configured property as the ID
59
+ const featureId = feature.properties?.[this.idProperty] || feature.id;
60
+ const siteId = feature.properties?.siteId;
61
+ const cellName = feature.properties?.cellName;
62
+ if (featureId) {
63
+ this.toggleFeatureSelection(String(featureId), feature.layer?.id, feature.properties || undefined, siteId, cellName);
64
+ break; // Only select the topmost feature
65
+ }
66
+ }
67
+ }
68
+ };
69
+ this.map.on('click', this.clickHandler);
70
+ }
71
+ /**
72
+ * Enable selection mode
73
+ */
74
+ enableSelectionMode() {
75
+ this.selectionMode = true;
76
+ }
77
+ /**
78
+ * Disable selection mode
79
+ */
80
+ disableSelectionMode() {
81
+ this.selectionMode = false;
82
+ }
83
+ /**
84
+ * Toggle a feature in the selection
85
+ */
86
+ toggleFeatureSelection(id, layerId, properties, siteId, cellName) {
87
+ const index = this.selectedFeatures.findIndex(f => f.id === id);
88
+ if (index >= 0) {
89
+ // Remove if already selected
90
+ this.selectedFeatures.splice(index, 1);
91
+ }
92
+ else {
93
+ // Add if not selected
94
+ this.selectedFeatures.push({ id, layerId, properties, siteId, cellName });
95
+ }
96
+ // Trigger callback
97
+ if (this.onSelectionChange) {
98
+ this.onSelectionChange(this.selectedFeatures);
99
+ }
100
+ }
101
+ /**
102
+ * Add a feature to the selection
103
+ */
104
+ addFeatureSelection(id, layerId, properties, siteId, cellName) {
105
+ const exists = this.selectedFeatures.some(f => f.id === id);
106
+ if (!exists) {
107
+ this.selectedFeatures.push({ id, layerId, properties, siteId, cellName });
108
+ if (this.onSelectionChange) {
109
+ this.onSelectionChange(this.selectedFeatures);
110
+ }
111
+ }
112
+ }
113
+ /**
114
+ * Remove a feature from the selection
115
+ */
116
+ removeFeatureSelection(id) {
117
+ const index = this.selectedFeatures.findIndex(f => f.id === id);
118
+ if (index >= 0) {
119
+ this.selectedFeatures.splice(index, 1);
120
+ if (this.onSelectionChange) {
121
+ this.onSelectionChange(this.selectedFeatures);
122
+ }
123
+ }
124
+ }
125
+ /**
126
+ * Clear all selections
127
+ */
128
+ clearSelection() {
129
+ this.selectedFeatures = [];
130
+ if (this.onSelectionChange) {
131
+ this.onSelectionChange(this.selectedFeatures);
132
+ }
133
+ }
134
+ /**
135
+ * Get all selected features
136
+ */
137
+ getSelectedFeatures() {
138
+ return this.selectedFeatures;
139
+ }
140
+ /**
141
+ * Get selected feature IDs only
142
+ */
143
+ getSelectedIds() {
144
+ return this.selectedFeatures.map(f => f.id);
145
+ }
146
+ /**
147
+ * Check if a feature is selected
148
+ */
149
+ isFeatureSelected(id) {
150
+ return this.selectedFeatures.some(f => f.id === id);
151
+ }
152
+ /**
153
+ * Get selection count
154
+ */
155
+ get count() {
156
+ return this.selectedFeatures.length;
157
+ }
158
+ /**
159
+ * Cleanup - remove event handlers
160
+ */
161
+ destroy() {
162
+ if (this.map && this.clickHandler) {
163
+ this.map.off('click', this.clickHandler);
164
+ }
165
+ this.clearSelection();
166
+ this.map = null;
167
+ }
168
+ }
169
+ /**
170
+ * Factory function to create a new feature selection store
171
+ */
172
+ export function createFeatureSelectionStore() {
173
+ return new FeatureSelectionStore();
174
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Feature Selection - Type Definitions
3
+ */
4
+ export interface SelectedFeature {
5
+ /** Feature identifier */
6
+ id: string;
7
+ /** Optional site ID */
8
+ siteId?: string;
9
+ /** Optional cell name */
10
+ cellName?: string;
11
+ /** Layer ID where feature was selected from */
12
+ layerId?: string;
13
+ /** Feature properties */
14
+ properties?: Record<string, any>;
15
+ }
16
+ export interface SelectionStoreState {
17
+ /** Array of selected features */
18
+ selectedFeatures: SelectedFeature[];
19
+ /** Whether selection mode is active */
20
+ selectionMode: boolean;
21
+ /** Property name to use as feature ID */
22
+ idProperty: string;
23
+ /** Selection count */
24
+ count: number;
25
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Feature Selection - Type Definitions
3
+ */
4
+ export {};
@@ -84,9 +84,23 @@
84
84
  }
85
85
  return '#3388ff';
86
86
  }
87
+
88
+ function toggleLabels() {
89
+ displayStore.showLabels = !displayStore.showLabels;
90
+ }
87
91
  </script>
88
92
 
89
93
  <MapControl {position} title="Sites" icon="broadcast-pin" controlWidth="300px">
94
+ {#snippet actions()}
95
+ <button
96
+ class="btn btn-sm btn-outline-secondary border-0 p-1 px-2"
97
+ title={displayStore.showLabels ? 'Hide Labels' : 'Show Labels'}
98
+ aria-label={displayStore.showLabels ? 'Hide Labels' : 'Show Labels'}
99
+ onclick={toggleLabels}
100
+ >
101
+ <i class="bi bi-tag{displayStore.showLabels ? '-fill' : ''}"></i>
102
+ </button>
103
+ {/snippet}
90
104
  <div class="p-2">
91
105
  <!-- <div class="mb-2 text-muted small">
92
106
  {dataStore.sites.length} Sites
@@ -118,35 +118,66 @@
118
118
  features
119
119
  };
120
120
 
121
+ // 4. Update Source
121
122
  const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
122
123
  if (source) {
123
124
  source.setData(geojson);
124
- } else {
125
- map.addSource(sourceId, {
126
- type: 'geojson',
127
- data: geojson
128
- });
129
-
130
- map.addLayer({
131
- id: layerId,
132
- type: 'symbol',
133
- source: sourceId,
134
- layout: {
135
- 'text-field': ['get', 'label'],
136
- 'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],
137
- 'text-size': displayStore.labelFontSize,
138
- 'text-offset': ['get', 'offset'],
139
- 'text-anchor': 'center',
140
- 'text-justify': 'auto',
141
- 'text-allow-overlap': false,
142
- 'text-ignore-placement': false
143
- },
144
- paint: {
145
- 'text-color': displayStore.labelColor,
146
- 'text-halo-color': displayStore.labelHaloColor,
147
- 'text-halo-width': displayStore.labelHaloWidth
148
- }
149
- });
150
125
  }
151
126
  }
127
+
128
+ // Initial Setup
129
+ $effect(() => {
130
+ const map = mapStore.map;
131
+ if (!map) return;
132
+
133
+ const addLayers = () => {
134
+ if (!map.getSource(sourceId)) {
135
+ map.addSource(sourceId, {
136
+ type: 'geojson',
137
+ data: { type: 'FeatureCollection', features: [] }
138
+ });
139
+ }
140
+
141
+ if (!map.getLayer(layerId)) {
142
+ map.addLayer({
143
+ id: layerId,
144
+ type: 'symbol',
145
+ source: sourceId,
146
+ layout: {
147
+ 'text-field': ['get', 'label'],
148
+ 'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],
149
+ 'text-size': displayStore.labelFontSize,
150
+ 'text-offset': ['get', 'offset'],
151
+ 'text-anchor': 'center',
152
+ 'text-justify': 'auto',
153
+ 'text-allow-overlap': false,
154
+ 'text-ignore-placement': false
155
+ },
156
+ paint: {
157
+ 'text-color': displayStore.labelColor,
158
+ 'text-halo-color': displayStore.labelHaloColor,
159
+ 'text-halo-width': displayStore.labelHaloWidth
160
+ }
161
+ });
162
+ }
163
+ };
164
+
165
+ const handleStyleLoad = () => {
166
+ addLayers();
167
+ updateLayer();
168
+ };
169
+
170
+ addLayers();
171
+ map.on('style.load', handleStyleLoad);
172
+ map.on('moveend', updateLayer);
173
+ map.on('zoomend', updateLayer);
174
+
175
+ return () => {
176
+ map.off('style.load', handleStyleLoad);
177
+ map.off('moveend', updateLayer);
178
+ map.off('zoomend', updateLayer);
179
+ if (map.getLayer(layerId)) map.removeLayer(layerId);
180
+ if (map.getSource(sourceId)) map.removeSource(sourceId);
181
+ };
182
+ });
152
183
  </script>
@@ -24,6 +24,7 @@ export { default as SiteSettingsPanel } from './features/sites/components/SiteSe
24
24
  export * from './features/sites/stores/site.data.svelte';
25
25
  export * from './features/sites/stores/site.display.svelte';
26
26
  export * from './features/sites/stores/site.registry.svelte';
27
+ export * from './features/selection';
27
28
  export { default as DemoMap } from './demo/DemoMap.svelte';
28
29
  export { demoCells } from './demo/demo-cells';
29
30
  export { demoRepeaters } from './demo/demo-repeaters';
@@ -29,6 +29,8 @@ export { default as SiteSettingsPanel } from './features/sites/components/SiteSe
29
29
  export * from './features/sites/stores/site.data.svelte';
30
30
  export * from './features/sites/stores/site.display.svelte';
31
31
  export * from './features/sites/stores/site.registry.svelte';
32
+ // Features - Selection
33
+ export * from './features/selection';
32
34
  // Demo
33
35
  export { default as DemoMap } from './demo/DemoMap.svelte';
34
36
  export { demoCells } from './demo/demo-cells';
@@ -28,6 +28,8 @@
28
28
  class?: string;
29
29
  /** Child content */
30
30
  children?: import('svelte').Snippet;
31
+ /** Optional action buttons in header */
32
+ actions?: import('svelte').Snippet;
31
33
  /** Optional offset from map edge (e.g., '12px') */
32
34
  edgeOffset?: string;
33
35
  /** Width of the map control (e.g., '360px') */
@@ -44,6 +46,7 @@
44
46
  onCollapseToggle,
45
47
  class: className = '',
46
48
  children,
49
+ actions,
47
50
  edgeOffset = '12px',
48
51
  controlWidth = '420px'
49
52
  }: Props = $props();
@@ -127,16 +130,21 @@
127
130
  {/if}
128
131
  {/if}
129
132
  </span>
130
- {#if collapsible}
131
- <button
132
- class="map-control-toggle"
133
- onclick={toggleCollapse}
134
- aria-label={collapsed ? 'Expand' : 'Collapse'}
135
- title={collapsed ? 'Expand' : 'Collapse'}
136
- >
137
- <i class="bi bi-chevron-{collapsed ? 'down' : 'up'}"></i>
138
- </button>
139
- {/if}
133
+ <div class="map-control-actions">
134
+ {#if actions}
135
+ {@render actions()}
136
+ {/if}
137
+ {#if collapsible}
138
+ <button
139
+ class="map-control-toggle"
140
+ onclick={toggleCollapse}
141
+ aria-label={collapsed ? 'Expand' : 'Collapse'}
142
+ title={collapsed ? 'Expand' : 'Collapse'}
143
+ >
144
+ <i class="bi bi-chevron-{collapsed ? 'down' : 'up'}"></i>
145
+ </button>
146
+ {/if}
147
+ </div>
140
148
  </div>
141
149
  {/if}
142
150
 
@@ -186,6 +194,25 @@
186
194
  font-size: 1.1rem;
187
195
  }
188
196
 
197
+ .map-control-actions {
198
+ display: flex;
199
+ align-items: center;
200
+ gap: 4px;
201
+ }
202
+
203
+ .map-control-actions :global(.btn) {
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ min-width: 28px;
208
+ height: 28px;
209
+ }
210
+
211
+ .map-control-actions :global(.btn i) {
212
+ font-size: 14px;
213
+ line-height: 1;
214
+ }
215
+
189
216
  .map-control-toggle {
190
217
  background: none;
191
218
  border: none;
@@ -17,6 +17,8 @@ interface Props {
17
17
  class?: string;
18
18
  /** Child content */
19
19
  children?: import('svelte').Snippet;
20
+ /** Optional action buttons in header */
21
+ actions?: import('svelte').Snippet;
20
22
  /** Optional offset from map edge (e.g., '12px') */
21
23
  edgeOffset?: string;
22
24
  /** Width of the map control (e.g., '360px') */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.87",
3
+ "version": "0.0.89",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",