@smartnet360/svelte-components 0.0.99 โ†’ 0.0.101

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.
@@ -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, getMessage } from '../../core/FeatureRegistry';
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
- // Available field options for grouping levels
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
- const storageKey = 'site-check:treeState';
59
+ // This includes both tree state AND chart settings to avoid cell mismatches
110
60
  if (typeof window !== 'undefined') {
111
- localStorage.removeItem(storageKey);
112
- log('๐Ÿงน Cleared localStorage:', storageKey);
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
- <!-- Main Content -->
254
- <div class="row flex-grow-1" style="min-height: 0;">
255
- <!-- Left: Tree View -->
256
- <div class="col-lg-3 col-md-4 border-end bg-white d-flex flex-column" style="min-height: 0; height: 100%;">
257
- <!-- Collapsible Controls Toggle -->
258
- {#if onSearch || showGroupingSelector}
259
- <button
260
- class="controls-toggle w-100 text-start p-2 bg-light border-bottom d-flex align-items-center flex-shrink-0"
261
- onclick={() => controlsExpanded = !controlsExpanded}
262
- aria-expanded={controlsExpanded}
263
- aria-label="Toggle controls"
264
- >
265
- <i class="bi bi-sliders me-2"></i>
266
- <span class="fw-semibold small">Controls</span>
267
- <i class="bi ms-auto"
268
- class:bi-chevron-down={controlsExpanded}
269
- class:bi-chevron-right={!controlsExpanded}>
270
- </i>
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
- <!-- Grouping Selector -->
320
- {#if showGroupingSelector}
321
- <div class="p-3 border-bottom flex-shrink-0">
322
- <div class="small fw-semibold mb-2">Tree Grouping</div>
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
- {/if}
428
- {/if} <!-- Tree View -->
429
- <div class="flex-grow-1" style="min-height: 0; overflow: hidden;">
430
- {#if treeStore}
431
- <TreeView store={$treeStore!} showControls={true} showIndeterminate={true} height="100%" />
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
- </div>
435
-
436
- <!-- Right: Charts -->
437
- <div class="col-lg-9 col-md-8 bg-light overflow-auto">
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;
@@ -87,9 +87,19 @@
87
87
  alert(`Selected ${siteIds.length} sites:\n${siteIds.join(', ')}`);
88
88
  }
89
89
 
90
- // Handler for generic feature selection action button
91
- function handleProcessFeatures(featureIds: string[]) {
92
- alert(`Selected ${featureIds.length} features:\n${featureIds.join(', ')}`);
90
+ // Handler for cluster processing
91
+ function handleProcessCluster(featureIds: string[]) {
92
+ alert(`Processing cluster with ${featureIds.length} features:\n${featureIds.join(', ')}`);
93
+ }
94
+
95
+ // Handler for data export
96
+ function handleExportData(featureIds: string[]) {
97
+ alert(`Exporting ${featureIds.length} features:\n${featureIds.join(', ')}`);
98
+ }
99
+
100
+ // Handler for feature analysis
101
+ function handleAnalyzeFeatures(featureIds: string[]) {
102
+ alert(`Analyzing ${featureIds.length} features:\n${featureIds.join(', ')}`);
93
103
  }
94
104
 
95
105
  // Cell filter grouping configuration
@@ -231,12 +241,34 @@
231
241
  <FeatureSelectionControl
232
242
  position="bottom-left"
233
243
  title="Cluster Tool"
234
- icon="speedometer2"
244
+ icon="bi bi-graph-up"
235
245
  iconOnlyWhenCollapsed={useIconHeaders}
236
- onAction={handleProcessFeatures}
237
- actionButtonLabel="Process Cluster"
238
246
  featureIcon="pin-map-fill"
239
- />
247
+ >
248
+ {#snippet children(selectedIds)}
249
+ <button
250
+ class="btn btn-primary"
251
+ disabled={selectedIds.length === 0}
252
+ onclick={() => handleProcessCluster(selectedIds)}
253
+ >
254
+ <i class="bi bi-lightning-charge-fill"></i> Process Cluster
255
+ </button>
256
+ <button
257
+ class="btn btn-success"
258
+ disabled={selectedIds.length === 0}
259
+ onclick={() => handleExportData(selectedIds)}
260
+ >
261
+ <i class="bi bi-download"></i> Export Data
262
+ </button>
263
+ <button
264
+ class="btn btn-info"
265
+ disabled={selectedIds.length === 0}
266
+ onclick={() => handleAnalyzeFeatures(selectedIds)}
267
+ >
268
+ <i class="bi bi-graph-up"></i> Analyze
269
+ </button>
270
+ {/snippet}
271
+ </FeatureSelectionControl>
240
272
 
241
273
  <!-- Unified feature settings control - Sites, Cells, and Repeaters -->
242
274
  <FeatureSettingsControl
@@ -28,16 +28,14 @@
28
28
  icon?: string;
29
29
  /** Show icon when collapsed (default: true) */
30
30
  iconOnlyWhenCollapsed?: boolean;
31
- /** Callback when action button clicked */
32
- onAction?: (featureIds: string[]) => void;
33
- /** Action button label */
34
- actionButtonLabel?: string;
35
31
  /** Feature icon (default: geo-alt-fill) */
36
32
  featureIcon?: string;
37
33
  /** Available property names to use as ID (default: ['id', 'siteId', 'cellName']) */
38
34
  idPropertyOptions?: string[];
39
35
  /** Default property to use as ID (default: 'id') */
40
36
  defaultIdProperty?: string;
37
+ /** Slot for custom action buttons that receive selected feature IDs */
38
+ children?: import('svelte').Snippet<[string[]]>;
41
39
  }
42
40
 
43
41
  let {
@@ -45,11 +43,10 @@
45
43
  title = 'Cluster Tool',
46
44
  icon = 'speedometer2',
47
45
  iconOnlyWhenCollapsed = true,
48
- onAction,
49
- actionButtonLabel = 'Process Cluster',
50
46
  featureIcon = 'geo-alt-fill',
51
47
  idPropertyOptions = ['none','siteId','sectorId', 'cellName','id'],
52
- defaultIdProperty = 'none'
48
+ defaultIdProperty = 'none',
49
+ children
53
50
  }: Props = $props();
54
51
 
55
52
  // Get map from context
@@ -228,13 +225,6 @@
228
225
  console.error('Failed to copy:', err);
229
226
  }
230
227
  }
231
-
232
- function handleAction() {
233
- if (onAction && hasSelection) {
234
- const ids = store.getSelectedIds();
235
- onAction(ids);
236
- }
237
- }
238
228
  </script>
239
229
 
240
230
  <MapControl {position} {title} {icon} {iconOnlyWhenCollapsed} collapsible={true} onCollapseToggle={handleCollapseToggle}>
@@ -316,17 +306,10 @@
316
306
  </div>
317
307
  {/if}
318
308
 
319
- <!-- Action Button -->
320
- {#if onAction}
321
- <div class="mt-3">
322
- <button
323
- type="button"
324
- class="btn btn-primary w-100"
325
- disabled={!hasSelection}
326
- onclick={handleAction}
327
- >
328
- <i class="bi bi-lightning-charge-fill"></i> {actionButtonLabel}
329
- </button>
309
+ <!-- Custom Action Buttons Slot -->
310
+ {#if children}
311
+ <div class="mt-3 action-slot">
312
+ {@render children(store.getSelectedIds())}
330
313
  </div>
331
314
  {/if}
332
315
  </div>
@@ -435,6 +418,18 @@
435
418
  background-color: #ffcccc;
436
419
  }
437
420
 
421
+ /* Action slot styles */
422
+ .action-slot {
423
+ display: flex;
424
+ flex-direction: column;
425
+ gap: 0.5rem;
426
+ }
427
+
428
+ /* Ensure action slot buttons maintain Bootstrap styling */
429
+ .action-slot :global(.btn) {
430
+ width: 100%;
431
+ }
432
+
438
433
  /* Ensure primary action button keeps Bootstrap styling inside Mapbox control */
439
434
  .feature-selection-control .btn-primary {
440
435
  background-color: var(--bs-btn-bg, var(--bs-primary));
@@ -7,16 +7,14 @@ interface Props {
7
7
  icon?: string;
8
8
  /** Show icon when collapsed (default: true) */
9
9
  iconOnlyWhenCollapsed?: boolean;
10
- /** Callback when action button clicked */
11
- onAction?: (featureIds: string[]) => void;
12
- /** Action button label */
13
- actionButtonLabel?: string;
14
10
  /** Feature icon (default: geo-alt-fill) */
15
11
  featureIcon?: string;
16
12
  /** Available property names to use as ID (default: ['id', 'siteId', 'cellName']) */
17
13
  idPropertyOptions?: string[];
18
14
  /** Default property to use as ID (default: 'id') */
19
15
  defaultIdProperty?: string;
16
+ /** Slot for custom action buttons that receive selected feature IDs */
17
+ children?: import('svelte').Snippet<[string[]]>;
20
18
  }
21
19
  declare const FeatureSelectionControl: import("svelte").Component<Props, {}, "">;
22
20
  type FeatureSelectionControl = ReturnType<typeof FeatureSelectionControl>;
@@ -0,0 +1,175 @@
1
+ <svelte:options runes={true} />
2
+
3
+ <script lang="ts">
4
+ import { onMount } from 'svelte';
5
+
6
+ interface Props {
7
+ /** Namespace for localStorage persistence */
8
+ namespace?: string;
9
+ /** Initial left panel width percentage (default: 25) */
10
+ defaultLeftWidth?: number;
11
+ /** Minimum left panel width percentage (default: 15) */
12
+ minLeftWidth?: number;
13
+ /** Maximum left panel width percentage (default: 60) */
14
+ maxLeftWidth?: number;
15
+ /** Left panel content */
16
+ left: import('svelte').Snippet;
17
+ /** Right panel content */
18
+ right: import('svelte').Snippet;
19
+ }
20
+
21
+ let {
22
+ namespace = 'split-panel',
23
+ defaultLeftWidth = 25,
24
+ minLeftWidth = 15,
25
+ maxLeftWidth = 60,
26
+ left,
27
+ right
28
+ }: Props = $props();
29
+
30
+ // State
31
+ let leftWidth = $state(defaultLeftWidth);
32
+ let isDragging = $state(false);
33
+ let containerRef: HTMLDivElement;
34
+
35
+ // Load saved split position from localStorage
36
+ onMount(() => {
37
+ const storageKey = `${namespace}:splitPosition`;
38
+ const saved = localStorage.getItem(storageKey);
39
+ if (saved) {
40
+ const parsed = parseFloat(saved);
41
+ if (!isNaN(parsed) && parsed >= minLeftWidth && parsed <= maxLeftWidth) {
42
+ leftWidth = parsed;
43
+ }
44
+ }
45
+ });
46
+
47
+ // Handle resize drag
48
+ function handleResizeStart(e: MouseEvent) {
49
+ isDragging = true;
50
+ e.preventDefault();
51
+
52
+ const handleMouseMove = (e: MouseEvent) => {
53
+ if (!isDragging || !containerRef) return;
54
+
55
+ const rect = containerRef.getBoundingClientRect();
56
+ const newWidth = ((e.clientX - rect.left) / rect.width) * 100;
57
+
58
+ // Clamp between min and max
59
+ leftWidth = Math.max(minLeftWidth, Math.min(maxLeftWidth, newWidth));
60
+ };
61
+
62
+ const handleMouseUp = () => {
63
+ isDragging = false;
64
+ // Save to localStorage
65
+ const storageKey = `${namespace}:splitPosition`;
66
+ localStorage.setItem(storageKey, leftWidth.toString());
67
+ document.removeEventListener('mousemove', handleMouseMove);
68
+ document.removeEventListener('mouseup', handleMouseUp);
69
+ };
70
+
71
+ document.addEventListener('mousemove', handleMouseMove);
72
+ document.addEventListener('mouseup', handleMouseUp);
73
+ }
74
+ </script>
75
+
76
+ <div class="resizable-container" bind:this={containerRef} class:dragging={isDragging}>
77
+ <!-- Left Panel -->
78
+ <div class="left-panel" style="width: {leftWidth}%;">
79
+ {@render left()}
80
+ </div>
81
+
82
+ <!-- Vertical Resize Handle -->
83
+ <button
84
+ class="resize-handle"
85
+ class:dragging={isDragging}
86
+ onmousedown={handleResizeStart}
87
+ aria-label="Resize split between panels"
88
+ type="button"
89
+ >
90
+ <div class="resize-handle-indicator"></div>
91
+ </button>
92
+
93
+ <!-- Right Panel -->
94
+ <div class="right-panel" style="width: {100 - leftWidth}%;">
95
+ {@render right()}
96
+ </div>
97
+ </div>
98
+
99
+ <style>
100
+ .resizable-container {
101
+ display: flex;
102
+ flex-direction: row;
103
+ width: 100%;
104
+ height: 100%;
105
+ overflow: hidden;
106
+ position: relative;
107
+ }
108
+
109
+ .left-panel {
110
+ position: relative;
111
+ border-right: 1px solid #dee2e6;
112
+ min-width: 200px;
113
+ height: 100%;
114
+ overflow: hidden;
115
+ }
116
+
117
+ .right-panel {
118
+ position: relative;
119
+ flex: 1;
120
+ height: 100%;
121
+ overflow: hidden;
122
+ }
123
+
124
+ /* Vertical Resize Handle */
125
+ .resize-handle {
126
+ position: relative;
127
+ width: 8px;
128
+ cursor: col-resize;
129
+ z-index: 100;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ transition: background-color 0.2s;
134
+ flex-shrink: 0;
135
+ border: none;
136
+ padding: 0;
137
+ background-color: transparent;
138
+ height: 100%;
139
+ }
140
+
141
+ .resize-handle:hover {
142
+ background-color: rgba(13, 110, 253, 0.1);
143
+ }
144
+
145
+ .resize-handle.dragging {
146
+ background-color: rgba(13, 110, 253, 0.2);
147
+ }
148
+
149
+ .resize-handle-indicator {
150
+ width: 2px;
151
+ height: 40px;
152
+ background-color: #dee2e6;
153
+ border-radius: 1px;
154
+ transition: all 0.2s;
155
+ }
156
+
157
+ .resize-handle:hover .resize-handle-indicator {
158
+ background-color: #0d6efd;
159
+ height: 60px;
160
+ width: 3px;
161
+ }
162
+
163
+ .resize-handle.dragging .resize-handle-indicator {
164
+ background-color: #0d6efd;
165
+ height: 80px;
166
+ width: 4px;
167
+ }
168
+
169
+ /* Prevent text selection during drag */
170
+ .resizable-container.dragging,
171
+ .resizable-container.dragging * {
172
+ user-select: none !important;
173
+ -webkit-user-select: none !important;
174
+ }
175
+ </style>
@@ -0,0 +1,17 @@
1
+ interface Props {
2
+ /** Namespace for localStorage persistence */
3
+ namespace?: string;
4
+ /** Initial left panel width percentage (default: 25) */
5
+ defaultLeftWidth?: number;
6
+ /** Minimum left panel width percentage (default: 15) */
7
+ minLeftWidth?: number;
8
+ /** Maximum left panel width percentage (default: 60) */
9
+ maxLeftWidth?: number;
10
+ /** Left panel content */
11
+ left: import('svelte').Snippet;
12
+ /** Right panel content */
13
+ right: import('svelte').Snippet;
14
+ }
15
+ declare const ResizableSplitPanel: import("svelte").Component<Props, {}, "">;
16
+ type ResizableSplitPanel = ReturnType<typeof ResizableSplitPanel>;
17
+ export default ResizableSplitPanel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.99",
3
+ "version": "0.0.101",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",