@smartnet360/svelte-components 0.0.100 โ 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.
- 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/shared/controls/FeatureSelectionControl.svelte +20 -25
- package/dist/map-v2/shared/controls/FeatureSelectionControl.svelte.d.ts +2 -4
- package/dist/shared/ResizableSplitPanel.svelte +175 -0
- package/dist/shared/ResizableSplitPanel.svelte.d.ts +17 -0
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -87,9 +87,19 @@
|
|
|
87
87
|
alert(`Selected ${siteIds.length} sites:\n${siteIds.join(', ')}`);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// Handler for
|
|
91
|
-
function
|
|
92
|
-
alert(`
|
|
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="
|
|
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
|
|
320
|
-
{#if
|
|
321
|
-
<div class="mt-3">
|
|
322
|
-
|
|
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;
|