@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.
- package/dist/map-v2/core/providers/MapboxProvider.svelte +29 -0
- package/dist/map-v2/core/providers/MapboxProvider.svelte.d.ts +2 -0
- package/dist/map-v2/features/cells/stores/cellStoreContext.svelte.js +4 -16
- package/dist/map-v2/features/repeaters/stores/repeaterStoreContext.svelte.js +4 -17
- package/dist/map-v2/shared/controls/FeatureSettingsControl.svelte +55 -142
- package/dist/map-v2/shared/controls/MapControl.svelte +16 -6
- package/dist/map-v2/shared/controls/MapControl.svelte.d.ts +4 -0
- package/dist/map-v2/shared/controls/panels/CellSettingsPanel.svelte +240 -306
- package/dist/map-v2/shared/controls/panels/RepeaterSettingsPanel.svelte +180 -232
- package/dist/map-v2/shared/controls/panels/SiteSettingsPanel.svelte +119 -178
- package/package.json +1 -1
|
@@ -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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
<
|
|
66
|
-
<button
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
143
|
-
|
|
90
|
+
{/if}
|
|
91
|
+
</div>
|
|
144
92
|
</div>
|
|
145
93
|
</MapControl>
|
|
146
94
|
|
|
147
95
|
<style>
|
|
148
|
-
.
|
|
149
|
-
|
|
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
|
-
.
|
|
157
|
-
|
|
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
|
-
.
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
115
|
+
|
|
116
|
+
.nav-link.active {
|
|
117
|
+
color: #495057;
|
|
118
|
+
background-color: #fff;
|
|
119
|
+
border-color: #dee2e6 #dee2e6 #fff;
|
|
206
120
|
}
|
|
207
|
-
|
|
208
|
-
.
|
|
209
|
-
padding:
|
|
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:
|
|
158
|
-
box-shadow: 0
|
|
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:
|
|
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:
|
|
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:
|
|
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>;
|