@smartnet360/svelte-components 0.0.65 → 0.0.67
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/controls/MapStyleControl.svelte +76 -81
- package/dist/map-v2/core/controls/MapStyleControl.svelte.d.ts +4 -0
- package/dist/map-v2/demo/DemoMap.svelte +75 -7
- package/dist/map-v2/features/cells/controls/CellFilterControl.svelte +14 -5
- package/dist/map-v2/features/cells/controls/CellFilterControl.svelte.d.ts +4 -0
- package/dist/map-v2/features/cells/controls/CellStyleControl.svelte +8 -2
- package/dist/map-v2/features/cells/controls/CellStyleControl.svelte.d.ts +4 -0
- package/dist/map-v2/features/cells/layers/CellsLayer.svelte +2 -1
- package/dist/map-v2/features/cells/stores/cellStoreContext.svelte.d.ts +3 -0
- package/dist/map-v2/features/cells/stores/cellStoreContext.svelte.js +5 -0
- package/dist/map-v2/features/cells/utils/cellGeoJSON.d.ts +2 -6
- package/dist/map-v2/features/cells/utils/cellGeoJSON.js +11 -13
- package/dist/map-v2/features/cells/utils/cellTree.d.ts +5 -2
- package/dist/map-v2/features/cells/utils/cellTree.js +17 -6
- package/dist/map-v2/features/sites/controls/SiteFilterControl.svelte +8 -2
- package/dist/map-v2/features/sites/controls/SiteFilterControl.svelte.d.ts +4 -0
- package/dist/map-v2/features/sites/controls/SiteSelectionControl.svelte +281 -0
- package/dist/map-v2/features/sites/controls/SiteSelectionControl.svelte.d.ts +20 -0
- package/dist/map-v2/features/sites/controls/SiteSizeSlider.svelte +8 -2
- package/dist/map-v2/features/sites/controls/SiteSizeSlider.svelte.d.ts +4 -0
- package/dist/map-v2/features/sites/index.d.ts +1 -0
- package/dist/map-v2/features/sites/index.js +1 -0
- package/dist/map-v2/features/sites/layers/SitesLayer.svelte +11 -1
- package/dist/map-v2/features/sites/stores/siteStoreContext.svelte.d.ts +7 -0
- package/dist/map-v2/features/sites/stores/siteStoreContext.svelte.js +38 -1
- package/dist/map-v2/features/sites/types.d.ts +2 -0
- package/dist/map-v2/index.d.ts +1 -1
- package/dist/map-v2/index.js +1 -1
- package/dist/map-v2/shared/controls/MapControl.svelte +41 -4
- package/dist/map-v2/shared/controls/MapControl.svelte.d.ts +4 -0
- package/package.json +1 -1
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
26
26
|
/** Control title */
|
|
27
27
|
title?: string;
|
|
28
|
+
/** Optional header icon */
|
|
29
|
+
icon?: string;
|
|
30
|
+
/** Show icon when collapsed (default: true) */
|
|
31
|
+
iconOnlyWhenCollapsed?: boolean;
|
|
28
32
|
/** Initially collapsed? */
|
|
29
33
|
initiallyCollapsed?: boolean;
|
|
30
34
|
}
|
|
@@ -33,7 +37,9 @@
|
|
|
33
37
|
store,
|
|
34
38
|
position = 'top-left',
|
|
35
39
|
title = 'Site Filter',
|
|
36
|
-
|
|
40
|
+
icon = 'funnel',
|
|
41
|
+
iconOnlyWhenCollapsed = true,
|
|
42
|
+
initiallyCollapsed = true
|
|
37
43
|
}: Props = $props();
|
|
38
44
|
|
|
39
45
|
let treeStore = $state<Writable<TreeStoreValue> | null>(null);
|
|
@@ -77,7 +83,7 @@
|
|
|
77
83
|
}
|
|
78
84
|
</script>
|
|
79
85
|
|
|
80
|
-
<MapControl {position} {title} collapsible={true} {initiallyCollapsed}>
|
|
86
|
+
<MapControl {position} {title} {icon} {iconOnlyWhenCollapsed} collapsible={true} {initiallyCollapsed}>
|
|
81
87
|
{#if treeStore && $treeStore}
|
|
82
88
|
<div class="site-filter-tree">
|
|
83
89
|
<TreeView store={$treeStore} showControls={false}>
|
|
@@ -6,6 +6,10 @@ interface Props {
|
|
|
6
6
|
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
7
7
|
/** Control title */
|
|
8
8
|
title?: string;
|
|
9
|
+
/** Optional header icon */
|
|
10
|
+
icon?: string;
|
|
11
|
+
/** Show icon when collapsed (default: true) */
|
|
12
|
+
iconOnlyWhenCollapsed?: boolean;
|
|
9
13
|
/** Initially collapsed? */
|
|
10
14
|
initiallyCollapsed?: boolean;
|
|
11
15
|
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SiteSelectionControl - Site selection list builder
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Toggle to enable/disable click-to-select mode
|
|
7
|
+
* - List of selected sites
|
|
8
|
+
* - Individual delete and clear all
|
|
9
|
+
* - Copy site IDs to clipboard
|
|
10
|
+
* - Custom action button with callback
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import MapControl from '../../../shared/controls/MapControl.svelte';
|
|
14
|
+
import type { SiteStoreContext } from '../stores/siteStoreContext.svelte';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
/** Site store context */
|
|
18
|
+
store: SiteStoreContext;
|
|
19
|
+
/** Control position */
|
|
20
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
21
|
+
/** Control title */
|
|
22
|
+
title?: string;
|
|
23
|
+
/** Optional header icon */
|
|
24
|
+
icon?: string;
|
|
25
|
+
/** Show icon when collapsed (default: true) */
|
|
26
|
+
iconOnlyWhenCollapsed?: boolean;
|
|
27
|
+
/** Callback when action button clicked */
|
|
28
|
+
onAction?: (siteIds: string[]) => void;
|
|
29
|
+
/** Action button label */
|
|
30
|
+
actionButtonLabel?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
store,
|
|
35
|
+
position = 'top-left',
|
|
36
|
+
title = 'Site Selection',
|
|
37
|
+
icon = 'check2-square',
|
|
38
|
+
iconOnlyWhenCollapsed = true,
|
|
39
|
+
onAction,
|
|
40
|
+
actionButtonLabel = 'Open'
|
|
41
|
+
}: Props = $props();
|
|
42
|
+
|
|
43
|
+
let selectedSites = $derived(store.getSelectedSites());
|
|
44
|
+
let selectionCount = $derived(selectedSites.length);
|
|
45
|
+
let hasSelection = $derived(selectionCount > 0);
|
|
46
|
+
|
|
47
|
+
function handleToggleMode() {
|
|
48
|
+
store.toggleSelectionMode();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleRemoveSite(siteId: string) {
|
|
52
|
+
store.removeSiteSelection(siteId);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function handleClearAll() {
|
|
56
|
+
store.clearSelection();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function handleCopy() {
|
|
60
|
+
const ids = selectedSites.map(s => s.id).join(',');
|
|
61
|
+
try {
|
|
62
|
+
await navigator.clipboard.writeText(ids);
|
|
63
|
+
// Optional: Show toast notification
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error('Failed to copy:', err);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleAction() {
|
|
70
|
+
if (onAction && hasSelection) {
|
|
71
|
+
const ids = selectedSites.map(s => s.id);
|
|
72
|
+
onAction(ids);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<MapControl {position} {title} {icon} {iconOnlyWhenCollapsed} collapsible={true}>
|
|
78
|
+
<div class="site-selection-control">
|
|
79
|
+
<!-- Selection Mode Toggle -->
|
|
80
|
+
<div class="selection-mode-toggle mb-3">
|
|
81
|
+
<div class="form-check form-switch">
|
|
82
|
+
<input
|
|
83
|
+
type="checkbox"
|
|
84
|
+
class="form-check-input"
|
|
85
|
+
id="selection-mode-toggle"
|
|
86
|
+
checked={store.selectionMode}
|
|
87
|
+
onchange={handleToggleMode}
|
|
88
|
+
/>
|
|
89
|
+
<label class="form-check-label" for="selection-mode-toggle">
|
|
90
|
+
Selection Mode
|
|
91
|
+
<span class="badge ms-2" class:bg-success={store.selectionMode} class:bg-secondary={!store.selectionMode}>
|
|
92
|
+
{store.selectionMode ? 'ON' : 'OFF'}
|
|
93
|
+
</span>
|
|
94
|
+
</label>
|
|
95
|
+
</div>
|
|
96
|
+
{#if store.selectionMode}
|
|
97
|
+
<small class="text-muted d-block mt-1">
|
|
98
|
+
Click sites on the map to select
|
|
99
|
+
</small>
|
|
100
|
+
{/if}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Selection Stats -->
|
|
104
|
+
<div class="selection-stats mb-2">
|
|
105
|
+
<strong>{selectionCount}</strong>
|
|
106
|
+
{selectionCount === 1 ? 'site' : 'sites'} selected
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- Action Buttons -->
|
|
110
|
+
{#if hasSelection}
|
|
111
|
+
<div class="action-buttons mb-3">
|
|
112
|
+
<div class="btn-group w-100" role="group">
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
class="btn btn-sm btn-outline-danger"
|
|
116
|
+
onclick={handleClearAll}
|
|
117
|
+
title="Clear all"
|
|
118
|
+
>
|
|
119
|
+
<i class="bi bi-trash"></i> Clear
|
|
120
|
+
</button>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
class="btn btn-sm btn-outline-secondary"
|
|
124
|
+
onclick={handleCopy}
|
|
125
|
+
title="Copy site IDs"
|
|
126
|
+
>
|
|
127
|
+
<i class="bi bi-clipboard"></i> Copy
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
{/if}
|
|
132
|
+
|
|
133
|
+
<!-- Site List -->
|
|
134
|
+
{#if hasSelection}
|
|
135
|
+
<div class="site-list">
|
|
136
|
+
{#each selectedSites as site (site.id)}
|
|
137
|
+
<div class="site-item">
|
|
138
|
+
<i class="bi bi-geo-alt-fill site-icon"></i>
|
|
139
|
+
<span class="site-id">{site.id}</span>
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
class="btn-remove"
|
|
143
|
+
onclick={() => handleRemoveSite(site.id)}
|
|
144
|
+
title="Remove"
|
|
145
|
+
aria-label="Remove {site.id}"
|
|
146
|
+
>
|
|
147
|
+
<i class="bi bi-x"></i>
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
{/each}
|
|
151
|
+
</div>
|
|
152
|
+
{:else}
|
|
153
|
+
<div class="text-muted small text-center py-2">
|
|
154
|
+
<i class="bi bi-inbox"></i>
|
|
155
|
+
<div class="mt-1">No sites selected</div>
|
|
156
|
+
</div>
|
|
157
|
+
{/if}
|
|
158
|
+
|
|
159
|
+
<!-- Action Button -->
|
|
160
|
+
{#if onAction}
|
|
161
|
+
<div class="mt-3">
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
class="btn btn-primary w-100"
|
|
165
|
+
disabled={!hasSelection}
|
|
166
|
+
onclick={handleAction}
|
|
167
|
+
>
|
|
168
|
+
<i class="bi bi-lightning-charge-fill"></i> {actionButtonLabel}
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
{/if}
|
|
172
|
+
</div>
|
|
173
|
+
</MapControl>
|
|
174
|
+
|
|
175
|
+
<style>
|
|
176
|
+
.site-selection-control {
|
|
177
|
+
width: 100%;
|
|
178
|
+
min-width: 250px;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.selection-mode-toggle {
|
|
182
|
+
padding-bottom: 0.75rem;
|
|
183
|
+
border-bottom: 1px solid #dee2e6;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.selection-stats {
|
|
187
|
+
font-size: 0.875rem;
|
|
188
|
+
color: #495057;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.action-buttons {
|
|
192
|
+
display: flex;
|
|
193
|
+
gap: 0.5rem;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.site-list {
|
|
197
|
+
max-height: 300px;
|
|
198
|
+
overflow-y: auto;
|
|
199
|
+
border: 1px solid #dee2e6;
|
|
200
|
+
border-radius: 4px;
|
|
201
|
+
padding: 0.5rem;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.site-item {
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
gap: 0.5rem;
|
|
208
|
+
padding: 0.5rem;
|
|
209
|
+
border-bottom: 1px solid #f1f3f5;
|
|
210
|
+
transition: background-color 0.15s;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.site-item:last-child {
|
|
214
|
+
border-bottom: none;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.site-item:hover {
|
|
218
|
+
background-color: #f8f9fa;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.site-icon {
|
|
222
|
+
font-size: 1rem;
|
|
223
|
+
flex-shrink: 0;
|
|
224
|
+
color: #0d6efd;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.site-id {
|
|
228
|
+
flex: 1;
|
|
229
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
230
|
+
font-size: 0.875rem;
|
|
231
|
+
color: #212529;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.btn-remove {
|
|
235
|
+
background: none;
|
|
236
|
+
border: none;
|
|
237
|
+
color: #6c757d;
|
|
238
|
+
font-size: 1.25rem;
|
|
239
|
+
line-height: 1;
|
|
240
|
+
padding: 0;
|
|
241
|
+
width: 24px;
|
|
242
|
+
height: 24px;
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
justify-content: center;
|
|
246
|
+
cursor: pointer;
|
|
247
|
+
border-radius: 4px;
|
|
248
|
+
transition: all 0.15s;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.btn-remove:hover {
|
|
252
|
+
background-color: #ffe6e6;
|
|
253
|
+
color: #dc3545;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.btn-remove:active {
|
|
257
|
+
background-color: #ffcccc;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* Ensure primary action button keeps Bootstrap styling inside Mapbox control */
|
|
261
|
+
.site-selection-control .btn-primary {
|
|
262
|
+
background-color: var(--bs-btn-bg, var(--bs-primary));
|
|
263
|
+
border-color: var(--bs-btn-border-color, var(--bs-primary));
|
|
264
|
+
color: var(--bs-btn-color, var(--bs-body-color));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.site-selection-control .btn-primary:hover,
|
|
268
|
+
.site-selection-control .btn-primary:focus {
|
|
269
|
+
background-color: var(--bs-btn-hover-bg, var(--bs-primary));
|
|
270
|
+
border-color: var(--bs-btn-hover-border-color, var(--bs-primary));
|
|
271
|
+
color: var(--bs-btn-hover-color, var(--bs-btn-color, var(--bs-body-color)));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.site-selection-control .btn-primary:disabled,
|
|
275
|
+
.site-selection-control .btn-primary:disabled:hover {
|
|
276
|
+
background-color: var(--bs-btn-disabled-bg, var(--bs-btn-bg, var(--bs-primary)));
|
|
277
|
+
border-color: var(--bs-btn-disabled-border-color, var(--bs-btn-border-color, var(--bs-primary)));
|
|
278
|
+
color: var(--bs-btn-disabled-color, var(--bs-btn-color, var(--bs-body-color)));
|
|
279
|
+
opacity: var(--bs-btn-disabled-opacity, 0.65);
|
|
280
|
+
}
|
|
281
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SiteStoreContext } from '../stores/siteStoreContext.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Site store context */
|
|
4
|
+
store: SiteStoreContext;
|
|
5
|
+
/** Control position */
|
|
6
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
7
|
+
/** Control title */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Optional header icon */
|
|
10
|
+
icon?: string;
|
|
11
|
+
/** Show icon when collapsed (default: true) */
|
|
12
|
+
iconOnlyWhenCollapsed?: boolean;
|
|
13
|
+
/** Callback when action button clicked */
|
|
14
|
+
onAction?: (siteIds: string[]) => void;
|
|
15
|
+
/** Action button label */
|
|
16
|
+
actionButtonLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const SiteSelectionControl: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type SiteSelectionControl = ReturnType<typeof SiteSelectionControl>;
|
|
20
|
+
export default SiteSelectionControl;
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
18
18
|
/** Control title */
|
|
19
19
|
title?: string;
|
|
20
|
+
/** Optional header icon */
|
|
21
|
+
icon?: string;
|
|
22
|
+
/** Show icon when collapsed (default: true) */
|
|
23
|
+
iconOnlyWhenCollapsed?: boolean;
|
|
20
24
|
/** Initially collapsed? */
|
|
21
25
|
initiallyCollapsed?: boolean;
|
|
22
26
|
/** Minimum size (default: 2) */
|
|
@@ -31,14 +35,16 @@
|
|
|
31
35
|
store,
|
|
32
36
|
position = 'top-right',
|
|
33
37
|
title = 'Site Size',
|
|
34
|
-
|
|
38
|
+
icon = 'sliders',
|
|
39
|
+
iconOnlyWhenCollapsed = true,
|
|
40
|
+
initiallyCollapsed = true,
|
|
35
41
|
min = 2,
|
|
36
42
|
max = 20,
|
|
37
43
|
step = 1
|
|
38
44
|
}: Props = $props();
|
|
39
45
|
</script>
|
|
40
46
|
|
|
41
|
-
<MapControl {position} {title} collapsible={true} {initiallyCollapsed}>
|
|
47
|
+
<MapControl {position} {title} {icon} {iconOnlyWhenCollapsed} collapsible={true} {initiallyCollapsed}>
|
|
42
48
|
<div class="site-size-controls">
|
|
43
49
|
<!-- Size slider -->
|
|
44
50
|
<div class="control-row">
|
|
@@ -6,6 +6,10 @@ interface Props {
|
|
|
6
6
|
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
7
7
|
/** Control title */
|
|
8
8
|
title?: string;
|
|
9
|
+
/** Optional header icon */
|
|
10
|
+
icon?: string;
|
|
11
|
+
/** Show icon when collapsed (default: true) */
|
|
12
|
+
iconOnlyWhenCollapsed?: boolean;
|
|
9
13
|
/** Initially collapsed? */
|
|
10
14
|
initiallyCollapsed?: boolean;
|
|
11
15
|
/** Minimum size (default: 2) */
|
|
@@ -10,5 +10,6 @@ export { createSiteStoreContext } from './stores/siteStoreContext.svelte';
|
|
|
10
10
|
export { default as SitesLayer } from './layers/SitesLayer.svelte';
|
|
11
11
|
export { default as SiteFilterControl } from './controls/SiteFilterControl.svelte';
|
|
12
12
|
export { default as SiteSizeSlider } from './controls/SiteSizeSlider.svelte';
|
|
13
|
+
export { default as SiteSelectionControl } from './controls/SiteSelectionControl.svelte';
|
|
13
14
|
export { sitesToGeoJSON, siteToFeature } from './utils/siteGeoJSON';
|
|
14
15
|
export { buildSiteTree, getFilteredSites } from './utils/siteTreeUtils';
|
|
@@ -11,6 +11,7 @@ export { default as SitesLayer } from './layers/SitesLayer.svelte';
|
|
|
11
11
|
// Controls
|
|
12
12
|
export { default as SiteFilterControl } from './controls/SiteFilterControl.svelte';
|
|
13
13
|
export { default as SiteSizeSlider } from './controls/SiteSizeSlider.svelte';
|
|
14
|
+
export { default as SiteSelectionControl } from './controls/SiteSelectionControl.svelte';
|
|
14
15
|
// Utils
|
|
15
16
|
export { sitesToGeoJSON, siteToFeature } from './utils/siteGeoJSON';
|
|
16
17
|
export { buildSiteTree, getFilteredSites } from './utils/siteTreeUtils';
|
|
@@ -188,6 +188,14 @@
|
|
|
188
188
|
if (!e.features || e.features.length === 0) return;
|
|
189
189
|
const feature = e.features[0];
|
|
190
190
|
const siteProps = feature.properties;
|
|
191
|
+
|
|
192
|
+
// If selection mode is enabled, toggle selection instead of showing popup
|
|
193
|
+
if (store.selectionMode && siteProps?.id) {
|
|
194
|
+
store.toggleSiteSelection(siteProps.id);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Otherwise, show popup as normal
|
|
191
199
|
if (siteProps && e.lngLat) {
|
|
192
200
|
createSitePopup(siteProps, e.lngLat);
|
|
193
201
|
}
|
|
@@ -240,7 +248,9 @@
|
|
|
240
248
|
|
|
241
249
|
function handleMouseEnter(e: mapboxgl.MapLayerMouseEvent): void {
|
|
242
250
|
if (!map) return;
|
|
243
|
-
|
|
251
|
+
|
|
252
|
+
// Change cursor based on mode
|
|
253
|
+
map.getCanvas().style.cursor = store.selectionMode ? 'crosshair' : 'pointer';
|
|
244
254
|
|
|
245
255
|
if (!e.features || e.features.length === 0) return;
|
|
246
256
|
const feature = e.features[0];
|
|
@@ -21,12 +21,19 @@ export declare function createSiteStoreContext(initialSites?: Site[]): {
|
|
|
21
21
|
strokeWidth: number;
|
|
22
22
|
strokeColor: string;
|
|
23
23
|
groupColorMap: Map<string, string>;
|
|
24
|
+
selectionMode: boolean;
|
|
25
|
+
readonly selectedSiteIds: Set<string>;
|
|
24
26
|
setAllSites(sites: Site[]): void;
|
|
25
27
|
setFilteredSites(sites: Site[]): void;
|
|
26
28
|
setSize(size: number): void;
|
|
27
29
|
setColor(color: string): void;
|
|
28
30
|
setOpacity(opacity: number): void;
|
|
29
31
|
setShowLabels(show: boolean): void;
|
|
32
|
+
toggleSelectionMode(): void;
|
|
33
|
+
toggleSiteSelection(siteId: string): void;
|
|
34
|
+
removeSiteSelection(siteId: string): void;
|
|
35
|
+
clearSelection(): void;
|
|
36
|
+
getSelectedSites(): Site[];
|
|
30
37
|
getGroupColor(groupKey: string): string | undefined;
|
|
31
38
|
setGroupColor(groupKey: string, color: string): void;
|
|
32
39
|
clearGroupColor(groupKey: string): void;
|
|
@@ -55,7 +55,9 @@ export function createSiteStoreContext(initialSites = []) {
|
|
|
55
55
|
labelProperty: persistedSettings.labelProperty ?? 'name',
|
|
56
56
|
groupColorMap: initialColorMap,
|
|
57
57
|
strokeWidth: persistedSettings.strokeWidth ?? 2,
|
|
58
|
-
strokeColor: persistedSettings.strokeColor ?? '#ffffff'
|
|
58
|
+
strokeColor: persistedSettings.strokeColor ?? '#ffffff',
|
|
59
|
+
selectionMode: false, // Not persisted - always starts disabled
|
|
60
|
+
selectedSiteIds: new Set() // Not persisted - always starts empty
|
|
59
61
|
});
|
|
60
62
|
// Auto-save settings when they change
|
|
61
63
|
$effect(() => {
|
|
@@ -110,6 +112,10 @@ export function createSiteStoreContext(initialSites = []) {
|
|
|
110
112
|
set strokeColor(value) { state.strokeColor = value; },
|
|
111
113
|
get groupColorMap() { return state.groupColorMap; },
|
|
112
114
|
set groupColorMap(value) { state.groupColorMap = value; },
|
|
115
|
+
// Selection properties
|
|
116
|
+
get selectionMode() { return state.selectionMode; },
|
|
117
|
+
set selectionMode(value) { state.selectionMode = value; },
|
|
118
|
+
get selectedSiteIds() { return state.selectedSiteIds; },
|
|
113
119
|
// Convenience methods (optional, but nice to have)
|
|
114
120
|
setAllSites(sites) { state.allSites = sites; },
|
|
115
121
|
setFilteredSites(sites) { state.filteredSites = sites; },
|
|
@@ -117,6 +123,35 @@ export function createSiteStoreContext(initialSites = []) {
|
|
|
117
123
|
setColor(color) { state.color = color; },
|
|
118
124
|
setOpacity(opacity) { state.opacity = opacity; },
|
|
119
125
|
setShowLabels(show) { state.showLabels = show; },
|
|
126
|
+
// Selection methods
|
|
127
|
+
toggleSelectionMode() {
|
|
128
|
+
state.selectionMode = !state.selectionMode;
|
|
129
|
+
// Clear selection when disabling selection mode
|
|
130
|
+
if (!state.selectionMode) {
|
|
131
|
+
state.selectedSiteIds = new Set();
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
toggleSiteSelection(siteId) {
|
|
135
|
+
const newSet = new Set(state.selectedSiteIds);
|
|
136
|
+
if (newSet.has(siteId)) {
|
|
137
|
+
newSet.delete(siteId);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
newSet.add(siteId);
|
|
141
|
+
}
|
|
142
|
+
state.selectedSiteIds = newSet;
|
|
143
|
+
},
|
|
144
|
+
removeSiteSelection(siteId) {
|
|
145
|
+
const newSet = new Set(state.selectedSiteIds);
|
|
146
|
+
newSet.delete(siteId);
|
|
147
|
+
state.selectedSiteIds = newSet;
|
|
148
|
+
},
|
|
149
|
+
clearSelection() {
|
|
150
|
+
state.selectedSiteIds = new Set();
|
|
151
|
+
},
|
|
152
|
+
getSelectedSites() {
|
|
153
|
+
return state.allSites.filter(site => state.selectedSiteIds.has(site.id));
|
|
154
|
+
},
|
|
120
155
|
// Group color methods
|
|
121
156
|
getGroupColor(groupKey) {
|
|
122
157
|
return state.groupColorMap.get(groupKey);
|
|
@@ -146,6 +181,8 @@ export function createSiteStoreContext(initialSites = []) {
|
|
|
146
181
|
state.strokeWidth = 2;
|
|
147
182
|
state.strokeColor = '#ffffff';
|
|
148
183
|
state.groupColorMap = new Map();
|
|
184
|
+
state.selectionMode = false;
|
|
185
|
+
state.selectedSiteIds = new Set();
|
|
149
186
|
},
|
|
150
187
|
// Get snapshot of current state (useful for debugging)
|
|
151
188
|
getState() {
|
package/dist/map-v2/index.d.ts
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { type MapStore, MAP_CONTEXT_KEY, MapboxProvider, ViewportSync, MapStyleControl, createMapStore, createViewportStore, type ViewportStore, type ViewportState, useMapbox, tryUseMapbox } from './core';
|
|
8
8
|
export { MapControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing, removeLayerIfExists, updateGeoJSONSource, removeLayerAndSource, isStyleLoaded, waitForStyleLoad, setFeatureState, removeFeatureState, generateLayerId, generateSourceId } from './shared';
|
|
9
|
-
export { type Site, type SiteStoreValue, type SiteStoreContext, createSiteStore, createSiteStoreContext, SitesLayer, SiteFilterControl, SiteSizeSlider, sitesToGeoJSON, siteToFeature, buildSiteTree, getFilteredSites } from './features/sites';
|
|
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 CellTreeConfig, type TechnologyBandKey, type CellStoreValue, type CellStoreContext, createCellStoreContext, CellsLayer, 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';
|
package/dist/map-v2/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export { MapControl, addSourceIfMissing, removeSourceIfExists, addLayerIfMissing
|
|
|
15
15
|
// ============================================================================
|
|
16
16
|
// SITE FEATURE
|
|
17
17
|
// ============================================================================
|
|
18
|
-
export { createSiteStore, createSiteStoreContext, SitesLayer, SiteFilterControl, SiteSizeSlider, sitesToGeoJSON, siteToFeature, buildSiteTree, getFilteredSites } from './features/sites';
|
|
18
|
+
export { createSiteStore, createSiteStoreContext, SitesLayer, SiteFilterControl, SiteSelectionControl, SiteSizeSlider, sitesToGeoJSON, siteToFeature, buildSiteTree, getFilteredSites } from './features/sites';
|
|
19
19
|
// ============================================================================
|
|
20
20
|
// CELL FEATURE
|
|
21
21
|
// ============================================================================
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
20
20
|
/** Control title (shown in header) */
|
|
21
21
|
title?: string;
|
|
22
|
+
/** Optional Bootstrap icon name to render when collapsed */
|
|
23
|
+
icon?: string;
|
|
24
|
+
/** When collapsed, should we show only the icon (if provided)? */
|
|
25
|
+
iconOnlyWhenCollapsed?: boolean;
|
|
22
26
|
/** Is the control collapsible? */
|
|
23
27
|
collapsible?: boolean;
|
|
24
28
|
/** Initial collapsed state */
|
|
@@ -32,8 +36,10 @@
|
|
|
32
36
|
let {
|
|
33
37
|
position = 'top-left',
|
|
34
38
|
title,
|
|
39
|
+
icon,
|
|
40
|
+
iconOnlyWhenCollapsed = true,
|
|
35
41
|
collapsible = true,
|
|
36
|
-
initiallyCollapsed =
|
|
42
|
+
initiallyCollapsed = true,
|
|
37
43
|
className = '',
|
|
38
44
|
children
|
|
39
45
|
}: Props = $props();
|
|
@@ -107,9 +113,22 @@
|
|
|
107
113
|
bind:this={controlElement}
|
|
108
114
|
class="mapboxgl-ctrl mapboxgl-ctrl-group map-control-container {className}"
|
|
109
115
|
>
|
|
110
|
-
{#if title}
|
|
111
|
-
<div class="map-control-header">
|
|
112
|
-
<span class="map-control-title">
|
|
116
|
+
{#if title || icon}
|
|
117
|
+
<div class="map-control-header" title={title}>
|
|
118
|
+
<span class="map-control-title">
|
|
119
|
+
{#if collapsed && iconOnlyWhenCollapsed && icon}
|
|
120
|
+
<i class="bi bi-{icon}" aria-hidden="true"></i>
|
|
121
|
+
{#if title}
|
|
122
|
+
<span class="visually-hidden">{title}</span>
|
|
123
|
+
{/if}
|
|
124
|
+
{:else}
|
|
125
|
+
{#if title}
|
|
126
|
+
{title}
|
|
127
|
+
{:else if icon}
|
|
128
|
+
<i class="bi bi-{icon}" aria-hidden="true"></i>
|
|
129
|
+
{/if}
|
|
130
|
+
{/if}
|
|
131
|
+
</span>
|
|
113
132
|
{#if collapsible}
|
|
114
133
|
<button
|
|
115
134
|
class="map-control-toggle"
|
|
@@ -159,6 +178,13 @@
|
|
|
159
178
|
user-select: none;
|
|
160
179
|
}
|
|
161
180
|
|
|
181
|
+
.map-control-title i {
|
|
182
|
+
display: inline-flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
justify-content: center;
|
|
185
|
+
font-size: 1.1rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
162
188
|
.map-control-toggle {
|
|
163
189
|
background: none;
|
|
164
190
|
border: none;
|
|
@@ -201,4 +227,15 @@
|
|
|
201
227
|
.map-control-content::-webkit-scrollbar-thumb:hover {
|
|
202
228
|
background: #555;
|
|
203
229
|
}
|
|
230
|
+
|
|
231
|
+
.visually-hidden {
|
|
232
|
+
position: absolute;
|
|
233
|
+
width: 1px;
|
|
234
|
+
height: 1px;
|
|
235
|
+
padding: 0;
|
|
236
|
+
overflow: hidden;
|
|
237
|
+
clip: rect(0, 0, 0, 0);
|
|
238
|
+
white-space: nowrap;
|
|
239
|
+
border: 0;
|
|
240
|
+
}
|
|
204
241
|
</style>
|
|
@@ -3,6 +3,10 @@ interface Props {
|
|
|
3
3
|
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
4
4
|
/** Control title (shown in header) */
|
|
5
5
|
title?: string;
|
|
6
|
+
/** Optional Bootstrap icon name to render when collapsed */
|
|
7
|
+
icon?: string;
|
|
8
|
+
/** When collapsed, should we show only the icon (if provided)? */
|
|
9
|
+
iconOnlyWhenCollapsed?: boolean;
|
|
6
10
|
/** Is the control collapsible? */
|
|
7
11
|
collapsible?: boolean;
|
|
8
12
|
/** Initial collapsed state */
|