@smartnet360/svelte-components 0.0.74 → 0.0.75

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.
@@ -40,6 +40,8 @@
40
40
  navigationPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
41
41
  /** Position for scale control */
42
42
  scalePosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
43
+ /** List of anchor layer IDs to create (invisible background layers for positioning) */
44
+ anchorLayerIds?: string[];
43
45
  /** Custom CSS class for container */
44
46
  class?: string;
45
47
  /** Optional child content (layers, controls, etc.) */
@@ -59,6 +61,7 @@
59
61
  controls = [],
60
62
  navigationPosition = 'top-right',
61
63
  scalePosition = 'bottom-left',
64
+ anchorLayerIds = [],
62
65
  class: className = '',
63
66
  children
64
67
  }: Props = $props();
@@ -71,6 +74,25 @@
71
74
  let map: MapboxMap | null = null;
72
75
  let isExternalMap = false;
73
76
 
77
+ /**
78
+ * Creates invisible anchor layers for predictable layer ordering
79
+ * These are background layers with visibility: none that serve as positioning markers
80
+ */
81
+ function createAnchorLayers(mapInstance: MapboxMap, layerIds: string[]) {
82
+ layerIds.forEach((layerId) => {
83
+ // Check if layer already exists (e.g., when style reloads)
84
+ if (!mapInstance.getLayer(layerId)) {
85
+ mapInstance.addLayer({
86
+ id: layerId,
87
+ type: 'background',
88
+ layout: {
89
+ visibility: 'none'
90
+ }
91
+ });
92
+ }
93
+ });
94
+ }
95
+
74
96
  onMount(() => {
75
97
  // If external map is provided, use it instead of creating a new one
76
98
  if (externalMap) {
@@ -129,10 +151,17 @@
129
151
 
130
152
  // Wait for style to load before distributing map
131
153
  const onStyleLoad = () => {
154
+ // Create anchor layers if specified
155
+ if (anchorLayerIds.length > 0 && map) {
156
+ createAnchorLayers(map, anchorLayerIds);
157
+ }
132
158
  mapStore.set(map);
133
159
  };
134
160
 
135
161
  if (map.isStyleLoaded()) {
162
+ if (anchorLayerIds.length > 0) {
163
+ createAnchorLayers(map, anchorLayerIds);
164
+ }
136
165
  mapStore.set(map);
137
166
  } else {
138
167
  map.once('style.load', onStyleLoad);
@@ -25,6 +25,8 @@ interface Props {
25
25
  navigationPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
26
26
  /** Position for scale control */
27
27
  scalePosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
28
+ /** List of anchor layer IDs to create (invisible background layers for positioning) */
29
+ anchorLayerIds?: string[];
28
30
  /** Custom CSS class for container */
29
31
  class?: string;
30
32
  /** Optional child content (layers, controls, etc.) */
@@ -74,22 +74,10 @@ export function createCellStoreContext(cells) {
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
77
- // Memoized to avoid re-filtering when cells array reference hasn't changed
78
- let lastCellsRef = null;
79
- let lastIncludePlanned = null;
80
- let cachedFilteredCells = [];
81
- let cellsFilteredByStatus = $derived.by(() => {
82
- // Only recompute if cells reference or flag changed
83
- if (state.cells === lastCellsRef && state.includePlannedCells === lastIncludePlanned) {
84
- return cachedFilteredCells;
85
- }
86
- lastCellsRef = state.cells;
87
- lastIncludePlanned = state.includePlannedCells;
88
- cachedFilteredCells = state.includePlannedCells
89
- ? state.cells // Include all cells
90
- : state.cells.filter(cell => cell.status.startsWith('On_Air')); // Only On Air cells
91
- return cachedFilteredCells;
92
- });
77
+ let cellsFilteredByStatus = $derived(state.includePlannedCells
78
+ ? state.cells // Include all cells
79
+ : state.cells.filter(cell => cell.status.startsWith('On_Air')) // Only On Air cells
80
+ );
93
81
  // Auto-save settings when they change
94
82
  $effect(() => {
95
83
  // Convert Map to plain object for serialization
@@ -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
@@ -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>
@@ -31,6 +31,10 @@
31
31
  className?: string;
32
32
  /** Child content */
33
33
  children?: import('svelte').Snippet;
34
+ /** Optional offset from map edge (e.g., '12px') */
35
+ edgeOffset?: string;
36
+ /** Width of the map control (e.g., '360px') */
37
+ controlWidth?: string;
34
38
  }
35
39
 
36
40
  let {
@@ -41,9 +45,12 @@
41
45
  collapsible = true,
42
46
  initiallyCollapsed = true,
43
47
  className = '',
44
- children
48
+ children,
49
+ edgeOffset = '12px',
50
+ controlWidth = '420px'
45
51
  }: Props = $props();
46
52
 
53
+
47
54
  const mapStore = tryUseMapbox();
48
55
 
49
56
  if (!mapStore) {
@@ -112,6 +119,8 @@
112
119
  <div
113
120
  bind:this={controlElement}
114
121
  class="mapboxgl-ctrl mapboxgl-ctrl-group map-control-container {className}"
122
+ style="--edge-offset: {edgeOffset}; --map-control-width: {controlWidth};"
123
+ data-edge-offset={edgeOffset}
115
124
  >
116
125
  {#if title || icon}
117
126
  <div class="map-control-header" title={title}>
@@ -154,10 +163,11 @@
154
163
  <style>
155
164
  .map-control-container {
156
165
  background: white;
157
- border-radius: 4px;
158
- box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
166
+ border-radius: 0.6rem; /* modern, slightly larger radius */
167
+ box-shadow: 0 6px 18px rgba(14, 30, 37, 0.08);
159
168
  overflow: hidden;
160
- max-width: 400px;
169
+ max-width: var(--map-control-width, 420px);
170
+ margin: var(--edge-offset, 12px); /* this ensures a default offset from the map edge */
161
171
  font-family: system-ui, -apple-system, sans-serif;
162
172
  }
163
173
 
@@ -165,7 +175,7 @@
165
175
  display: flex;
166
176
  align-items: center;
167
177
  justify-content: space-between;
168
- padding: 8px 12px;
178
+ padding: 10px 14px;
169
179
  background: #f8f9fa;
170
180
  border-bottom: 1px solid #dee2e6;
171
181
  font-weight: 600;
@@ -205,7 +215,7 @@
205
215
 
206
216
  .map-control-content {
207
217
  padding: 12px;
208
- max-height: 400px;
218
+ max-height: var(--map-control-max-height, 420px);
209
219
  overflow-y: auto;
210
220
  font-size: 13px;
211
221
  }
@@ -15,6 +15,10 @@ interface Props {
15
15
  className?: string;
16
16
  /** Child content */
17
17
  children?: import('svelte').Snippet;
18
+ /** Optional offset from map edge (e.g., '12px') */
19
+ edgeOffset?: string;
20
+ /** Width of the map control (e.g., '360px') */
21
+ controlWidth?: string;
18
22
  }
19
23
  declare const MapControl: import("svelte").Component<Props, {}, "">;
20
24
  type MapControl = ReturnType<typeof MapControl>;