@smartnet360/svelte-components 0.0.124 → 0.0.126

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 (53) hide show
  1. package/dist/apps/antenna-tools/components/AntennaSettingsModal.svelte +4 -174
  2. package/dist/apps/antenna-tools/components/DatabaseViewer.svelte +2 -2
  3. package/dist/apps/antenna-tools/components/MSIConverter.svelte +302 -43
  4. package/dist/apps/antenna-tools/db.js +4 -0
  5. package/dist/apps/antenna-tools/utils/db-utils.d.ts +19 -0
  6. package/dist/apps/antenna-tools/utils/db-utils.js +108 -0
  7. package/dist/core/Auth/LoginForm.svelte +397 -0
  8. package/dist/core/Auth/LoginForm.svelte.d.ts +16 -0
  9. package/dist/core/Auth/auth.svelte.d.ts +22 -0
  10. package/dist/core/Auth/auth.svelte.js +229 -0
  11. package/dist/core/Auth/config.d.ts +25 -0
  12. package/dist/core/Auth/config.js +256 -0
  13. package/dist/core/Auth/index.d.ts +4 -0
  14. package/dist/core/Auth/index.js +5 -0
  15. package/dist/core/Auth/types.d.ts +140 -0
  16. package/dist/core/Auth/types.js +2 -0
  17. package/dist/core/LandingPage/App.svelte +102 -0
  18. package/dist/core/LandingPage/App.svelte.d.ts +20 -0
  19. package/dist/core/LandingPage/LandingPage.svelte +480 -0
  20. package/dist/core/LandingPage/LandingPage.svelte.d.ts +21 -0
  21. package/dist/core/LandingPage/index.d.ts +2 -0
  22. package/dist/core/LandingPage/index.js +3 -0
  23. package/dist/core/index.d.ts +2 -0
  24. package/dist/core/index.js +4 -0
  25. package/dist/map-v3/demo/DemoMap.svelte +18 -0
  26. package/dist/map-v3/demo/demo-custom-cells.d.ts +21 -0
  27. package/dist/map-v3/demo/demo-custom-cells.js +48 -0
  28. package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte +220 -0
  29. package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte.d.ts +15 -0
  30. package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte +306 -0
  31. package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte.d.ts +10 -0
  32. package/dist/map-v3/features/cells/custom/components/index.d.ts +5 -0
  33. package/dist/map-v3/features/cells/custom/components/index.js +5 -0
  34. package/dist/map-v3/features/cells/custom/index.d.ts +32 -0
  35. package/dist/map-v3/features/cells/custom/index.js +35 -0
  36. package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte +262 -0
  37. package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte.d.ts +10 -0
  38. package/dist/map-v3/features/cells/custom/layers/index.d.ts +4 -0
  39. package/dist/map-v3/features/cells/custom/layers/index.js +4 -0
  40. package/dist/map-v3/features/cells/custom/logic/csv-parser.d.ts +20 -0
  41. package/dist/map-v3/features/cells/custom/logic/csv-parser.js +164 -0
  42. package/dist/map-v3/features/cells/custom/logic/index.d.ts +5 -0
  43. package/dist/map-v3/features/cells/custom/logic/index.js +5 -0
  44. package/dist/map-v3/features/cells/custom/logic/tree-adapter.d.ts +24 -0
  45. package/dist/map-v3/features/cells/custom/logic/tree-adapter.js +67 -0
  46. package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.d.ts +78 -0
  47. package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.js +242 -0
  48. package/dist/map-v3/features/cells/custom/stores/index.d.ts +4 -0
  49. package/dist/map-v3/features/cells/custom/stores/index.js +4 -0
  50. package/dist/map-v3/features/cells/custom/types.d.ts +83 -0
  51. package/dist/map-v3/features/cells/custom/types.js +23 -0
  52. package/dist/map-v3/shared/controls/MapControl.svelte +27 -3
  53. package/package.json +1 -1
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Demo Custom Cells Data
3
+ *
4
+ * Sample CSV content for testing custom cells feature.
5
+ * Uses txIds from the demo cells data.
6
+ *
7
+ * txId format: SSSS S BB where:
8
+ * - SSSS = site number (1000+)
9
+ * - S = sector (1, 2, or 3)
10
+ * - BB = band index (41-51)
11
+ * 41=GSM900, 42=GSM1800, 43=LTE700, 44=LTE800, 45=LTE900
12
+ * 46=LTE1800, 47=LTE2100, 48=LTE2600, 49=5G700, 50=5G2100, 51=5G3500
13
+ */
14
+ /**
15
+ * Sample CSV content with various groups and size factors
16
+ */
17
+ export declare const demoCustomCellsCsv = "txId,customGroup,sizeFactor,congestion,trafficGB\n1000141,High Traffic,3,critical,250\n1000241,High Traffic,2.5,high,180\n1000341,High Traffic,2,medium,120\n1001145,High Traffic,3.5,critical,300\n1001245,High Traffic,2,medium,100\n1002147,Medium Traffic,1.5,low,60\n1002247,Medium Traffic,1.2,low,45\n1002347,Medium Traffic,1,minimal,30\n1003142,Medium Traffic,1.5,low,55\n1003242,Medium Traffic,1.3,low,40\n1004151,Low Traffic,0.8,minimal,15\n1004251,Low Traffic,0.7,minimal,10\n1004351,Low Traffic,0.6,minimal,8\n1005149,Problem Cells,2,error,0\n1005249,Problem Cells,2,error,0\n1006144,Problem Cells,1.8,warning,5\n";
18
+ /**
19
+ * Another sample CSV with different groupings
20
+ */
21
+ export declare const demoCustomCellsCsv2 = "txId,customGroup,sizeFactor,priority\n1010141,Expansion Zone A,2,high\n1010241,Expansion Zone A,2,high\n1010341,Expansion Zone A,2,high\n1011145,Expansion Zone A,1.5,medium\n1012147,Expansion Zone B,1.8,high\n1012247,Expansion Zone B,1.8,high\n1013142,Existing Coverage,1,low\n1013242,Existing Coverage,1,low\n1013342,Existing Coverage,1,low\n";
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Demo Custom Cells Data
3
+ *
4
+ * Sample CSV content for testing custom cells feature.
5
+ * Uses txIds from the demo cells data.
6
+ *
7
+ * txId format: SSSS S BB where:
8
+ * - SSSS = site number (1000+)
9
+ * - S = sector (1, 2, or 3)
10
+ * - BB = band index (41-51)
11
+ * 41=GSM900, 42=GSM1800, 43=LTE700, 44=LTE800, 45=LTE900
12
+ * 46=LTE1800, 47=LTE2100, 48=LTE2600, 49=5G700, 50=5G2100, 51=5G3500
13
+ */
14
+ /**
15
+ * Sample CSV content with various groups and size factors
16
+ */
17
+ export const demoCustomCellsCsv = `txId,customGroup,sizeFactor,congestion,trafficGB
18
+ 1000141,High Traffic,3,critical,250
19
+ 1000241,High Traffic,2.5,high,180
20
+ 1000341,High Traffic,2,medium,120
21
+ 1001145,High Traffic,3.5,critical,300
22
+ 1001245,High Traffic,2,medium,100
23
+ 1002147,Medium Traffic,1.5,low,60
24
+ 1002247,Medium Traffic,1.2,low,45
25
+ 1002347,Medium Traffic,1,minimal,30
26
+ 1003142,Medium Traffic,1.5,low,55
27
+ 1003242,Medium Traffic,1.3,low,40
28
+ 1004151,Low Traffic,0.8,minimal,15
29
+ 1004251,Low Traffic,0.7,minimal,10
30
+ 1004351,Low Traffic,0.6,minimal,8
31
+ 1005149,Problem Cells,2,error,0
32
+ 1005249,Problem Cells,2,error,0
33
+ 1006144,Problem Cells,1.8,warning,5
34
+ `;
35
+ /**
36
+ * Another sample CSV with different groupings
37
+ */
38
+ export const demoCustomCellsCsv2 = `txId,customGroup,sizeFactor,priority
39
+ 1010141,Expansion Zone A,2,high
40
+ 1010241,Expansion Zone A,2,high
41
+ 1010341,Expansion Zone A,2,high
42
+ 1011145,Expansion Zone A,1.5,medium
43
+ 1012147,Expansion Zone B,1.8,high
44
+ 1012247,Expansion Zone B,1.8,high
45
+ 1013142,Existing Coverage,1,low
46
+ 1013242,Existing Coverage,1,low
47
+ 1013342,Existing Coverage,1,low
48
+ `;
@@ -0,0 +1,220 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Custom Cell Filter Control
4
+ *
5
+ * TreeView with color pickers for a single custom cell set.
6
+ * Shows groups with their cell counts and allows color customization.
7
+ */
8
+ import { untrack, onDestroy } from 'svelte';
9
+ import { MapControl } from '../../../../shared';
10
+ import { createTreeStore, TreeView } from '../../../../../core/TreeView';
11
+ import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
12
+ import type { CustomCellSet } from '../types';
13
+ import { buildCustomCellTree } from '../logic/tree-adapter';
14
+
15
+ interface Props {
16
+ /** The custom cell sets store */
17
+ setsStore: CustomCellSetsStore;
18
+ /** The specific set to display */
19
+ set: CustomCellSet;
20
+ /** Control position on map */
21
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
22
+ /** Callback when set is removed */
23
+ onremove?: (setId: string) => void;
24
+ }
25
+
26
+ let {
27
+ setsStore,
28
+ set,
29
+ position = 'top-left',
30
+ onremove
31
+ }: Props = $props();
32
+
33
+ // Reference to MapControl for explicit cleanup
34
+ let mapControlRef: MapControl | undefined;
35
+
36
+ // Track if this component should be destroyed (used for delayed removal)
37
+ let isDestroying = $state(false);
38
+
39
+ onDestroy(() => {
40
+ console.log(`[CustomCellFilterControl] onDestroy called for set: ${set.id}`);
41
+ });
42
+
43
+ // Build tree from set
44
+ let treeStore = $derived.by(() => {
45
+ const _version = setsStore.version;
46
+ const _set = set;
47
+
48
+ return untrack(() => {
49
+ const nodes = buildCustomCellTree(_set);
50
+ return createTreeStore({
51
+ nodes,
52
+ namespace: `custom-cells:${_set.id}`,
53
+ persistState: true,
54
+ defaultExpandAll: true
55
+ });
56
+ });
57
+ });
58
+
59
+ // Sync tree selection -> store visibility
60
+ $effect(() => {
61
+ const val = treeStore;
62
+ let changes = 0;
63
+
64
+ val.state.nodes.forEach((nodeState) => {
65
+ // Skip root node
66
+ if (nodeState.node.id === `root-${set.id}`) return;
67
+ // Skip folder nodes
68
+ if (nodeState.node.children && nodeState.node.children.length > 0) return;
69
+
70
+ const groupId = nodeState.node.metadata?.groupId;
71
+ if (!groupId) return;
72
+
73
+ const isVisible = val.state.checkedPaths.has(nodeState.path);
74
+ const currentlyVisible = set.visibleGroups.has(groupId);
75
+
76
+ if (isVisible !== currentlyVisible) {
77
+ setsStore.toggleGroupVisibility(set.id, groupId);
78
+ changes++;
79
+ }
80
+ });
81
+
82
+ if (changes > 0) {
83
+ console.log(`[CustomCellFilterControl] Synced ${changes} visibility changes`);
84
+ }
85
+ });
86
+
87
+ function handleColorChange(groupId: string, event: Event) {
88
+ const input = event.target as HTMLInputElement;
89
+ setsStore.setGroupColor(set.id, groupId, input.value);
90
+ }
91
+
92
+ function handleRemove() {
93
+ if (onremove) {
94
+ onremove(set.id);
95
+ }
96
+ }
97
+
98
+ function handleToggleVisibility() {
99
+ setsStore.toggleSetVisibility(set.id);
100
+ }
101
+ </script>
102
+
103
+ <MapControl {position} title={set.name} icon="layers" controlWidth="280px">
104
+ {#snippet actions()}
105
+ <button
106
+ class="btn btn-sm btn-outline-secondary border-0 p-1 px-2"
107
+ title={set.visible ? 'Hide Layer' : 'Show Layer'}
108
+ onclick={handleToggleVisibility}
109
+ >
110
+ <i class="bi bi-eye{set.visible ? '-fill' : '-slash'}"></i>
111
+ </button>
112
+ <button
113
+ class="btn btn-sm btn-outline-danger border-0 p-1 px-2"
114
+ title="Remove Set"
115
+ onclick={handleRemove}
116
+ >
117
+ <i class="bi bi-trash"></i>
118
+ </button>
119
+ {/snippet}
120
+
121
+ <div class="custom-cell-filter-control">
122
+ <!-- Set Info -->
123
+ <div class="set-info mb-2 px-1">
124
+ <small class="text-muted">
125
+ {set.cells.length} cells in {set.groups.length} groups
126
+ {#if set.unmatchedTxIds.length > 0}
127
+ <span class="text-warning ms-1" title="Some cells not found">
128
+ ({set.unmatchedTxIds.length} unmatched)
129
+ </span>
130
+ {/if}
131
+ </small>
132
+ </div>
133
+
134
+ <!-- Size Slider -->
135
+ <div class="size-control mb-2 px-1">
136
+ <label for="baseSize-{set.id}" class="form-label small mb-1">
137
+ Base Size: {set.baseSize}px
138
+ </label>
139
+ <input
140
+ type="range"
141
+ class="form-range"
142
+ id="baseSize-{set.id}"
143
+ min="10"
144
+ max="150"
145
+ step="5"
146
+ value={set.baseSize}
147
+ oninput={(e) => setsStore.updateSetSettings(set.id, {
148
+ baseSize: parseInt((e.target as HTMLInputElement).value)
149
+ })}
150
+ />
151
+ </div>
152
+
153
+ <!-- Opacity Slider -->
154
+ <div class="opacity-control mb-2 px-1">
155
+ <label for="opacity-{set.id}" class="form-label small mb-1">
156
+ Opacity: {Math.round(set.opacity * 100)}%
157
+ </label>
158
+ <input
159
+ type="range"
160
+ class="form-range"
161
+ id="opacity-{set.id}"
162
+ min="0.1"
163
+ max="1"
164
+ step="0.1"
165
+ value={set.opacity}
166
+ oninput={(e) => setsStore.updateSetSettings(set.id, {
167
+ opacity: parseFloat((e.target as HTMLInputElement).value)
168
+ })}
169
+ />
170
+ </div>
171
+
172
+ <!-- Tree View -->
173
+ <div class="custom-cell-tree">
174
+ {#if set.cells.length === 0}
175
+ <div class="text-muted p-3 text-center small">
176
+ No cells in this set.
177
+ </div>
178
+ {:else}
179
+ <TreeView showControls={false} store={treeStore} height="200px">
180
+ {#snippet children({ node, state })}
181
+ <!-- Color Picker (Only for group leaves) -->
182
+ {#if (!node.children || node.children.length === 0) && node.metadata?.groupId !== '__root__'}
183
+ <div
184
+ class="d-flex align-items-center"
185
+ role="group"
186
+ onclick={(e) => e.stopPropagation()}
187
+ onkeydown={(e) => e.stopPropagation()}
188
+ >
189
+ <input
190
+ type="color"
191
+ class="form-control form-control-color form-control-sm border-0 p-0"
192
+ style="width: 16px; height: 16px; min-height: 0;"
193
+ value={node.metadata?.color}
194
+ oninput={(e) => handleColorChange(node.metadata?.groupId || '', e)}
195
+ title="Change color"
196
+ />
197
+ </div>
198
+ {/if}
199
+ {/snippet}
200
+ </TreeView>
201
+ {/if}
202
+ </div>
203
+ </div>
204
+ </MapControl>
205
+
206
+ <style>
207
+ .custom-cell-filter-control {
208
+ font-size: 0.875rem;
209
+ }
210
+
211
+ .set-info {
212
+ border-bottom: 1px solid var(--bs-border-color, #dee2e6);
213
+ padding-bottom: 0.5rem;
214
+ }
215
+
216
+ .custom-cell-tree {
217
+ max-height: 250px;
218
+ overflow-y: auto;
219
+ }
220
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
2
+ import type { CustomCellSet } from '../types';
3
+ interface Props {
4
+ /** The custom cell sets store */
5
+ setsStore: CustomCellSetsStore;
6
+ /** The specific set to display */
7
+ set: CustomCellSet;
8
+ /** Control position on map */
9
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
10
+ /** Callback when set is removed */
11
+ onremove?: (setId: string) => void;
12
+ }
13
+ declare const CustomCellFilterControl: import("svelte").Component<Props, {}, "">;
14
+ type CustomCellFilterControl = ReturnType<typeof CustomCellFilterControl>;
15
+ export default CustomCellFilterControl;
@@ -0,0 +1,306 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Custom Cell Set Manager
4
+ *
5
+ * Upload CSV files, view import results, and manage custom cell sets.
6
+ * Acts as the main control panel for the custom cells feature.
7
+ * Includes filter controls for each loaded set.
8
+ */
9
+ import { MapControl } from '../../../../shared';
10
+ import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
11
+ import type { CustomCellImportResult } from '../types';
12
+ import CustomCellFilterControl from './CustomCellFilterControl.svelte';
13
+
14
+ interface Props {
15
+ /** The custom cell sets store */
16
+ setsStore: CustomCellSetsStore;
17
+ /** Control position on map */
18
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
19
+ }
20
+
21
+ let {
22
+ setsStore,
23
+ position = 'top-left'
24
+ }: Props = $props();
25
+
26
+ // Derived for reactivity
27
+ let setsArray = $derived(setsStore.sets);
28
+
29
+ // State
30
+ let showImportModal = $state(false);
31
+ let importResult = $state<CustomCellImportResult | null>(null);
32
+ let importFileName = $state('');
33
+ let importError = $state('');
34
+ let fileInput: HTMLInputElement;
35
+
36
+ /**
37
+ * Handle file selection
38
+ */
39
+ function handleFileSelect(event: Event) {
40
+ const input = event.target as HTMLInputElement;
41
+ const file = input.files?.[0];
42
+
43
+ if (!file) return;
44
+
45
+ importFileName = file.name.replace(/\.csv$/i, '');
46
+ importError = '';
47
+
48
+ const reader = new FileReader();
49
+ reader.onload = (e) => {
50
+ try {
51
+ const content = e.target?.result as string;
52
+ importResult = setsStore.importFromCsv(content, file.name);
53
+ showImportModal = true;
54
+ } catch (err) {
55
+ importError = err instanceof Error ? err.message : 'Failed to parse CSV';
56
+ importResult = null;
57
+ }
58
+ };
59
+ reader.onerror = () => {
60
+ importError = 'Failed to read file';
61
+ importResult = null;
62
+ };
63
+ reader.readAsText(file);
64
+
65
+ // Reset input so same file can be selected again
66
+ input.value = '';
67
+ }
68
+
69
+ /**
70
+ * Confirm import and create set
71
+ */
72
+ function confirmImport() {
73
+ if (!importResult) return;
74
+
75
+ setsStore.createSet(importFileName, importResult);
76
+ closeModal();
77
+ }
78
+
79
+ /**
80
+ * Close the modal
81
+ */
82
+ function closeModal() {
83
+ showImportModal = false;
84
+ importResult = null;
85
+ importFileName = '';
86
+ importError = '';
87
+ }
88
+
89
+ /**
90
+ * Trigger file input click
91
+ */
92
+ function triggerFileInput() {
93
+ fileInput?.click();
94
+ }
95
+
96
+ /**
97
+ * Remove a set
98
+ */
99
+ function removeSet(setId: string) {
100
+ if (confirm('Remove this custom cell set?')) {
101
+ setsStore.removeSet(setId);
102
+ }
103
+ }
104
+ </script>
105
+
106
+ <MapControl {position} title="Custom Cells" icon="plus-circle" controlWidth="300px">
107
+ {#snippet actions()}
108
+ <button
109
+ class="btn btn-sm btn-outline-primary border-0 p-1 px-2"
110
+ title="Import CSV"
111
+ onclick={triggerFileInput}
112
+ >
113
+ <i class="bi bi-upload"></i>
114
+ </button>
115
+ {/snippet}
116
+
117
+ <div class="custom-cell-manager">
118
+ <!-- Hidden file input -->
119
+ <input
120
+ type="file"
121
+ accept=".csv"
122
+ class="d-none"
123
+ bind:this={fileInput}
124
+ onchange={handleFileSelect}
125
+ />
126
+
127
+ <!-- Error message -->
128
+ {#if importError}
129
+ <div class="alert alert-danger py-1 px-2 mb-2 small">
130
+ <i class="bi bi-exclamation-triangle me-1"></i>
131
+ {importError}
132
+ </div>
133
+ {/if}
134
+
135
+ <!-- Empty state -->
136
+ {#if setsStore.sets.length === 0}
137
+ <div class="text-center p-3">
138
+ <i class="bi bi-layers fs-1 text-muted"></i>
139
+ <p class="text-muted small mt-2 mb-0">
140
+ No custom cell sets loaded.
141
+ </p>
142
+ <button
143
+ class="btn btn-sm btn-outline-primary mt-2"
144
+ onclick={triggerFileInput}
145
+ >
146
+ <i class="bi bi-upload me-1"></i> Import CSV
147
+ </button>
148
+ </div>
149
+ {:else}
150
+ <!-- List of sets -->
151
+ <ul class="list-group list-group-flush">
152
+ {#each setsStore.sets as set (set.id)}
153
+ <li class="list-group-item d-flex justify-content-between align-items-center py-2 px-2">
154
+ <div class="d-flex align-items-center">
155
+ <button
156
+ class="btn btn-sm p-0 me-2"
157
+ title={set.visible ? 'Hide' : 'Show'}
158
+ onclick={() => setsStore.toggleSetVisibility(set.id)}
159
+ >
160
+ <i class="bi bi-eye{set.visible ? '-fill text-primary' : '-slash text-muted'}"></i>
161
+ </button>
162
+ <div>
163
+ <div class="fw-medium small">{set.name}</div>
164
+ <small class="text-muted">
165
+ {set.cells.length} cells · {set.groups.length} groups
166
+ </small>
167
+ </div>
168
+ </div>
169
+ <button
170
+ class="btn btn-sm btn-outline-danger border-0 p-1"
171
+ title="Remove"
172
+ onclick={() => removeSet(set.id)}
173
+ >
174
+ <i class="bi bi-trash"></i>
175
+ </button>
176
+ </li>
177
+ {/each}
178
+ </ul>
179
+ {/if}
180
+ </div>
181
+ </MapControl>
182
+
183
+ <!-- Import Result Modal -->
184
+ {#if showImportModal && importResult}
185
+ <div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
186
+ <div class="modal-dialog modal-dialog-centered">
187
+ <div class="modal-content">
188
+ <div class="modal-header py-2">
189
+ <h6 class="modal-title">
190
+ <i class="bi bi-file-earmark-spreadsheet me-2"></i>
191
+ Import: {importFileName}
192
+ </h6>
193
+ <button type="button" class="btn-close" onclick={closeModal}></button>
194
+ </div>
195
+ <div class="modal-body">
196
+ <!-- Import Summary -->
197
+ <div class="mb-3">
198
+ <div class="d-flex justify-content-between align-items-center mb-2">
199
+ <span class="text-success">
200
+ <i class="bi bi-check-circle me-1"></i>
201
+ {importResult.cells.length} cells matched
202
+ </span>
203
+ <span class="badge bg-success">{importResult.totalRows} rows</span>
204
+ </div>
205
+
206
+ {#if importResult.unmatchedTxIds.length > 0}
207
+ <div class="text-warning small mb-2">
208
+ <i class="bi bi-exclamation-triangle me-1"></i>
209
+ {importResult.unmatchedTxIds.length} cells not found
210
+ <button
211
+ class="btn btn-link btn-sm p-0 ms-1"
212
+ type="button"
213
+ data-bs-toggle="collapse"
214
+ data-bs-target="#unmatchedList"
215
+ >
216
+ (show)
217
+ </button>
218
+ </div>
219
+ <div class="collapse" id="unmatchedList">
220
+ <div class="small bg-light p-2 rounded" style="max-height: 100px; overflow-y: auto;">
221
+ {importResult.unmatchedTxIds.slice(0, 20).join(', ')}
222
+ {#if importResult.unmatchedTxIds.length > 20}
223
+ <span class="text-muted">... and {importResult.unmatchedTxIds.length - 20} more</span>
224
+ {/if}
225
+ </div>
226
+ </div>
227
+ {/if}
228
+ </div>
229
+
230
+ <!-- Groups found -->
231
+ <div class="mb-3">
232
+ <label class="form-label small fw-medium">Groups Found ({importResult.groups.length})</label>
233
+ <div class="d-flex flex-wrap gap-1">
234
+ {#each importResult.groups as group}
235
+ <span class="badge bg-secondary">{group}</span>
236
+ {/each}
237
+ </div>
238
+ </div>
239
+
240
+ <!-- Extra columns -->
241
+ {#if importResult.extraColumns.length > 0}
242
+ <div class="mb-3">
243
+ <label class="form-label small fw-medium">Extra Columns (for tooltips)</label>
244
+ <div class="d-flex flex-wrap gap-1">
245
+ {#each importResult.extraColumns as col}
246
+ <span class="badge bg-info">{col}</span>
247
+ {/each}
248
+ </div>
249
+ </div>
250
+ {/if}
251
+
252
+ <!-- Set Name -->
253
+ <div class="mb-2">
254
+ <label for="setName" class="form-label small fw-medium">Set Name</label>
255
+ <input
256
+ type="text"
257
+ class="form-control form-control-sm"
258
+ id="setName"
259
+ bind:value={importFileName}
260
+ />
261
+ </div>
262
+ </div>
263
+ <div class="modal-footer py-2">
264
+ <button type="button" class="btn btn-secondary btn-sm" onclick={closeModal}>
265
+ Cancel
266
+ </button>
267
+ <button
268
+ type="button"
269
+ class="btn btn-primary btn-sm"
270
+ onclick={confirmImport}
271
+ disabled={importResult.cells.length === 0}
272
+ >
273
+ <i class="bi bi-check-lg me-1"></i>
274
+ Import {importResult.cells.length} cells
275
+ </button>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ {/if}
281
+
282
+ <style>
283
+ .custom-cell-manager {
284
+ font-size: 0.875rem;
285
+ }
286
+
287
+ .list-group-item {
288
+ background: transparent;
289
+ }
290
+
291
+ .modal {
292
+ z-index: 2000;
293
+ }
294
+ </style>
295
+
296
+ <!-- Filter Controls for each set (rendered outside the manager control) -->
297
+ {#each setsArray as set (set.id)}
298
+ {#key set.id}
299
+ <CustomCellFilterControl
300
+ {position}
301
+ {setsStore}
302
+ {set}
303
+ onremove={(id) => setsStore.removeSet(id)}
304
+ />
305
+ {/key}
306
+ {/each}
@@ -0,0 +1,10 @@
1
+ import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
2
+ interface Props {
3
+ /** The custom cell sets store */
4
+ setsStore: CustomCellSetsStore;
5
+ /** Control position on map */
6
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
7
+ }
8
+ declare const CustomCellSetManager: import("svelte").Component<Props, {}, "">;
9
+ type CustomCellSetManager = ReturnType<typeof CustomCellSetManager>;
10
+ export default CustomCellSetManager;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Custom Cells Components - Barrel Export
3
+ */
4
+ export { default as CustomCellFilterControl } from './CustomCellFilterControl.svelte';
5
+ export { default as CustomCellSetManager } from './CustomCellSetManager.svelte';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Custom Cells Components - Barrel Export
3
+ */
4
+ export { default as CustomCellFilterControl } from './CustomCellFilterControl.svelte';
5
+ export { default as CustomCellSetManager } from './CustomCellSetManager.svelte';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Custom Cells Feature - Main Barrel Export
3
+ *
4
+ * Ad-hoc cell layers loaded from CSV files.
5
+ * Reference existing cells by txId with custom grouping and sizing.
6
+ *
7
+ * @example
8
+ * ```svelte
9
+ * <script>
10
+ * import {
11
+ * CustomCellsLayer,
12
+ * CustomCellSetManager,
13
+ * CustomCellFilterControl,
14
+ * createCustomCellSetsStore
15
+ * } from './';
16
+ *
17
+ * const customSetsStore = createCustomCellSetsStore(cellDataStore, 'my-namespace');
18
+ * </script>
19
+ *
20
+ * <CustomCellSetManager setsStore={customSetsStore} position="top-left" />
21
+ * <CustomCellsLayer setsStore={customSetsStore} />
22
+ * {#each customSetsStore.sets as set (set.id)}
23
+ * <CustomCellFilterControl setsStore={customSetsStore} {set} position="top-left" />
24
+ * {/each}
25
+ * ```
26
+ */
27
+ export type { CustomCell, CustomCellSet, CustomCellSetConfig, CustomCellImportResult } from './types';
28
+ export { CUSTOM_CELL_PALETTE } from './types';
29
+ export { CustomCellSetsStore, createCustomCellSetsStore } from './stores';
30
+ export { CustomCellFilterControl, CustomCellSetManager } from './components';
31
+ export { CustomCellsLayer } from './layers';
32
+ export { parseCustomCellsCsv, buildCellLookup, buildCustomCellTree, getGroupCounts } from './logic';