@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.
- package/dist/apps/antenna-tools/components/AntennaSettingsModal.svelte +4 -174
- package/dist/apps/antenna-tools/components/DatabaseViewer.svelte +2 -2
- package/dist/apps/antenna-tools/components/MSIConverter.svelte +302 -43
- package/dist/apps/antenna-tools/db.js +4 -0
- package/dist/apps/antenna-tools/utils/db-utils.d.ts +19 -0
- package/dist/apps/antenna-tools/utils/db-utils.js +108 -0
- package/dist/core/Auth/LoginForm.svelte +397 -0
- package/dist/core/Auth/LoginForm.svelte.d.ts +16 -0
- package/dist/core/Auth/auth.svelte.d.ts +22 -0
- package/dist/core/Auth/auth.svelte.js +229 -0
- package/dist/core/Auth/config.d.ts +25 -0
- package/dist/core/Auth/config.js +256 -0
- package/dist/core/Auth/index.d.ts +4 -0
- package/dist/core/Auth/index.js +5 -0
- package/dist/core/Auth/types.d.ts +140 -0
- package/dist/core/Auth/types.js +2 -0
- package/dist/core/LandingPage/App.svelte +102 -0
- package/dist/core/LandingPage/App.svelte.d.ts +20 -0
- package/dist/core/LandingPage/LandingPage.svelte +480 -0
- package/dist/core/LandingPage/LandingPage.svelte.d.ts +21 -0
- package/dist/core/LandingPage/index.d.ts +2 -0
- package/dist/core/LandingPage/index.js +3 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +4 -0
- package/dist/map-v3/demo/DemoMap.svelte +18 -0
- package/dist/map-v3/demo/demo-custom-cells.d.ts +21 -0
- package/dist/map-v3/demo/demo-custom-cells.js +48 -0
- package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte +220 -0
- package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte.d.ts +15 -0
- package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte +306 -0
- package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte.d.ts +10 -0
- package/dist/map-v3/features/cells/custom/components/index.d.ts +5 -0
- package/dist/map-v3/features/cells/custom/components/index.js +5 -0
- package/dist/map-v3/features/cells/custom/index.d.ts +32 -0
- package/dist/map-v3/features/cells/custom/index.js +35 -0
- package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte +262 -0
- package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte.d.ts +10 -0
- package/dist/map-v3/features/cells/custom/layers/index.d.ts +4 -0
- package/dist/map-v3/features/cells/custom/layers/index.js +4 -0
- package/dist/map-v3/features/cells/custom/logic/csv-parser.d.ts +20 -0
- package/dist/map-v3/features/cells/custom/logic/csv-parser.js +164 -0
- package/dist/map-v3/features/cells/custom/logic/index.d.ts +5 -0
- package/dist/map-v3/features/cells/custom/logic/index.js +5 -0
- package/dist/map-v3/features/cells/custom/logic/tree-adapter.d.ts +24 -0
- package/dist/map-v3/features/cells/custom/logic/tree-adapter.js +67 -0
- package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.d.ts +78 -0
- package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.js +242 -0
- package/dist/map-v3/features/cells/custom/stores/index.d.ts +4 -0
- package/dist/map-v3/features/cells/custom/stores/index.js +4 -0
- package/dist/map-v3/features/cells/custom/types.d.ts +83 -0
- package/dist/map-v3/features/cells/custom/types.js +23 -0
- package/dist/map-v3/shared/controls/MapControl.svelte +27 -3
- 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,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';
|