@smartnet360/svelte-components 0.0.129 → 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.
- package/dist/map-v3/demo/DemoMap.svelte +1 -6
- package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellFilterControl.svelte +11 -4
- package/dist/map-v3/features/custom/components/CustomCellSetManager.svelte +692 -0
- package/dist/map-v3/features/{cells/custom → custom}/index.d.ts +1 -1
- package/dist/map-v3/features/{cells/custom → custom}/index.js +1 -1
- package/dist/map-v3/features/custom/layers/CustomCellsLayer.svelte +399 -0
- package/dist/map-v3/features/custom/logic/csv-parser.d.ts +31 -0
- package/dist/map-v3/features/{cells/custom → custom}/logic/csv-parser.js +85 -26
- package/dist/map-v3/features/{cells/custom → custom}/logic/index.d.ts +1 -1
- package/dist/map-v3/features/{cells/custom → custom}/logic/tree-adapter.d.ts +1 -1
- package/dist/map-v3/features/{cells/custom → custom}/stores/custom-cell-sets.svelte.d.ts +4 -3
- package/dist/map-v3/features/{cells/custom → custom}/stores/custom-cell-sets.svelte.js +30 -10
- package/dist/map-v3/features/{cells/custom → custom}/types.d.ts +32 -12
- package/dist/map-v3/features/{cells/custom → custom}/types.js +5 -3
- package/dist/map-v3/index.d.ts +1 -1
- package/dist/map-v3/index.js +1 -1
- package/dist/map-v3/shared/controls/MapControl.svelte +43 -15
- package/dist/map-v3/shared/controls/MapControl.svelte.d.ts +3 -1
- package/package.json +1 -1
- package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte +0 -306
- package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte +0 -262
- package/dist/map-v3/features/cells/custom/logic/csv-parser.d.ts +0 -21
- /package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellFilterControl.svelte.d.ts +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellSetManager.svelte.d.ts +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/components/index.d.ts +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/components/index.js +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/layers/CustomCellsLayer.svelte.d.ts +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/layers/index.d.ts +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/layers/index.js +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/logic/index.js +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/logic/tree-adapter.js +0 -0
- /package/dist/map-v3/features/{cells/custom → custom}/stores/index.d.ts +0 -0
- /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>
|