@smartnet360/svelte-components 0.0.69 → 0.0.70

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.
@@ -17,12 +17,11 @@
17
17
  import ViewportSync from '../core/components/ViewportSync.svelte';
18
18
  import SitesLayer from '../features/sites/layers/SitesLayer.svelte';
19
19
  import SiteFilterControl from '../features/sites/controls/SiteFilterControl.svelte';
20
- import SiteSizeSlider from '../features/sites/controls/SiteSizeSlider.svelte';
21
20
  import SiteSelectionControl from '../features/sites/controls/SiteSelectionControl.svelte';
22
21
  import CellsLayer from '../features/cells/layers/CellsLayer.svelte';
23
22
  import CellLabelsLayer from '../features/cells/layers/CellLabelsLayer.svelte';
24
23
  import CellFilterControl from '../features/cells/controls/CellFilterControl.svelte';
25
- import CellStyleControl from '../features/cells/controls/CellStyleControl.svelte';
24
+ import FeatureSettingsControl from '../shared/controls/FeatureSettingsControl.svelte';
26
25
  import { createSiteStoreContext } from '../features/sites/stores/siteStoreContext.svelte';
27
26
  import { createCellStoreContext } from '../features/cells/stores/cellStoreContext.svelte';
28
27
  import { createViewportStore } from '../core/stores/viewportStore.svelte';
@@ -196,11 +195,12 @@
196
195
  actionButtonLabel="Open Cluster KPIs"
197
196
  />
198
197
 
199
- <!-- Cell style control - visual settings for cells -->
200
- <CellStyleControl
201
- store={cellStore}
198
+ <!-- Unified feature settings control - Sites, Cells, and Repeaters -->
199
+ <FeatureSettingsControl
200
+ siteStore={siteStore}
201
+ cellStore={cellStore}
202
202
  position="top-right"
203
- title="Cell Settings"
203
+ title="Feature Settings"
204
204
  initiallyCollapsed={true}
205
205
  icon={controlIcons.cellStyle}
206
206
  iconOnlyWhenCollapsed={useIconHeaders}
@@ -208,15 +208,6 @@
208
208
  labelFieldOptions2G={labelFieldOptions2G}
209
209
  />
210
210
 
211
- <!-- Site size control - updates store visual properties -->
212
- <SiteSizeSlider
213
- store={siteStore}
214
- position="top-right"
215
- title="Site Settings"
216
- icon={controlIcons.siteSize}
217
- iconOnlyWhenCollapsed={useIconHeaders}
218
- />
219
-
220
211
 
221
212
  </MapboxProvider>
222
213
  </div>
@@ -34,6 +34,23 @@
34
34
 
35
35
  let map = $state<mapboxgl.Map | null>(null);
36
36
  let unsubscribe: (() => void) | null = null;
37
+ let viewportUpdateTimer: ReturnType<typeof setTimeout> | null = null;
38
+ let viewportVersion = $state(0); // Increment to force $effect re-run on viewport changes
39
+
40
+ // Viewport change handler (pan/zoom/move) with debouncing
41
+ function handleViewportChange() {
42
+ if (!map) return;
43
+
44
+ // Debounce viewport updates to avoid excessive re-renders
45
+ if (viewportUpdateTimer) {
46
+ clearTimeout(viewportUpdateTimer);
47
+ }
48
+
49
+ // Wait 150ms after last pan/zoom before updating
50
+ viewportUpdateTimer = setTimeout(() => {
51
+ viewportVersion++;
52
+ }, 150);
53
+ }
37
54
 
38
55
  /**
39
56
  * Get label text for a cell based on tech and selected field
@@ -84,8 +101,18 @@
84
101
  // Get current zoom level
85
102
  const currentZoom = map.getZoom();
86
103
 
87
- // Group cells by site + azimuth
88
- const cellGroups = groupCellsByAzimuth(store.filteredCells, store.azimuthTolerance);
104
+ // Filter cells by viewport bounds (only render labels for visible cells)
105
+ const bounds = map.getBounds();
106
+ if (!bounds) {
107
+ return { type: 'FeatureCollection', features: [] };
108
+ }
109
+
110
+ const visibleCells = store.filteredCells.filter(cell =>
111
+ bounds.contains([cell.longitude, cell.latitude])
112
+ );
113
+
114
+ // Group visible cells by site + azimuth
115
+ const cellGroups = groupCellsByAzimuth(visibleCells, store.azimuthTolerance);
89
116
 
90
117
  cellGroups.forEach((cells, groupKey) => {
91
118
  // Sort cells within group for consistent stacking
@@ -123,8 +150,9 @@
123
150
  // labelOffset is treated as a percentage (e.g., 300 = 300% = beyond arc, 70 = 70% = inside arc)
124
151
  const labelDistance = (cellRadius * store.labelOffset) / 100;
125
152
 
126
- // Project label position along azimuth
127
- const origin = turf.point([cell.siteLongitude, cell.siteLatitude]);
153
+ // Project label position along azimuth from the CELL'S location (not site)
154
+ // This allows labels to follow cells that may be offset from the site center
155
+ const origin = turf.point([cell.longitude, cell.latitude]);
128
156
  const labelPosition = turf.destination(
129
157
  origin,
130
158
  labelDistance / 1000, // Convert meters to kilometers
@@ -171,6 +199,65 @@
171
199
  }
172
200
  }
173
201
 
202
+ /**
203
+ * Initialize or reinitialize the label layer
204
+ * Called on mount and when map style changes
205
+ */
206
+ async function initializeLayer() {
207
+ if (!map) return;
208
+
209
+ // Wait for style to load
210
+ await waitForStyleLoad(map);
211
+
212
+ // Add source if missing
213
+ if (!map.getSource(sourceId)) {
214
+ map.addSource(sourceId, {
215
+ type: 'geojson',
216
+ data: { type: 'FeatureCollection', features: [] }
217
+ });
218
+ }
219
+
220
+ // Add label layer if missing
221
+ if (!map.getLayer(layerId)) {
222
+ map.addLayer({
223
+ id: layerId,
224
+ type: 'symbol',
225
+ source: sourceId,
226
+ minzoom: store.minLabelZoom,
227
+ layout: {
228
+ 'text-field': ['get', 'text'],
229
+ 'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
230
+ 'text-size': store.labelSize,
231
+ 'text-anchor': 'center',
232
+ 'text-offset': [
233
+ 0,
234
+ ['/', ['get', 'stackOffset'], store.labelSize]
235
+ ],
236
+ 'text-allow-overlap': true,
237
+ 'text-ignore-placement': false,
238
+ 'text-max-width': 999,
239
+ 'text-justify': 'center'
240
+ },
241
+ paint: {
242
+ 'text-color': store.labelColor,
243
+ 'text-halo-color': store.labelHaloColor,
244
+ 'text-halo-width': store.labelHaloWidth
245
+ }
246
+ });
247
+ }
248
+
249
+ // Initial render
250
+ updateLabels();
251
+
252
+ // Add event listeners (only once)
253
+ // Note: These will persist across style changes, but the layer needs reinit
254
+ if (!map.listens('zoom')) {
255
+ map.on('zoom', updateLabels);
256
+ map.on('moveend', handleViewportChange);
257
+ map.on('zoomend', handleViewportChange);
258
+ }
259
+ }
260
+
174
261
  onMount(async () => {
175
262
  unsubscribe = mapStore.subscribe(async (m) => {
176
263
  if (!m) {
@@ -180,56 +267,20 @@
180
267
 
181
268
  map = m;
182
269
 
183
- // Wait for style to load
184
- await waitForStyleLoad(map);
270
+ // Initial layer setup
271
+ await initializeLayer();
185
272
 
186
- // Add source
187
- if (!map.getSource(sourceId)) {
188
- map.addSource(sourceId, {
189
- type: 'geojson',
190
- data: { type: 'FeatureCollection', features: [] }
191
- });
192
- }
193
-
194
- // Add label layer
195
- if (!map.getLayer(layerId)) {
196
- map.addLayer({
197
- id: layerId,
198
- type: 'symbol',
199
- source: sourceId,
200
- minzoom: store.minLabelZoom,
201
- layout: {
202
- 'text-field': ['get', 'text'],
203
- 'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
204
- 'text-size': store.labelSize,
205
- 'text-anchor': 'center', // Center horizontally only
206
- 'text-offset': [
207
- 0,
208
- ['/', ['get', 'stackOffset'], store.labelSize] // Offset includes centering adjustment
209
- ],
210
- 'text-allow-overlap': true,
211
- 'text-ignore-placement': false,
212
- 'text-max-width': 999, // Essentially disable wrapping
213
- 'text-justify': 'center' // Center horizontally
214
- },
215
- paint: {
216
- 'text-color': store.labelColor,
217
- 'text-halo-color': store.labelHaloColor,
218
- 'text-halo-width': store.labelHaloWidth
219
- }
220
- });
221
- }
222
-
223
- // Initial render
224
- updateLabels();
225
-
226
- // Listen for zoom changes to update label positions
227
- map.on('zoom', updateLabels);
273
+ // Re-initialize layer when map style changes
274
+ // Mapbox removes all custom layers/sources when setStyle() is called
275
+ map.on('style.load', initializeLayer);
228
276
  });
229
277
  });
230
278
 
231
279
  // Watch for changes in store properties and update labels
232
280
  $effect(() => {
281
+ // Track viewportVersion to force re-run on pan (even without zoom change)
282
+ viewportVersion;
283
+
233
284
  // Dependencies that should trigger label refresh
234
285
  store.filteredCells;
235
286
  store.showLabels;
@@ -255,9 +306,19 @@
255
306
  onDestroy(() => {
256
307
  unsubscribe?.();
257
308
 
309
+ // Clear any pending viewport update timer
310
+ if (viewportUpdateTimer) {
311
+ clearTimeout(viewportUpdateTimer);
312
+ }
313
+
258
314
  if (map) {
259
- // Remove zoom listener
315
+ // Remove style.load listener
316
+ map.off('style.load', initializeLayer);
317
+
318
+ // Remove all event listeners
260
319
  map.off('zoom', updateLabels);
320
+ map.off('moveend', handleViewportChange);
321
+ map.off('zoomend', handleViewportChange);
261
322
 
262
323
  if (map.getLayer(layerId)) {
263
324
  map.removeLayer(layerId);
@@ -58,6 +58,32 @@
58
58
  }, 200);
59
59
  }
60
60
 
61
+ /**
62
+ * Initialize or reinitialize the cell layers
63
+ * Called on mount and when map style changes
64
+ */
65
+ function initializeLayer() {
66
+ if (!map) return;
67
+
68
+ console.log('CellsLayer: initializeLayer called');
69
+
70
+ // Set initial zoom
71
+ store.setCurrentZoom(map.getZoom());
72
+
73
+ // Add moveend listener if not already added
74
+ // Note: We need to be careful not to duplicate listeners
75
+ map.off('moveend', handleMoveEnd); // Remove if exists
76
+ map.on('moveend', handleMoveEnd); // Add it back
77
+
78
+ // Mark as mounted to trigger $effect
79
+ mounted = true;
80
+
81
+ // Force $effect to re-run by incrementing viewportVersion
82
+ // This is critical after style.load when layers need to be recreated
83
+ // but mounted/map haven't changed (they're already true/set)
84
+ viewportVersion++;
85
+ }
86
+
61
87
  onMount(() => {
62
88
  console.log('CellsLayer: onMount, waiting for map...');
63
89
 
@@ -66,13 +92,13 @@
66
92
  if (mapInstance && !map) {
67
93
  console.log('CellsLayer: Map available, initializing...');
68
94
  map = mapInstance;
69
- mounted = true;
70
95
 
71
- // Set initial zoom
72
- store.setCurrentZoom(mapInstance.getZoom());
96
+ // Initial layer setup
97
+ initializeLayer();
73
98
 
74
- // Listen to viewport changes (pan + zoom, debounced)
75
- mapInstance.on('moveend', handleMoveEnd);
99
+ // Re-initialize layer when map style changes
100
+ // Mapbox removes all custom layers/sources when setStyle() is called
101
+ map.on('style.load', initializeLayer);
76
102
  }
77
103
  });
78
104
 
@@ -89,6 +115,9 @@
89
115
  clearTimeout(viewportUpdateTimer);
90
116
  }
91
117
 
118
+ // Remove style.load listener
119
+ map.off('style.load', initializeLayer);
120
+
92
121
  map.off('moveend', handleMoveEnd);
93
122
 
94
123
  // Clean up layers and source
@@ -15,7 +15,14 @@ export function createSiteStore(initialSites = []) {
15
15
  showLabels: false,
16
16
  showOnHover: true,
17
17
  strokeWidth: 2,
18
- strokeColor: '#ffffff'
18
+ strokeColor: '#ffffff',
19
+ labelSize: 12,
20
+ labelColor: '#000000',
21
+ labelOffset: 0,
22
+ labelProperty: 'name',
23
+ groupColorMap: new Map(),
24
+ selectionMode: false,
25
+ selectedSiteIds: new Set()
19
26
  };
20
27
  const { subscribe, update, set } = writable(defaultState);
21
28
  return {
@@ -5,7 +5,7 @@
5
5
  * Each feature (sites, cells) is completely independent with its own store, layers, and controls.
6
6
  */
7
7
  export { type MapStore, MAP_CONTEXT_KEY, MapboxProvider, ViewportSync, MapStoreBridge, MapStyleControl, createMapStore, createViewportStore, type ViewportStore, type ViewportState, useMapbox, tryUseMapbox } from './core';
8
- export { MapControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
8
+ export { MapControl, FeatureSettingsControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
9
9
  export { type Site, type SiteStoreValue, type SiteStoreContext, createSiteStore, createSiteStoreContext, SitesLayer, SiteFilterControl, SiteSelectionControl, SiteSizeSlider, sitesToGeoJSON, siteToFeature, buildSiteTree, getFilteredSites } from './features/sites';
10
10
  export { type Cell, type CellStatus, type CellStatusStyle, type CellGroupingField, type CellGroupingLabels, type CellTreeConfig, type TechnologyBandKey, type CellStoreValue, type CellStoreContext, createCellStoreContext, CellsLayer, CellLabelsLayer, CellFilterControl, CellStyleControl, cellsToGeoJSON, buildCellTree, getFilteredCells, calculateRadius, getZoomFactor, createArcPolygon, DEFAULT_CELL_TREE_CONFIG, TECHNOLOGY_BAND_COLORS, DEFAULT_STATUS_STYLES, RADIUS_MULTIPLIER } from './features/cells';
11
11
  export { DemoMap, demoSites, demoCells } from './demo';
@@ -11,7 +11,7 @@ export { MAP_CONTEXT_KEY, MapboxProvider, ViewportSync, MapStoreBridge, MapStyle
11
11
  // ============================================================================
12
12
  // SHARED UTILITIES
13
13
  // ============================================================================
14
- export { MapControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
14
+ export { MapControl, FeatureSettingsControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
15
15
  // ============================================================================
16
16
  // SITE FEATURE
17
17
  // ============================================================================
@@ -0,0 +1,206 @@
1
+ <script lang="ts">
2
+ /**
3
+ * FeatureSettingsControl - Unified settings control for Sites, Cells, and Repeaters
4
+ *
5
+ * Uses Bootstrap accordion to organize settings by feature type.
6
+ * Consolidates SiteSizeSlider and CellStyleControl into a single UI.
7
+ */
8
+
9
+ import MapControl from './MapControl.svelte';
10
+ import SiteSettingsPanel from './panels/SiteSettingsPanel.svelte';
11
+ import CellSettingsPanel from './panels/CellSettingsPanel.svelte';
12
+ import RepeaterSettingsPanel from './panels/RepeaterSettingsPanel.svelte';
13
+ import type { SiteStoreContext } from '../../features/sites/stores/siteStoreContext.svelte';
14
+ import type { CellStoreContext } from '../../features/cells/stores/cellStoreContext.svelte';
15
+ import type { Cell, CellGroupingLabels } from '../../features/cells/types';
16
+
17
+ interface Props {
18
+ /** Site store context */
19
+ siteStore: SiteStoreContext;
20
+ /** Cell store context */
21
+ cellStore: CellStoreContext;
22
+ /** Available label field options for 4G/5G cells */
23
+ labelFieldOptions4G5G: Array<keyof Cell>;
24
+ /** Available label field options for 2G cells */
25
+ labelFieldOptions2G: Array<keyof Cell>;
26
+ /** Optional label map for human-readable field names */
27
+ fieldLabels?: CellGroupingLabels;
28
+ /** Control position */
29
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
30
+ /** Control title */
31
+ title?: string;
32
+ /** Optional header icon */
33
+ icon?: string;
34
+ /** Show icon when collapsed (default: true) */
35
+ iconOnlyWhenCollapsed?: boolean;
36
+ /** Initially collapsed? */
37
+ initiallyCollapsed?: boolean;
38
+ }
39
+
40
+ let {
41
+ siteStore,
42
+ cellStore,
43
+ labelFieldOptions4G5G,
44
+ labelFieldOptions2G,
45
+ fieldLabels,
46
+ position = 'top-right',
47
+ title = 'Feature Settings',
48
+ icon = 'sliders',
49
+ iconOnlyWhenCollapsed = true,
50
+ initiallyCollapsed = true
51
+ }: Props = $props();
52
+
53
+ // Generate unique IDs for accordion items
54
+ const accordionId = `settings-accordion-${Math.random().toString(36).substring(7)}`;
55
+ </script>
56
+
57
+ <MapControl {position} {title} {icon} {iconOnlyWhenCollapsed} collapsible={true} {initiallyCollapsed}>
58
+ <div class="accordion" id={accordionId}>
59
+ <!-- Site Settings Accordion Item -->
60
+ <div class="accordion-item">
61
+ <h2 class="accordion-header" id="heading-sites">
62
+ <button
63
+ class="accordion-button collapsed"
64
+ type="button"
65
+ data-bs-toggle="collapse"
66
+ data-bs-target="#collapse-sites"
67
+ aria-expanded="false"
68
+ aria-controls="collapse-sites"
69
+ >
70
+ Site Settings
71
+ </button>
72
+ </h2>
73
+ <div
74
+ id="collapse-sites"
75
+ class="accordion-collapse collapse"
76
+ aria-labelledby="heading-sites"
77
+ >
78
+ <div class="accordion-body">
79
+ <SiteSettingsPanel store={siteStore} />
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- Cell Settings Accordion Item -->
85
+ <div class="accordion-item">
86
+ <h2 class="accordion-header" id="heading-cells">
87
+ <button
88
+ class="accordion-button collapsed"
89
+ type="button"
90
+ data-bs-toggle="collapse"
91
+ data-bs-target="#collapse-cells"
92
+ aria-expanded="false"
93
+ aria-controls="collapse-cells"
94
+ >
95
+ Cell Settings
96
+ </button>
97
+ </h2>
98
+ <div
99
+ id="collapse-cells"
100
+ class="accordion-collapse collapse"
101
+ aria-labelledby="heading-cells"
102
+ >
103
+ <div class="accordion-body">
104
+ <CellSettingsPanel
105
+ store={cellStore}
106
+ {labelFieldOptions4G5G}
107
+ {labelFieldOptions2G}
108
+ {fieldLabels}
109
+ />
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- Repeater Settings Accordion Item (Placeholder) -->
115
+ <div class="accordion-item">
116
+ <h2 class="accordion-header" id="heading-repeaters">
117
+ <button
118
+ class="accordion-button collapsed"
119
+ type="button"
120
+ data-bs-toggle="collapse"
121
+ data-bs-target="#collapse-repeaters"
122
+ aria-expanded="false"
123
+ aria-controls="collapse-repeaters"
124
+ >
125
+ Repeater Settings
126
+ </button>
127
+ </h2>
128
+ <div
129
+ id="collapse-repeaters"
130
+ class="accordion-collapse collapse"
131
+ aria-labelledby="heading-repeaters"
132
+ >
133
+ <div class="accordion-body">
134
+ <RepeaterSettingsPanel />
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </MapControl>
140
+
141
+ <style>
142
+ .accordion {
143
+ --bs-accordion-border-width: 0;
144
+ /* Fixed width to prevent resizing when sections expand/collapse */
145
+ width: 320px;
146
+ min-width: 320px;
147
+ max-width: 320px;
148
+ }
149
+
150
+ .accordion-item {
151
+ background-color: transparent;
152
+ border: none;
153
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
154
+ }
155
+
156
+ .accordion-item:last-child {
157
+ border-bottom: none;
158
+ }
159
+
160
+ .accordion-button {
161
+ font-size: 0.875rem;
162
+ font-weight: 600;
163
+ padding: 0.75rem 1rem;
164
+ background-color: transparent;
165
+ color: var(--bs-body-color);
166
+ /* Ensure chevron is visible and properly positioned */
167
+ position: relative;
168
+ display: flex;
169
+ align-items: center;
170
+ width: 100%;
171
+ text-align: left;
172
+ }
173
+
174
+ /* Preserve Bootstrap's chevron */
175
+ .accordion-button::after {
176
+ flex-shrink: 0;
177
+ width: 1.25rem;
178
+ height: 1.25rem;
179
+ margin-left: auto;
180
+ content: "";
181
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
182
+ background-repeat: no-repeat;
183
+ background-size: 1.25rem;
184
+ transition: transform 0.2s ease-in-out;
185
+ }
186
+
187
+ /* Rotate chevron when expanded */
188
+ .accordion-button:not(.collapsed)::after {
189
+ transform: rotate(-180deg);
190
+ }
191
+
192
+ .accordion-button:not(.collapsed) {
193
+ background-color: rgba(13, 110, 253, 0.05);
194
+ color: #0d6efd;
195
+ }
196
+
197
+ .accordion-button:focus {
198
+ box-shadow: none;
199
+ border-color: rgba(0, 0, 0, 0.125);
200
+ }
201
+
202
+ .accordion-body {
203
+ padding: 1rem;
204
+ padding-top: 0.5rem;
205
+ }
206
+ </style>
@@ -0,0 +1,28 @@
1
+ import type { SiteStoreContext } from '../../features/sites/stores/siteStoreContext.svelte';
2
+ import type { CellStoreContext } from '../../features/cells/stores/cellStoreContext.svelte';
3
+ import type { Cell, CellGroupingLabels } from '../../features/cells/types';
4
+ interface Props {
5
+ /** Site store context */
6
+ siteStore: SiteStoreContext;
7
+ /** Cell store context */
8
+ cellStore: CellStoreContext;
9
+ /** Available label field options for 4G/5G cells */
10
+ labelFieldOptions4G5G: Array<keyof Cell>;
11
+ /** Available label field options for 2G cells */
12
+ labelFieldOptions2G: Array<keyof Cell>;
13
+ /** Optional label map for human-readable field names */
14
+ fieldLabels?: CellGroupingLabels;
15
+ /** Control position */
16
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
17
+ /** Control title */
18
+ title?: string;
19
+ /** Optional header icon */
20
+ icon?: string;
21
+ /** Show icon when collapsed (default: true) */
22
+ iconOnlyWhenCollapsed?: boolean;
23
+ /** Initially collapsed? */
24
+ initiallyCollapsed?: boolean;
25
+ }
26
+ declare const FeatureSettingsControl: import("svelte").Component<Props, {}, "">;
27
+ type FeatureSettingsControl = ReturnType<typeof FeatureSettingsControl>;
28
+ export default FeatureSettingsControl;
@@ -157,7 +157,7 @@
157
157
  border-radius: 4px;
158
158
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
159
159
  overflow: hidden;
160
- max-width: 300px;
160
+ max-width: 400px;
161
161
  font-family: system-ui, -apple-system, sans-serif;
162
162
  }
163
163
 
@@ -0,0 +1,342 @@
1
+ <script lang="ts">
2
+ /**
3
+ * CellSettingsPanel - Cell visualization and label settings
4
+ * Reusable panel component for cell display configuration
5
+ */
6
+ import type { CellStoreContext } from '../../../features/cells/stores/cellStoreContext.svelte';
7
+ import type { Cell, CellGroupingLabels } from '../../../features/cells/types';
8
+
9
+ interface Props {
10
+ store: CellStoreContext;
11
+ labelFieldOptions4G5G: Array<keyof Cell>;
12
+ labelFieldOptions2G: Array<keyof Cell>;
13
+ fieldLabels?: CellGroupingLabels;
14
+ }
15
+
16
+ let { store, labelFieldOptions4G5G, labelFieldOptions2G, fieldLabels }: Props = $props();
17
+
18
+ // Get label for a field from the fieldLabels map or fallback to field name
19
+ function getFieldLabel(field: keyof Cell | 'none'): string {
20
+ if (field === 'none') return 'None';
21
+ return fieldLabels?.[field] || String(field);
22
+ }
23
+ </script>
24
+
25
+ <div class="settings-panel">
26
+
27
+ <!-- Base radius slider -->
28
+ <div class="control-row">
29
+ <label for="cell-radius-slider" class="control-label">
30
+ Base Radius: <strong>{store.baseRadius}m</strong>
31
+ </label>
32
+ <input
33
+ id="cell-radius-slider"
34
+ type="range"
35
+ class="form-range"
36
+ min="100"
37
+ max="2000"
38
+ step="50"
39
+ bind:value={store.baseRadius}
40
+ />
41
+ </div>
42
+
43
+ <!-- Line width slider -->
44
+ <div class="control-row">
45
+ <label for="cell-line-width-slider" class="control-label">
46
+ Line Width: <strong>{store.lineWidth}px</strong>
47
+ </label>
48
+ <input
49
+ id="cell-line-width-slider"
50
+ type="range"
51
+ class="form-range"
52
+ min="1"
53
+ max="5"
54
+ step="0.5"
55
+ bind:value={store.lineWidth}
56
+ />
57
+ </div>
58
+
59
+ <!-- Fill opacity slider -->
60
+ <div class="control-row">
61
+ <label for="cell-opacity-slider" class="control-label">
62
+ Fill Opacity: <strong>{Math.round(store.fillOpacity * 100)}%</strong>
63
+ </label>
64
+ <input
65
+ id="cell-opacity-slider"
66
+ type="range"
67
+ class="form-range"
68
+ min="0"
69
+ max="1"
70
+ step="0.1"
71
+ bind:value={store.fillOpacity}
72
+ />
73
+ </div>
74
+
75
+ <!-- Cell Labels Section -->
76
+ <hr class="section-divider" />
77
+
78
+
79
+ <!-- Show labels toggle -->
80
+ <div class="control-row">
81
+ <label for="cell-labels-toggle" class="control-label">
82
+ Show Cell Labels
83
+ </label>
84
+ <div class="form-check form-switch">
85
+ <input
86
+ id="cell-labels-toggle"
87
+ type="checkbox"
88
+ class="form-check-input"
89
+ role="switch"
90
+ checked={store.showLabels}
91
+ onchange={(e) => store.setShowLabels(e.currentTarget.checked)}
92
+ />
93
+ </div>
94
+ </div>
95
+
96
+ {#if store.showLabels}
97
+ <!-- 4G/5G Primary Label Field -->
98
+ <div class="control-row">
99
+ <label for="label-primary-4g5g" class="control-label">
100
+ 4G/5G Primary
101
+ </label>
102
+ <select
103
+ id="label-primary-4g5g"
104
+ class="form-select form-select-sm"
105
+ value={store.primaryLabelField4G5G}
106
+ onchange={(e) => store.setPrimaryLabelField4G5G(e.currentTarget.value as keyof Cell)}
107
+ >
108
+ {#each labelFieldOptions4G5G as field}
109
+ <option value={field}>{getFieldLabel(field)}</option>
110
+ {/each}
111
+ </select>
112
+ </div>
113
+
114
+ <!-- 4G/5G Secondary Label Field -->
115
+ <div class="control-row">
116
+ <label for="label-secondary-4g5g" class="control-label">
117
+ 4G/5G Secondary
118
+ </label>
119
+ <select
120
+ id="label-secondary-4g5g"
121
+ class="form-select form-select-sm"
122
+ value={store.secondaryLabelField4G5G}
123
+ onchange={(e) => store.setSecondaryLabelField4G5G(e.currentTarget.value as keyof Cell | 'none')}
124
+ >
125
+ <option value="none">None</option>
126
+ {#each labelFieldOptions4G5G as field}
127
+ <option value={field}>{getFieldLabel(field)}</option>
128
+ {/each}
129
+ </select>
130
+ </div>
131
+
132
+ <!-- 2G Primary Label Field -->
133
+ <div class="control-row">
134
+ <label for="label-primary-2g" class="control-label">
135
+ 2G Primary
136
+ </label>
137
+ <select
138
+ id="label-primary-2g"
139
+ class="form-select form-select-sm"
140
+ value={store.primaryLabelField2G}
141
+ onchange={(e) => store.setPrimaryLabelField2G(e.currentTarget.value as keyof Cell)}
142
+ >
143
+ {#each labelFieldOptions2G as field}
144
+ <option value={field}>{getFieldLabel(field)}</option>
145
+ {/each}
146
+ </select>
147
+ </div>
148
+
149
+ <!-- 2G Secondary Label Field -->
150
+ <div class="control-row">
151
+ <label for="label-secondary-2g" class="control-label">
152
+ 2G Secondary
153
+ </label>
154
+ <select
155
+ id="label-secondary-2g"
156
+ class="form-select form-select-sm"
157
+ value={store.secondaryLabelField2G}
158
+ onchange={(e) => store.setSecondaryLabelField2G(e.currentTarget.value as keyof Cell | 'none')}
159
+ >
160
+ <option value="none">None</option>
161
+ {#each labelFieldOptions2G as field}
162
+ <option value={field}>{getFieldLabel(field)}</option>
163
+ {/each}
164
+ </select>
165
+ </div>
166
+
167
+ <!-- Label Size -->
168
+ <div class="control-row">
169
+ <label for="label-size-slider" class="control-label">
170
+ Label Size: <strong>{store.labelSize}px</strong>
171
+ </label>
172
+ <input
173
+ id="label-size-slider"
174
+ type="range"
175
+ class="form-range"
176
+ min="8"
177
+ max="20"
178
+ step="1"
179
+ value={store.labelSize}
180
+ oninput={(e) => store.setLabelSize(Number(e.currentTarget.value))}
181
+ />
182
+ </div>
183
+
184
+ <!-- Label Color -->
185
+ <div class="control-row">
186
+ <label for="label-color-picker" class="control-label">
187
+ Label Color
188
+ </label>
189
+ <input
190
+ id="label-color-picker"
191
+ type="color"
192
+ class="form-control form-control-color"
193
+ value={store.labelColor}
194
+ oninput={(e) => store.setLabelColor(e.currentTarget.value)}
195
+ />
196
+ </div>
197
+
198
+ <!-- Label Offset -->
199
+ <div class="control-row">
200
+ <label for="label-offset-slider" class="control-label">
201
+ Label Offset: <strong>{store.labelOffset}%</strong>
202
+ </label>
203
+ <input
204
+ id="label-offset-slider"
205
+ type="range"
206
+ class="form-range"
207
+ min="100"
208
+ max="1000"
209
+ step="50"
210
+ value={store.labelOffset}
211
+ oninput={(e) => store.setLabelOffset(Number(e.currentTarget.value))}
212
+ />
213
+ </div>
214
+
215
+ <!-- Label Halo Color -->
216
+ <div class="control-row">
217
+ <label for="label-halo-color-picker" class="control-label">
218
+ Halo Color
219
+ </label>
220
+ <input
221
+ id="label-halo-color-picker"
222
+ type="color"
223
+ class="form-control form-control-color"
224
+ value={store.labelHaloColor}
225
+ oninput={(e) => store.setLabelHaloColor(e.currentTarget.value)}
226
+ />
227
+ </div>
228
+
229
+ <!-- Label Halo Width -->
230
+ <div class="control-row">
231
+ <label for="label-halo-width-slider" class="control-label">
232
+ Halo Width: <strong>{store.labelHaloWidth}px</strong>
233
+ </label>
234
+ <input
235
+ id="label-halo-width-slider"
236
+ type="range"
237
+ class="form-range"
238
+ min="0"
239
+ max="3"
240
+ step="0.5"
241
+ value={store.labelHaloWidth}
242
+ oninput={(e) => store.setLabelHaloWidth(Number(e.currentTarget.value))}
243
+ />
244
+ </div>
245
+
246
+ <!-- Min Label Zoom -->
247
+ <div class="control-row">
248
+ <label for="min-label-zoom-slider" class="control-label">
249
+ Min Zoom: <strong>{store.minLabelZoom}</strong>
250
+ </label>
251
+ <input
252
+ id="min-label-zoom-slider"
253
+ type="range"
254
+ class="form-range"
255
+ min="10"
256
+ max="18"
257
+ step="1"
258
+ value={store.minLabelZoom}
259
+ oninput={(e) => store.setMinLabelZoom(Number(e.currentTarget.value))}
260
+ />
261
+ </div>
262
+
263
+ <!-- Azimuth Tolerance -->
264
+ <!-- <div class="control-row">
265
+ <label for="azimuth-tolerance-slider" class="control-label">
266
+ Azimuth Tolerance: <strong>±{store.azimuthTolerance}°</strong>
267
+ </label>
268
+ <input
269
+ id="azimuth-tolerance-slider"
270
+ type="range"
271
+ class="form-range"
272
+ min="1"
273
+ max="45"
274
+ step="1"
275
+ value={store.azimuthTolerance}
276
+ oninput={(e) => store.setAzimuthTolerance(Number(e.currentTarget.value))}
277
+ />
278
+ </div> -->
279
+ {/if}
280
+ </div>
281
+
282
+ <style>
283
+ .settings-panel {
284
+ width: 100%;
285
+ padding: 0.75rem;
286
+ border: 1px solid rgba(0, 0, 0, 0.1);
287
+ border-radius: 0.375rem;
288
+ background-color: rgba(255, 255, 255, 0.02);
289
+ }
290
+
291
+ .section-divider {
292
+ margin: 1rem 0;
293
+ opacity: 0.3;
294
+ }
295
+
296
+ .control-row {
297
+ display: flex;
298
+ align-items: center;
299
+ justify-content: space-between;
300
+ gap: 0.75rem;
301
+ margin-bottom: 0.875rem;
302
+ }
303
+
304
+ .control-row:last-child {
305
+ margin-bottom: 0;
306
+ }
307
+
308
+ .control-label {
309
+ font-size: 0.8125rem;
310
+ font-weight: 500;
311
+ margin: 0;
312
+ white-space: nowrap;
313
+ flex-shrink: 0;
314
+ min-width: 110px;
315
+ }
316
+
317
+ .form-range,
318
+ .form-select {
319
+ width: 140px;
320
+ min-width: 140px;
321
+ max-width: 140px;
322
+ flex: 0 0 140px;
323
+ }
324
+
325
+ .form-control-color {
326
+ width: 45px;
327
+ height: 28px;
328
+ padding: 2px;
329
+ border-radius: 4px;
330
+ flex-shrink: 0;
331
+ margin-right: auto;
332
+ }
333
+
334
+ .form-check {
335
+ margin: 0;
336
+ margin-right: auto;
337
+ }
338
+
339
+ .form-switch .form-check-input {
340
+ cursor: pointer;
341
+ }
342
+ </style>
@@ -0,0 +1,15 @@
1
+ /**
2
+ * CellSettingsPanel - Cell visualization and label settings
3
+ * Reusable panel component for cell display configuration
4
+ */
5
+ import type { CellStoreContext } from '../../../features/cells/stores/cellStoreContext.svelte';
6
+ import type { Cell, CellGroupingLabels } from '../../../features/cells/types';
7
+ interface Props {
8
+ store: CellStoreContext;
9
+ labelFieldOptions4G5G: Array<keyof Cell>;
10
+ labelFieldOptions2G: Array<keyof Cell>;
11
+ fieldLabels?: CellGroupingLabels;
12
+ }
13
+ declare const CellSettingsPanel: import("svelte").Component<Props, {}, "">;
14
+ type CellSettingsPanel = ReturnType<typeof CellSettingsPanel>;
15
+ export default CellSettingsPanel;
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ /**
3
+ * RepeaterSettingsPanel - Repeater visualization settings
4
+ * Placeholder component for future repeater functionality
5
+ */
6
+ </script>
7
+
8
+ <div class="settings-panel">
9
+ <p class="text-muted text-center mb-0" style="font-size: 0.875rem;">
10
+ Coming soon...
11
+ </p>
12
+ </div>
13
+
14
+ <style>
15
+ .settings-panel {
16
+ width: 100%;
17
+ padding: 1rem 0;
18
+ }
19
+ </style>
@@ -0,0 +1,18 @@
1
+ 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> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const RepeaterSettingsPanel: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type RepeaterSettingsPanel = InstanceType<typeof RepeaterSettingsPanel>;
18
+ export default RepeaterSettingsPanel;
@@ -0,0 +1,199 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SiteSettingsPanel - Site visualization settings
4
+ * Reusable panel component for site display configuration
5
+ */
6
+ import type { SiteStoreContext } from '../../../features/sites/stores/siteStoreContext.svelte';
7
+
8
+ interface Props {
9
+ store: SiteStoreContext;
10
+ }
11
+
12
+ let { store }: Props = $props();
13
+ </script>
14
+
15
+ <div class="settings-panel">
16
+ <!-- Site Size slider -->
17
+ <div class="control-row">
18
+ <label for="site-size-slider" class="control-label">
19
+ Radius: <strong>{store.size}px</strong>
20
+ </label>
21
+ <input
22
+ id="site-size-slider"
23
+ type="range"
24
+ class="form-range"
25
+ min="2"
26
+ max="20"
27
+ step="1"
28
+ bind:value={store.size}
29
+ />
30
+ </div>
31
+
32
+ <!-- Site Opacity slider -->
33
+ <div class="control-row">
34
+ <label for="site-opacity-slider" class="control-label">
35
+ Opacity: <strong>{Math.round(store.opacity * 100)}%</strong>
36
+ </label>
37
+ <input
38
+ id="site-opacity-slider"
39
+ type="range"
40
+ class="form-range"
41
+ min="0"
42
+ max="1"
43
+ step="0.1"
44
+ bind:value={store.opacity}
45
+ />
46
+ </div>
47
+
48
+ <!-- Site Circle Color -->
49
+ <!-- <div class="control-row">
50
+ <label for="site-color-picker" class="control-label">
51
+ Base Color
52
+ </label>
53
+ <input
54
+ id="site-color-picker"
55
+ type="color"
56
+ class="form-control form-control-color"
57
+ bind:value={store.color}
58
+ />
59
+ </div> -->
60
+ <!-- Cell Labels Section -->
61
+ <hr class="section-divider" />
62
+ <!-- Show site labels checkbox -->
63
+ <div class="control-row">
64
+ <label for="site-show-labels" class="control-label">
65
+ Show Site Labels
66
+ </label>
67
+ <div class="form-check form-switch">
68
+ <input
69
+ id="site-show-labels"
70
+ type="checkbox"
71
+ class="form-check-input"
72
+ role="switch"
73
+ bind:checked={store.showLabels}
74
+ />
75
+ </div>
76
+ </div>
77
+
78
+ {#if store.showLabels}
79
+ <!-- Site Label size slider -->
80
+ <div class="control-row">
81
+ <label for="site-label-size-slider" class="control-label">
82
+ Label Size: <strong>{store.labelSize}px</strong>
83
+ </label>
84
+ <input
85
+ id="site-label-size-slider"
86
+ type="range"
87
+ class="form-range"
88
+ min="8"
89
+ max="20"
90
+ step="1"
91
+ bind:value={store.labelSize}
92
+ />
93
+ </div>
94
+
95
+ <!-- Site Label color picker -->
96
+ <div class="control-row">
97
+ <label for="site-label-color-picker" class="control-label">
98
+ Label Color
99
+ </label>
100
+ <input
101
+ id="site-label-color-picker"
102
+ type="color"
103
+ class="form-control form-control-color"
104
+ bind:value={store.labelColor}
105
+ />
106
+ </div>
107
+
108
+ <!-- Site Label offset slider -->
109
+ <div class="control-row">
110
+ <label for="site-label-offset-slider" class="control-label">
111
+ Label Offset: <strong>{store.labelOffset.toFixed(1)}</strong>
112
+ </label>
113
+ <input
114
+ id="site-label-offset-slider"
115
+ type="range"
116
+ class="form-range"
117
+ min="-3"
118
+ max="3"
119
+ step="0.1"
120
+ bind:value={store.labelOffset}
121
+ />
122
+ </div>
123
+
124
+ <!-- Site Label property dropdown -->
125
+ <div class="control-row">
126
+ <label for="site-label-property-select" class="control-label">
127
+ Label Field
128
+ </label>
129
+ <select
130
+ id="site-label-property-select"
131
+ class="form-select form-select-sm"
132
+ bind:value={store.labelProperty}
133
+ >
134
+ <option value="name">Name</option>
135
+ <option value="id">ID</option>
136
+ <option value="provider">Provider</option>
137
+ <option value="technology">Technology</option>
138
+ <option value="featureGroup">Feature Group</option>
139
+ </select>
140
+ </div>
141
+ {/if}
142
+ </div>
143
+
144
+ <style>
145
+ .settings-panel {
146
+ width: 100%;
147
+ padding: 0.75rem;
148
+ border: 1px solid rgba(0, 0, 0, 0.1);
149
+ border-radius: 0.375rem;
150
+ background-color: rgba(255, 255, 255, 0.02);
151
+ }
152
+
153
+ .control-row {
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: space-between;
157
+ gap: 0.75rem;
158
+ margin-bottom: 0.875rem;
159
+ }
160
+
161
+ .control-row:last-child {
162
+ margin-bottom: 0;
163
+ }
164
+
165
+ .control-label {
166
+ font-size: 0.8125rem;
167
+ font-weight: 500;
168
+ margin: 0;
169
+ white-space: nowrap;
170
+ flex-shrink: 0;
171
+ min-width: 110px;
172
+ }
173
+
174
+ .form-range,
175
+ .form-select {
176
+ width: 140px;
177
+ min-width: 140px;
178
+ max-width: 140px;
179
+ flex: 0 0 140px;
180
+ }
181
+
182
+ .form-control-color {
183
+ width: 45px;
184
+ height: 28px;
185
+ padding: 2px;
186
+ border-radius: 4px;
187
+ flex-shrink: 0;
188
+ margin-right: auto;
189
+ }
190
+
191
+ .form-check {
192
+ margin: 0;
193
+ margin-right: auto;
194
+ }
195
+
196
+ .form-switch .form-check-input {
197
+ cursor: pointer;
198
+ }
199
+ </style>
@@ -0,0 +1,11 @@
1
+ /**
2
+ * SiteSettingsPanel - Site visualization settings
3
+ * Reusable panel component for site display configuration
4
+ */
5
+ import type { SiteStoreContext } from '../../../features/sites/stores/siteStoreContext.svelte';
6
+ interface Props {
7
+ store: SiteStoreContext;
8
+ }
9
+ declare const SiteSettingsPanel: import("svelte").Component<Props, {}, "">;
10
+ type SiteSettingsPanel = ReturnType<typeof SiteSettingsPanel>;
11
+ export default SiteSettingsPanel;
@@ -4,4 +4,5 @@
4
4
  * Exports shared controls and utilities used across features
5
5
  */
6
6
  export { default as MapControl } from './controls/MapControl.svelte';
7
+ export { default as FeatureSettingsControl } from './controls/FeatureSettingsControl.svelte';
7
8
  export { addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './utils/mapboxHelpers';
@@ -5,5 +5,6 @@
5
5
  */
6
6
  // Controls
7
7
  export { default as MapControl } from './controls/MapControl.svelte';
8
+ export { default as FeatureSettingsControl } from './controls/FeatureSettingsControl.svelte';
8
9
  // Mapbox Helpers
9
10
  export { addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './utils/mapboxHelpers';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.69",
3
+ "version": "0.0.70",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",