@smartnet360/svelte-components 0.0.100 โ 0.0.102
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 +54 -272
- package/dist/apps/site-check/SiteCheckControls.svelte +294 -0
- package/dist/apps/site-check/SiteCheckControls.svelte.d.ts +30 -0
- package/dist/map-v2/demo/DemoMap.svelte +39 -7
- package/dist/map-v2/features/cells/utils/cellGeoJSON.js +1 -0
- package/dist/map-v2/shared/controls/FeatureSelectionControl.svelte +20 -25
- package/dist/map-v2/shared/controls/FeatureSelectionControl.svelte.d.ts +2 -4
- package/dist/map-v3/demo/DemoMap.svelte +31 -5
- package/dist/map-v3/demo/demo-cells.js +51 -22
- package/dist/map-v3/features/cells/layers/CellsLayer.svelte +29 -9
- package/dist/map-v3/features/cells/logic/geometry.js +3 -0
- package/dist/map-v3/features/cells/stores/cell.data.svelte.d.ts +27 -0
- package/dist/map-v3/features/cells/stores/cell.data.svelte.js +65 -0
- package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte +82 -65
- package/dist/map-v3/features/selection/components/FeatureSelectionControl.svelte.d.ts +5 -9
- package/dist/map-v3/features/selection/index.d.ts +1 -2
- package/dist/map-v3/features/selection/index.js +0 -1
- package/dist/map-v3/features/selection/stores/selection.store.svelte.d.ts +44 -15
- package/dist/map-v3/features/selection/stores/selection.store.svelte.js +163 -40
- package/dist/map-v3/features/selection/types.d.ts +4 -2
- package/dist/shared/ResizableSplitPanel.svelte +175 -0
- package/dist/shared/ResizableSplitPanel.svelte.d.ts +17 -0
- package/package.json +1 -1
- package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte +0 -209
- package/dist/map-v3/features/selection/layers/SelectionHighlightLayers.svelte.d.ts +0 -13
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
import { expandLayoutForCells } from './helper';
|
|
8
8
|
import { log } from '../../core/logger';
|
|
9
9
|
import type {ChartMarker, Mode } from '../../index.js';
|
|
10
|
-
import { checkHealth
|
|
10
|
+
import { checkHealth } from '../../core/FeatureRegistry';
|
|
11
|
+
import SiteCheckControls from './SiteCheckControls.svelte';
|
|
12
|
+
import ResizableSplitPanel from '../../shared/ResizableSplitPanel.svelte';
|
|
11
13
|
|
|
12
14
|
interface Props {
|
|
13
15
|
rawData: CellTrafficRecord[];
|
|
@@ -31,28 +33,6 @@
|
|
|
31
33
|
cellStyling = defaultCellStyling, initialGrouping = defaultTreeGrouping,
|
|
32
34
|
showGroupingSelector = true, useSectorLineStyles = false, onSearch, searchPlaceholder = "Search...", plotlyLayout }: Props = $props();
|
|
33
35
|
|
|
34
|
-
// Search state
|
|
35
|
-
let searchTerm = $state('');
|
|
36
|
-
|
|
37
|
-
// Controls visibility state (starts expanded)
|
|
38
|
-
let controlsExpanded = $state(true);
|
|
39
|
-
|
|
40
|
-
// Handlers
|
|
41
|
-
function handleSearch() {
|
|
42
|
-
if (onSearch) {
|
|
43
|
-
onSearch(searchTerm);
|
|
44
|
-
log('๐ Search triggered:', searchTerm);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function handleClearSearch() {
|
|
49
|
-
searchTerm = '';
|
|
50
|
-
if (onSearch) {
|
|
51
|
-
onSearch('');
|
|
52
|
-
log('๐งน Search cleared');
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
36
|
// Check feature health
|
|
57
37
|
let isHealthy = $state(checkHealth('sitecheck'));
|
|
58
38
|
|
|
@@ -68,37 +48,7 @@
|
|
|
68
48
|
// Single Level 1 select mode - only one Level 1 node per parent at a time (radio behavior)
|
|
69
49
|
let singleLevel1Select = $state(false);
|
|
70
50
|
|
|
71
|
-
|
|
72
|
-
const fieldOptions: { value: TreeGroupField; label: string }[] = [
|
|
73
|
-
{ value: 'site', label: 'Site' },
|
|
74
|
-
{ value: 'band', label: 'Band' },
|
|
75
|
-
{ value: 'azimuth', label: 'Azimuth' },
|
|
76
|
-
{ value: 'sector', label: 'Sector' },
|
|
77
|
-
{ value: 'cellName', label: 'Cell Name' }
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
// Handlers for level changes
|
|
81
|
-
function handleLevel0Change(value: TreeGroupField) {
|
|
82
|
-
// Clear level1 if it conflicts with new level0
|
|
83
|
-
const newLevel1 = treeGrouping.level1 === value ? null : treeGrouping.level1;
|
|
84
|
-
treeGrouping = {
|
|
85
|
-
level0: value,
|
|
86
|
-
level1: newLevel1
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function handleLevel1Change(value: TreeGroupField | 'none') {
|
|
91
|
-
const newLevel1 = value === 'none' ? null : value;
|
|
92
|
-
treeGrouping = {
|
|
93
|
-
level0: treeGrouping.level0,
|
|
94
|
-
level1: newLevel1
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Get available options for level1 (exclude level0)
|
|
99
|
-
let availableLevel1Options = $derived.by(() => {
|
|
100
|
-
return fieldOptions.filter(opt => opt.value !== treeGrouping.level0);
|
|
101
|
-
}); let treeStore = $state<ReturnType<typeof createTreeStore> | null>(null);
|
|
51
|
+
let treeStore = $state<ReturnType<typeof createTreeStore> | null>(null);
|
|
102
52
|
|
|
103
53
|
// Rebuild tree whenever treeGrouping, singleRootSelect, or singleLevel1Select changes
|
|
104
54
|
$effect(() => {
|
|
@@ -106,10 +56,11 @@
|
|
|
106
56
|
log('๐ Rebuilding tree with grouping', { treeGrouping, singleRootSelect, singleLevel1Select });
|
|
107
57
|
|
|
108
58
|
// Clear any existing localStorage data to prevent stale state
|
|
109
|
-
|
|
59
|
+
// This includes both tree state AND chart settings to avoid cell mismatches
|
|
110
60
|
if (typeof window !== 'undefined') {
|
|
111
|
-
localStorage.removeItem(
|
|
112
|
-
|
|
61
|
+
localStorage.removeItem('site-check:treeState');
|
|
62
|
+
localStorage.removeItem('charts:globalControls');
|
|
63
|
+
log('๐งน Cleared localStorage: tree state and chart settings');
|
|
113
64
|
}
|
|
114
65
|
|
|
115
66
|
// Build tree nodes from raw data with custom grouping
|
|
@@ -250,227 +201,58 @@
|
|
|
250
201
|
</script>
|
|
251
202
|
|
|
252
203
|
<div class="container-fluid vh-100 d-flex flex-column">
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
</button>
|
|
272
|
-
{/if}
|
|
273
|
-
|
|
274
|
-
<!-- Collapsible Controls Content -->
|
|
275
|
-
{#if controlsExpanded}
|
|
276
|
-
<!-- Search Box -->
|
|
277
|
-
{#if onSearch}
|
|
278
|
-
<div class="p-3 border-bottom flex-shrink-0">
|
|
279
|
-
<label for="searchInput" class="form-label small fw-semibold mb-2">
|
|
280
|
-
Search
|
|
281
|
-
</label>
|
|
282
|
-
<div class="input-group input-group-sm">
|
|
283
|
-
<input
|
|
284
|
-
type="text"
|
|
285
|
-
id="searchInput"
|
|
286
|
-
class="form-control"
|
|
287
|
-
placeholder={searchPlaceholder}
|
|
288
|
-
bind:value={searchTerm}
|
|
289
|
-
onkeydown={(e) => {
|
|
290
|
-
if (e.key === 'Enter') {
|
|
291
|
-
handleSearch();
|
|
292
|
-
}
|
|
293
|
-
}}
|
|
294
|
-
/>
|
|
295
|
-
{#if searchTerm}
|
|
296
|
-
<button
|
|
297
|
-
class="btn btn-outline-secondary"
|
|
298
|
-
type="button"
|
|
299
|
-
onclick={handleClearSearch}
|
|
300
|
-
title="Clear search"
|
|
301
|
-
aria-label="Clear search"
|
|
302
|
-
>
|
|
303
|
-
<i class="bi bi-x-lg"></i>
|
|
304
|
-
</button>
|
|
305
|
-
{/if}
|
|
306
|
-
<button
|
|
307
|
-
class="btn btn-primary"
|
|
308
|
-
type="button"
|
|
309
|
-
onclick={handleSearch}
|
|
310
|
-
title="Search"
|
|
311
|
-
aria-label="Search"
|
|
312
|
-
>
|
|
313
|
-
<i class="bi bi-search"></i>
|
|
314
|
-
</button>
|
|
315
|
-
</div>
|
|
316
|
-
</div>
|
|
317
|
-
{/if}
|
|
204
|
+
<ResizableSplitPanel namespace="site-check" defaultLeftWidth={25}>
|
|
205
|
+
{#snippet left()}
|
|
206
|
+
<div class="bg-white d-flex flex-column" style="height: 100%;">
|
|
207
|
+
<!-- Controls -->
|
|
208
|
+
<SiteCheckControls
|
|
209
|
+
{treeGrouping}
|
|
210
|
+
{colorDimension}
|
|
211
|
+
{singleRootSelect}
|
|
212
|
+
{singleLevel1Select}
|
|
213
|
+
treeStore={$treeStore}
|
|
214
|
+
{showGroupingSelector}
|
|
215
|
+
{onSearch}
|
|
216
|
+
{searchPlaceholder}
|
|
217
|
+
onGroupingChange={(g) => (treeGrouping = g)}
|
|
218
|
+
onColorDimensionChange={(d) => (colorDimension = d)}
|
|
219
|
+
onSingleRootSelectChange={(e) => (singleRootSelect = e)}
|
|
220
|
+
onSingleLevel1SelectChange={(e) => (singleLevel1Select = e)}
|
|
221
|
+
/>
|
|
318
222
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
<div class="row g-2 mb-2">
|
|
325
|
-
<!-- Level 0 (Mandatory) -->
|
|
326
|
-
<div class="col-4">
|
|
327
|
-
<label for="level0Select" class="form-label small mb-1">Level 0</label>
|
|
328
|
-
<select
|
|
329
|
-
id="level0Select"
|
|
330
|
-
class="form-select form-select-sm"
|
|
331
|
-
value={treeGrouping.level0}
|
|
332
|
-
onchange={(e) => handleLevel0Change(e.currentTarget.value as TreeGroupField)}
|
|
333
|
-
>
|
|
334
|
-
{#each fieldOptions as option}
|
|
335
|
-
<option value={option.value}>{option.label}</option>
|
|
336
|
-
{/each}
|
|
337
|
-
</select>
|
|
338
|
-
</div>
|
|
339
|
-
|
|
340
|
-
<!-- Level 1 (Optional) -->
|
|
341
|
-
<div class="col-4">
|
|
342
|
-
<label for="level1Select" class="form-label small mb-1">Level 1</label>
|
|
343
|
-
<select
|
|
344
|
-
id="level1Select"
|
|
345
|
-
class="form-select form-select-sm"
|
|
346
|
-
value={treeGrouping.level1 ?? 'none'}
|
|
347
|
-
onchange={(e) => handleLevel1Change(e.currentTarget.value as TreeGroupField | 'none')}
|
|
348
|
-
>
|
|
349
|
-
<option value="none">None</option>
|
|
350
|
-
{#each availableLevel1Options as option}
|
|
351
|
-
<option value={option.value}>{option.label}</option>
|
|
352
|
-
{/each}
|
|
353
|
-
</select>
|
|
354
|
-
</div>
|
|
355
|
-
|
|
356
|
-
<!-- Color By -->
|
|
357
|
-
<div class="col-4">
|
|
358
|
-
<label for="colorDimensionSelect" class="form-label small mb-1">Color By</label>
|
|
359
|
-
<select
|
|
360
|
-
id="colorDimensionSelect"
|
|
361
|
-
class="form-select form-select-sm"
|
|
362
|
-
value={colorDimension}
|
|
363
|
-
onchange={(e) => {
|
|
364
|
-
colorDimension = e.currentTarget.value as ColorDimension;
|
|
365
|
-
log('๐จ Color dimension changed:', colorDimension);
|
|
366
|
-
}}
|
|
367
|
-
>
|
|
368
|
-
<option value="band">Band</option>
|
|
369
|
-
<option value="site">Site</option>
|
|
370
|
-
<option value="sector">Sector</option>
|
|
371
|
-
<option value="cellName">Cell Name</option>
|
|
372
|
-
</select>
|
|
373
|
-
</div>
|
|
374
|
-
</div>
|
|
375
|
-
|
|
376
|
-
<!-- Single Root Select Toggle -->
|
|
377
|
-
<div class="form-check mt-2">
|
|
378
|
-
<input
|
|
379
|
-
class="form-check-input"
|
|
380
|
-
type="checkbox"
|
|
381
|
-
id="singleRootSelectCheck"
|
|
382
|
-
checked={singleRootSelect}
|
|
383
|
-
onchange={(e) => {
|
|
384
|
-
singleRootSelect = e.currentTarget.checked;
|
|
385
|
-
log('๐ Single root select mode:', singleRootSelect);
|
|
386
|
-
|
|
387
|
-
// When enabling single root mode, uncheck all roots except the first one
|
|
388
|
-
if (singleRootSelect && treeStore) {
|
|
389
|
-
const store = $treeStore;
|
|
390
|
-
if (store) {
|
|
391
|
-
const checkedRoots = store.state.rootPaths.filter(path =>
|
|
392
|
-
store.state.checkedPaths.has(path)
|
|
393
|
-
);
|
|
394
|
-
if (checkedRoots.length > 1) {
|
|
395
|
-
log('๐ Multiple roots selected, keeping only first one:', checkedRoots[0]);
|
|
396
|
-
// Uncheck all except the first
|
|
397
|
-
for (let i = 1; i < checkedRoots.length; i++) {
|
|
398
|
-
store.toggle(checkedRoots[i]);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}}
|
|
404
|
-
/>
|
|
405
|
-
<label class="form-check-label small" for="singleRootSelectCheck">
|
|
406
|
-
Single selection on level 0
|
|
407
|
-
</label>
|
|
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>
|
|
223
|
+
<!-- Tree View -->
|
|
224
|
+
<div class="flex-grow-1" style="min-height: 0; overflow: hidden;">
|
|
225
|
+
{#if treeStore}
|
|
226
|
+
<TreeView store={$treeStore!} showControls={true} showIndeterminate={true} height="100%" />
|
|
227
|
+
{/if}
|
|
425
228
|
</div>
|
|
426
229
|
</div>
|
|
427
|
-
{/
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
230
|
+
{/snippet}
|
|
231
|
+
|
|
232
|
+
{#snippet right()}
|
|
233
|
+
<div class="bg-light d-flex flex-column" style="height: 100%;">
|
|
234
|
+
{#if chartData.length > 0}
|
|
235
|
+
<ChartComponent
|
|
236
|
+
layout={chartLayout}
|
|
237
|
+
data={chartData}
|
|
238
|
+
{mode}
|
|
239
|
+
{markers}
|
|
240
|
+
showGlobalControls={true}
|
|
241
|
+
enableAdaptation={true}
|
|
242
|
+
{plotlyLayout}
|
|
243
|
+
persistSettings={true}
|
|
244
|
+
/>
|
|
245
|
+
{:else}
|
|
246
|
+
<div class="d-flex align-items-center justify-content-center h-100">
|
|
247
|
+
<div class="text-center text-muted">
|
|
248
|
+
<h5>No Data Selected</h5>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
432
251
|
{/if}
|
|
433
252
|
</div>
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
<!-- Right: Charts -->
|
|
437
|
-
<div class="col-lg-9 col-md-8 bg-light d-flex flex-column" style="min-height: 0; height: 100%; overflow: hidden;">
|
|
438
|
-
{#if chartData.length > 0}
|
|
439
|
-
<ChartComponent
|
|
440
|
-
layout={chartLayout}
|
|
441
|
-
data={chartData}
|
|
442
|
-
mode={mode}
|
|
443
|
-
markers={markers}
|
|
444
|
-
showGlobalControls={true}
|
|
445
|
-
enableAdaptation={true}
|
|
446
|
-
plotlyLayout={plotlyLayout}
|
|
447
|
-
persistSettings={true}
|
|
448
|
-
/>
|
|
449
|
-
{:else}
|
|
450
|
-
<div class="d-flex align-items-center justify-content-center h-100">
|
|
451
|
-
<div class="text-center text-muted">
|
|
452
|
-
<h5>No Data Selected</h5>
|
|
453
|
-
</div>
|
|
454
|
-
</div>
|
|
455
|
-
{/if}
|
|
456
|
-
</div>
|
|
457
|
-
</div>
|
|
253
|
+
{/snippet}
|
|
254
|
+
</ResizableSplitPanel>
|
|
458
255
|
</div>
|
|
459
256
|
|
|
460
|
-
<style>
|
|
461
|
-
.controls-toggle {
|
|
462
|
-
cursor: pointer;
|
|
463
|
-
border: none;
|
|
464
|
-
transition: background-color 0.2s;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
.controls-toggle:hover {
|
|
468
|
-
background-color: #e9ecef !important;
|
|
469
|
-
}
|
|
470
257
|
|
|
471
|
-
.controls-toggle:focus {
|
|
472
|
-
outline: 2px solid #0d6efd;
|
|
473
|
-
outline-offset: -2px;
|
|
474
|
-
}
|
|
475
|
-
</style>
|
|
476
258
|
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<svelte:options runes={true} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { log } from '../../core/logger';
|
|
5
|
+
import type { TreeGroupingConfig, TreeGroupField, ColorDimension } from './index';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Current tree grouping configuration */
|
|
9
|
+
treeGrouping: TreeGroupingConfig;
|
|
10
|
+
/** Current color dimension */
|
|
11
|
+
colorDimension: ColorDimension;
|
|
12
|
+
/** Single root select mode */
|
|
13
|
+
singleRootSelect: boolean;
|
|
14
|
+
/** Single level 1 select mode */
|
|
15
|
+
singleLevel1Select: boolean;
|
|
16
|
+
/** Tree store for enforcing single root selection */
|
|
17
|
+
treeStore?: any;
|
|
18
|
+
/** Show grouping selector controls */
|
|
19
|
+
showGroupingSelector?: boolean;
|
|
20
|
+
/** Optional search callback */
|
|
21
|
+
onSearch?: (searchTerm: string) => void;
|
|
22
|
+
/** Search placeholder text */
|
|
23
|
+
searchPlaceholder?: string;
|
|
24
|
+
/** Callback when grouping changes */
|
|
25
|
+
onGroupingChange?: (grouping: TreeGroupingConfig) => void;
|
|
26
|
+
/** Callback when color dimension changes */
|
|
27
|
+
onColorDimensionChange?: (dimension: ColorDimension) => void;
|
|
28
|
+
/** Callback when single root select changes */
|
|
29
|
+
onSingleRootSelectChange?: (enabled: boolean) => void;
|
|
30
|
+
/** Callback when single level 1 select changes */
|
|
31
|
+
onSingleLevel1SelectChange?: (enabled: boolean) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let {
|
|
35
|
+
treeGrouping,
|
|
36
|
+
colorDimension,
|
|
37
|
+
singleRootSelect,
|
|
38
|
+
singleLevel1Select,
|
|
39
|
+
treeStore,
|
|
40
|
+
showGroupingSelector = true,
|
|
41
|
+
onSearch,
|
|
42
|
+
searchPlaceholder = 'Search...',
|
|
43
|
+
onGroupingChange,
|
|
44
|
+
onColorDimensionChange,
|
|
45
|
+
onSingleRootSelectChange,
|
|
46
|
+
onSingleLevel1SelectChange
|
|
47
|
+
}: Props = $props();
|
|
48
|
+
|
|
49
|
+
// Local state
|
|
50
|
+
let searchTerm = $state('');
|
|
51
|
+
let controlsExpanded = $state(true);
|
|
52
|
+
|
|
53
|
+
// Available field options for grouping levels
|
|
54
|
+
const fieldOptions: { value: TreeGroupField; label: string }[] = [
|
|
55
|
+
{ value: 'site', label: 'Site' },
|
|
56
|
+
{ value: 'band', label: 'Band' },
|
|
57
|
+
{ value: 'azimuth', label: 'Azimuth' },
|
|
58
|
+
{ value: 'sector', label: 'Sector' },
|
|
59
|
+
{ value: 'cellName', label: 'Cell Name' }
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Get available options for level1 (exclude level0)
|
|
63
|
+
let availableLevel1Options = $derived.by(() => {
|
|
64
|
+
return fieldOptions.filter(opt => opt.value !== treeGrouping.level0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Handlers
|
|
68
|
+
function handleSearch() {
|
|
69
|
+
if (onSearch) {
|
|
70
|
+
onSearch(searchTerm);
|
|
71
|
+
log('๐ Search triggered:', searchTerm);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function handleClearSearch() {
|
|
76
|
+
searchTerm = '';
|
|
77
|
+
if (onSearch) {
|
|
78
|
+
onSearch('');
|
|
79
|
+
log('๐งน Search cleared');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function handleLevel0Change(value: TreeGroupField) {
|
|
84
|
+
const newLevel1 = treeGrouping.level1 === value ? null : treeGrouping.level1;
|
|
85
|
+
const newGrouping = {
|
|
86
|
+
level0: value,
|
|
87
|
+
level1: newLevel1
|
|
88
|
+
};
|
|
89
|
+
onGroupingChange?.(newGrouping);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handleLevel1Change(value: TreeGroupField | 'none') {
|
|
93
|
+
const newLevel1 = value === 'none' ? null : value;
|
|
94
|
+
const newGrouping = {
|
|
95
|
+
level0: treeGrouping.level0,
|
|
96
|
+
level1: newLevel1
|
|
97
|
+
};
|
|
98
|
+
onGroupingChange?.(newGrouping);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function handleColorDimensionChange(dimension: ColorDimension) {
|
|
102
|
+
onColorDimensionChange?.(dimension);
|
|
103
|
+
log('๐จ Color dimension changed:', dimension);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function handleSingleRootSelectChange(enabled: boolean) {
|
|
107
|
+
onSingleRootSelectChange?.(enabled);
|
|
108
|
+
log('๐ Single root select mode:', enabled);
|
|
109
|
+
|
|
110
|
+
// When enabling single root mode, uncheck all roots except the first one
|
|
111
|
+
if (enabled && treeStore) {
|
|
112
|
+
const store = treeStore;
|
|
113
|
+
if (store) {
|
|
114
|
+
const checkedRoots = store.state.rootPaths.filter((path: string) =>
|
|
115
|
+
store.state.checkedPaths.has(path)
|
|
116
|
+
);
|
|
117
|
+
if (checkedRoots.length > 1) {
|
|
118
|
+
log('๐ Multiple roots selected, keeping only first one:', checkedRoots[0]);
|
|
119
|
+
for (let i = 1; i < checkedRoots.length; i++) {
|
|
120
|
+
store.toggle(checkedRoots[i]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function handleSingleLevel1SelectChange(enabled: boolean) {
|
|
128
|
+
onSingleLevel1SelectChange?.(enabled);
|
|
129
|
+
log('๐ Single Level 1 select mode:', enabled);
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
{#if onSearch || showGroupingSelector}
|
|
134
|
+
<!-- Collapsible Controls Toggle -->
|
|
135
|
+
<button
|
|
136
|
+
class="controls-toggle w-100 text-start p-2 bg-light border-bottom d-flex align-items-center flex-shrink-0"
|
|
137
|
+
onclick={() => (controlsExpanded = !controlsExpanded)}
|
|
138
|
+
aria-expanded={controlsExpanded}
|
|
139
|
+
aria-label="Toggle controls"
|
|
140
|
+
>
|
|
141
|
+
<i class="bi bi-sliders me-2"></i>
|
|
142
|
+
<span class="fw-semibold small">Controls</span>
|
|
143
|
+
<i
|
|
144
|
+
class="bi ms-auto"
|
|
145
|
+
class:bi-chevron-down={controlsExpanded}
|
|
146
|
+
class:bi-chevron-right={!controlsExpanded}
|
|
147
|
+
></i>
|
|
148
|
+
</button>
|
|
149
|
+
|
|
150
|
+
<!-- Collapsible Controls Content -->
|
|
151
|
+
{#if controlsExpanded}
|
|
152
|
+
<!-- Search Box -->
|
|
153
|
+
{#if onSearch}
|
|
154
|
+
<div class="p-3 border-bottom flex-shrink-0">
|
|
155
|
+
<label for="searchInput" class="form-label small fw-semibold mb-2"> Search </label>
|
|
156
|
+
<div class="input-group input-group-sm">
|
|
157
|
+
<input
|
|
158
|
+
type="text"
|
|
159
|
+
id="searchInput"
|
|
160
|
+
class="form-control"
|
|
161
|
+
placeholder={searchPlaceholder}
|
|
162
|
+
bind:value={searchTerm}
|
|
163
|
+
onkeydown={(e) => {
|
|
164
|
+
if (e.key === 'Enter') {
|
|
165
|
+
handleSearch();
|
|
166
|
+
}
|
|
167
|
+
}}
|
|
168
|
+
/>
|
|
169
|
+
{#if searchTerm}
|
|
170
|
+
<button
|
|
171
|
+
class="btn btn-outline-secondary"
|
|
172
|
+
type="button"
|
|
173
|
+
onclick={handleClearSearch}
|
|
174
|
+
title="Clear search"
|
|
175
|
+
aria-label="Clear search"
|
|
176
|
+
>
|
|
177
|
+
<i class="bi bi-x-lg"></i>
|
|
178
|
+
</button>
|
|
179
|
+
{/if}
|
|
180
|
+
<button
|
|
181
|
+
class="btn btn-primary"
|
|
182
|
+
type="button"
|
|
183
|
+
onclick={handleSearch}
|
|
184
|
+
title="Search"
|
|
185
|
+
aria-label="Search"
|
|
186
|
+
>
|
|
187
|
+
<i class="bi bi-search"></i>
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
{/if}
|
|
192
|
+
|
|
193
|
+
<!-- Grouping Selector -->
|
|
194
|
+
{#if showGroupingSelector}
|
|
195
|
+
<div class="p-3 border-bottom flex-shrink-0">
|
|
196
|
+
<div class="small fw-semibold mb-2">Tree Grouping</div>
|
|
197
|
+
|
|
198
|
+
<div class="row g-2 mb-2">
|
|
199
|
+
<!-- Level 0 (Mandatory) -->
|
|
200
|
+
<div class="col-4">
|
|
201
|
+
<label for="level0Select" class="form-label small mb-1">Level 0</label>
|
|
202
|
+
<select
|
|
203
|
+
id="level0Select"
|
|
204
|
+
class="form-select form-select-sm"
|
|
205
|
+
value={treeGrouping.level0}
|
|
206
|
+
onchange={(e) => handleLevel0Change(e.currentTarget.value as TreeGroupField)}
|
|
207
|
+
>
|
|
208
|
+
{#each fieldOptions as option}
|
|
209
|
+
<option value={option.value}>{option.label}</option>
|
|
210
|
+
{/each}
|
|
211
|
+
</select>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<!-- Level 1 (Optional) -->
|
|
215
|
+
<div class="col-4">
|
|
216
|
+
<label for="level1Select" class="form-label small mb-1">Level 1</label>
|
|
217
|
+
<select
|
|
218
|
+
id="level1Select"
|
|
219
|
+
class="form-select form-select-sm"
|
|
220
|
+
value={treeGrouping.level1 ?? 'none'}
|
|
221
|
+
onchange={(e) => handleLevel1Change(e.currentTarget.value as TreeGroupField | 'none')}
|
|
222
|
+
>
|
|
223
|
+
<option value="none">None</option>
|
|
224
|
+
{#each availableLevel1Options as option}
|
|
225
|
+
<option value={option.value}>{option.label}</option>
|
|
226
|
+
{/each}
|
|
227
|
+
</select>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<!-- Color By -->
|
|
231
|
+
<div class="col-4">
|
|
232
|
+
<label for="colorDimensionSelect" class="form-label small mb-1">Color By</label>
|
|
233
|
+
<select
|
|
234
|
+
id="colorDimensionSelect"
|
|
235
|
+
class="form-select form-select-sm"
|
|
236
|
+
value={colorDimension}
|
|
237
|
+
onchange={(e) => handleColorDimensionChange(e.currentTarget.value as ColorDimension)}
|
|
238
|
+
>
|
|
239
|
+
<option value="band">Band</option>
|
|
240
|
+
<option value="site">Site</option>
|
|
241
|
+
<option value="sector">Sector</option>
|
|
242
|
+
<option value="cellName">Cell Name</option>
|
|
243
|
+
</select>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<!-- Single Root Select Toggle -->
|
|
248
|
+
<div class="form-check mt-2">
|
|
249
|
+
<input
|
|
250
|
+
class="form-check-input"
|
|
251
|
+
type="checkbox"
|
|
252
|
+
id="singleRootSelectCheck"
|
|
253
|
+
checked={singleRootSelect}
|
|
254
|
+
onchange={(e) => handleSingleRootSelectChange(e.currentTarget.checked)}
|
|
255
|
+
/>
|
|
256
|
+
<label class="form-check-label small" for="singleRootSelectCheck">
|
|
257
|
+
Single selection on level 0
|
|
258
|
+
</label>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<!-- Single Level 1 Select Toggle -->
|
|
262
|
+
<div class="form-check mt-2">
|
|
263
|
+
<input
|
|
264
|
+
class="form-check-input"
|
|
265
|
+
type="checkbox"
|
|
266
|
+
id="singleLevel1SelectCheck"
|
|
267
|
+
checked={singleLevel1Select}
|
|
268
|
+
onchange={(e) => handleSingleLevel1SelectChange(e.currentTarget.checked)}
|
|
269
|
+
/>
|
|
270
|
+
<label class="form-check-label small" for="singleLevel1SelectCheck">
|
|
271
|
+
Single selection on level 1
|
|
272
|
+
</label>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
{/if}
|
|
276
|
+
{/if}
|
|
277
|
+
{/if}
|
|
278
|
+
|
|
279
|
+
<style>
|
|
280
|
+
.controls-toggle {
|
|
281
|
+
cursor: pointer;
|
|
282
|
+
border: none;
|
|
283
|
+
transition: background-color 0.2s;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.controls-toggle:hover {
|
|
287
|
+
background-color: #e9ecef !important;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.controls-toggle:focus {
|
|
291
|
+
outline: 2px solid #0d6efd;
|
|
292
|
+
outline-offset: -2px;
|
|
293
|
+
}
|
|
294
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TreeGroupingConfig, ColorDimension } from './index';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Current tree grouping configuration */
|
|
4
|
+
treeGrouping: TreeGroupingConfig;
|
|
5
|
+
/** Current color dimension */
|
|
6
|
+
colorDimension: ColorDimension;
|
|
7
|
+
/** Single root select mode */
|
|
8
|
+
singleRootSelect: boolean;
|
|
9
|
+
/** Single level 1 select mode */
|
|
10
|
+
singleLevel1Select: boolean;
|
|
11
|
+
/** Tree store for enforcing single root selection */
|
|
12
|
+
treeStore?: any;
|
|
13
|
+
/** Show grouping selector controls */
|
|
14
|
+
showGroupingSelector?: boolean;
|
|
15
|
+
/** Optional search callback */
|
|
16
|
+
onSearch?: (searchTerm: string) => void;
|
|
17
|
+
/** Search placeholder text */
|
|
18
|
+
searchPlaceholder?: string;
|
|
19
|
+
/** Callback when grouping changes */
|
|
20
|
+
onGroupingChange?: (grouping: TreeGroupingConfig) => void;
|
|
21
|
+
/** Callback when color dimension changes */
|
|
22
|
+
onColorDimensionChange?: (dimension: ColorDimension) => void;
|
|
23
|
+
/** Callback when single root select changes */
|
|
24
|
+
onSingleRootSelectChange?: (enabled: boolean) => void;
|
|
25
|
+
/** Callback when single level 1 select changes */
|
|
26
|
+
onSingleLevel1SelectChange?: (enabled: boolean) => void;
|
|
27
|
+
}
|
|
28
|
+
declare const SiteCheckControls: import("svelte").Component<Props, {}, "">;
|
|
29
|
+
type SiteCheckControls = ReturnType<typeof SiteCheckControls>;
|
|
30
|
+
export default SiteCheckControls;
|