@smartnet360/svelte-components 0.0.126 → 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.
@@ -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}
@@ -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';