@smartnet360/svelte-components 0.0.130 → 0.0.131

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.
Files changed (32) hide show
  1. package/dist/map-v3/demo/DemoMap.svelte +1 -6
  2. package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellFilterControl.svelte +11 -4
  3. package/dist/map-v3/features/custom/components/CustomCellSetManager.svelte +692 -0
  4. package/dist/map-v3/features/{cells/custom → custom}/index.d.ts +1 -1
  5. package/dist/map-v3/features/{cells/custom → custom}/index.js +1 -1
  6. package/dist/map-v3/features/custom/layers/CustomCellsLayer.svelte +399 -0
  7. package/dist/map-v3/features/{cells/custom → custom}/logic/csv-parser.d.ts +11 -7
  8. package/dist/map-v3/features/{cells/custom → custom}/logic/csv-parser.js +64 -20
  9. package/dist/map-v3/features/{cells/custom → custom}/logic/tree-adapter.d.ts +1 -1
  10. package/dist/map-v3/features/{cells/custom → custom}/stores/custom-cell-sets.svelte.d.ts +4 -3
  11. package/dist/map-v3/features/{cells/custom → custom}/stores/custom-cell-sets.svelte.js +30 -10
  12. package/dist/map-v3/features/{cells/custom → custom}/types.d.ts +32 -12
  13. package/dist/map-v3/features/{cells/custom → custom}/types.js +5 -3
  14. package/dist/map-v3/index.d.ts +1 -1
  15. package/dist/map-v3/index.js +1 -1
  16. package/dist/map-v3/shared/controls/MapControl.svelte +43 -15
  17. package/dist/map-v3/shared/controls/MapControl.svelte.d.ts +3 -1
  18. package/package.json +1 -1
  19. package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte +0 -306
  20. package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte +0 -262
  21. /package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellFilterControl.svelte.d.ts +0 -0
  22. /package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellSetManager.svelte.d.ts +0 -0
  23. /package/dist/map-v3/features/{cells/custom → custom}/components/index.d.ts +0 -0
  24. /package/dist/map-v3/features/{cells/custom → custom}/components/index.js +0 -0
  25. /package/dist/map-v3/features/{cells/custom → custom}/layers/CustomCellsLayer.svelte.d.ts +0 -0
  26. /package/dist/map-v3/features/{cells/custom → custom}/layers/index.d.ts +0 -0
  27. /package/dist/map-v3/features/{cells/custom → custom}/layers/index.js +0 -0
  28. /package/dist/map-v3/features/{cells/custom → custom}/logic/index.d.ts +0 -0
  29. /package/dist/map-v3/features/{cells/custom → custom}/logic/index.js +0 -0
  30. /package/dist/map-v3/features/{cells/custom → custom}/logic/tree-adapter.js +0 -0
  31. /package/dist/map-v3/features/{cells/custom → custom}/stores/index.d.ts +0 -0
  32. /package/dist/map-v3/features/{cells/custom → custom}/stores/index.js +0 -0
@@ -0,0 +1,692 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Custom Layers Manager (Unified Control)
4
+ *
5
+ * Single control that handles:
6
+ * - CSV import
7
+ * - Quick Add (paste cell names)
8
+ * - Global size/opacity sliders
9
+ * - Collapsible set sections with TreeView + color pickers
10
+ */
11
+ import { untrack } from 'svelte';
12
+ import { MapControl } from '../../../shared';
13
+ import { createTreeStore, TreeView } from '../../../../core/TreeView';
14
+ import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
15
+ import type { CustomCellImportResult, CustomCellSet } from '../types';
16
+ import { buildCustomCellTree } from '../logic/tree-adapter';
17
+
18
+ interface Props {
19
+ /** The custom cell sets store */
20
+ setsStore: CustomCellSetsStore;
21
+ /** Control position on map */
22
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
23
+ }
24
+
25
+ let {
26
+ setsStore,
27
+ position = 'top-left'
28
+ }: Props = $props();
29
+
30
+ // Global display settings - initialized from first set if available
31
+ let globalSectorSize = $state(50);
32
+ let globalPointSize = $state(8);
33
+ let globalOpacity = $state(0.7);
34
+ let initialized = $state(false);
35
+
36
+ // Sync global sliders from first set's values on load
37
+ $effect(() => {
38
+ if (!initialized && setsStore.sets.length > 0) {
39
+ const firstSet = setsStore.sets[0];
40
+ globalSectorSize = firstSet.baseSize;
41
+ globalPointSize = firstSet.pointSize;
42
+ globalOpacity = firstSet.opacity;
43
+ initialized = true;
44
+ }
45
+ });
46
+
47
+ // UI State
48
+ let showImportModal = $state(false);
49
+ let importResult = $state<CustomCellImportResult | null>(null);
50
+ let importFileName = $state('');
51
+ let importError = $state('');
52
+ let fileInput: HTMLInputElement;
53
+
54
+ // Quick Add state
55
+ let showQuickAdd = $state(false);
56
+ let quickAddText = $state('');
57
+ let quickAddName = $state('Quick Selection');
58
+ let quickAddResult = $state<CustomCellImportResult | null>(null);
59
+
60
+ // Track expanded sets
61
+ let expandedSets = $state<Set<string>>(new Set());
62
+
63
+ // Tree stores per set (keyed by set id)
64
+ let treeStores = $derived.by(() => {
65
+ const _version = setsStore.version;
66
+ const stores = new Map<string, ReturnType<typeof createTreeStore>>();
67
+
68
+ for (const set of setsStore.sets) {
69
+ const store = untrack(() => {
70
+ const nodes = buildCustomCellTree(set);
71
+ return createTreeStore({
72
+ nodes,
73
+ namespace: `custom-cells:${set.id}`,
74
+ persistState: true,
75
+ defaultExpandAll: true
76
+ });
77
+ });
78
+ stores.set(set.id, store);
79
+ }
80
+
81
+ return stores;
82
+ });
83
+
84
+ // Sync tree selections to store visibility
85
+ $effect(() => {
86
+ for (const set of setsStore.sets) {
87
+ const treeStore = treeStores.get(set.id);
88
+ if (!treeStore) continue;
89
+
90
+ treeStore.state.nodes.forEach((nodeState) => {
91
+ // Skip root and folder nodes
92
+ if (nodeState.node.id === `root-${set.id}`) return;
93
+ if (nodeState.node.children && nodeState.node.children.length > 0) return;
94
+
95
+ const groupId = nodeState.node.metadata?.groupId;
96
+ if (!groupId) return;
97
+
98
+ const isVisible = treeStore.state.checkedPaths.has(nodeState.path);
99
+ const currentlyVisible = set.visibleGroups.has(groupId);
100
+
101
+ if (isVisible !== currentlyVisible) {
102
+ setsStore.toggleGroupVisibility(set.id, groupId);
103
+ }
104
+ });
105
+ }
106
+ });
107
+
108
+ // Apply global settings to all sets when sliders change
109
+ function handleGlobalSectorSizeChange(e: Event) {
110
+ const value = parseInt((e.target as HTMLInputElement).value);
111
+ globalSectorSize = value;
112
+ for (const set of setsStore.sets) {
113
+ setsStore.updateSetSettings(set.id, { baseSize: value });
114
+ }
115
+ }
116
+
117
+ function handleGlobalPointSizeChange(e: Event) {
118
+ const value = parseInt((e.target as HTMLInputElement).value);
119
+ globalPointSize = value;
120
+ for (const set of setsStore.sets) {
121
+ setsStore.updateSetSettings(set.id, { pointSize: value });
122
+ }
123
+ }
124
+
125
+ function handleGlobalOpacityChange(e: Event) {
126
+ const value = parseFloat((e.target as HTMLInputElement).value);
127
+ globalOpacity = value;
128
+ for (const set of setsStore.sets) {
129
+ setsStore.updateSetSettings(set.id, { opacity: value });
130
+ }
131
+ }
132
+
133
+ function handleFileSelect(event: Event) {
134
+ const input = event.target as HTMLInputElement;
135
+ const file = input.files?.[0];
136
+
137
+ if (!file) return;
138
+
139
+ importFileName = file.name.replace(/\.csv$/i, '');
140
+ importError = '';
141
+
142
+ const reader = new FileReader();
143
+ reader.onload = (e) => {
144
+ try {
145
+ const content = e.target?.result as string;
146
+ importResult = setsStore.importFromCsv(content, file.name);
147
+ showImportModal = true;
148
+ } catch (err) {
149
+ importError = err instanceof Error ? err.message : 'Failed to parse CSV';
150
+ importResult = null;
151
+ }
152
+ };
153
+ reader.onerror = () => {
154
+ importError = 'Failed to read file';
155
+ importResult = null;
156
+ };
157
+ reader.readAsText(file);
158
+ input.value = '';
159
+ }
160
+
161
+ function confirmImport() {
162
+ if (!importResult) return;
163
+ const newSet = setsStore.createSet(importFileName, importResult);
164
+ // Auto-expand newly imported set
165
+ expandedSets = new Set([...expandedSets, newSet.id]);
166
+ closeModal();
167
+ }
168
+
169
+ function closeModal() {
170
+ showImportModal = false;
171
+ importResult = null;
172
+ importFileName = '';
173
+ importError = '';
174
+ }
175
+
176
+ function triggerFileInput() {
177
+ fileInput?.click();
178
+ }
179
+
180
+ // Quick Add functions
181
+ function parseQuickAddText() {
182
+ if (!quickAddText.trim()) {
183
+ quickAddResult = null;
184
+ return;
185
+ }
186
+
187
+ // Parse cell names: split by newlines, commas, semicolons, or whitespace
188
+ const ids = quickAddText
189
+ .split(/[\n,;\s]+/)
190
+ .map(s => s.trim())
191
+ .filter(s => s.length > 0);
192
+
193
+ if (ids.length === 0) {
194
+ quickAddResult = null;
195
+ return;
196
+ }
197
+
198
+ // Create a simple CSV and use existing import logic
199
+ const csvContent = `id,customGroup\n${ids.map(id => `${id},manual`).join('\n')}`;
200
+ quickAddResult = setsStore.importFromCsv(csvContent, 'quick-add');
201
+ }
202
+
203
+ function confirmQuickAdd() {
204
+ if (!quickAddResult || quickAddResult.cells.length === 0) return;
205
+
206
+ const newSet = setsStore.createSet(quickAddName || 'Quick Selection', quickAddResult);
207
+ expandedSets = new Set([...expandedSets, newSet.id]);
208
+ closeQuickAdd();
209
+ }
210
+
211
+ function closeQuickAdd() {
212
+ showQuickAdd = false;
213
+ quickAddText = '';
214
+ quickAddName = 'Quick Selection';
215
+ quickAddResult = null;
216
+ }
217
+
218
+ function removeSet(setId: string) {
219
+ if (confirm('Remove this custom layer set?')) {
220
+ setsStore.removeSet(setId);
221
+ expandedSets.delete(setId);
222
+ expandedSets = new Set(expandedSets);
223
+ }
224
+ }
225
+
226
+ function toggleExpanded(setId: string) {
227
+ if (expandedSets.has(setId)) {
228
+ expandedSets.delete(setId);
229
+ } else {
230
+ expandedSets.add(setId);
231
+ }
232
+ expandedSets = new Set(expandedSets);
233
+ }
234
+
235
+ function handleColorChange(setId: string, groupId: string, event: Event) {
236
+ const input = event.target as HTMLInputElement;
237
+ setsStore.setGroupColor(setId, groupId, input.value);
238
+ }
239
+
240
+ function getSetItemCounts(set: CustomCellSet): string {
241
+ const cellCount = set.cells.filter(c => c.geometry === 'cell').length;
242
+ const pointCount = set.cells.filter(c => c.geometry === 'point').length;
243
+ if (cellCount > 0 && pointCount > 0) {
244
+ return `${cellCount} cells, ${pointCount} points`;
245
+ } else if (cellCount > 0) {
246
+ return `${cellCount} cells`;
247
+ } else {
248
+ return `${pointCount} points`;
249
+ }
250
+ }
251
+ </script>
252
+
253
+ <MapControl {position} title="Custom Layers" icon="layers" controlWidth="320px">
254
+ {#snippet actions()}
255
+ <button
256
+ class="btn btn-sm btn-outline-primary border-0 p-1 px-2"
257
+ title="Quick Add (paste cell names)"
258
+ onclick={() => showQuickAdd = true}
259
+ >
260
+ <i class="bi bi-pencil-square"></i>
261
+ </button>
262
+ <button
263
+ class="btn btn-sm btn-outline-primary border-0 p-1 px-2"
264
+ title="Import CSV"
265
+ onclick={triggerFileInput}
266
+ >
267
+ <i class="bi bi-upload"></i>
268
+ </button>
269
+ {/snippet}
270
+
271
+ {#snippet collapsedActions()}
272
+ <!-- No actions when collapsed - just click icon to expand -->
273
+ {/snippet}
274
+
275
+ <div class="custom-layers-manager">
276
+ <!-- Hidden file input -->
277
+ <input
278
+ type="file"
279
+ accept=".csv"
280
+ class="d-none"
281
+ bind:this={fileInput}
282
+ onchange={handleFileSelect}
283
+ />
284
+
285
+ <!-- Error message -->
286
+ {#if importError}
287
+ <div class="alert alert-danger py-1 px-2 mb-2 small">
288
+ <i class="bi bi-exclamation-triangle me-1"></i>
289
+ {importError}
290
+ </div>
291
+ {/if}
292
+
293
+ <!-- Empty state -->
294
+ {#if setsStore.sets.length === 0}
295
+ <div class="text-center p-3">
296
+ <i class="bi bi-layers fs-1 text-muted"></i>
297
+ <p class="text-muted small mt-2 mb-0">
298
+ No custom layers loaded.
299
+ </p>
300
+ <div class="d-flex gap-2 justify-content-center mt-2">
301
+ <button
302
+ class="btn btn-sm btn-outline-primary"
303
+ onclick={() => showQuickAdd = true}
304
+ >
305
+ <i class="bi bi-pencil-square me-1"></i> Quick Add
306
+ </button>
307
+ <button
308
+ class="btn btn-sm btn-outline-secondary"
309
+ onclick={triggerFileInput}
310
+ >
311
+ <i class="bi bi-upload me-1"></i> Import CSV
312
+ </button>
313
+ </div>
314
+ </div>
315
+ {:else}
316
+ <!-- Global Controls -->
317
+ <div class="global-controls px-2 py-2 border-bottom">
318
+ <div class="mb-2">
319
+ <label class="form-label small mb-1 d-flex justify-content-between">
320
+ <span><i class="bi bi-broadcast me-1"></i>Sector Size</span>
321
+ <span class="text-muted">{globalSectorSize}px</span>
322
+ </label>
323
+ <input
324
+ type="range"
325
+ class="form-range"
326
+ min="10"
327
+ max="200"
328
+ step="5"
329
+ value={globalSectorSize}
330
+ oninput={handleGlobalSectorSizeChange}
331
+ />
332
+ </div>
333
+ <div class="mb-2">
334
+ <label class="form-label small mb-1 d-flex justify-content-between">
335
+ <span><i class="bi bi-geo-alt me-1"></i>Point Size</span>
336
+ <span class="text-muted">{globalPointSize}px</span>
337
+ </label>
338
+ <input
339
+ type="range"
340
+ class="form-range"
341
+ min="2"
342
+ max="30"
343
+ step="1"
344
+ value={globalPointSize}
345
+ oninput={handleGlobalPointSizeChange}
346
+ />
347
+ </div>
348
+ <div>
349
+ <label class="form-label small mb-1 d-flex justify-content-between">
350
+ <span>Opacity</span>
351
+ <span class="text-muted">{Math.round(globalOpacity * 100)}%</span>
352
+ </label>
353
+ <input
354
+ type="range"
355
+ class="form-range"
356
+ min="0.1"
357
+ max="1"
358
+ step="0.1"
359
+ value={globalOpacity}
360
+ oninput={handleGlobalOpacityChange}
361
+ />
362
+ </div>
363
+ </div>
364
+
365
+ <!-- Sets Accordion -->
366
+ <div class="sets-accordion">
367
+ {#each setsStore.sets as set (set.id)}
368
+ {@const isExpanded = expandedSets.has(set.id)}
369
+ {@const treeStore = treeStores.get(set.id)}
370
+ <div class="set-section" class:expanded={isExpanded}>
371
+ <!-- Set Header -->
372
+ <div class="set-header d-flex align-items-center justify-content-between px-2 py-2">
373
+ <div class="d-flex align-items-center flex-grow-1" style="min-width: 0;">
374
+ <button
375
+ class="btn btn-sm p-0 me-2 expand-btn"
376
+ onclick={() => toggleExpanded(set.id)}
377
+ title={isExpanded ? 'Collapse' : 'Expand'}
378
+ >
379
+ <i class="bi bi-chevron-{isExpanded ? 'down' : 'right'}"></i>
380
+ </button>
381
+ <button
382
+ class="btn btn-sm p-0 me-2"
383
+ title={set.visible ? 'Hide' : 'Show'}
384
+ onclick={() => setsStore.toggleSetVisibility(set.id)}
385
+ >
386
+ <i class="bi bi-eye{set.visible ? '-fill text-primary' : '-slash text-muted'}"></i>
387
+ </button>
388
+ <div class="set-info text-truncate" onclick={() => toggleExpanded(set.id)} style="cursor: pointer;">
389
+ <div class="fw-medium small text-truncate">{set.name}</div>
390
+ <small class="text-muted">{getSetItemCounts(set)} · {set.groups.length} groups</small>
391
+ </div>
392
+ </div>
393
+ <button
394
+ class="btn btn-sm btn-outline-danger border-0 p-1 ms-2"
395
+ title="Remove"
396
+ onclick={() => removeSet(set.id)}
397
+ >
398
+ <i class="bi bi-trash"></i>
399
+ </button>
400
+ </div>
401
+
402
+ <!-- Set Content (TreeView) -->
403
+ {#if isExpanded && treeStore}
404
+ <div class="set-content px-2 pb-2">
405
+ {#if set.cells.length === 0}
406
+ <div class="text-muted p-2 text-center small">
407
+ No items in this set.
408
+ </div>
409
+ {:else}
410
+ <TreeView showControls={false} store={treeStore} height="180px">
411
+ {#snippet children({ node, state })}
412
+ {#if (!node.children || node.children.length === 0) && node.metadata?.groupId}
413
+ <div
414
+ class="d-flex align-items-center"
415
+ role="group"
416
+ onclick={(e) => e.stopPropagation()}
417
+ onkeydown={(e) => e.stopPropagation()}
418
+ >
419
+ <input
420
+ type="color"
421
+ class="form-control form-control-color form-control-sm border-0 p-0"
422
+ style="width: 16px; height: 16px; min-height: 0;"
423
+ value={node.metadata?.color}
424
+ oninput={(e) => handleColorChange(set.id, node.metadata?.groupId || '', e)}
425
+ title="Change color"
426
+ />
427
+ </div>
428
+ {/if}
429
+ {/snippet}
430
+ </TreeView>
431
+ {/if}
432
+ </div>
433
+ {/if}
434
+ </div>
435
+ {/each}
436
+ </div>
437
+ {/if}
438
+ </div>
439
+ </MapControl>
440
+
441
+ <!-- Import Result Modal -->
442
+ {#if showImportModal && importResult}
443
+ <div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
444
+ <div class="modal-dialog modal-dialog-centered">
445
+ <div class="modal-content">
446
+ <div class="modal-header py-2">
447
+ <h6 class="modal-title">
448
+ <i class="bi bi-file-earmark-spreadsheet me-2"></i>
449
+ Import: {importFileName}
450
+ </h6>
451
+ <button type="button" class="btn-close" onclick={closeModal}></button>
452
+ </div>
453
+ <div class="modal-body">
454
+ <!-- Import Summary -->
455
+ <div class="mb-3">
456
+ <div class="d-flex justify-content-between align-items-center mb-2">
457
+ <div>
458
+ {#if importResult.cellCount > 0}
459
+ <span class="text-success me-2">
460
+ <i class="bi bi-broadcast me-1"></i>
461
+ {importResult.cellCount} cells
462
+ </span>
463
+ {/if}
464
+ {#if importResult.pointCount > 0}
465
+ <span class="text-info">
466
+ <i class="bi bi-geo-alt me-1"></i>
467
+ {importResult.pointCount} points
468
+ </span>
469
+ {/if}
470
+ </div>
471
+ <span class="badge bg-secondary">{importResult.totalRows} rows</span>
472
+ </div>
473
+
474
+ {#if importResult.unmatchedTxIds.length > 0}
475
+ <div class="text-warning small mb-2">
476
+ <i class="bi bi-exclamation-triangle me-1"></i>
477
+ {importResult.unmatchedTxIds.length} IDs not found
478
+ <button
479
+ class="btn btn-link btn-sm p-0 ms-1"
480
+ type="button"
481
+ data-bs-toggle="collapse"
482
+ data-bs-target="#unmatchedList"
483
+ >
484
+ (show)
485
+ </button>
486
+ </div>
487
+ <div class="collapse" id="unmatchedList">
488
+ <div class="small bg-light p-2 rounded" style="max-height: 100px; overflow-y: auto;">
489
+ {importResult.unmatchedTxIds.slice(0, 20).join(', ')}
490
+ {#if importResult.unmatchedTxIds.length > 20}
491
+ <span class="text-muted">... and {importResult.unmatchedTxIds.length - 20} more</span>
492
+ {/if}
493
+ </div>
494
+ </div>
495
+ {/if}
496
+ </div>
497
+
498
+ <!-- Groups found -->
499
+ <div class="mb-3">
500
+ <label class="form-label small fw-medium">Groups Found ({importResult.groups.length})</label>
501
+ <div class="d-flex flex-wrap gap-1">
502
+ {#each importResult.groups as group}
503
+ <span class="badge bg-secondary">{group}</span>
504
+ {/each}
505
+ </div>
506
+ </div>
507
+
508
+ <!-- Extra columns -->
509
+ {#if importResult.extraColumns.length > 0}
510
+ <div class="mb-3">
511
+ <label class="form-label small fw-medium">Extra Columns (for tooltips)</label>
512
+ <div class="d-flex flex-wrap gap-1">
513
+ {#each importResult.extraColumns as col}
514
+ <span class="badge bg-info">{col}</span>
515
+ {/each}
516
+ </div>
517
+ </div>
518
+ {/if}
519
+
520
+ <!-- Set Name -->
521
+ <div class="mb-2">
522
+ <label for="setName" class="form-label small fw-medium">Set Name</label>
523
+ <input
524
+ type="text"
525
+ class="form-control form-control-sm"
526
+ id="setName"
527
+ bind:value={importFileName}
528
+ />
529
+ </div>
530
+ </div>
531
+ <div class="modal-footer py-2">
532
+ <button type="button" class="btn btn-secondary btn-sm" onclick={closeModal}>
533
+ Cancel
534
+ </button>
535
+ <button
536
+ type="button"
537
+ class="btn btn-primary btn-sm"
538
+ onclick={confirmImport}
539
+ disabled={importResult.cells.length === 0}
540
+ >
541
+ <i class="bi bi-check-lg me-1"></i>
542
+ Import {importResult.cells.length} cells
543
+ </button>
544
+ </div>
545
+ </div>
546
+ </div>
547
+ </div>
548
+ {/if}
549
+
550
+ <!-- Quick Add Modal -->
551
+ {#if showQuickAdd}
552
+ <div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
553
+ <div class="modal-dialog modal-dialog-centered">
554
+ <div class="modal-content">
555
+ <div class="modal-header py-2">
556
+ <h6 class="modal-title">
557
+ <i class="bi bi-pencil-square me-2"></i>
558
+ Quick Add Cells
559
+ </h6>
560
+ <button type="button" class="btn-close" onclick={closeQuickAdd}></button>
561
+ </div>
562
+ <div class="modal-body">
563
+ <div class="mb-3">
564
+ <label for="quickAddText" class="form-label small fw-medium">
565
+ Paste cellName or trxId (one per line, or comma/space separated)
566
+ </label>
567
+ <textarea
568
+ class="form-control form-control-sm font-monospace"
569
+ id="quickAddText"
570
+ rows="6"
571
+ bind:value={quickAddText}
572
+ oninput={parseQuickAddText}
573
+ ></textarea>
574
+ </div>
575
+
576
+ <!-- Preview -->
577
+ {#if quickAddResult}
578
+ <div class="mb-3">
579
+ <div class="d-flex justify-content-between align-items-center">
580
+ <div>
581
+ {#if quickAddResult.cellCount > 0}
582
+ <span class="text-success">
583
+ <i class="bi bi-check-circle me-1"></i>
584
+ {quickAddResult.cellCount} cells found
585
+ </span>
586
+ {:else}
587
+ <span class="text-muted">No cells found</span>
588
+ {/if}
589
+ </div>
590
+ {#if quickAddResult.unmatchedTxIds.length > 0}
591
+ <span class="text-warning small">
592
+ <i class="bi bi-exclamation-triangle me-1"></i>
593
+ {quickAddResult.unmatchedTxIds.length} not found
594
+ </span>
595
+ {/if}
596
+ </div>
597
+ {#if quickAddResult.unmatchedTxIds.length > 0}
598
+ <div class="small text-muted mt-1" style="max-height: 60px; overflow-y: auto;">
599
+ Not found: {quickAddResult.unmatchedTxIds.slice(0, 10).join(', ')}
600
+ {#if quickAddResult.unmatchedTxIds.length > 10}
601
+ <span>... +{quickAddResult.unmatchedTxIds.length - 10} more</span>
602
+ {/if}
603
+ </div>
604
+ {/if}
605
+ </div>
606
+ {/if}
607
+
608
+ <!-- Set Name -->
609
+ <div class="mb-2">
610
+ <label for="quickAddName" class="form-label small fw-medium">Set Name</label>
611
+ <input
612
+ type="text"
613
+ class="form-control form-control-sm"
614
+ id="quickAddName"
615
+ bind:value={quickAddName}
616
+ />
617
+ </div>
618
+ </div>
619
+ <div class="modal-footer py-2">
620
+ <button type="button" class="btn btn-secondary btn-sm" onclick={closeQuickAdd}>
621
+ Cancel
622
+ </button>
623
+ <button
624
+ type="button"
625
+ class="btn btn-primary btn-sm"
626
+ onclick={confirmQuickAdd}
627
+ disabled={!quickAddResult || quickAddResult.cells.length === 0}
628
+ >
629
+ <i class="bi bi-plus-lg me-1"></i>
630
+ Add {quickAddResult?.cells.length || 0} cells
631
+ </button>
632
+ </div>
633
+ </div>
634
+ </div>
635
+ </div>
636
+ {/if}
637
+
638
+ <style>
639
+ .custom-layers-manager {
640
+ font-size: 0.875rem;
641
+ }
642
+
643
+ .global-controls {
644
+ background: rgba(0, 0, 0, 0.02);
645
+ }
646
+
647
+ .set-section {
648
+ border-bottom: 1px solid var(--bs-border-color-translucent);
649
+ }
650
+
651
+ .set-section:last-child {
652
+ border-bottom: none;
653
+ }
654
+
655
+ .set-header {
656
+ background: transparent;
657
+ transition: background-color 0.15s ease;
658
+ }
659
+
660
+ .set-header:hover {
661
+ background: rgba(0, 0, 0, 0.03);
662
+ }
663
+
664
+ .set-section.expanded .set-header {
665
+ background: rgba(0, 0, 0, 0.02);
666
+ }
667
+
668
+ .expand-btn {
669
+ width: 20px;
670
+ height: 20px;
671
+ line-height: 1;
672
+ transition: transform 0.15s ease;
673
+ }
674
+
675
+ .set-info {
676
+ flex: 1;
677
+ min-width: 0;
678
+ }
679
+
680
+ .set-content {
681
+ background: rgba(0, 0, 0, 0.01);
682
+ }
683
+
684
+ .modal {
685
+ z-index: 2000;
686
+ }
687
+
688
+ /* Custom form-range styling for sliders */
689
+ .form-range {
690
+ height: 1rem;
691
+ }
692
+ </style>
@@ -12,7 +12,7 @@
12
12
  * CustomCellSetManager,
13
13
  * CustomCellFilterControl,
14
14
  * createCustomCellSetsStore
15
- * } from './';
15
+ * } from '../cells/custom';
16
16
  *
17
17
  * const customSetsStore = createCustomCellSetsStore(cellDataStore, 'my-namespace');
18
18
  * </script>
@@ -12,7 +12,7 @@
12
12
  * CustomCellSetManager,
13
13
  * CustomCellFilterControl,
14
14
  * createCustomCellSetsStore
15
- * } from './';
15
+ * } from '../cells/custom';
16
16
  *
17
17
  * const customSetsStore = createCustomCellSetsStore(cellDataStore, 'my-namespace');
18
18
  * </script>