@smartnet360/svelte-components 0.0.74 → 0.0.76

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,6 +17,7 @@
17
17
  import { useMapbox } from '../../../core/hooks/useMapbox';
18
18
  import { repeatersToGeoJSON } from '../utils/repeaterGeoJSON';
19
19
  import { REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX } from '../constants/zIndex';
20
+ import { Z_INDEX_BY_BAND } from '../constants/techBandZOrder';
20
21
 
21
22
  interface Props {
22
23
  /** Repeater store context */
@@ -26,28 +27,34 @@
26
27
  }
27
28
 
28
29
  let { store, namespace }: Props = $props();
29
-
30
- const FILL_LAYER_ID = `${namespace}-repeaters-fill`;
31
- const LINE_LAYER_ID = `${namespace}-repeaters-line`;
30
+
32
31
  const SOURCE_ID = `${namespace}-repeaters`;
33
-
32
+
34
33
  // Get map from mapbox hook
35
34
  const mapStore = useMapbox();
36
-
35
+
37
36
  let map = $state<MapboxMap | null>(null);
38
37
  let mounted = $state(false);
39
38
  let viewportUpdateTimer: ReturnType<typeof setTimeout> | null = null;
40
39
  let viewportVersion = $state(0); // Increment to force $effect re-run on viewport changes
41
-
40
+
41
+ // Get all unique z-index values and sort them
42
+ const sortedZIndexes = [...new Set(Object.values(Z_INDEX_BY_BAND))].sort((a, b) => a - b);
43
+
44
+ // Create layer IDs for each z-index
45
+ const fillLayerIds = sortedZIndexes.map((zIndex) => `${namespace}-repeaters-fill-${zIndex}`);
46
+ const lineLayerIds = sortedZIndexes.map((zIndex) => `${namespace}-repeaters-line-${zIndex}`);
47
+ const allLayerIds = [...fillLayerIds, ...lineLayerIds];
48
+
42
49
  // Viewport change handler with debouncing
43
50
  function handleMoveEnd() {
44
51
  if (!map) return;
45
-
52
+
46
53
  // Clear any existing timer
47
54
  if (viewportUpdateTimer) {
48
55
  clearTimeout(viewportUpdateTimer);
49
56
  }
50
-
57
+
51
58
  // Debounce: wait 200ms after last move event before re-rendering
52
59
  viewportUpdateTimer = setTimeout(() => {
53
60
  if (!map) return;
@@ -59,73 +66,72 @@
59
66
  viewportVersion++;
60
67
  }, 200);
61
68
  }
62
-
69
+
63
70
  /**
64
71
  * Initialize or reinitialize the repeater layers
65
72
  * Called on mount and when map style changes
66
73
  */
67
74
  function initializeLayer() {
68
75
  if (!map) return;
69
-
76
+
70
77
  console.log('RepeatersLayer: initializeLayer called');
71
-
78
+
72
79
  // Set initial zoom
73
80
  store.setCurrentZoom(map.getZoom());
74
-
81
+
75
82
  // Add moveend listener (remove first to avoid duplicates)
76
83
  map.off('moveend', handleMoveEnd);
77
84
  map.on('moveend', handleMoveEnd);
78
-
85
+
79
86
  // Mark as mounted to trigger $effect
80
87
  mounted = true;
81
-
88
+
82
89
  // Force $effect to re-run
83
90
  viewportVersion++;
84
91
  }
85
-
92
+
86
93
  onMount(() => {
87
94
  console.log('RepeatersLayer: onMount, waiting for map...');
88
-
95
+
89
96
  // Subscribe to map store
90
97
  const unsubscribe = mapStore.subscribe((mapInstance) => {
91
98
  if (mapInstance && !map) {
92
99
  console.log('RepeatersLayer: Map available, initializing...');
93
100
  map = mapInstance;
94
-
101
+
95
102
  // Initial layer setup
96
103
  initializeLayer();
97
-
104
+
98
105
  // Re-initialize layer when map style changes
99
106
  // Mapbox removes all custom layers/sources when setStyle() is called
100
107
  map.on('style.load', initializeLayer);
101
108
  }
102
109
  });
103
-
110
+
104
111
  return () => {
105
112
  unsubscribe();
106
113
  };
107
114
  });
108
-
115
+
109
116
  onDestroy(() => {
110
117
  if (!map) return;
111
-
118
+
112
119
  // Clean up timer
113
120
  if (viewportUpdateTimer) {
114
121
  clearTimeout(viewportUpdateTimer);
115
122
  }
116
-
123
+
117
124
  // Remove style.load listener
118
125
  map.off('style.load', initializeLayer);
119
-
126
+
120
127
  map.off('moveend', handleMoveEnd);
121
-
128
+
122
129
  // Clean up layers and source
123
- if (map.getLayer(LINE_LAYER_ID)) {
124
- map.removeLayer(LINE_LAYER_ID);
125
- }
126
- if (map.getLayer(FILL_LAYER_ID)) {
127
- map.removeLayer(FILL_LAYER_ID);
128
- }
130
+ allLayerIds.forEach((id) => {
131
+ if (map.getLayer(id)) {
132
+ map.removeLayer(id);
133
+ }
134
+ });
129
135
  if (map.getSource(SOURCE_ID)) {
130
136
  map.removeSource(SOURCE_ID);
131
137
  }
@@ -135,7 +141,7 @@
135
141
  $effect(() => {
136
142
  // Track viewportVersion to force re-run on pan
137
143
  viewportVersion;
138
-
144
+
139
145
  console.log('RepeatersLayer $effect triggered:', {
140
146
  mounted,
141
147
  hasMap: !!map,
@@ -145,40 +151,40 @@
145
151
  baseRadius: store.baseRadius,
146
152
  viewportVersion
147
153
  });
148
-
149
- if (!mounted || !map || !store.showRepeaters) {
154
+
155
+ if (!mounted || !map) {
156
+ return;
157
+ }
158
+
159
+ if (!store.showRepeaters) {
150
160
  // Remove layers if showRepeaters is false
151
- if (mounted && map) {
152
- console.log('RepeatersLayer: Removing layers (showRepeaters=false or not mounted)');
153
- if (map.getLayer(LINE_LAYER_ID)) {
154
- map.removeLayer(LINE_LAYER_ID);
155
- }
156
- if (map.getLayer(FILL_LAYER_ID)) {
157
- map.removeLayer(FILL_LAYER_ID);
158
- }
159
- if (map.getSource(SOURCE_ID)) {
160
- map.removeSource(SOURCE_ID);
161
+ allLayerIds.forEach((id) => {
162
+ if (map.getLayer(id)) {
163
+ map.removeLayer(id);
161
164
  }
165
+ });
166
+ if (map.getSource(SOURCE_ID)) {
167
+ map.removeSource(SOURCE_ID);
162
168
  }
163
169
  return;
164
170
  }
165
-
171
+
166
172
  // Filter repeaters by viewport bounds (only render visible repeaters)
167
173
  const bounds = map.getBounds();
168
174
  if (!bounds) {
169
175
  console.warn('RepeatersLayer: Cannot get map bounds, skipping viewport filter');
170
176
  return;
171
177
  }
172
-
173
- const visibleRepeaters = store.filteredRepeaters.filter(repeater =>
178
+
179
+ const visibleRepeaters = store.filteredRepeaters.filter((repeater) =>
174
180
  bounds.contains([repeater.longitude, repeater.latitude])
175
181
  );
176
-
182
+
177
183
  console.log('RepeatersLayer: Viewport filtering:', {
178
184
  totalFiltered: store.filteredRepeaters.length,
179
185
  visibleInViewport: visibleRepeaters.length
180
186
  });
181
-
187
+
182
188
  // Generate GeoJSON from visible repeaters only
183
189
  const geoJSON = repeatersToGeoJSON(
184
190
  visibleRepeaters,
@@ -186,12 +192,12 @@
186
192
  store.baseRadius,
187
193
  store.techBandColorMap
188
194
  );
189
-
195
+
190
196
  console.log('RepeatersLayer: Generated GeoJSON:', {
191
197
  featureCount: geoJSON.features.length,
192
198
  firstFeature: geoJSON.features[0]
193
199
  });
194
-
200
+
195
201
  // Update or create source
196
202
  const source = map.getSource(SOURCE_ID);
197
203
  if (source && source.type === 'geojson') {
@@ -204,55 +210,65 @@
204
210
  data: geoJSON
205
211
  });
206
212
  }
207
-
208
- // Add fill layer if not exists
209
- if (!map.getLayer(FILL_LAYER_ID)) {
210
- console.log('RepeatersLayer: Creating fill layer');
211
- map.addLayer({
212
- id: FILL_LAYER_ID,
213
- type: 'fill',
214
- source: SOURCE_ID,
215
- layout: {
216
- 'fill-sort-key': ['get', 'sortKey'] // Use sortKey for z-ordering
217
- },
218
- paint: {
219
- 'fill-color': ['get', 'techBandColor'],
220
- 'fill-opacity': store.fillOpacity
221
- },
222
- metadata: {
223
- zIndex: REPEATER_FILL_Z_INDEX
224
- }
225
- });
226
- } else {
227
- // Update fill opacity
228
- console.log('RepeatersLayer: Updating fill opacity:', store.fillOpacity);
229
- map.setPaintProperty(FILL_LAYER_ID, 'fill-opacity', store.fillOpacity);
230
- }
231
-
232
- // Add line layer if not exists
233
- if (!map.getLayer(LINE_LAYER_ID)) {
234
- console.log('RepeatersLayer: Creating line layer');
235
- map.addLayer({
236
- id: LINE_LAYER_ID,
237
- type: 'line',
238
- source: SOURCE_ID,
239
- layout: {
240
- 'line-sort-key': ['get', 'sortKey'] // Use sortKey for z-ordering
241
- },
242
- paint: {
243
- 'line-color': ['get', 'lineColor'],
244
- 'line-width': store.lineWidth,
245
- 'line-opacity': ['get', 'lineOpacity']
246
- },
247
- metadata: {
248
- zIndex: REPEATER_LINE_Z_INDEX
249
- }
250
- });
251
- } else {
252
- // Update line width
253
- console.log('RepeatersLayer: Updating line width:', store.lineWidth);
254
- map.setPaintProperty(LINE_LAYER_ID, 'line-width', store.lineWidth);
213
+
214
+ // Get all unique bands for the current z-index
215
+ const bandsByZIndex = new Map<number, string[]>();
216
+ for (const [band, zIndex] of Object.entries(Z_INDEX_BY_BAND)) {
217
+ if (!bandsByZIndex.has(zIndex)) {
218
+ bandsByZIndex.set(zIndex, []);
219
+ }
220
+ bandsByZIndex.get(zIndex)?.push(band);
255
221
  }
222
+
223
+ // Add/update layers for each z-index level
224
+ sortedZIndexes.forEach((zIndex) => {
225
+ const fillLayerId = `${namespace}-repeaters-fill-${zIndex}`;
226
+ const lineLayerId = `${namespace}-repeaters-line-${zIndex}`;
227
+ const bands = bandsByZIndex.get(zIndex) || [];
228
+
229
+ // Add fill layer if not exists
230
+ if (!map.getLayer(fillLayerId)) {
231
+ console.log(`RepeatersLayer: Creating fill layer for z-index ${zIndex}`);
232
+ map.addLayer({
233
+ id: fillLayerId,
234
+ type: 'fill',
235
+ source: SOURCE_ID,
236
+ filter: ['in', ['get', 'techBandKey'], ['literal', bands]],
237
+ paint: {
238
+ 'fill-color': ['get', 'techBandColor'],
239
+ 'fill-opacity': store.fillOpacity
240
+ },
241
+ metadata: {
242
+ zIndex: REPEATER_FILL_Z_INDEX + zIndex
243
+ }
244
+ });
245
+ } else {
246
+ // Update fill opacity
247
+ map.setPaintProperty(fillLayerId, 'fill-opacity', store.fillOpacity);
248
+ }
249
+
250
+ // Add line layer if not exists
251
+ if (!map.getLayer(lineLayerId)) {
252
+ console.log(`RepeatersLayer: Creating line layer for z-index ${zIndex}`);
253
+ map.addLayer({
254
+ id: lineLayerId,
255
+ type: 'line',
256
+ source: SOURCE_ID,
257
+ filter: ['in', ['get', 'techBandKey'], ['literal', bands]],
258
+ paint: {
259
+ 'line-color': ['get', 'lineColor'],
260
+ 'line-width': store.lineWidth,
261
+ 'line-opacity': ['get', 'lineOpacity']
262
+ },
263
+ metadata: {
264
+ zIndex: REPEATER_LINE_Z_INDEX + zIndex
265
+ }
266
+ });
267
+ } else {
268
+ // Update line width
269
+ map.setPaintProperty(lineLayerId, 'line-width', store.lineWidth);
270
+ }
271
+ });
256
272
  });
257
273
  </script>
258
274
 
@@ -90,23 +90,10 @@ export function createRepeaterStoreContext(repeaters) {
90
90
  minLabelZoom: persistedSettings.minLabelZoom ?? 10 // Lower default to show labels at more zoom levels
91
91
  });
92
92
  // Derived: Filter repeaters by visible tech:fband combinations
93
- // Memoized to avoid re-filtering when repeaters array or visibleTechBands haven't changed
94
- let lastRepeatersRef = null;
95
- let lastVisibleTechBands = null;
96
- let cachedFilteredRepeaters = [];
97
- let filteredRepeaters = $derived.by(() => {
98
- // Only recompute if repeaters reference or visible tech bands changed
99
- if (state.repeaters === lastRepeatersRef && state.visibleTechBands === lastVisibleTechBands) {
100
- return cachedFilteredRepeaters;
101
- }
102
- lastRepeatersRef = state.repeaters;
103
- lastVisibleTechBands = state.visibleTechBands;
104
- cachedFilteredRepeaters = state.repeaters.filter(r => {
105
- const key = `${r.tech}:${r.fband}`;
106
- return state.visibleTechBands.has(key);
107
- });
108
- return cachedFilteredRepeaters;
109
- });
93
+ let filteredRepeaters = $derived(state.repeaters.filter(r => {
94
+ const key = `${r.tech}:${r.fband}`;
95
+ return state.visibleTechBands.has(key);
96
+ }));
110
97
  // Auto-save settings when they change
111
98
  $effect(() => {
112
99
  // Convert Set to Array and Map to Object for serialization
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { createArcPolygon } from '../../cells/utils/arcGeometry';
10
10
  import { getRepeaterRadiusMultiplier } from '../constants/radiusMultipliers';
11
- import { getRepeaterZOrder } from '../constants/techBandZOrder';
11
+ import { Z_INDEX_BY_BAND } from '../constants/techBandZOrder';
12
12
  /** Fixed beamwidth for all repeaters */
13
13
  const REPEATER_BEAMWIDTH = 30;
14
14
  /** Reference zoom level where base radius is used as-is */
@@ -26,6 +26,18 @@ const REFERENCE_ZOOM = 12;
26
26
  function getZoomFactor(currentZoom) {
27
27
  return Math.pow(2, (REFERENCE_ZOOM - currentZoom) / 2);
28
28
  }
29
+ /**
30
+ * Parse repeater tech and fband into a TechnologyBandKey
31
+ * Repeaters use format like tech="4G", fband="LTE1800" or "1800"
32
+ * We need to convert to "4G_1800" format
33
+ */
34
+ function parseTechBand(tech, fband) {
35
+ // Extract numeric band from fband (handles "LTE1800", "1800", "GSM900", "900", etc.)
36
+ const bandMatch = fband.match(/(\d+)/);
37
+ const band = bandMatch ? bandMatch[1] : fband;
38
+ // Build TechnologyBandKey
39
+ return `${tech}_${band}`;
40
+ }
29
41
  /**
30
42
  * Convert repeaters to GeoJSON FeatureCollection with arc geometries
31
43
  *
@@ -37,16 +49,18 @@ function getZoomFactor(currentZoom) {
37
49
  */
38
50
  export function repeatersToGeoJSON(repeaters, currentZoom, baseRadius = 500, techBandColorMap) {
39
51
  const features = repeaters.map((repeater) => {
40
- // Get color for this tech:fband combination
41
- const key = `${repeater.tech}:${repeater.fband}`;
42
- const color = techBandColorMap.get(key) || '#888888'; // Fallback gray
52
+ // Get color for this tech:fband combination (old format for color map)
53
+ const colorKey = `${repeater.tech}:${repeater.fband}`;
54
+ const color = techBandColorMap.get(colorKey) || '#888888'; // Fallback gray
55
+ // Parse to TechnologyBandKey for z-index lookup
56
+ const techBandKey = parseTechBand(repeater.tech, repeater.fband);
43
57
  // Calculate zoom-reactive radius with inverse scaling (like cells)
44
58
  const zoomFactor = getZoomFactor(currentZoom);
45
59
  // Apply tech-band specific radius multiplier
46
60
  const techBandMultiplier = getRepeaterRadiusMultiplier(repeater.tech, repeater.fband);
47
61
  const radius = baseRadius * zoomFactor * techBandMultiplier;
48
- // Get z-order for layering (higher frequency = higher z-order = on top)
49
- const zOrder = getRepeaterZOrder(repeater.tech, repeater.fband);
62
+ // Get z-order for layering using shared constant
63
+ const zIndex = Z_INDEX_BY_BAND[techBandKey] || 0;
50
64
  // Create arc polygon geometry with fixed 30° beamwidth
51
65
  const center = [repeater.longitude, repeater.latitude];
52
66
  const arc = createArcPolygon(center, repeater.azimuth, REPEATER_BEAMWIDTH, radius);
@@ -73,13 +87,13 @@ export function repeatersToGeoJSON(repeaters, currentZoom, baseRadius = 500, tec
73
87
  featureGroup: repeater.featureGroup || '',
74
88
  status: repeater.status || 'active',
75
89
  // Styling properties for Mapbox
76
- techBandKey: key,
90
+ techBandKey: techBandKey, // TechnologyBandKey format for consistency
77
91
  techBandColor: color,
78
92
  lineColor: color,
79
93
  lineOpacity: 0.8,
80
- // Z-order for layering (higher frequency on top)
81
- zOrder: zOrder,
82
- sortKey: zOrder // Used by Mapbox for layer ordering
94
+ // Z-index for layering (higher frequency on top)
95
+ zIndex: zIndex,
96
+ sortKey: zIndex // Used by Mapbox for layer ordering
83
97
  }
84
98
  };
85
99
  });
@@ -8,5 +8,5 @@ export { type MapStore, MAP_CONTEXT_KEY, MapboxProvider, ViewportSync, MapStoreB
8
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
- export { type Repeater, type RepeaterTreeNode, type RepeaterStoreValue, type RepeaterStoreContext, createRepeaterStoreContext, RepeatersLayer, RepeaterLabelsLayer, RepeaterFilterControl, repeatersToGeoJSON, buildRepeaterTree, getFilteredRepeaters, getRepeaterRadiusMultiplier, getRepeaterZOrder, REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX, REPEATER_LABEL_Z_INDEX, REPEATER_RADIUS_MULTIPLIER, REPEATER_TECH_BAND_Z_ORDER } from './features/repeaters';
11
+ export { type Repeater, type RepeaterTreeNode, type RepeaterStoreValue, type RepeaterStoreContext, createRepeaterStoreContext, RepeatersLayer, RepeaterLabelsLayer, RepeaterFilterControl, repeatersToGeoJSON, buildRepeaterTree, getFilteredRepeaters, getRepeaterRadiusMultiplier, REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX, REPEATER_LABEL_Z_INDEX, REPEATER_RADIUS_MULTIPLIER } from './features/repeaters';
12
12
  export { DemoMap, demoSites, demoCells, demoRepeaters } from './demo';
@@ -23,7 +23,7 @@ export { createCellStoreContext, CellsLayer, CellLabelsLayer, CellFilterControl,
23
23
  // ============================================================================
24
24
  // REPEATER FEATURE
25
25
  // ============================================================================
26
- export { createRepeaterStoreContext, RepeatersLayer, RepeaterLabelsLayer, RepeaterFilterControl, repeatersToGeoJSON, buildRepeaterTree, getFilteredRepeaters, getRepeaterRadiusMultiplier, getRepeaterZOrder, REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX, REPEATER_LABEL_Z_INDEX, REPEATER_RADIUS_MULTIPLIER, REPEATER_TECH_BAND_Z_ORDER } from './features/repeaters';
26
+ export { createRepeaterStoreContext, RepeatersLayer, RepeaterLabelsLayer, RepeaterFilterControl, repeatersToGeoJSON, buildRepeaterTree, getFilteredRepeaters, getRepeaterRadiusMultiplier, REPEATER_FILL_Z_INDEX, REPEATER_LINE_Z_INDEX, REPEATER_LABEL_Z_INDEX, REPEATER_RADIUS_MULTIPLIER } from './features/repeaters';
27
27
  // ============================================================================
28
28
  // DEMO
29
29
  // ============================================================================
@@ -53,160 +53,73 @@
53
53
  iconOnlyWhenCollapsed = true,
54
54
  initiallyCollapsed = true
55
55
  }: Props = $props();
56
-
57
- // Generate unique IDs for accordion items
58
- const accordionId = `settings-accordion-${Math.random().toString(36).substring(7)}`;
59
56
  </script>
60
57
 
61
- <MapControl {position} {title} {icon} {iconOnlyWhenCollapsed} collapsible={true} {initiallyCollapsed}>
62
- <div class="accordion" id={accordionId}>
63
- <!-- Site Settings Accordion Item -->
64
- <div class="accordion-item">
65
- <h2 class="accordion-header" id="heading-sites">
66
- <button
67
- class="accordion-button collapsed"
68
- type="button"
69
- data-bs-toggle="collapse"
70
- data-bs-target="#collapse-sites"
71
- aria-expanded="false"
72
- aria-controls="collapse-sites"
73
- >
74
- Site Settings
75
- </button>
76
- </h2>
77
- <div
78
- id="collapse-sites"
79
- class="accordion-collapse collapse"
80
- aria-labelledby="heading-sites"
81
- >
82
- <div class="accordion-body">
83
- <SiteSettingsPanel store={siteStore} />
84
- </div>
58
+ <MapControl {position} {title} {icon} {iconOnlyWhenCollapsed} collapsible={true} {initiallyCollapsed} controlWidth="380px" edgeOffset="12px">
59
+ <!-- use CSS variables for width & offsets so MapControl can pick these up -->
60
+ <div class="d-flex flex-column p-0">
61
+ <ul class="nav nav-tabs" id="settings-tabs" role="tablist">
62
+ <li class="nav-item" role="presentation">
63
+ <button class="nav-link active" id="site-tab" data-bs-toggle="tab" data-bs-target="#site" type="button" role="tab" aria-controls="site" aria-selected="true">Site</button>
64
+ </li>
65
+ <li class="nav-item" role="presentation">
66
+ <button class="nav-link" id="cell-tab" data-bs-toggle="tab" data-bs-target="#cell" type="button" role="tab" aria-controls="cell" aria-selected="false">Cell</button>
67
+ </li>
68
+ {#if repeaterStore}
69
+ <li class="nav-item" role="presentation">
70
+ <button class="nav-link" id="repeater-tab" data-bs-toggle="tab" data-bs-target="#repeater" type="button" role="tab" aria-controls="repeater" aria-selected="false">Repeater</button>
71
+ </li>
72
+ {/if}
73
+ </ul>
74
+ <div class="tab-content rounded-2 shadow-sm bg-white border p-2" id="settings-tab-content" style="width: 360px;">
75
+ <div class="tab-pane show active" id="site" role="tabpanel" aria-labelledby="site-tab">
76
+ <SiteSettingsPanel store={siteStore} />
85
77
  </div>
86
- </div>
87
-
88
- <!-- Cell Settings Accordion Item -->
89
- <div class="accordion-item">
90
- <h2 class="accordion-header" id="heading-cells">
91
- <button
92
- class="accordion-button collapsed"
93
- type="button"
94
- data-bs-toggle="collapse"
95
- data-bs-target="#collapse-cells"
96
- aria-expanded="false"
97
- aria-controls="collapse-cells"
98
- >
99
- Cell Settings
100
- </button>
101
- </h2>
102
- <div
103
- id="collapse-cells"
104
- class="accordion-collapse collapse"
105
- aria-labelledby="heading-cells"
106
- >
107
- <div class="accordion-body">
108
- <CellSettingsPanel
109
- store={cellStore}
110
- {labelFieldOptions4G5G}
111
- {labelFieldOptions2G}
112
- {fieldLabels}
113
- />
114
- </div>
78
+ <div class="tab-pane" id="cell" role="tabpanel" aria-labelledby="cell-tab">
79
+ <CellSettingsPanel
80
+ store={cellStore}
81
+ {labelFieldOptions4G5G}
82
+ {labelFieldOptions2G}
83
+ {fieldLabels}
84
+ />
115
85
  </div>
116
- </div>
117
-
118
- <!-- Repeater Settings Accordion Item -->
119
- {#if repeaterStore}
120
- <div class="accordion-item">
121
- <h2 class="accordion-header" id="heading-repeaters">
122
- <button
123
- class="accordion-button collapsed"
124
- type="button"
125
- data-bs-toggle="collapse"
126
- data-bs-target="#collapse-repeaters"
127
- aria-expanded="false"
128
- aria-controls="collapse-repeaters"
129
- >
130
- Repeater Settings
131
- </button>
132
- </h2>
133
- <div
134
- id="collapse-repeaters"
135
- class="accordion-collapse collapse"
136
- aria-labelledby="heading-repeaters"
137
- >
138
- <div class="accordion-body">
139
- <RepeaterSettingsPanel store={repeaterStore} />
140
- </div>
86
+ {#if repeaterStore}
87
+ <div class="tab-pane" id="repeater" role="tabpanel" aria-labelledby="repeater-tab">
88
+ <RepeaterSettingsPanel store={repeaterStore} />
141
89
  </div>
142
- </div>
143
- {/if}
90
+ {/if}
91
+ </div>
144
92
  </div>
145
93
  </MapControl>
146
94
 
147
95
  <style>
148
- .accordion {
149
- --bs-accordion-border-width: 0;
150
- /* Fixed width to prevent resizing when sections expand/collapse */
151
- width: 320px;
152
- min-width: 320px;
153
- max-width: 320px;
96
+ :global(.map-control-content) {
97
+ padding: 0.25rem;
154
98
  }
155
-
156
- .accordion-item {
157
- background-color: transparent;
158
- border: none;
159
- border-bottom: 1px solid rgba(0, 0, 0, 0.1);
160
- }
161
-
162
- .accordion-item:last-child {
163
- border-bottom: none;
164
- }
165
-
166
- .accordion-button {
167
- font-size: 0.875rem;
168
- font-weight: 600;
169
- padding: 0.75rem 1rem;
170
- background-color: transparent;
171
- color: var(--bs-body-color);
172
- /* Ensure chevron is visible and properly positioned */
173
- position: relative;
174
- display: flex;
175
- align-items: center;
176
- width: 100%;
177
- text-align: left;
178
- }
179
-
180
- /* Preserve Bootstrap's chevron */
181
- .accordion-button::after {
182
- flex-shrink: 0;
183
- width: 1.25rem;
184
- height: 1.25rem;
185
- margin-left: auto;
186
- content: "";
187
- 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");
188
- background-repeat: no-repeat;
189
- background-size: 1.25rem;
190
- transition: transform 0.2s ease-in-out;
191
- }
192
-
193
- /* Rotate chevron when expanded */
194
- .accordion-button:not(.collapsed)::after {
195
- transform: rotate(-180deg);
99
+
100
+ .nav-tabs {
101
+ border-bottom: 1px solid #dee2e6;
196
102
  }
197
-
198
- .accordion-button:not(.collapsed) {
199
- background-color: rgba(13, 110, 253, 0.05);
200
- color: #0d6efd;
103
+
104
+ .nav-link {
105
+ border: 1px solid transparent;
106
+ border-top-left-radius: 0.375rem;
107
+ border-top-right-radius: 0.375rem;
108
+ font-size: 0.95rem;
109
+ font-weight: 500;
110
+ padding: 0.5rem 1rem; /* increased horizontal padding to fit text better */
111
+ color: #6c757d;
112
+ white-space: nowrap; /* prevent text wrapping */
113
+ min-width: fit-content; /* ensure tab is at least as wide as content */
201
114
  }
202
-
203
- .accordion-button:focus {
204
- box-shadow: none;
205
- border-color: rgba(0, 0, 0, 0.125);
115
+
116
+ .nav-link.active {
117
+ color: #495057;
118
+ background-color: #fff;
119
+ border-color: #dee2e6 #dee2e6 #fff;
206
120
  }
207
-
208
- .accordion-body {
209
- padding: 1rem;
210
- padding-top: 0.5rem;
121
+
122
+ .tab-content {
123
+ padding: 0;
211
124
  }
212
125
  </style>