@smartnet360/svelte-components 0.0.100 → 0.0.102

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 (25) hide show
  1. package/dist/apps/site-check/SiteCheck.svelte +54 -272
  2. package/dist/apps/site-check/SiteCheckControls.svelte +294 -0
  3. package/dist/apps/site-check/SiteCheckControls.svelte.d.ts +30 -0
  4. package/dist/map-v2/demo/DemoMap.svelte +39 -7
  5. package/dist/map-v2/features/cells/utils/cellGeoJSON.js +1 -0
  6. package/dist/map-v2/shared/controls/FeatureSelectionControl.svelte +20 -25
  7. package/dist/map-v2/shared/controls/FeatureSelectionControl.svelte.d.ts +2 -4
  8. package/dist/map-v3/demo/DemoMap.svelte +31 -5
  9. package/dist/map-v3/demo/demo-cells.js +51 -22
  10. package/dist/map-v3/features/cells/layers/CellsLayer.svelte +29 -9
  11. package/dist/map-v3/features/cells/logic/geometry.js +3 -0
  12. package/dist/map-v3/features/cells/stores/cell.data.svelte.d.ts +27 -0
  13. package/dist/map-v3/features/cells/stores/cell.data.svelte.js +65 -0
  14. package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte +82 -65
  15. package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte.d.ts +5 -9
  16. package/dist/map-v3/features/selection/index.d.ts +1 -2
  17. package/dist/map-v3/features/selection/index.js +0 -1
  18. package/dist/map-v3/features/selection/stores/selection.store.svelte.d.ts +44 -15
  19. package/dist/map-v3/features/selection/stores/selection.store.svelte.js +163 -40
  20. package/dist/map-v3/features/selection/types.d.ts +4 -2
  21. package/dist/shared/ResizableSplitPanel.svelte +175 -0
  22. package/dist/shared/ResizableSplitPanel.svelte.d.ts +17 -0
  23. package/package.json +1 -1
  24. package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte +0 -209
  25. package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte.d.ts +0 -13
@@ -2,12 +2,15 @@
2
2
  * Feature Selection Store - Svelte 5 Runes Implementation
3
3
  *
4
4
  * Manages selection of map features (cells, sites) with click detection.
5
+ * Supports multi-cell selection based on site/sector hierarchy.
5
6
  */
6
7
  export class FeatureSelectionStore {
7
8
  selectedFeatures = $state([]);
9
+ selectedCellNames = $state(new Set()); // Track all selected cell names
8
10
  map = $state(null);
11
+ cellDataStore = null;
9
12
  selectionMode = $state(false);
10
- idProperty = $state('siteId');
13
+ idProperty = $state('site'); // 'cell', 'sector', or 'site'
11
14
  queryLayers = $state([
12
15
  'cells-layer',
13
16
  'sites-layer'
@@ -22,7 +25,13 @@ export class FeatureSelectionStore {
22
25
  this.setupClickHandler();
23
26
  }
24
27
  /**
25
- * Set which property to use as the ID
28
+ * Set the cell data store for lookup maps
29
+ */
30
+ setCellDataStore(dataStore) {
31
+ this.cellDataStore = dataStore;
32
+ }
33
+ /**
34
+ * Set which property to use as the ID (selection mode)
26
35
  */
27
36
  setIdProperty(property) {
28
37
  this.idProperty = property;
@@ -55,13 +64,16 @@ export class FeatureSelectionStore {
55
64
  if (features && features.length > 0) {
56
65
  // Get the topmost feature with an id
57
66
  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
67
  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
68
+ const siteId = feature.properties?.siteId;
69
+ if (cellName && this.cellDataStore) {
70
+ this.handleCellClick(cellName, feature.layer?.id, feature.properties || undefined);
71
+ break;
72
+ }
73
+ else if (siteId) {
74
+ // Fallback for site clicks (if sites layer exists)
75
+ this.handleSiteClick(siteId, feature.layer?.id, feature.properties || undefined);
76
+ break;
65
77
  }
66
78
  }
67
79
  }
@@ -69,64 +81,156 @@ export class FeatureSelectionStore {
69
81
  this.map.on('click', this.clickHandler);
70
82
  }
71
83
  /**
72
- * Enable selection mode
84
+ * Handle click on a cell - expand to site/sector based on mode
73
85
  */
74
- enableSelectionMode() {
75
- this.selectionMode = true;
86
+ handleCellClick(cellName, layerId, properties) {
87
+ if (!this.cellDataStore)
88
+ return;
89
+ console.log('[Selection] Clicked cell:', cellName, 'Mode:', this.idProperty);
90
+ let cellNamesToSelect = [];
91
+ let selectionId;
92
+ switch (this.idProperty) {
93
+ case 'site': {
94
+ // Select all cells in the site (first 4 digits)
95
+ const siteId = this.cellDataStore.getSiteIdFromCellName(cellName);
96
+ cellNamesToSelect = this.cellDataStore.getCellsBySiteId(siteId);
97
+ selectionId = siteId;
98
+ console.log('[Selection] Site mode - selecting', cellNamesToSelect.length, 'cells for site', siteId);
99
+ break;
100
+ }
101
+ case 'sector': {
102
+ // Select all cells in the sector (first 5 digits)
103
+ const sectorId = this.cellDataStore.getSectorIdFromCellName(cellName);
104
+ cellNamesToSelect = this.cellDataStore.getCellsBySectorId(sectorId);
105
+ selectionId = sectorId;
106
+ console.log('[Selection] Sector mode - selecting', cellNamesToSelect.length, 'cells for sector', sectorId);
107
+ break;
108
+ }
109
+ case 'cell':
110
+ default: {
111
+ // Select just this cell
112
+ cellNamesToSelect = [cellName];
113
+ selectionId = cellName;
114
+ console.log('[Selection] Cell mode - selecting 1 cell');
115
+ break;
116
+ }
117
+ }
118
+ // Toggle the selection group
119
+ this.toggleGroupSelection(selectionId, cellNamesToSelect, layerId, properties);
76
120
  }
77
121
  /**
78
- * Disable selection mode
122
+ * Handle click on a site marker (if sites layer exists)
79
123
  */
80
- disableSelectionMode() {
81
- this.selectionMode = false;
124
+ handleSiteClick(siteId, layerId, properties) {
125
+ if (!this.cellDataStore)
126
+ return;
127
+ // Always select all cells in the site when clicking site marker
128
+ const cellNamesToSelect = this.cellDataStore.getCellsBySiteId(siteId);
129
+ this.toggleGroupSelection(siteId, cellNamesToSelect, layerId, properties);
82
130
  }
83
131
  /**
84
- * Toggle a feature in the selection
132
+ * Toggle a group of cells (site or sector)
85
133
  */
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);
134
+ toggleGroupSelection(groupId, cellNames, layerId, properties) {
135
+ // Check if this group is already selected
136
+ const isSelected = this.selectedFeatures.some(f => f.id === groupId);
137
+ if (isSelected) {
138
+ // Remove the group
139
+ this.selectedFeatures = this.selectedFeatures.filter(f => f.id !== groupId);
140
+ // Remove all cell names from the set
141
+ cellNames.forEach(name => this.selectedCellNames.delete(name));
91
142
  }
92
143
  else {
93
- // Add if not selected
94
- this.selectedFeatures.push({ id, layerId, properties, siteId, cellName });
144
+ // Add the group
145
+ this.selectedFeatures.push({
146
+ id: groupId,
147
+ layerId,
148
+ properties,
149
+ cellNames // Store which cells belong to this group
150
+ });
151
+ // Add all cell names to the set
152
+ cellNames.forEach(name => this.selectedCellNames.add(name));
95
153
  }
154
+ // Update feature-state for all affected cells
155
+ this.updateFeatureStates(cellNames, !isSelected);
96
156
  // Trigger callback
97
157
  if (this.onSelectionChange) {
98
158
  this.onSelectionChange(this.selectedFeatures);
99
159
  }
100
160
  }
101
161
  /**
102
- * Add a feature to the selection
162
+ * Update Mapbox feature-state for cells
103
163
  */
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);
164
+ updateFeatureStates(cellNames, selected) {
165
+ if (!this.map || !this.cellDataStore)
166
+ return;
167
+ console.log('[Selection] Updating feature-state for', cellNames.length, 'cells, selected:', selected);
168
+ const startTime = performance.now();
169
+ let successCount = 0;
170
+ let failCount = 0;
171
+ for (const cellName of cellNames) {
172
+ const numericId = this.cellDataStore.getNumericId(cellName);
173
+ if (numericId !== undefined) {
174
+ try {
175
+ this.map.setFeatureState({ source: 'cells-source', id: numericId }, { selected });
176
+ successCount++;
177
+ }
178
+ catch (error) {
179
+ console.error('[Selection] Failed to set feature-state for', cellName, numericId, error);
180
+ failCount++;
181
+ }
182
+ }
183
+ else {
184
+ console.warn('[Selection] No numeric ID found for cell:', cellName);
185
+ failCount++;
110
186
  }
111
187
  }
188
+ const endTime = performance.now();
189
+ console.log('[Selection] Feature-state update complete:', successCount, 'success,', failCount, 'failed in', (endTime - startTime).toFixed(2), 'ms');
112
190
  }
113
191
  /**
114
- * Remove a feature from the selection
192
+ * Enable selection mode
115
193
  */
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
- }
194
+ enableSelectionMode() {
195
+ this.selectionMode = true;
196
+ }
197
+ /**
198
+ * Disable selection mode
199
+ */
200
+ disableSelectionMode() {
201
+ this.selectionMode = false;
124
202
  }
125
203
  /**
126
204
  * Clear all selections
127
205
  */
128
206
  clearSelection() {
207
+ // Clear feature-state for all currently selected cells
208
+ if (this.map && this.cellDataStore) {
209
+ for (const cellName of this.selectedCellNames) {
210
+ const numericId = this.cellDataStore.getNumericId(cellName);
211
+ if (numericId !== undefined) {
212
+ this.map.setFeatureState({ source: 'cells-source', id: numericId }, { selected: false });
213
+ }
214
+ }
215
+ }
129
216
  this.selectedFeatures = [];
217
+ this.selectedCellNames.clear();
218
+ if (this.onSelectionChange) {
219
+ this.onSelectionChange(this.selectedFeatures);
220
+ }
221
+ }
222
+ /**
223
+ * Remove a selection group by ID
224
+ */
225
+ removeFeatureSelection(id) {
226
+ const feature = this.selectedFeatures.find(f => f.id === id);
227
+ if (feature && feature.cellNames) {
228
+ // Clear feature-state for cells in this group
229
+ this.updateFeatureStates(feature.cellNames, false);
230
+ // Remove cell names from set
231
+ feature.cellNames.forEach(name => this.selectedCellNames.delete(name));
232
+ }
233
+ this.selectedFeatures = this.selectedFeatures.filter(f => f.id !== id);
130
234
  if (this.onSelectionChange) {
131
235
  this.onSelectionChange(this.selectedFeatures);
132
236
  }
@@ -138,11 +242,17 @@ export class FeatureSelectionStore {
138
242
  return this.selectedFeatures;
139
243
  }
140
244
  /**
141
- * Get selected feature IDs only
245
+ * Get selected group IDs (site/sector/cell IDs)
142
246
  */
143
247
  getSelectedIds() {
144
248
  return this.selectedFeatures.map(f => f.id);
145
249
  }
250
+ /**
251
+ * Get all selected cell names (flattened from all groups)
252
+ */
253
+ getSelectedCellNames() {
254
+ return Array.from(this.selectedCellNames);
255
+ }
146
256
  /**
147
257
  * Check if a feature is selected
148
258
  */
@@ -150,11 +260,23 @@ export class FeatureSelectionStore {
150
260
  return this.selectedFeatures.some(f => f.id === id);
151
261
  }
152
262
  /**
153
- * Get selection count
263
+ * Check if a specific cell is selected
264
+ */
265
+ isCellSelected(cellName) {
266
+ return this.selectedCellNames.has(cellName);
267
+ }
268
+ /**
269
+ * Get selection count (number of groups, not individual cells)
154
270
  */
155
271
  get count() {
156
272
  return this.selectedFeatures.length;
157
273
  }
274
+ /**
275
+ * Get total number of selected cells
276
+ */
277
+ get cellCount() {
278
+ return this.selectedCellNames.size;
279
+ }
158
280
  /**
159
281
  * Cleanup - remove event handlers
160
282
  */
@@ -164,6 +286,7 @@ export class FeatureSelectionStore {
164
286
  }
165
287
  this.clearSelection();
166
288
  this.map = null;
289
+ this.cellDataStore = null;
167
290
  }
168
291
  }
169
292
  /**
@@ -2,12 +2,14 @@
2
2
  * Feature Selection - Type Definitions
3
3
  */
4
4
  export interface SelectedFeature {
5
- /** Feature identifier */
5
+ /** Feature identifier (can be site ID, sector ID, or cell name) */
6
6
  id: string;
7
7
  /** Optional site ID */
8
8
  siteId?: string;
9
- /** Optional cell name */
9
+ /** Optional cell name (for single cell selections) */
10
10
  cellName?: string;
11
+ /** Array of cell names (for site/sector selections) */
12
+ cellNames?: string[];
11
13
  /** Layer ID where feature was selected from */
12
14
  layerId?: string;
13
15
  /** Feature properties */
@@ -0,0 +1,175 @@
1
+ <svelte:options runes={true} />
2
+
3
+ <script lang="ts">
4
+ import { onMount } from 'svelte';
5
+
6
+ interface Props {
7
+ /** Namespace for localStorage persistence */
8
+ namespace?: string;
9
+ /** Initial left panel width percentage (default: 25) */
10
+ defaultLeftWidth?: number;
11
+ /** Minimum left panel width percentage (default: 15) */
12
+ minLeftWidth?: number;
13
+ /** Maximum left panel width percentage (default: 60) */
14
+ maxLeftWidth?: number;
15
+ /** Left panel content */
16
+ left: import('svelte').Snippet;
17
+ /** Right panel content */
18
+ right: import('svelte').Snippet;
19
+ }
20
+
21
+ let {
22
+ namespace = 'split-panel',
23
+ defaultLeftWidth = 25,
24
+ minLeftWidth = 15,
25
+ maxLeftWidth = 60,
26
+ left,
27
+ right
28
+ }: Props = $props();
29
+
30
+ // State
31
+ let leftWidth = $state(defaultLeftWidth);
32
+ let isDragging = $state(false);
33
+ let containerRef: HTMLDivElement;
34
+
35
+ // Load saved split position from localStorage
36
+ onMount(() => {
37
+ const storageKey = `${namespace}:splitPosition`;
38
+ const saved = localStorage.getItem(storageKey);
39
+ if (saved) {
40
+ const parsed = parseFloat(saved);
41
+ if (!isNaN(parsed) && parsed >= minLeftWidth && parsed <= maxLeftWidth) {
42
+ leftWidth = parsed;
43
+ }
44
+ }
45
+ });
46
+
47
+ // Handle resize drag
48
+ function handleResizeStart(e: MouseEvent) {
49
+ isDragging = true;
50
+ e.preventDefault();
51
+
52
+ const handleMouseMove = (e: MouseEvent) => {
53
+ if (!isDragging || !containerRef) return;
54
+
55
+ const rect = containerRef.getBoundingClientRect();
56
+ const newWidth = ((e.clientX - rect.left) / rect.width) * 100;
57
+
58
+ // Clamp between min and max
59
+ leftWidth = Math.max(minLeftWidth, Math.min(maxLeftWidth, newWidth));
60
+ };
61
+
62
+ const handleMouseUp = () => {
63
+ isDragging = false;
64
+ // Save to localStorage
65
+ const storageKey = `${namespace}:splitPosition`;
66
+ localStorage.setItem(storageKey, leftWidth.toString());
67
+ document.removeEventListener('mousemove', handleMouseMove);
68
+ document.removeEventListener('mouseup', handleMouseUp);
69
+ };
70
+
71
+ document.addEventListener('mousemove', handleMouseMove);
72
+ document.addEventListener('mouseup', handleMouseUp);
73
+ }
74
+ </script>
75
+
76
+ <div class="resizable-container" bind:this={containerRef} class:dragging={isDragging}>
77
+ <!-- Left Panel -->
78
+ <div class="left-panel" style="width: {leftWidth}%;">
79
+ {@render left()}
80
+ </div>
81
+
82
+ <!-- Vertical Resize Handle -->
83
+ <button
84
+ class="resize-handle"
85
+ class:dragging={isDragging}
86
+ onmousedown={handleResizeStart}
87
+ aria-label="Resize split between panels"
88
+ type="button"
89
+ >
90
+ <div class="resize-handle-indicator"></div>
91
+ </button>
92
+
93
+ <!-- Right Panel -->
94
+ <div class="right-panel" style="width: {100 - leftWidth}%;">
95
+ {@render right()}
96
+ </div>
97
+ </div>
98
+
99
+ <style>
100
+ .resizable-container {
101
+ display: flex;
102
+ flex-direction: row;
103
+ width: 100%;
104
+ height: 100%;
105
+ overflow: hidden;
106
+ position: relative;
107
+ }
108
+
109
+ .left-panel {
110
+ position: relative;
111
+ border-right: 1px solid #dee2e6;
112
+ min-width: 200px;
113
+ height: 100%;
114
+ overflow: hidden;
115
+ }
116
+
117
+ .right-panel {
118
+ position: relative;
119
+ flex: 1;
120
+ height: 100%;
121
+ overflow: hidden;
122
+ }
123
+
124
+ /* Vertical Resize Handle */
125
+ .resize-handle {
126
+ position: relative;
127
+ width: 8px;
128
+ cursor: col-resize;
129
+ z-index: 100;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ transition: background-color 0.2s;
134
+ flex-shrink: 0;
135
+ border: none;
136
+ padding: 0;
137
+ background-color: transparent;
138
+ height: 100%;
139
+ }
140
+
141
+ .resize-handle:hover {
142
+ background-color: rgba(13, 110, 253, 0.1);
143
+ }
144
+
145
+ .resize-handle.dragging {
146
+ background-color: rgba(13, 110, 253, 0.2);
147
+ }
148
+
149
+ .resize-handle-indicator {
150
+ width: 2px;
151
+ height: 40px;
152
+ background-color: #dee2e6;
153
+ border-radius: 1px;
154
+ transition: all 0.2s;
155
+ }
156
+
157
+ .resize-handle:hover .resize-handle-indicator {
158
+ background-color: #0d6efd;
159
+ height: 60px;
160
+ width: 3px;
161
+ }
162
+
163
+ .resize-handle.dragging .resize-handle-indicator {
164
+ background-color: #0d6efd;
165
+ height: 80px;
166
+ width: 4px;
167
+ }
168
+
169
+ /* Prevent text selection during drag */
170
+ .resizable-container.dragging,
171
+ .resizable-container.dragging * {
172
+ user-select: none !important;
173
+ -webkit-user-select: none !important;
174
+ }
175
+ </style>
@@ -0,0 +1,17 @@
1
+ interface Props {
2
+ /** Namespace for localStorage persistence */
3
+ namespace?: string;
4
+ /** Initial left panel width percentage (default: 25) */
5
+ defaultLeftWidth?: number;
6
+ /** Minimum left panel width percentage (default: 15) */
7
+ minLeftWidth?: number;
8
+ /** Maximum left panel width percentage (default: 60) */
9
+ maxLeftWidth?: number;
10
+ /** Left panel content */
11
+ left: import('svelte').Snippet;
12
+ /** Right panel content */
13
+ right: import('svelte').Snippet;
14
+ }
15
+ declare const ResizableSplitPanel: import("svelte").Component<Props, {}, "">;
16
+ type ResizableSplitPanel = ReturnType<typeof ResizableSplitPanel>;
17
+ export default ResizableSplitPanel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.100",
3
+ "version": "0.0.102",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",