@smartnet360/svelte-components 0.0.125 → 0.0.127

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 (48) hide show
  1. package/dist/core/Auth/auth.svelte.js +47 -2
  2. package/dist/map-v3/demo/DemoMap.svelte +36 -0
  3. package/dist/map-v3/demo/demo-custom-cells.d.ts +21 -0
  4. package/dist/map-v3/demo/demo-custom-cells.js +48 -0
  5. package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte +220 -0
  6. package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte.d.ts +15 -0
  7. package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte +306 -0
  8. package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte.d.ts +10 -0
  9. package/dist/map-v3/features/cells/custom/components/index.d.ts +5 -0
  10. package/dist/map-v3/features/cells/custom/components/index.js +5 -0
  11. package/dist/map-v3/features/cells/custom/index.d.ts +32 -0
  12. package/dist/map-v3/features/cells/custom/index.js +35 -0
  13. package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte +262 -0
  14. package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte.d.ts +10 -0
  15. package/dist/map-v3/features/cells/custom/layers/index.d.ts +4 -0
  16. package/dist/map-v3/features/cells/custom/layers/index.js +4 -0
  17. package/dist/map-v3/features/cells/custom/logic/csv-parser.d.ts +20 -0
  18. package/dist/map-v3/features/cells/custom/logic/csv-parser.js +164 -0
  19. package/dist/map-v3/features/cells/custom/logic/index.d.ts +5 -0
  20. package/dist/map-v3/features/cells/custom/logic/index.js +5 -0
  21. package/dist/map-v3/features/cells/custom/logic/tree-adapter.d.ts +24 -0
  22. package/dist/map-v3/features/cells/custom/logic/tree-adapter.js +67 -0
  23. package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.d.ts +78 -0
  24. package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.js +242 -0
  25. package/dist/map-v3/features/cells/custom/stores/index.d.ts +4 -0
  26. package/dist/map-v3/features/cells/custom/stores/index.js +4 -0
  27. package/dist/map-v3/features/cells/custom/types.d.ts +83 -0
  28. package/dist/map-v3/features/cells/custom/types.js +23 -0
  29. package/dist/map-v3/features/sites/custom/components/CustomSiteFilterControl.svelte +203 -0
  30. package/dist/map-v3/features/sites/custom/components/CustomSiteFilterControl.svelte.d.ts +15 -0
  31. package/dist/map-v3/features/sites/custom/components/CustomSiteSetManager.svelte +261 -0
  32. package/dist/map-v3/features/sites/custom/components/CustomSiteSetManager.svelte.d.ts +10 -0
  33. package/dist/map-v3/features/sites/custom/index.d.ts +13 -0
  34. package/dist/map-v3/features/sites/custom/index.js +16 -0
  35. package/dist/map-v3/features/sites/custom/layers/CustomSitesLayer.svelte +201 -0
  36. package/dist/map-v3/features/sites/custom/layers/CustomSitesLayer.svelte.d.ts +8 -0
  37. package/dist/map-v3/features/sites/custom/logic/csv-parser.d.ts +12 -0
  38. package/dist/map-v3/features/sites/custom/logic/csv-parser.js +182 -0
  39. package/dist/map-v3/features/sites/custom/logic/tree-adapter.d.ts +16 -0
  40. package/dist/map-v3/features/sites/custom/logic/tree-adapter.js +59 -0
  41. package/dist/map-v3/features/sites/custom/stores/custom-site-sets.svelte.d.ts +78 -0
  42. package/dist/map-v3/features/sites/custom/stores/custom-site-sets.svelte.js +248 -0
  43. package/dist/map-v3/features/sites/custom/types.d.ts +74 -0
  44. package/dist/map-v3/features/sites/custom/types.js +8 -0
  45. package/dist/map-v3/index.d.ts +2 -0
  46. package/dist/map-v3/index.js +4 -0
  47. package/dist/map-v3/shared/controls/MapControl.svelte +27 -3
  48. package/package.json +1 -1
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Custom Cells Feature - Type Definitions
3
+ *
4
+ * Types for ad-hoc cell layers loaded from CSV files.
5
+ * Custom cells reference existing cells by txId and add custom grouping/sizing.
6
+ */
7
+ import type { Cell } from '../../../../shared/demo';
8
+ /**
9
+ * A custom cell entry from CSV import
10
+ */
11
+ export interface CustomCell {
12
+ /** Required: Reference to cell in main data store */
13
+ txId: string;
14
+ /** Grouping field for TreeView (defaults to 'default' if not in CSV) */
15
+ customGroup: string;
16
+ /** Size multiplier for arc radius (defaults to 1) */
17
+ sizeFactor: number;
18
+ /** Any additional columns from CSV for tooltips */
19
+ extraFields: Record<string, string | number>;
20
+ /** Resolved cell data from main store (populated after import) */
21
+ resolvedCell?: Cell;
22
+ }
23
+ /**
24
+ * Import result from CSV parsing
25
+ */
26
+ export interface CustomCellImportResult {
27
+ /** Successfully parsed and matched cells */
28
+ cells: CustomCell[];
29
+ /** Cell IDs that weren't found in main data store */
30
+ unmatchedTxIds: string[];
31
+ /** Discovered custom groups */
32
+ groups: string[];
33
+ /** Extra column names found in CSV */
34
+ extraColumns: string[];
35
+ /** Total rows in CSV */
36
+ totalRows: number;
37
+ }
38
+ /**
39
+ * A custom cell set - represents one uploaded CSV
40
+ */
41
+ export interface CustomCellSet {
42
+ /** Unique identifier for this set */
43
+ id: string;
44
+ /** Display name (from filename or user input) */
45
+ name: string;
46
+ /** Cells in this set with resolved data */
47
+ cells: CustomCell[];
48
+ /** Cell IDs that weren't found during import */
49
+ unmatchedTxIds: string[];
50
+ /** All custom groups in this set */
51
+ groups: string[];
52
+ /** Extra column names for tooltip display */
53
+ extraColumns: string[];
54
+ /** Base arc radius in pixels (multiplied by sizeFactor per cell) */
55
+ baseSize: number;
56
+ /** Layer opacity (0-1) */
57
+ opacity: number;
58
+ /** Default color for groups without override */
59
+ defaultColor: string;
60
+ /** Per-group color overrides */
61
+ groupColors: Record<string, string>;
62
+ /** Whether this set's layer is visible */
63
+ visible: boolean;
64
+ /** Which groups are currently visible */
65
+ visibleGroups: Set<string>;
66
+ }
67
+ /**
68
+ * Configuration for creating a new custom cell set
69
+ */
70
+ export interface CustomCellSetConfig {
71
+ name: string;
72
+ cells: CustomCell[];
73
+ unmatchedTxIds?: string[];
74
+ groups?: string[];
75
+ extraColumns?: string[];
76
+ baseSize?: number;
77
+ opacity?: number;
78
+ defaultColor?: string;
79
+ }
80
+ /**
81
+ * Default color palette for custom cell groups
82
+ */
83
+ export declare const CUSTOM_CELL_PALETTE: string[];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Custom Cells Feature - Type Definitions
3
+ *
4
+ * Types for ad-hoc cell layers loaded from CSV files.
5
+ * Custom cells reference existing cells by txId and add custom grouping/sizing.
6
+ */
7
+ /**
8
+ * Default color palette for custom cell groups
9
+ */
10
+ export const CUSTOM_CELL_PALETTE = [
11
+ '#e41a1c', // Red
12
+ '#377eb8', // Blue
13
+ '#4daf4a', // Green
14
+ '#984ea3', // Purple
15
+ '#ff7f00', // Orange
16
+ '#ffff33', // Yellow
17
+ '#a65628', // Brown
18
+ '#f781bf', // Pink
19
+ '#999999', // Gray
20
+ '#66c2a5', // Teal
21
+ '#fc8d62', // Coral
22
+ '#8da0cb', // Periwinkle
23
+ ];
@@ -0,0 +1,203 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Custom Site Filter Control
4
+ *
5
+ * TreeView with color pickers for a single custom site set.
6
+ * Shows groups with their site 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 { CustomSiteSetsStore } from '../stores/custom-site-sets.svelte';
12
+ import type { CustomSiteSet } from '../types';
13
+ import { buildCustomSiteTree } from '../logic/tree-adapter';
14
+
15
+ interface Props {
16
+ /** The custom site sets store */
17
+ setsStore: CustomSiteSetsStore;
18
+ /** The specific set to display */
19
+ set: CustomSiteSet;
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
+ onDestroy(() => {
34
+ console.log(`[CustomSiteFilterControl] onDestroy called for set: ${set.id}`);
35
+ });
36
+
37
+ // Build tree from set
38
+ let treeStore = $derived.by(() => {
39
+ const _version = setsStore.version;
40
+ const _set = set;
41
+
42
+ return untrack(() => {
43
+ const nodes = buildCustomSiteTree(_set);
44
+ return createTreeStore({
45
+ nodes,
46
+ namespace: `custom-sites:${_set.id}`,
47
+ persistState: true,
48
+ defaultExpandAll: true
49
+ });
50
+ });
51
+ });
52
+
53
+ // Sync tree selection -> store visibility
54
+ $effect(() => {
55
+ const val = treeStore;
56
+ let changes = 0;
57
+
58
+ val.state.nodes.forEach((nodeState) => {
59
+ // Skip root node
60
+ if (nodeState.node.id === `root-${set.id}`) return;
61
+ // Skip folder nodes
62
+ if (nodeState.node.children && nodeState.node.children.length > 0) return;
63
+
64
+ const groupId = nodeState.node.metadata?.groupId;
65
+ if (!groupId) return;
66
+
67
+ const isVisible = val.state.checkedPaths.has(nodeState.path);
68
+ const currentlyVisible = set.visibleGroups.has(groupId);
69
+
70
+ if (isVisible !== currentlyVisible) {
71
+ setsStore.toggleGroupVisibility(set.id, groupId);
72
+ changes++;
73
+ }
74
+ });
75
+
76
+ if (changes > 0) {
77
+ console.log(`[CustomSiteFilterControl] Synced ${changes} visibility changes`);
78
+ }
79
+ });
80
+
81
+ function handleColorChange(groupId: string, event: Event) {
82
+ const input = event.target as HTMLInputElement;
83
+ setsStore.setGroupColor(set.id, groupId, input.value);
84
+ }
85
+
86
+ function handleRemove() {
87
+ if (onremove) {
88
+ onremove(set.id);
89
+ }
90
+ }
91
+
92
+ function handleToggleVisibility() {
93
+ setsStore.toggleSetVisibility(set.id);
94
+ }
95
+ </script>
96
+
97
+ <MapControl {position} title={set.name} icon="geo-alt" controlWidth="280px">
98
+ {#snippet actions()}
99
+ <button
100
+ class="btn btn-sm btn-outline-secondary border-0 p-1 px-2"
101
+ title={set.visible ? 'Hide Layer' : 'Show Layer'}
102
+ aria-label={set.visible ? 'Hide Layer' : 'Show Layer'}
103
+ onclick={handleToggleVisibility}
104
+ >
105
+ <i class="bi bi-eye{set.visible ? '-fill' : '-slash'}"></i>
106
+ </button>
107
+ <button
108
+ class="btn btn-sm btn-outline-danger border-0 p-1 px-2"
109
+ title="Remove Set"
110
+ aria-label="Remove Set"
111
+ onclick={handleRemove}
112
+ >
113
+ <i class="bi bi-trash"></i>
114
+ </button>
115
+ {/snippet}
116
+
117
+ <div class="custom-site-filter-control">
118
+ <!-- Set Info -->
119
+ <div class="set-info mb-2 px-1">
120
+ <small class="text-muted">
121
+ {set.sites.length} sites in {set.groups.length} groups
122
+ </small>
123
+ </div>
124
+
125
+ <!-- Size Slider -->
126
+ <div class="size-control mb-2 px-1">
127
+ <label for="baseSize-{set.id}" class="form-label small mb-1">
128
+ Base Size: {set.baseSize}px
129
+ </label>
130
+ <input
131
+ type="range"
132
+ class="form-range"
133
+ id="baseSize-{set.id}"
134
+ min="2"
135
+ max="30"
136
+ step="1"
137
+ value={set.baseSize}
138
+ oninput={(e) => setsStore.setBaseSize(set.id, parseInt((e.target as HTMLInputElement).value))}
139
+ />
140
+ </div>
141
+
142
+ <!-- Opacity Slider -->
143
+ <div class="opacity-control mb-2 px-1">
144
+ <label for="opacity-{set.id}" class="form-label small mb-1">
145
+ Opacity: {Math.round(set.opacity * 100)}%
146
+ </label>
147
+ <input
148
+ type="range"
149
+ class="form-range"
150
+ id="opacity-{set.id}"
151
+ min="0.1"
152
+ max="1"
153
+ step="0.1"
154
+ value={set.opacity}
155
+ oninput={(e) => setsStore.setOpacity(set.id, parseFloat((e.target as HTMLInputElement).value))}
156
+ />
157
+ </div>
158
+
159
+ <!-- Group Color Pickers -->
160
+ <div class="group-colors mb-2">
161
+ <div class="small text-muted mb-1 px-1">Group Colors:</div>
162
+ {#each set.groups as group}
163
+ <div class="d-flex align-items-center gap-2 px-1 py-1">
164
+ <input
165
+ type="color"
166
+ class="form-control form-control-color"
167
+ value={set.groupColors.get(group) || '#666666'}
168
+ onchange={(e) => handleColorChange(group, e)}
169
+ title="Color for {group}"
170
+ style="width: 28px; height: 28px; padding: 2px;"
171
+ />
172
+ <span class="small flex-grow-1 text-truncate">{group}</span>
173
+ <span class="badge bg-secondary">
174
+ {set.sites.filter(s => s.customGroup === group).length}
175
+ </span>
176
+ </div>
177
+ {/each}
178
+ </div>
179
+
180
+ <!-- TreeView -->
181
+ <div class="tree-container">
182
+ <TreeView store={treeStore} />
183
+ </div>
184
+ </div>
185
+ </MapControl>
186
+
187
+ <style>
188
+ .custom-site-filter-control {
189
+ max-height: 400px;
190
+ overflow-y: auto;
191
+ }
192
+
193
+ .tree-container {
194
+ max-height: 200px;
195
+ overflow-y: auto;
196
+ border-top: 1px solid var(--bs-border-color);
197
+ padding-top: 0.5rem;
198
+ }
199
+
200
+ .form-control-color {
201
+ cursor: pointer;
202
+ }
203
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { CustomSiteSetsStore } from '../stores/custom-site-sets.svelte';
2
+ import type { CustomSiteSet } from '../types';
3
+ interface Props {
4
+ /** The custom site sets store */
5
+ setsStore: CustomSiteSetsStore;
6
+ /** The specific set to display */
7
+ set: CustomSiteSet;
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 CustomSiteFilterControl: import("svelte").Component<Props, {}, "">;
14
+ type CustomSiteFilterControl = ReturnType<typeof CustomSiteFilterControl>;
15
+ export default CustomSiteFilterControl;
@@ -0,0 +1,261 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Custom Site Set Manager
4
+ *
5
+ * Main control panel for managing custom site sets.
6
+ * Provides CSV upload, set listing, and integrates filter controls.
7
+ */
8
+ import { MapControl } from '../../../../shared';
9
+ import { CustomSiteSetsStore } from '../stores/custom-site-sets.svelte';
10
+ import CustomSiteFilterControl from './CustomSiteFilterControl.svelte';
11
+ import type { CustomSiteImportResult } from '../types';
12
+
13
+ interface Props {
14
+ /** Control position on map */
15
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
16
+ /** Optional external store (creates internal one if not provided) */
17
+ setsStore?: CustomSiteSetsStore;
18
+ }
19
+
20
+ let {
21
+ position = 'top-left',
22
+ setsStore: externalStore
23
+ }: Props = $props();
24
+
25
+ // Use external store or create internal one
26
+ const setsStore = externalStore ?? new CustomSiteSetsStore();
27
+
28
+ // Reactive array for iteration
29
+ let setsArray = $derived(setsStore.sets);
30
+
31
+ // Modal state
32
+ let showImportModal = $state(false);
33
+ let importResult = $state<(CustomSiteImportResult & { setId?: string }) | null>(null);
34
+ let selectedFile = $state<File | null>(null);
35
+ let isImporting = $state(false);
36
+
37
+ function handleFileSelect(event: Event) {
38
+ const input = event.target as HTMLInputElement;
39
+ selectedFile = input.files?.[0] ?? null;
40
+ importResult = null;
41
+ }
42
+
43
+ async function handleImport() {
44
+ if (!selectedFile) return;
45
+
46
+ isImporting = true;
47
+ importResult = null;
48
+
49
+ try {
50
+ const content = await selectedFile.text();
51
+ importResult = setsStore.importFromCsv(content, selectedFile.name);
52
+
53
+ if (importResult.setId) {
54
+ // Success - close modal after a short delay to show results
55
+ setTimeout(() => {
56
+ showImportModal = false;
57
+ selectedFile = null;
58
+ importResult = null;
59
+ }, 1500);
60
+ }
61
+ } catch (e) {
62
+ importResult = {
63
+ sites: [],
64
+ groups: [],
65
+ invalidRows: 0,
66
+ errors: [`Failed to read file: ${e}`]
67
+ };
68
+ } finally {
69
+ isImporting = false;
70
+ }
71
+ }
72
+
73
+ function handleCloseModal() {
74
+ showImportModal = false;
75
+ selectedFile = null;
76
+ importResult = null;
77
+ }
78
+
79
+ function handleRemoveSet(setId: string) {
80
+ setsStore.removeSet(setId);
81
+ }
82
+ </script>
83
+
84
+ <MapControl {position} title="Custom Sites" icon="geo-alt-fill" controlWidth="300px">
85
+ {#snippet actions()}
86
+ <button
87
+ class="btn btn-sm btn-outline-primary border-0 p-1 px-2"
88
+ title="Import CSV"
89
+ aria-label="Import CSV"
90
+ onclick={() => showImportModal = true}
91
+ >
92
+ <i class="bi bi-upload"></i>
93
+ </button>
94
+ {/snippet}
95
+
96
+ <div class="custom-site-manager">
97
+ {#if setsArray.length === 0}
98
+ <div class="text-center text-muted py-3">
99
+ <i class="bi bi-geo-alt fs-3 d-block mb-2"></i>
100
+ <p class="small mb-2">No custom sites loaded</p>
101
+ <button
102
+ class="btn btn-sm btn-outline-primary"
103
+ onclick={() => showImportModal = true}
104
+ >
105
+ <i class="bi bi-upload me-1"></i>
106
+ Import CSV
107
+ </button>
108
+ </div>
109
+ {:else}
110
+ <ul class="list-group list-group-flush">
111
+ {#each setsArray as set (set.id)}
112
+ <li class="list-group-item d-flex justify-content-between align-items-center px-2 py-2">
113
+ <div class="d-flex align-items-center gap-2 flex-grow-1 min-w-0">
114
+ <button
115
+ class="btn btn-sm p-0 border-0"
116
+ title={set.visible ? 'Hide' : 'Show'}
117
+ aria-label={set.visible ? 'Hide' : 'Show'}
118
+ onclick={() => setsStore.toggleSetVisibility(set.id)}
119
+ >
120
+ <i class="bi bi-eye{set.visible ? '-fill text-primary' : '-slash text-muted'}"></i>
121
+ </button>
122
+ <span class="text-truncate small" title={set.name}>
123
+ {set.name}
124
+ </span>
125
+ </div>
126
+ <div class="d-flex align-items-center gap-1">
127
+ <span class="badge bg-secondary small">
128
+ {set.sites.length}
129
+ </span>
130
+ <button
131
+ class="btn btn-sm btn-outline-danger border-0 p-1"
132
+ title="Remove"
133
+ aria-label="Remove"
134
+ onclick={() => handleRemoveSet(set.id)}
135
+ >
136
+ <i class="bi bi-x"></i>
137
+ </button>
138
+ </div>
139
+ </li>
140
+ {/each}
141
+ </ul>
142
+ {/if}
143
+
144
+ <!-- CSV Format Help -->
145
+ <div class="format-help mt-2 px-2">
146
+ <details class="small">
147
+ <summary class="text-muted">CSV Format</summary>
148
+ <div class="mt-1 text-muted" style="font-size: 0.75rem;">
149
+ <strong>Required:</strong> id, lat, lon<br>
150
+ <strong>Optional:</strong> customGroup, sizeFactor<br>
151
+ <em>Extra columns available in tooltips</em>
152
+ </div>
153
+ </details>
154
+ </div>
155
+ </div>
156
+ </MapControl>
157
+
158
+ <!-- Import Modal -->
159
+ {#if showImportModal}
160
+ <div class="modal show d-block" tabindex="-1" role="dialog">
161
+ <div class="modal-dialog modal-dialog-centered">
162
+ <div class="modal-content">
163
+ <div class="modal-header">
164
+ <h5 class="modal-title">
165
+ <i class="bi bi-upload me-2"></i>
166
+ Import Custom Sites
167
+ </h5>
168
+ <button type="button" class="btn-close" aria-label="Close" onclick={handleCloseModal}></button>
169
+ </div>
170
+ <div class="modal-body">
171
+ <!-- File Input -->
172
+ <div class="mb-3">
173
+ <label for="csvFile" class="form-label">Select CSV File</label>
174
+ <input
175
+ type="file"
176
+ class="form-control"
177
+ id="csvFile"
178
+ accept=".csv"
179
+ onchange={handleFileSelect}
180
+ />
181
+ </div>
182
+
183
+ <!-- Format Info -->
184
+ <div class="alert alert-info small py-2">
185
+ <strong>Required columns:</strong> id, lat, lon<br>
186
+ <strong>Optional:</strong> customGroup (or subgroup), sizeFactor
187
+ </div>
188
+
189
+ <!-- Import Result -->
190
+ {#if importResult}
191
+ {#if importResult.setId}
192
+ <div class="alert alert-success py-2">
193
+ <i class="bi bi-check-circle me-1"></i>
194
+ Imported {importResult.sites.length} sites in {importResult.groups.length} groups
195
+ </div>
196
+ {:else if importResult.errors.length > 0}
197
+ <div class="alert alert-danger py-2">
198
+ <strong>Import failed:</strong>
199
+ <ul class="mb-0 ps-3 mt-1">
200
+ {#each importResult.errors.slice(0, 5) as error}
201
+ <li class="small">{error}</li>
202
+ {/each}
203
+ </ul>
204
+ </div>
205
+ {/if}
206
+ {/if}
207
+ </div>
208
+ <div class="modal-footer">
209
+ <button type="button" class="btn btn-secondary" onclick={handleCloseModal}>
210
+ Cancel
211
+ </button>
212
+ <button
213
+ type="button"
214
+ class="btn btn-primary"
215
+ disabled={!selectedFile || isImporting}
216
+ onclick={handleImport}
217
+ >
218
+ {#if isImporting}
219
+ <span class="spinner-border spinner-border-sm me-1"></span>
220
+ Importing...
221
+ {:else}
222
+ <i class="bi bi-upload me-1"></i>
223
+ Import
224
+ {/if}
225
+ </button>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </div>
230
+ <div class="modal-backdrop show"></div>
231
+ {/if}
232
+
233
+ <style>
234
+ .custom-site-manager {
235
+ font-size: 0.875rem;
236
+ }
237
+
238
+ .list-group-item {
239
+ background: transparent;
240
+ }
241
+
242
+ .modal {
243
+ z-index: 2000;
244
+ }
245
+
246
+ .modal-backdrop {
247
+ z-index: 1999;
248
+ }
249
+ </style>
250
+
251
+ <!-- Filter Controls for each set (rendered outside the manager control) -->
252
+ {#each setsArray as set (set.id)}
253
+ {#key set.id}
254
+ <CustomSiteFilterControl
255
+ {position}
256
+ {setsStore}
257
+ {set}
258
+ onremove={(id) => setsStore.removeSet(id)}
259
+ />
260
+ {/key}
261
+ {/each}
@@ -0,0 +1,10 @@
1
+ import { CustomSiteSetsStore } from '../stores/custom-site-sets.svelte';
2
+ interface Props {
3
+ /** Control position on map */
4
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
5
+ /** Optional external store (creates internal one if not provided) */
6
+ setsStore?: CustomSiteSetsStore;
7
+ }
8
+ declare const CustomSiteSetManager: import("svelte").Component<Props, {}, "">;
9
+ type CustomSiteSetManager = ReturnType<typeof CustomSiteSetManager>;
10
+ export default CustomSiteSetManager;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Custom Sites Feature
3
+ *
4
+ * Allows users to upload CSV files with custom site locations
5
+ * and display them as styled circle markers on the map.
6
+ */
7
+ export type { CustomSite, CustomSiteSet, CustomSiteImportResult, CustomSiteSetSerialized } from './types';
8
+ export { CustomSiteSetsStore } from './stores/custom-site-sets.svelte';
9
+ export { parseCustomSitesCsv } from './logic/csv-parser';
10
+ export { buildCustomSiteTree, getGroupCounts } from './logic/tree-adapter';
11
+ export { default as CustomSiteSetManager } from './components/CustomSiteSetManager.svelte';
12
+ export { default as CustomSiteFilterControl } from './components/CustomSiteFilterControl.svelte';
13
+ export { default as CustomSitesLayer } from './layers/CustomSitesLayer.svelte';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Custom Sites Feature
3
+ *
4
+ * Allows users to upload CSV files with custom site locations
5
+ * and display them as styled circle markers on the map.
6
+ */
7
+ // Store
8
+ export { CustomSiteSetsStore } from './stores/custom-site-sets.svelte';
9
+ // Logic
10
+ export { parseCustomSitesCsv } from './logic/csv-parser';
11
+ export { buildCustomSiteTree, getGroupCounts } from './logic/tree-adapter';
12
+ // Components
13
+ export { default as CustomSiteSetManager } from './components/CustomSiteSetManager.svelte';
14
+ export { default as CustomSiteFilterControl } from './components/CustomSiteFilterControl.svelte';
15
+ // Layers
16
+ export { default as CustomSitesLayer } from './layers/CustomSitesLayer.svelte';