@smartnet360/svelte-components 0.0.98 → 0.0.99
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/apps/site-check/SiteCheck.svelte +30 -5
- package/dist/apps/site-check/data-loader.d.ts +2 -2
- package/dist/core/TreeView/tree.model.d.ts +2 -0
- package/dist/core/TreeView/tree.store.js +18 -0
- package/dist/map-v2/demo/demo-cells.d.ts +2 -1
- package/dist/map-v2/demo/demo-cells.js +107 -21
- package/dist/map-v2/shared/controls/FeatureSelectionControl.svelte +77 -24
- package/package.json +1 -1
|
@@ -65,12 +65,16 @@
|
|
|
65
65
|
// Single root select mode - only one Level 0 node at a time (radio behavior)
|
|
66
66
|
let singleRootSelect = $state(false);
|
|
67
67
|
|
|
68
|
+
// Single Level 1 select mode - only one Level 1 node per parent at a time (radio behavior)
|
|
69
|
+
let singleLevel1Select = $state(false);
|
|
70
|
+
|
|
68
71
|
// Available field options for grouping levels
|
|
69
72
|
const fieldOptions: { value: TreeGroupField; label: string }[] = [
|
|
70
73
|
{ value: 'site', label: 'Site' },
|
|
71
74
|
{ value: 'band', label: 'Band' },
|
|
72
75
|
{ value: 'azimuth', label: 'Azimuth' },
|
|
73
|
-
{ value: 'sector', label: 'Sector' }
|
|
76
|
+
{ value: 'sector', label: 'Sector' },
|
|
77
|
+
{ value: 'cellName', label: 'Cell Name' }
|
|
74
78
|
];
|
|
75
79
|
|
|
76
80
|
// Handlers for level changes
|
|
@@ -96,10 +100,10 @@
|
|
|
96
100
|
return fieldOptions.filter(opt => opt.value !== treeGrouping.level0);
|
|
97
101
|
}); let treeStore = $state<ReturnType<typeof createTreeStore> | null>(null);
|
|
98
102
|
|
|
99
|
-
// Rebuild tree whenever treeGrouping or
|
|
103
|
+
// Rebuild tree whenever treeGrouping, singleRootSelect, or singleLevel1Select changes
|
|
100
104
|
$effect(() => {
|
|
101
105
|
|
|
102
|
-
log('🔄 Rebuilding tree with grouping', { treeGrouping, singleRootSelect });
|
|
106
|
+
log('🔄 Rebuilding tree with grouping', { treeGrouping, singleRootSelect, singleLevel1Select });
|
|
103
107
|
|
|
104
108
|
// Clear any existing localStorage data to prevent stale state
|
|
105
109
|
const storageKey = 'site-check:treeState';
|
|
@@ -124,12 +128,14 @@
|
|
|
124
128
|
namespace: 'site-check',
|
|
125
129
|
persistState: false, // Don't persist when grouping changes dynamically
|
|
126
130
|
defaultExpandAll: false,
|
|
127
|
-
singleRootSelect // Pass single root select mode
|
|
131
|
+
singleRootSelect, // Pass single root select mode
|
|
132
|
+
singleLevel1Select // Pass single Level 1 select mode
|
|
128
133
|
});
|
|
129
134
|
log('✅ Tree Store Created', {
|
|
130
135
|
namespace: 'site-check',
|
|
131
136
|
grouping: treeGrouping,
|
|
132
|
-
singleRootSelect
|
|
137
|
+
singleRootSelect,
|
|
138
|
+
singleLevel1Select
|
|
133
139
|
});
|
|
134
140
|
});
|
|
135
141
|
|
|
@@ -361,6 +367,8 @@
|
|
|
361
367
|
>
|
|
362
368
|
<option value="band">Band</option>
|
|
363
369
|
<option value="site">Site</option>
|
|
370
|
+
<option value="sector">Sector</option>
|
|
371
|
+
<option value="cellName">Cell Name</option>
|
|
364
372
|
</select>
|
|
365
373
|
</div>
|
|
366
374
|
</div>
|
|
@@ -398,6 +406,23 @@
|
|
|
398
406
|
Single selection on level 0
|
|
399
407
|
</label>
|
|
400
408
|
</div>
|
|
409
|
+
|
|
410
|
+
<!-- Single Level 1 Select Toggle -->
|
|
411
|
+
<div class="form-check mt-2">
|
|
412
|
+
<input
|
|
413
|
+
class="form-check-input"
|
|
414
|
+
type="checkbox"
|
|
415
|
+
id="singleLevel1SelectCheck"
|
|
416
|
+
checked={singleLevel1Select}
|
|
417
|
+
onchange={(e) => {
|
|
418
|
+
singleLevel1Select = e.currentTarget.checked;
|
|
419
|
+
log('🔘 Single Level 1 select mode:', singleLevel1Select);
|
|
420
|
+
}}
|
|
421
|
+
/>
|
|
422
|
+
<label class="form-check-label small" for="singleLevel1SelectCheck">
|
|
423
|
+
Single selection on level 1
|
|
424
|
+
</label>
|
|
425
|
+
</div>
|
|
401
426
|
</div>
|
|
402
427
|
{/if}
|
|
403
428
|
{/if} <!-- Tree View -->
|
|
@@ -14,11 +14,11 @@ export interface CellTrafficRecord {
|
|
|
14
14
|
/**
|
|
15
15
|
* Tree grouping field types
|
|
16
16
|
*/
|
|
17
|
-
export type TreeGroupField = 'site' | 'azimuth' | 'band' | 'sector';
|
|
17
|
+
export type TreeGroupField = 'site' | 'azimuth' | 'band' | 'sector' | 'cellName';
|
|
18
18
|
/**
|
|
19
19
|
* Color dimension types - determines which field is used for chart coloring
|
|
20
20
|
*/
|
|
21
|
-
export type ColorDimension = 'site' | 'band';
|
|
21
|
+
export type ColorDimension = 'site' | 'azimuth' | 'band' | 'sector' | 'cellName';
|
|
22
22
|
/**
|
|
23
23
|
* Configuration for tree hierarchy grouping
|
|
24
24
|
* Defines which fields appear at each level of the tree
|
|
@@ -76,6 +76,8 @@ export interface TreeConfig<T = any> {
|
|
|
76
76
|
showIndeterminate?: boolean;
|
|
77
77
|
/** Single root selection mode - only one root node can be checked at a time (radio behavior) */
|
|
78
78
|
singleRootSelect?: boolean;
|
|
79
|
+
/** Single Level 1 selection mode - only one Level 1 node per parent can be checked at a time (radio behavior) */
|
|
80
|
+
singleLevel1Select?: boolean;
|
|
79
81
|
}
|
|
80
82
|
/**
|
|
81
83
|
* Store value exposed to consumers
|
|
@@ -102,6 +102,24 @@ export function createTreeStore(config) {
|
|
|
102
102
|
}
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
|
+
// STEP 0.5: If singleLevel1Select mode and this is a Level 1 node being checked, uncheck sibling Level 1 nodes
|
|
106
|
+
if (config.singleLevel1Select && newChecked && nodeState.level === 1) {
|
|
107
|
+
log('🔘 Single Level 1 select mode: unchecking sibling Level 1 nodes', { path });
|
|
108
|
+
const parentPath = nodeState.parentPath;
|
|
109
|
+
// Find and uncheck all Level 1 siblings (same parent, same level, different path)
|
|
110
|
+
state.nodes.forEach((node, nodePath) => {
|
|
111
|
+
if (node.level === 1 &&
|
|
112
|
+
node.parentPath === parentPath &&
|
|
113
|
+
nodePath !== path) {
|
|
114
|
+
newCheckedPaths.delete(nodePath);
|
|
115
|
+
// Also uncheck all descendants of this sibling
|
|
116
|
+
const siblingDescendants = getDescendantPaths(nodePath, state.nodes, separator);
|
|
117
|
+
siblingDescendants.forEach(descendantPath => {
|
|
118
|
+
newCheckedPaths.delete(descendantPath);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
105
123
|
// STEP 1: Update this node
|
|
106
124
|
if (newChecked) {
|
|
107
125
|
newCheckedPaths.add(path);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { Cell } from '../features/cells/types';
|
|
10
10
|
/**
|
|
11
|
-
* Generate demo cells
|
|
11
|
+
* Generate demo cells with varied density patterns in circular distribution
|
|
12
|
+
* Creates density zones radiating from center with random placement
|
|
12
13
|
*/
|
|
13
14
|
export declare const demoCells: Cell[];
|
|
@@ -6,14 +6,68 @@
|
|
|
6
6
|
* Each sector has 12 cells (all tech-band combinations)
|
|
7
7
|
* Total: 100 sites × 3 sectors × 12 tech-bands = 3,600 cells
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const
|
|
9
|
+
const BASE_LAT = 47.4979;
|
|
10
|
+
const BASE_LNG = 19.0402;
|
|
11
|
+
// Generate sites in a circular pattern with varying density
|
|
12
|
+
const NUM_SITES = 2000;
|
|
13
|
+
const RADIUS_KM = 15; // 15km radius circle
|
|
14
|
+
const RADIUS_DEGREES = RADIUS_KM / 111; // Approximate conversion
|
|
15
|
+
// Density zones (distance from center)
|
|
16
|
+
const DENSITY_ZONES = [
|
|
17
|
+
{ maxRadius: 0.3, minSpacing: 0.0008, maxSpacing: 0.0015, name: 'Very Dense Core' }, // 0-3km: 80-150m spacing
|
|
18
|
+
{ maxRadius: 0.5, minSpacing: 0.0015, maxSpacing: 0.003, name: 'Dense Inner' }, // 3-5km: 150-300m spacing
|
|
19
|
+
{ maxRadius: 0.7, minSpacing: 0.003, maxSpacing: 0.006, name: 'Medium' }, // 5-7km: 300-600m spacing
|
|
20
|
+
{ maxRadius: 0.85, minSpacing: 0.006, maxSpacing: 0.012, name: 'Sparse Suburban' }, // 7-12km: 600m-1.2km spacing
|
|
21
|
+
{ maxRadius: 1.0, minSpacing: 0.012, maxSpacing: 0.025, name: 'Very Sparse Rural' } // 12-15km: 1.2-2.5km spacing
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Get density zone for a given normalized radius
|
|
25
|
+
*/
|
|
26
|
+
function getDensityZone(normalizedRadius) {
|
|
27
|
+
for (const zone of DENSITY_ZONES) {
|
|
28
|
+
if (normalizedRadius <= zone.maxRadius) {
|
|
29
|
+
return zone;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return DENSITY_ZONES[DENSITY_ZONES.length - 1];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Generate random point within circle using polar coordinates
|
|
36
|
+
*/
|
|
37
|
+
function generateRandomPointInCircle() {
|
|
38
|
+
// Use square root for uniform distribution in circle
|
|
39
|
+
const r = Math.sqrt(Math.random()) * RADIUS_DEGREES;
|
|
40
|
+
const theta = Math.random() * 2 * Math.PI;
|
|
41
|
+
const lat = BASE_LAT + r * Math.cos(theta);
|
|
42
|
+
const lng = BASE_LNG + r * Math.sin(theta);
|
|
43
|
+
const normalizedRadius = r / RADIUS_DEGREES;
|
|
44
|
+
return { lat, lng, normalizedRadius };
|
|
45
|
+
}
|
|
46
|
+
// Cluster configuration for varied density
|
|
47
|
+
// (kept for backward compatibility but not used with circular generation)
|
|
48
|
+
const CLUSTERS = [
|
|
49
|
+
// Dense urban cluster (top-left) - very tight spacing
|
|
50
|
+
{ startRow: 0, endRow: 3, startCol: 0, endCol: 3, spacing: 0.3 },
|
|
51
|
+
// Medium density cluster (center) - normal spacing
|
|
52
|
+
{ startRow: 3, endRow: 7, startCol: 3, endCol: 7, spacing: 1.0 },
|
|
53
|
+
// Sparse rural cluster (bottom-right) - wide spacing
|
|
54
|
+
{ startRow: 7, endRow: 10, startCol: 7, endCol: 10, spacing: 2.5 },
|
|
55
|
+
// Random outliers scattered around
|
|
56
|
+
{ startRow: 0, endRow: 10, startCol: 0, endCol: 10, spacing: 1.5 }
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Add random jitter to coordinates for natural variation
|
|
60
|
+
*/
|
|
61
|
+
function addJitter(value, maxJitter) {
|
|
62
|
+
return value + (Math.random() - 0.5) * 2 * maxJitter;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Determine if site should be skipped (for creating gaps)
|
|
66
|
+
*/
|
|
67
|
+
function shouldSkipSite(row, col) {
|
|
68
|
+
// Skip some sites randomly to create density variation (20% skip rate)
|
|
69
|
+
return Math.random() < 0.2;
|
|
70
|
+
}
|
|
17
71
|
// Standard beamwidth for sectors
|
|
18
72
|
const BEAMWIDTH = 65;
|
|
19
73
|
// Cell tech-band definitions with proper fband format
|
|
@@ -51,20 +105,51 @@ const STATUSES = [
|
|
|
51
105
|
'On_Air'
|
|
52
106
|
];
|
|
53
107
|
/**
|
|
54
|
-
* Generate demo cells
|
|
108
|
+
* Generate demo cells with varied density patterns in circular distribution
|
|
109
|
+
* Creates density zones radiating from center with random placement
|
|
55
110
|
*/
|
|
56
111
|
export const demoCells = [];
|
|
57
112
|
let cellCounter = 1;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
113
|
+
let actualSiteIndex = 0;
|
|
114
|
+
// Track used positions to maintain minimum spacing
|
|
115
|
+
const usedPositions = [];
|
|
116
|
+
/**
|
|
117
|
+
* Check if position is too close to existing sites
|
|
118
|
+
*/
|
|
119
|
+
function isTooClose(lat, lng, minSpacing) {
|
|
120
|
+
for (const pos of usedPositions) {
|
|
121
|
+
const distance = Math.sqrt(Math.pow(lat - pos.lat, 2) + Math.pow(lng - pos.lng, 2));
|
|
122
|
+
const requiredSpacing = (minSpacing + pos.minSpacing) / 2;
|
|
123
|
+
if (distance < requiredSpacing) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
// Generate sites in a circular pattern with density-based placement
|
|
130
|
+
for (let attempt = 0; attempt < NUM_SITES * 3 && actualSiteIndex < NUM_SITES; attempt++) {
|
|
131
|
+
// Generate random point in circle
|
|
132
|
+
const { lat, lng, normalizedRadius } = generateRandomPointInCircle();
|
|
133
|
+
// Get density zone for this radius
|
|
134
|
+
const zone = getDensityZone(normalizedRadius);
|
|
135
|
+
// Random spacing within zone range
|
|
136
|
+
const minSpacing = zone.minSpacing + Math.random() * (zone.maxSpacing - zone.minSpacing);
|
|
137
|
+
// Check if too close to existing sites
|
|
138
|
+
if (isTooClose(lat, lng, minSpacing)) {
|
|
139
|
+
continue; // Try another position
|
|
140
|
+
}
|
|
141
|
+
// Add random jitter for natural variation
|
|
142
|
+
const jitterAmount = minSpacing * 0.3; // 30% of spacing
|
|
143
|
+
const siteLat = addJitter(lat, jitterAmount);
|
|
144
|
+
const siteLng = addJitter(lng, jitterAmount);
|
|
145
|
+
// Record position
|
|
146
|
+
usedPositions.push({ lat: siteLat, lng: siteLng, minSpacing });
|
|
147
|
+
const siteId = `DEMO-SITE-${String(actualSiteIndex + 1).padStart(4, '0')}`;
|
|
148
|
+
actualSiteIndex++;
|
|
149
|
+
// Generate 3 sectors per site (with some random 1 or 2 sector sites)
|
|
150
|
+
const numSectors = Math.random() < 0.1 ? (Math.random() < 0.5 ? 1 : 2) : 3; // 10% chance of 1-2 sectors
|
|
151
|
+
const sectorsToGenerate = AZIMUTHS.slice(0, numSectors);
|
|
152
|
+
sectorsToGenerate.forEach((azimuth, sectorIndex) => {
|
|
68
153
|
// Generate 12 tech-bands per sector
|
|
69
154
|
TECH_BANDS.forEach((techBand, techIndex) => {
|
|
70
155
|
const cellId = `CELL-${String(cellCounter).padStart(4, '0')}`;
|
|
@@ -118,10 +203,11 @@ for (let siteIndex = 0; siteIndex < NUM_SITES; siteIndex++) {
|
|
|
118
203
|
// Other
|
|
119
204
|
other: {
|
|
120
205
|
demoCell: true,
|
|
121
|
-
siteNumber:
|
|
206
|
+
siteNumber: actualSiteIndex,
|
|
122
207
|
sector: sectorIndex + 1,
|
|
123
208
|
techBandKey: `${techBand.tech}_${techBand.band}`,
|
|
124
|
-
|
|
209
|
+
radius: normalizedRadius,
|
|
210
|
+
densityZone: zone.name
|
|
125
211
|
},
|
|
126
212
|
customSubgroup: `Sector-${sectorIndex + 1}`
|
|
127
213
|
});
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
onAction,
|
|
49
49
|
actionButtonLabel = 'Process Cluster',
|
|
50
50
|
featureIcon = 'geo-alt-fill',
|
|
51
|
-
idPropertyOptions = ['siteId','sectorId', 'cellName','id'],
|
|
52
|
-
defaultIdProperty = '
|
|
51
|
+
idPropertyOptions = ['none','siteId','sectorId', 'cellName','id'],
|
|
52
|
+
defaultIdProperty = 'none'
|
|
53
53
|
}: Props = $props();
|
|
54
54
|
|
|
55
55
|
// Get map from context
|
|
@@ -114,33 +114,73 @@
|
|
|
114
114
|
const map = get(mapStore);
|
|
115
115
|
if (!map) return;
|
|
116
116
|
|
|
117
|
-
//
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
// Group features by coordinates
|
|
118
|
+
const featuresByLocation = new Map<string, SelectedFeature[]>();
|
|
119
|
+
|
|
120
|
+
for (const feature of features) {
|
|
121
|
+
const lat = feature.properties?.latitude || feature.properties?.lat;
|
|
122
|
+
const lon = feature.properties?.longitude || feature.properties?.lon || feature.properties?.lng;
|
|
123
|
+
|
|
124
|
+
if (lat && lon) {
|
|
125
|
+
const key = `${lon.toFixed(6)},${lat.toFixed(6)}`; // Round to avoid floating point issues
|
|
126
|
+
if (!featuresByLocation.has(key)) {
|
|
127
|
+
featuresByLocation.set(key, []);
|
|
128
|
+
}
|
|
129
|
+
featuresByLocation.get(key)!.push(feature);
|
|
130
|
+
} else {
|
|
131
|
+
console.warn('[FeatureSelectionControl] No coordinates found for feature', feature.id);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Track which location keys are currently active
|
|
136
|
+
const activeLocationKeys = new Set(featuresByLocation.keys());
|
|
137
|
+
|
|
138
|
+
// Remove markers that are no longer needed
|
|
139
|
+
for (const [key, marker] of markers.entries()) {
|
|
140
|
+
if (!activeLocationKeys.has(key)) {
|
|
121
141
|
marker.remove();
|
|
122
|
-
markers.delete(
|
|
123
|
-
console.log('[FeatureSelectionControl] Removed marker
|
|
142
|
+
markers.delete(key);
|
|
143
|
+
console.log('[FeatureSelectionControl] Removed marker at', key);
|
|
124
144
|
}
|
|
125
145
|
}
|
|
126
146
|
|
|
127
|
-
//
|
|
128
|
-
for (const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
// Create or update markers for each unique location
|
|
148
|
+
for (const [locationKey, featuresAtLocation] of featuresByLocation) {
|
|
149
|
+
const [lon, lat] = locationKey.split(',').map(Number);
|
|
150
|
+
|
|
151
|
+
// Build multi-line label HTML
|
|
152
|
+
const labelHTML = featuresAtLocation
|
|
153
|
+
.map(f => `<div style="padding: 2px 0;">${f.id}</div>`)
|
|
154
|
+
.join('');
|
|
155
|
+
|
|
156
|
+
// Check if marker already exists at this location
|
|
157
|
+
if (markers.has(locationKey)) {
|
|
158
|
+
// Update existing marker's popup
|
|
159
|
+
const marker = markers.get(locationKey)!;
|
|
160
|
+
const popup = marker.getPopup();
|
|
161
|
+
if (popup) {
|
|
162
|
+
popup.setHTML(`<div class="marker-label" style="font-size: 12px; line-height: 1.4;">${labelHTML}</div>`);
|
|
143
163
|
}
|
|
164
|
+
} else {
|
|
165
|
+
// Create new marker with popup
|
|
166
|
+
const popup = new mapboxgl.Popup({
|
|
167
|
+
closeButton: false,
|
|
168
|
+
closeOnClick: false,
|
|
169
|
+
offset: 45,
|
|
170
|
+
className: 'selection-marker-popup',
|
|
171
|
+
anchor: 'bottom'
|
|
172
|
+
}).setHTML(`<div class="marker-label" style="font-size: 12px; line-height: 1.4;">${labelHTML}</div>`);
|
|
173
|
+
|
|
174
|
+
const marker = new mapboxgl.Marker({ color: '#FF6B35' })
|
|
175
|
+
.setLngLat([lon, lat])
|
|
176
|
+
.setPopup(popup)
|
|
177
|
+
.addTo(map);
|
|
178
|
+
|
|
179
|
+
// Show popup immediately
|
|
180
|
+
marker.togglePopup();
|
|
181
|
+
|
|
182
|
+
markers.set(locationKey, marker);
|
|
183
|
+
console.log('[FeatureSelectionControl] Added marker at', locationKey, 'with', featuresAtLocation.length, 'items');
|
|
144
184
|
}
|
|
145
185
|
}
|
|
146
186
|
}
|
|
@@ -416,4 +456,17 @@
|
|
|
416
456
|
color: var(--bs-btn-disabled-color, var(--bs-btn-color, var(--bs-body-color)));
|
|
417
457
|
opacity: var(--bs-btn-disabled-opacity, 0.65);
|
|
418
458
|
}
|
|
459
|
+
|
|
460
|
+
/* Style for marker popup labels */
|
|
461
|
+
:global(.selection-marker-popup .mapboxgl-popup-content) {
|
|
462
|
+
padding: 8px 12px;
|
|
463
|
+
background: rgba(255, 255, 255, 0.95);
|
|
464
|
+
border-radius: 4px;
|
|
465
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
466
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
:global(.selection-marker-popup .mapboxgl-popup-tip) {
|
|
470
|
+
border-top-color: rgba(255, 255, 255, 0.95);
|
|
471
|
+
}
|
|
419
472
|
</style>
|