@smartnet360/svelte-components 0.0.68 → 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>
@@ -317,7 +317,7 @@
317
317
  type="range"
318
318
  class="form-range"
319
319
  min="1"
320
- max="15"
320
+ max="45"
321
321
  step="1"
322
322
  value={store.azimuthTolerance}
323
323
  oninput={(e) => store.setAzimuthTolerance(Number(e.currentTarget.value))}
@@ -15,6 +15,8 @@
15
15
  import { waitForStyleLoad, generateLayerId, generateSourceId } from '../../../shared/utils/mapboxHelpers';
16
16
  import type { CellStoreContext } from '../stores/cellStoreContext.svelte';
17
17
  import type { Cell } from '../types';
18
+ import { parseTechBand } from '../utils/techBandParser';
19
+ import { calculateRadius } from '../utils/zoomScaling';
18
20
  import * as turf from '@turf/turf';
19
21
 
20
22
  interface Props {
@@ -32,6 +34,23 @@
32
34
 
33
35
  let map = $state<mapboxgl.Map | null>(null);
34
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
+ }
35
54
 
36
55
  /**
37
56
  * Get label text for a cell based on tech and selected field
@@ -75,17 +94,35 @@
75
94
  function buildLabelFeatures(): GeoJSON.FeatureCollection {
76
95
  const features: GeoJSON.Feature[] = [];
77
96
 
78
- if (!store.showLabels || store.filteredCells.length === 0) {
97
+ if (!store.showLabels || store.filteredCells.length === 0 || !map) {
98
+ return { type: 'FeatureCollection', features: [] };
99
+ }
100
+
101
+ // Get current zoom level
102
+ const currentZoom = map.getZoom();
103
+
104
+ // Filter cells by viewport bounds (only render labels for visible cells)
105
+ const bounds = map.getBounds();
106
+ if (!bounds) {
79
107
  return { type: 'FeatureCollection', features: [] };
80
108
  }
81
109
 
82
- // Group cells by site + azimuth
83
- const cellGroups = groupCellsByAzimuth(store.filteredCells, store.azimuthTolerance);
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);
84
116
 
85
117
  cellGroups.forEach((cells, groupKey) => {
86
118
  // Sort cells within group for consistent stacking
87
119
  cells.sort((a, b) => a.id.localeCompare(b.id));
88
120
 
121
+ // Calculate total height of all labels in this group
122
+ const totalGroupHeight = cells.length * store.labelSize * 1.5;
123
+ // Center offset: shift entire group up by half its height
124
+ const groupCenterOffset = -totalGroupHeight / 2;
125
+
89
126
  cells.forEach((cell, index) => {
90
127
  // Determine which fields to use based on tech
91
128
  const is2G = cell.tech === '2G';
@@ -104,17 +141,28 @@
104
141
 
105
142
  if (!labelText.trim()) return; // Skip if no text
106
143
 
107
- // Project label position along azimuth
108
- const origin = turf.point([cell.siteLongitude, cell.siteLatitude]);
144
+ // Calculate zoom-aware radius for this cell
145
+ const techBandParsed = parseTechBand(cell.fband);
146
+ const techBandKey = techBandParsed?.key || '4G_1800'; // Fallback to default
147
+ const cellRadius = calculateRadius(store.baseRadius, techBandKey, currentZoom);
148
+
149
+ // Position label at a percentage of the arc radius (configurable via labelOffset)
150
+ // labelOffset is treated as a percentage (e.g., 300 = 300% = beyond arc, 70 = 70% = inside arc)
151
+ const labelDistance = (cellRadius * store.labelOffset) / 100;
152
+
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]);
109
156
  const labelPosition = turf.destination(
110
157
  origin,
111
- store.labelOffset / 1000, // Convert meters to kilometers
158
+ labelDistance / 1000, // Convert meters to kilometers
112
159
  cell.azimuth,
113
160
  { units: 'kilometers' }
114
161
  );
115
162
 
116
163
  // Calculate vertical offset for stacking (pixels)
117
- const stackOffset = index * (store.labelSize + 4);
164
+ // Position within the centered group
165
+ const stackOffset = groupCenterOffset + (index * store.labelSize * 1.5) + (store.labelSize * 0.75);
118
166
 
119
167
  features.push({
120
168
  type: 'Feature',
@@ -140,6 +188,74 @@
140
188
  const geojson = buildLabelFeatures();
141
189
  const source = map.getSource(sourceId) as mapboxgl.GeoJSONSource;
142
190
  source.setData(geojson);
191
+
192
+ // Also update paint/layout properties when labels are refreshed
193
+ if (map.getLayer(layerId)) {
194
+ map.setLayoutProperty(layerId, 'text-size', store.labelSize);
195
+ map.setPaintProperty(layerId, 'text-color', store.labelColor);
196
+ map.setPaintProperty(layerId, 'text-halo-color', store.labelHaloColor);
197
+ map.setPaintProperty(layerId, 'text-halo-width', store.labelHaloWidth);
198
+ map.setLayerZoomRange(layerId, store.minLabelZoom, 24);
199
+ }
200
+ }
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
+ }
143
259
  }
144
260
 
145
261
  onMount(async () => {
@@ -151,53 +267,20 @@
151
267
 
152
268
  map = m;
153
269
 
154
- // Wait for style to load
155
- await waitForStyleLoad(map);
156
-
157
- // Add source
158
- if (!map.getSource(sourceId)) {
159
- map.addSource(sourceId, {
160
- type: 'geojson',
161
- data: { type: 'FeatureCollection', features: [] }
162
- });
163
- }
164
-
165
- // Add label layer
166
- if (!map.getLayer(layerId)) {
167
- map.addLayer({
168
- id: layerId,
169
- type: 'symbol',
170
- source: sourceId,
171
- minzoom: store.minLabelZoom,
172
- layout: {
173
- 'text-field': ['get', 'text'],
174
- 'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'],
175
- 'text-size': store.labelSize,
176
- 'text-anchor': 'center',
177
- 'text-offset': [
178
- 0,
179
- ['/', ['get', 'stackOffset'], store.labelSize] // Dynamic vertical offset
180
- ],
181
- 'text-allow-overlap': true,
182
- 'text-ignore-placement': false,
183
- 'text-max-width': 999, // Essentially disable wrapping
184
- 'text-justify': 'center' // Center multi-line text
185
- },
186
- paint: {
187
- 'text-color': store.labelColor,
188
- 'text-halo-color': store.labelHaloColor,
189
- 'text-halo-width': store.labelHaloWidth
190
- }
191
- });
192
- }
270
+ // Initial layer setup
271
+ await initializeLayer();
193
272
 
194
- // Initial render
195
- 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);
196
276
  });
197
277
  });
198
278
 
199
279
  // Watch for changes in store properties and update labels
200
280
  $effect(() => {
281
+ // Track viewportVersion to force re-run on pan (even without zoom change)
282
+ viewportVersion;
283
+
201
284
  // Dependencies that should trigger label refresh
202
285
  store.filteredCells;
203
286
  store.showLabels;
@@ -207,25 +290,36 @@
207
290
  store.secondaryLabelField2G;
208
291
  store.labelOffset;
209
292
  store.azimuthTolerance;
293
+ store.baseRadius; // Zoom-based radius calculation depends on baseRadius
210
294
 
211
- updateLabels();
212
- });
213
-
214
- // Watch for style changes and update layer paint/layout properties
215
- $effect(() => {
216
- if (!map || !map.getLayer(layerId)) return;
295
+ // Also watch style properties - when they change, updateLabels() will
296
+ // rebuild the GeoJSON which forces Mapbox to re-read paint properties from store
297
+ store.labelSize;
298
+ store.labelColor;
299
+ store.labelHaloColor;
300
+ store.labelHaloWidth;
301
+ store.minLabelZoom;
217
302
 
218
- map.setLayoutProperty(layerId, 'text-size', store.labelSize);
219
- map.setPaintProperty(layerId, 'text-color', store.labelColor);
220
- map.setPaintProperty(layerId, 'text-halo-color', store.labelHaloColor);
221
- map.setPaintProperty(layerId, 'text-halo-width', store.labelHaloWidth);
222
- map.setLayerZoomRange(layerId, store.minLabelZoom, 24);
303
+ updateLabels();
223
304
  });
224
305
 
225
306
  onDestroy(() => {
226
307
  unsubscribe?.();
227
308
 
309
+ // Clear any pending viewport update timer
310
+ if (viewportUpdateTimer) {
311
+ clearTimeout(viewportUpdateTimer);
312
+ }
313
+
228
314
  if (map) {
315
+ // Remove style.load listener
316
+ map.off('style.load', initializeLayer);
317
+
318
+ // Remove all event listeners
319
+ map.off('zoom', updateLabels);
320
+ map.off('moveend', handleViewportChange);
321
+ map.off('zoomend', handleViewportChange);
322
+
229
323
  if (map.getLayer(layerId)) {
230
324
  map.removeLayer(layerId);
231
325
  }
@@ -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
@@ -70,7 +70,7 @@ export function createCellStoreContext(cells) {
70
70
  labelHaloColor: persistedSettings.labelHaloColor ?? '#ffffff',
71
71
  labelHaloWidth: persistedSettings.labelHaloWidth ?? 1,
72
72
  minLabelZoom: persistedSettings.minLabelZoom ?? 14,
73
- azimuthTolerance: persistedSettings.azimuthTolerance ?? 5
73
+ azimuthTolerance: persistedSettings.azimuthTolerance ?? 25
74
74
  });
75
75
  // Derived: Filter cells by status based on includePlannedCells flag
76
76
  // IMPORTANT: This is a pure $derived - it only READS from state, never writes
@@ -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.68",
3
+ "version": "0.0.70",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",