@smartnet360/svelte-components 0.0.126 → 0.0.128

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 (22) hide show
  1. package/dist/map-v3/demo/DemoMap.svelte +18 -0
  2. package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.d.ts +16 -5
  3. package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.js +26 -13
  4. package/dist/map-v3/features/sites/custom/components/CustomSiteFilterControl.svelte +203 -0
  5. package/dist/map-v3/features/sites/custom/components/CustomSiteFilterControl.svelte.d.ts +15 -0
  6. package/dist/map-v3/features/sites/custom/components/CustomSiteSetManager.svelte +261 -0
  7. package/dist/map-v3/features/sites/custom/components/CustomSiteSetManager.svelte.d.ts +10 -0
  8. package/dist/map-v3/features/sites/custom/index.d.ts +13 -0
  9. package/dist/map-v3/features/sites/custom/index.js +16 -0
  10. package/dist/map-v3/features/sites/custom/layers/CustomSitesLayer.svelte +201 -0
  11. package/dist/map-v3/features/sites/custom/layers/CustomSitesLayer.svelte.d.ts +8 -0
  12. package/dist/map-v3/features/sites/custom/logic/csv-parser.d.ts +12 -0
  13. package/dist/map-v3/features/sites/custom/logic/csv-parser.js +182 -0
  14. package/dist/map-v3/features/sites/custom/logic/tree-adapter.d.ts +16 -0
  15. package/dist/map-v3/features/sites/custom/logic/tree-adapter.js +59 -0
  16. package/dist/map-v3/features/sites/custom/stores/custom-site-sets.svelte.d.ts +78 -0
  17. package/dist/map-v3/features/sites/custom/stores/custom-site-sets.svelte.js +248 -0
  18. package/dist/map-v3/features/sites/custom/types.d.ts +74 -0
  19. package/dist/map-v3/features/sites/custom/types.js +8 -0
  20. package/dist/map-v3/index.d.ts +2 -0
  21. package/dist/map-v3/index.js +4 -0
  22. package/package.json +1 -1
@@ -26,6 +26,12 @@
26
26
  CustomCellSetManager,
27
27
  createCustomCellSetsStore
28
28
  } from '../features/cells/custom';
29
+ // Custom Sites Feature
30
+ import {
31
+ CustomSitesLayer,
32
+ CustomSiteSetManager,
33
+ CustomSiteSetsStore
34
+ } from '../features/sites/custom';
29
35
  import { demoCells } from './demo-cells';
30
36
  import { demoRepeaters } from './demo-repeaters';
31
37
 
@@ -51,6 +57,9 @@
51
57
  // Custom Cells Store
52
58
  const customCellSets = createCustomCellSetsStore(cellData, 'demo-map');
53
59
 
60
+ // Custom Sites Store
61
+ const customSiteSets = new CustomSiteSetsStore();
62
+
54
63
  onMount(() => {
55
64
  // Load dummy data
56
65
  // Need to cast or map if types slightly differ, but they should match
@@ -96,6 +105,12 @@
96
105
  setsStore={customCellSets}
97
106
  />
98
107
 
108
+ <!-- Custom Sites Manager (includes filter controls for each set) -->
109
+ <CustomSiteSetManager
110
+ position="top-left"
111
+ setsStore={customSiteSets}
112
+ />
113
+
99
114
  <FeatureSettingsControl
100
115
  position="top-right"
101
116
  cellDisplayStore={cellDisplay}
@@ -117,6 +132,9 @@
117
132
  <!-- Custom Cells Layer (renders on top of regular cells) -->
118
133
  <CustomCellsLayer setsStore={customCellSets} />
119
134
 
135
+ <!-- Custom Sites Layer (renders custom point markers) -->
136
+ <CustomSitesLayer setsStore={customSiteSets} />
137
+
120
138
  <FeatureSelectionControl
121
139
  position="bottom-left"
122
140
  cellDataStore={cellData}
@@ -2,10 +2,13 @@
2
2
  * Custom Cell Sets Store
3
3
  *
4
4
  * Manages multiple custom cell sets, each loaded from a CSV file.
5
- * Resolves cell data from the parent CellDataStore.
5
+ * Resolves cell data from a provided cell array.
6
6
  */
7
+ import type { Cell } from '../../types';
7
8
  import type { CellDataStore } from '../../stores/cell.data.svelte';
8
9
  import type { CustomCellSet, CustomCellImportResult } from '../types';
10
+ /** Function that returns the current cells array */
11
+ type CellsGetter = () => Cell[];
9
12
  /**
10
13
  * Store for managing custom cell sets
11
14
  */
@@ -14,11 +17,16 @@ export declare class CustomCellSetsStore {
14
17
  sets: CustomCellSet[];
15
18
  /** Version counter for reactivity */
16
19
  version: number;
17
- /** Reference to parent cell data store */
18
- private cellDataStore;
20
+ /** Function to get current cells */
21
+ private getCells;
19
22
  /** Storage key for persistence */
20
23
  private storageKey;
21
- constructor(cellDataStore: CellDataStore, namespace?: string);
24
+ /**
25
+ * Create a new CustomCellSetsStore
26
+ * @param cells - Either a Cell array or a getter function that returns cells
27
+ * @param namespace - Storage namespace for persistence
28
+ */
29
+ constructor(cells: Cell[] | CellsGetter, namespace?: string);
22
30
  /**
23
31
  * Import a CSV file and create a new custom cell set
24
32
  */
@@ -74,5 +82,8 @@ export declare class CustomCellSetsStore {
74
82
  }
75
83
  /**
76
84
  * Factory function to create a custom cell sets store
85
+ * @param cells - Cell array, getter function, or CellDataStore
86
+ * @param namespace - Storage namespace for persistence
77
87
  */
78
- export declare function createCustomCellSetsStore(cellDataStore: CellDataStore, namespace?: string): CustomCellSetsStore;
88
+ export declare function createCustomCellSetsStore(cells: Cell[] | CellsGetter | CellDataStore, namespace?: string): CustomCellSetsStore;
89
+ export {};
@@ -2,7 +2,7 @@
2
2
  * Custom Cell Sets Store
3
3
  *
4
4
  * Manages multiple custom cell sets, each loaded from a CSV file.
5
- * Resolves cell data from the parent CellDataStore.
5
+ * Resolves cell data from a provided cell array.
6
6
  */
7
7
  import { browser } from '$app/environment';
8
8
  import { CUSTOM_CELL_PALETTE } from '../types';
@@ -21,12 +21,18 @@ export class CustomCellSetsStore {
21
21
  sets = $state([]);
22
22
  /** Version counter for reactivity */
23
23
  version = $state(0);
24
- /** Reference to parent cell data store */
25
- cellDataStore;
24
+ /** Function to get current cells */
25
+ getCells;
26
26
  /** Storage key for persistence */
27
27
  storageKey;
28
- constructor(cellDataStore, namespace = 'default') {
29
- this.cellDataStore = cellDataStore;
28
+ /**
29
+ * Create a new CustomCellSetsStore
30
+ * @param cells - Either a Cell array or a getter function that returns cells
31
+ * @param namespace - Storage namespace for persistence
32
+ */
33
+ constructor(cells, namespace = 'default') {
34
+ // Normalize to a getter function
35
+ this.getCells = typeof cells === 'function' ? cells : () => cells;
30
36
  this.storageKey = `${namespace}:custom-cell-sets`;
31
37
  if (browser) {
32
38
  this.load();
@@ -36,14 +42,15 @@ export class CustomCellSetsStore {
36
42
  const _v = this.version;
37
43
  this.save();
38
44
  });
39
- // Re-resolve cells when main cell data changes
45
+ // Re-resolve cells when cell data changes (only works if getter is reactive)
40
46
  $effect(() => {
41
- const cellCount = this.cellDataStore.rawCells.length;
47
+ const currentCells = this.getCells();
48
+ const cellCount = currentCells.length;
42
49
  if (cellCount > 0 && this.sets.length > 0) {
43
50
  // Check if any cells need resolution
44
51
  const needsResolution = this.sets.some(set => set.cells.some(c => !c.resolvedCell));
45
52
  if (needsResolution) {
46
- console.log('[CustomCellSetsStore] Re-resolving cells after main data loaded');
53
+ console.log('[CustomCellSetsStore] Re-resolving cells after data loaded');
47
54
  this.refreshResolutions();
48
55
  }
49
56
  }
@@ -54,8 +61,8 @@ export class CustomCellSetsStore {
54
61
  * Import a CSV file and create a new custom cell set
55
62
  */
56
63
  importFromCsv(csvContent, fileName) {
57
- // Build lookup from all cells (unfiltered)
58
- const cellLookup = buildCellLookup(this.cellDataStore.rawCells);
64
+ // Build lookup from all cells
65
+ const cellLookup = buildCellLookup(this.getCells());
59
66
  // Parse CSV
60
67
  const result = parseCustomCellsCsv(csvContent, cellLookup);
61
68
  return result;
@@ -180,7 +187,7 @@ export class CustomCellSetsStore {
180
187
  * Re-resolve cells after main cell data changes
181
188
  */
182
189
  refreshResolutions() {
183
- const cellLookup = buildCellLookup(this.cellDataStore.rawCells);
190
+ const cellLookup = buildCellLookup(this.getCells());
184
191
  for (const set of this.sets) {
185
192
  for (const cell of set.cells) {
186
193
  cell.resolvedCell = cellLookup.get(cell.txId);
@@ -236,7 +243,13 @@ export class CustomCellSetsStore {
236
243
  }
237
244
  /**
238
245
  * Factory function to create a custom cell sets store
246
+ * @param cells - Cell array, getter function, or CellDataStore
247
+ * @param namespace - Storage namespace for persistence
239
248
  */
240
- export function createCustomCellSetsStore(cellDataStore, namespace = 'default') {
241
- return new CustomCellSetsStore(cellDataStore, namespace);
249
+ export function createCustomCellSetsStore(cells, namespace = 'default') {
250
+ // Handle CellDataStore by extracting a getter
251
+ if (cells && typeof cells === 'object' && 'rawCells' in cells) {
252
+ return new CustomCellSetsStore(() => cells.rawCells, namespace);
253
+ }
254
+ return new CustomCellSetsStore(cells, namespace);
242
255
  }
@@ -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';