@smartnet360/svelte-components 0.0.130 → 0.0.132
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/core/CellTable/CellHistoryDemo.svelte +106 -65
- package/dist/core/CellTable/column-config.d.ts +12 -0
- package/dist/core/CellTable/column-config.js +79 -2
- package/dist/core/CellTable/history-api-helper.d.ts +79 -0
- package/dist/core/CellTable/history-api-helper.js +83 -0
- package/dist/core/CellTable/index.d.ts +3 -2
- package/dist/core/CellTable/index.js +3 -1
- package/dist/core/CellTable/types.d.ts +26 -0
- 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/{cells/custom → custom}/logic/csv-parser.d.ts +11 -7
- package/dist/map-v3/features/{cells/custom → custom}/logic/csv-parser.js +64 -20
- 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 → 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.d.ts +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
|
@@ -43,15 +43,23 @@ export class CustomCellSetsStore {
|
|
|
43
43
|
this.save();
|
|
44
44
|
});
|
|
45
45
|
// Re-resolve cells when cell data changes (only works if getter is reactive)
|
|
46
|
+
// Track cell count to detect when cell data is loaded
|
|
47
|
+
let lastCellCount = 0;
|
|
46
48
|
$effect(() => {
|
|
47
49
|
const currentCells = this.getCells();
|
|
48
50
|
const cellCount = currentCells.length;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
// Only trigger when cell count changes (data loaded/updated)
|
|
52
|
+
if (cellCount !== lastCellCount && cellCount > 0) {
|
|
53
|
+
lastCellCount = cellCount;
|
|
54
|
+
// Use untrack to read sets without creating a dependency
|
|
55
|
+
const sets = $state.snapshot(this.sets);
|
|
56
|
+
if (sets.length > 0) {
|
|
57
|
+
// Check if any cell-type items need resolution
|
|
58
|
+
const needsResolution = sets.some(set => set.cells.some(c => c.geometry === 'cell' && !c.resolvedCell));
|
|
59
|
+
if (needsResolution) {
|
|
60
|
+
console.log('[CustomCellSetsStore] Re-resolving cells after data loaded');
|
|
61
|
+
this.refreshResolutions();
|
|
62
|
+
}
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
65
|
});
|
|
@@ -85,6 +93,7 @@ export class CustomCellSetsStore {
|
|
|
85
93
|
groups: importResult.groups,
|
|
86
94
|
extraColumns: importResult.extraColumns,
|
|
87
95
|
baseSize: 50,
|
|
96
|
+
pointSize: 8,
|
|
88
97
|
opacity: 0.7,
|
|
89
98
|
defaultColor: CUSTOM_CELL_PALETTE[0],
|
|
90
99
|
groupColors,
|
|
@@ -157,6 +166,8 @@ export class CustomCellSetsStore {
|
|
|
157
166
|
if (set) {
|
|
158
167
|
if (settings.baseSize !== undefined)
|
|
159
168
|
set.baseSize = settings.baseSize;
|
|
169
|
+
if (settings.pointSize !== undefined)
|
|
170
|
+
set.pointSize = settings.pointSize;
|
|
160
171
|
if (settings.opacity !== undefined)
|
|
161
172
|
set.opacity = settings.opacity;
|
|
162
173
|
if (settings.defaultColor !== undefined)
|
|
@@ -181,16 +192,22 @@ export class CustomCellSetsStore {
|
|
|
181
192
|
const set = this.getSet(setId);
|
|
182
193
|
if (!set || !set.visible)
|
|
183
194
|
return [];
|
|
184
|
-
return set.cells.filter(c =>
|
|
195
|
+
return set.cells.filter(c =>
|
|
196
|
+
// For cells: need resolved cell. For points: need lat/lon
|
|
197
|
+
(c.geometry === 'cell' ? c.resolvedCell : (c.lat !== undefined && c.lon !== undefined)) &&
|
|
198
|
+
set.visibleGroups.has(c.customGroup));
|
|
185
199
|
}
|
|
186
200
|
/**
|
|
187
201
|
* Re-resolve cells after main cell data changes
|
|
202
|
+
* Only affects items with 'cell' geometry
|
|
188
203
|
*/
|
|
189
204
|
refreshResolutions() {
|
|
190
205
|
const cellLookup = buildCellLookup(this.getCells());
|
|
191
206
|
for (const set of this.sets) {
|
|
192
207
|
for (const cell of set.cells) {
|
|
193
|
-
cell.
|
|
208
|
+
if (cell.geometry === 'cell') {
|
|
209
|
+
cell.resolvedCell = cellLookup.get(cell.id);
|
|
210
|
+
}
|
|
194
211
|
}
|
|
195
212
|
}
|
|
196
213
|
this.version++;
|
|
@@ -228,10 +245,13 @@ export class CustomCellSetsStore {
|
|
|
228
245
|
visibleGroups: Array.from(set.visibleGroups),
|
|
229
246
|
// Don't persist resolved cells - will re-resolve on load
|
|
230
247
|
cells: set.cells.map(c => ({
|
|
231
|
-
|
|
248
|
+
id: c.id,
|
|
232
249
|
customGroup: c.customGroup,
|
|
233
250
|
sizeFactor: c.sizeFactor,
|
|
234
|
-
extraFields: c.extraFields
|
|
251
|
+
extraFields: c.extraFields,
|
|
252
|
+
geometry: c.geometry,
|
|
253
|
+
// Persist lat/lon for point geometry
|
|
254
|
+
...(c.geometry === 'point' ? { lat: c.lat, lon: c.lon } : {})
|
|
235
255
|
}))
|
|
236
256
|
}));
|
|
237
257
|
localStorage.setItem(this.storageKey, JSON.stringify(data));
|
|
@@ -1,32 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Custom
|
|
2
|
+
* Custom Feature - Type Definitions
|
|
3
3
|
*
|
|
4
|
-
* Types for ad-hoc
|
|
5
|
-
*
|
|
4
|
+
* Types for ad-hoc layers loaded from CSV files.
|
|
5
|
+
* Supports two geometry types:
|
|
6
|
+
* - 'cell': References existing cells by id (renders as sector arcs)
|
|
7
|
+
* - 'point': Standalone lat/lon coordinates (renders as circles)
|
|
6
8
|
*/
|
|
7
|
-
import type { Cell } from '
|
|
9
|
+
import type { Cell } from '../../../shared/demo';
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
11
|
+
* Geometry type for custom items
|
|
12
|
+
* - 'cell': Sector arc using resolved cell's azimuth/beamwidth
|
|
13
|
+
* - 'point': Simple circle at lat/lon coordinates
|
|
14
|
+
*/
|
|
15
|
+
export type CustomItemGeometry = 'cell' | 'point';
|
|
16
|
+
/**
|
|
17
|
+
* A custom item entry from CSV import
|
|
10
18
|
*/
|
|
11
19
|
export interface CustomCell {
|
|
12
|
-
/**
|
|
13
|
-
|
|
20
|
+
/** Generic identifier (cellName, txId, or any unique ID) */
|
|
21
|
+
id: string;
|
|
14
22
|
/** Grouping field for TreeView (defaults to 'default' if not in CSV) */
|
|
15
23
|
customGroup: string;
|
|
16
|
-
/** Size multiplier for
|
|
24
|
+
/** Size multiplier for radius (defaults to 1) */
|
|
17
25
|
sizeFactor: number;
|
|
18
26
|
/** Any additional columns from CSV for tooltips */
|
|
19
27
|
extraFields: Record<string, string | number>;
|
|
20
|
-
/**
|
|
28
|
+
/** Geometry type: 'cell' for sector arcs, 'point' for circles */
|
|
29
|
+
geometry: CustomItemGeometry;
|
|
30
|
+
/** Resolved cell data from main store (only for 'cell' geometry) */
|
|
21
31
|
resolvedCell?: Cell;
|
|
32
|
+
/** Latitude coordinate (only for 'point' geometry) */
|
|
33
|
+
lat?: number;
|
|
34
|
+
/** Longitude coordinate (only for 'point' geometry) */
|
|
35
|
+
lon?: number;
|
|
22
36
|
}
|
|
23
37
|
/**
|
|
24
38
|
* Import result from CSV parsing
|
|
25
39
|
*/
|
|
26
40
|
export interface CustomCellImportResult {
|
|
27
|
-
/** Successfully parsed and
|
|
41
|
+
/** Successfully parsed items (cells and/or points) */
|
|
28
42
|
cells: CustomCell[];
|
|
29
|
-
/**
|
|
43
|
+
/** IDs that weren't found in main data store (for cell geometry only) */
|
|
30
44
|
unmatchedTxIds: string[];
|
|
31
45
|
/** Discovered custom groups */
|
|
32
46
|
groups: string[];
|
|
@@ -34,6 +48,10 @@ export interface CustomCellImportResult {
|
|
|
34
48
|
extraColumns: string[];
|
|
35
49
|
/** Total rows in CSV */
|
|
36
50
|
totalRows: number;
|
|
51
|
+
/** Count of cell geometry items */
|
|
52
|
+
cellCount: number;
|
|
53
|
+
/** Count of point geometry items */
|
|
54
|
+
pointCount: number;
|
|
37
55
|
}
|
|
38
56
|
/**
|
|
39
57
|
* A custom cell set - represents one uploaded CSV
|
|
@@ -51,8 +69,10 @@ export interface CustomCellSet {
|
|
|
51
69
|
groups: string[];
|
|
52
70
|
/** Extra column names for tooltip display */
|
|
53
71
|
extraColumns: string[];
|
|
54
|
-
/** Base arc radius in pixels (multiplied by sizeFactor per cell) */
|
|
72
|
+
/** Base arc radius in pixels for sector cells (multiplied by sizeFactor per cell) */
|
|
55
73
|
baseSize: number;
|
|
74
|
+
/** Circle radius in pixels for point geometry */
|
|
75
|
+
pointSize: number;
|
|
56
76
|
/** Layer opacity (0-1) */
|
|
57
77
|
opacity: number;
|
|
58
78
|
/** Default color for groups without override */
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Custom
|
|
2
|
+
* Custom Feature - Type Definitions
|
|
3
3
|
*
|
|
4
|
-
* Types for ad-hoc
|
|
5
|
-
*
|
|
4
|
+
* Types for ad-hoc layers loaded from CSV files.
|
|
5
|
+
* Supports two geometry types:
|
|
6
|
+
* - 'cell': References existing cells by id (renders as sector arcs)
|
|
7
|
+
* - 'point': Standalone lat/lon coordinates (renders as circles)
|
|
6
8
|
*/
|
|
7
9
|
/**
|
|
8
10
|
* Default color palette for custom cell groups
|
package/dist/map-v3/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { default as CellSettingsPanel } from './features/cells/components/CellSe
|
|
|
8
8
|
export * from './features/cells/stores/cell.data.svelte';
|
|
9
9
|
export * from './features/cells/stores/cell.display.svelte';
|
|
10
10
|
export * from './features/cells/stores/cell.registry.svelte';
|
|
11
|
-
export * as CustomCells from './features/
|
|
11
|
+
export * as CustomCells from './features/custom';
|
|
12
12
|
export * from './features/repeaters/types';
|
|
13
13
|
export { default as RepeatersLayer } from './features/repeaters/layers/RepeatersLayer.svelte';
|
|
14
14
|
export { default as RepeaterLabelsLayer } from './features/repeaters/layers/RepeaterLabelsLayer.svelte';
|
package/dist/map-v3/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export * from './features/cells/stores/cell.data.svelte';
|
|
|
12
12
|
export * from './features/cells/stores/cell.display.svelte';
|
|
13
13
|
export * from './features/cells/stores/cell.registry.svelte';
|
|
14
14
|
// Features - Cells Custom
|
|
15
|
-
export * as CustomCells from './features/
|
|
15
|
+
export * as CustomCells from './features/custom';
|
|
16
16
|
// Features - Repeaters
|
|
17
17
|
export * from './features/repeaters/types';
|
|
18
18
|
export { default as RepeatersLayer } from './features/repeaters/layers/RepeatersLayer.svelte';
|
|
@@ -28,8 +28,10 @@
|
|
|
28
28
|
class?: string;
|
|
29
29
|
/** Child content */
|
|
30
30
|
children?: import('svelte').Snippet;
|
|
31
|
-
/** Optional action buttons in header */
|
|
31
|
+
/** Optional action buttons in header (shown when expanded) */
|
|
32
32
|
actions?: import('svelte').Snippet;
|
|
33
|
+
/** Optional action buttons shown when collapsed */
|
|
34
|
+
collapsedActions?: import('svelte').Snippet;
|
|
33
35
|
/** Optional offset from map edge (e.g., '12px') */
|
|
34
36
|
edgeOffset?: string;
|
|
35
37
|
/** Width of the map control (e.g., '360px') */
|
|
@@ -47,6 +49,7 @@
|
|
|
47
49
|
class: className = '',
|
|
48
50
|
children,
|
|
49
51
|
actions,
|
|
52
|
+
collapsedActions,
|
|
50
53
|
edgeOffset = '12px',
|
|
51
54
|
controlWidth = '420px'
|
|
52
55
|
}: Props = $props();
|
|
@@ -139,7 +142,15 @@
|
|
|
139
142
|
data-edge-offset={edgeOffset}
|
|
140
143
|
>
|
|
141
144
|
{#if title || icon}
|
|
142
|
-
<div
|
|
145
|
+
<div
|
|
146
|
+
class="map-control-header"
|
|
147
|
+
class:clickable={collapsed && collapsible}
|
|
148
|
+
title={title}
|
|
149
|
+
onclick={collapsed && collapsible ? toggleCollapse : undefined}
|
|
150
|
+
role={collapsed && collapsible ? 'button' : undefined}
|
|
151
|
+
tabindex={collapsed && collapsible ? 0 : undefined}
|
|
152
|
+
onkeydown={collapsed && collapsible ? (e) => e.key === 'Enter' && toggleCollapse() : undefined}
|
|
153
|
+
>
|
|
143
154
|
<span class="map-control-title">
|
|
144
155
|
{#if collapsed && iconOnlyWhenCollapsed && icon}
|
|
145
156
|
<i class="bi bi-{icon}" aria-hidden="true"></i>
|
|
@@ -154,19 +165,27 @@
|
|
|
154
165
|
{/if}
|
|
155
166
|
{/if}
|
|
156
167
|
</span>
|
|
157
|
-
<div class="map-control-actions">
|
|
158
|
-
{#if
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
<div class="map-control-actions" onclick={(e) => e.stopPropagation()}>
|
|
169
|
+
{#if collapsed}
|
|
170
|
+
<!-- When collapsed: show only collapsedActions -->
|
|
171
|
+
{#if collapsedActions}
|
|
172
|
+
{@render collapsedActions()}
|
|
173
|
+
{/if}
|
|
174
|
+
{:else}
|
|
175
|
+
<!-- When expanded: show actions + close button -->
|
|
176
|
+
{#if actions}
|
|
177
|
+
{@render actions()}
|
|
178
|
+
{/if}
|
|
179
|
+
{#if collapsible}
|
|
180
|
+
<button
|
|
181
|
+
class="map-control-toggle"
|
|
182
|
+
onclick={toggleCollapse}
|
|
183
|
+
aria-label="Close"
|
|
184
|
+
title="Close"
|
|
185
|
+
>
|
|
186
|
+
<i class="bi bi-x-lg"></i>
|
|
187
|
+
</button>
|
|
188
|
+
{/if}
|
|
170
189
|
{/if}
|
|
171
190
|
</div>
|
|
172
191
|
</div>
|
|
@@ -206,6 +225,15 @@
|
|
|
206
225
|
color: #212529;
|
|
207
226
|
}
|
|
208
227
|
|
|
228
|
+
.map-control-header.clickable {
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
transition: background-color 0.15s ease;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.map-control-header.clickable:hover {
|
|
234
|
+
background: #e9ecef;
|
|
235
|
+
}
|
|
236
|
+
|
|
209
237
|
.map-control-title {
|
|
210
238
|
flex: 1;
|
|
211
239
|
user-select: none;
|
|
@@ -17,8 +17,10 @@ interface Props {
|
|
|
17
17
|
class?: string;
|
|
18
18
|
/** Child content */
|
|
19
19
|
children?: import('svelte').Snippet;
|
|
20
|
-
/** Optional action buttons in header */
|
|
20
|
+
/** Optional action buttons in header (shown when expanded) */
|
|
21
21
|
actions?: import('svelte').Snippet;
|
|
22
|
+
/** Optional action buttons shown when collapsed */
|
|
23
|
+
collapsedActions?: import('svelte').Snippet;
|
|
22
24
|
/** Optional offset from map edge (e.g., '12px') */
|
|
23
25
|
edgeOffset?: string;
|
|
24
26
|
/** Width of the map control (e.g., '360px') */
|
package/package.json
CHANGED
|
@@ -1,306 +0,0 @@
|
|
|
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}
|