@smartnet360/svelte-components 0.0.142 → 0.0.144

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 (37) hide show
  1. package/dist/apps/antenna-tools/components/AntennaControls.svelte +14 -6
  2. package/dist/apps/antenna-tools/components/AntennaTools.svelte +6 -5
  3. package/dist/core/Auth/auth.svelte.js +72 -39
  4. package/dist/core/Auth/config.d.ts +1 -0
  5. package/dist/core/Auth/config.js +3 -4
  6. package/dist/map-v3/demo/DemoMap.svelte +3 -1
  7. package/dist/map-v3/features/custom/components/CustomCellSetManager.svelte +205 -14
  8. package/dist/map-v3/features/custom/components/CustomCellSetManager.svelte.d.ts +3 -0
  9. package/dist/map-v3/features/custom/components/ServerSetBrowser.svelte +398 -0
  10. package/dist/map-v3/features/custom/components/ServerSetBrowser.svelte.d.ts +22 -0
  11. package/dist/map-v3/features/custom/components/index.d.ts +1 -0
  12. package/dist/map-v3/features/custom/components/index.js +1 -0
  13. package/dist/map-v3/features/custom/db/custom-sets-api.d.ts +65 -0
  14. package/dist/map-v3/features/custom/db/custom-sets-api.js +220 -0
  15. package/dist/map-v3/features/custom/db/custom-sets-repository.d.ts +77 -0
  16. package/dist/map-v3/features/custom/db/custom-sets-repository.js +195 -0
  17. package/dist/map-v3/features/custom/db/index.d.ts +10 -0
  18. package/dist/map-v3/features/custom/db/index.js +9 -0
  19. package/dist/map-v3/features/custom/db/schema.sql +102 -0
  20. package/dist/map-v3/features/custom/db/types.d.ts +95 -0
  21. package/dist/map-v3/features/custom/db/types.js +95 -0
  22. package/dist/map-v3/features/custom/index.d.ts +2 -0
  23. package/dist/map-v3/features/custom/index.js +2 -0
  24. package/dist/map-v3/features/custom/logic/csv-parser.d.ts +12 -1
  25. package/dist/map-v3/features/custom/logic/csv-parser.js +54 -16
  26. package/dist/map-v3/features/custom/logic/tree-adapter.js +5 -3
  27. package/dist/map-v3/features/custom/stores/custom-cell-sets.svelte.d.ts +5 -1
  28. package/dist/map-v3/features/custom/stores/custom-cell-sets.svelte.js +6 -3
  29. package/dist/shared/csv-import/ColumnMapper.svelte +194 -0
  30. package/dist/shared/csv-import/ColumnMapper.svelte.d.ts +22 -0
  31. package/dist/shared/csv-import/column-detector.d.ts +58 -0
  32. package/dist/shared/csv-import/column-detector.js +228 -0
  33. package/dist/shared/csv-import/index.d.ts +10 -0
  34. package/dist/shared/csv-import/index.js +12 -0
  35. package/dist/shared/csv-import/types.d.ts +67 -0
  36. package/dist/shared/csv-import/types.js +70 -0
  37. package/package.json +1 -1
@@ -67,6 +67,10 @@
67
67
  // Avoid overwriting user selections
68
68
  if (selectedAntenna !== internalSelectedAntenna) {
69
69
  internalSelectedAntenna = selectedAntenna;
70
+ // Also sync frequency when antenna is set externally
71
+ if (selectedAntenna && selectedAntenna.frequency) {
72
+ selectedFrequency = selectedAntenna.frequency;
73
+ }
70
74
  }
71
75
  });
72
76
 
@@ -76,12 +80,13 @@
76
80
  let newTilts: string[] = ['0'];
77
81
 
78
82
  if (internalSelectedAntenna) {
79
- // Find ALL antennas with the same name and frequency to collect all electrical tilts
83
+ // Find antennas with the same name
80
84
  let sameName = antennas.filter(a => a.name === internalSelectedAntenna!.name);
81
85
 
82
- // If frequency is selected, filter by it
83
- if (selectedFrequency) {
84
- sameName = sameName.filter(a => a.frequency === selectedFrequency);
86
+ // Always filter by frequency - use selectedFrequency or fall back to antenna's frequency
87
+ const freqToUse = selectedFrequency ?? internalSelectedAntenna.frequency;
88
+ if (freqToUse) {
89
+ sameName = sameName.filter(a => a.frequency === freqToUse);
85
90
  }
86
91
 
87
92
  const allTilts = new Set<string>();
@@ -114,12 +119,15 @@
114
119
  // Handle case where availableElectricalTilts might not be populated yet
115
120
  const targetTilt = availableElectricalTilts[tiltIndex] || '0';
116
121
 
122
+ // Always use frequency filter - selectedFrequency or the current antenna's frequency
123
+ const freqToUse = selectedFrequency ?? internalSelectedAntenna?.frequency;
124
+
117
125
  // Find antenna with matching name, frequency, and electrical tilt
118
126
  return antennas.find(antenna => {
119
127
  if (antenna.name !== antennaName) return false;
120
128
 
121
- // If frequency is selected, must match
122
- if (selectedFrequency && antenna.frequency !== selectedFrequency) return false;
129
+ // Must match frequency
130
+ if (freqToUse && antenna.frequency !== freqToUse) return false;
123
131
 
124
132
  if (antenna.tilt) {
125
133
  const tiltString = antenna.tilt.toString();
@@ -197,10 +197,10 @@
197
197
 
198
198
  // === Helpers ===
199
199
  function updateAvailableTilts(antenna: Antenna) {
200
- // Collect ALL available tilts from all antennas with the same name
201
- const sameName = antennas.filter(a => a.name === antenna.name);
200
+ // Collect available tilts from antennas with the same name AND frequency
201
+ const sameNameAndFreq = antennas.filter(a => a.name === antenna.name && a.frequency === antenna.frequency);
202
202
  const allTilts = new Set<string>();
203
- sameName.forEach(a => {
203
+ sameNameAndFreq.forEach(a => {
204
204
  if (a.tilt) {
205
205
  const tiltStr = a.tilt.toString();
206
206
  if (tiltStr.includes(',')) {
@@ -214,9 +214,10 @@
214
214
  }
215
215
 
216
216
  function getAvailableTiltsForAntenna(antenna: Antenna): string[] {
217
- const sameName = antennas.filter(a => a.name === antenna.name);
217
+ // Collect available tilts from antennas with the same name AND frequency
218
+ const sameNameAndFreq = antennas.filter(a => a.name === antenna.name && a.frequency === antenna.frequency);
218
219
  const allTilts = new Set<string>();
219
- sameName.forEach(a => {
220
+ sameNameAndFreq.forEach(a => {
220
221
  if (a.tilt) {
221
222
  const tiltStr = a.tilt.toString();
222
223
  if (tiltStr.includes(',')) {
@@ -2,30 +2,61 @@
2
2
  import { browser } from '$app/environment';
3
3
  // Demo mode - enabled via PUBLIC_DEMO_MODE env var (GitHub Pages)
4
4
  const DEMO_MODE = import.meta.env.PUBLIC_DEMO_MODE === 'true';
5
- const DEMO_USERNAME = 'demo';
6
- const DEMO_PASSWORD = 'demo123';
7
- // Demo user session (client-side only, no server needed)
8
- const DEMO_SESSION = {
9
- user: {
10
- id: 'demo-user',
11
- username: 'demo',
12
- displayName: 'Demo User',
13
- email: 'demo@example.com',
14
- groups: ['demo'],
15
- loginTime: Date.now()
5
+ // Test users configuration (no backend required)
6
+ const TEST_USERS = {
7
+ // Demo user (view-only permissions)
8
+ demo: {
9
+ password: 'demo123',
10
+ session: {
11
+ user: {
12
+ id: 'demo-user',
13
+ username: 'demo',
14
+ displayName: 'Demo User',
15
+ email: 'demo@example.com',
16
+ groups: ['demo'],
17
+ loginTime: Date.now()
18
+ },
19
+ permissions: [
20
+ { featureId: 'map-view', canView: true, canEdit: false, canAdmin: false },
21
+ { featureId: 'coverage-view', canView: true, canEdit: false, canAdmin: false },
22
+ { featureId: 'charts-view', canView: true, canEdit: false, canAdmin: false },
23
+ { featureId: 'desktop-view', canView: true, canEdit: false, canAdmin: false },
24
+ { featureId: 'table-view', canView: true, canEdit: false, canAdmin: false },
25
+ { featureId: 'antenna-view', canView: true, canEdit: false, canAdmin: false },
26
+ { featureId: 'site-check', canView: true, canEdit: false, canAdmin: false },
27
+ { featureId: 'settings-view', canView: true, canEdit: false, canAdmin: false }
28
+ ],
29
+ token: 'demo-token',
30
+ expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours
31
+ }
16
32
  },
17
- permissions: [
18
- { featureId: 'map-view', canView: true, canEdit: false, canAdmin: false },
19
- { featureId: 'coverage-view', canView: true, canEdit: false, canAdmin: false },
20
- { featureId: 'charts-view', canView: true, canEdit: false, canAdmin: false },
21
- { featureId: 'desktop-view', canView: true, canEdit: false, canAdmin: false },
22
- { featureId: 'table-view', canView: true, canEdit: false, canAdmin: false },
23
- { featureId: 'antenna-view', canView: true, canEdit: false, canAdmin: false },
24
- { featureId: 'site-check', canView: true, canEdit: false, canAdmin: false },
25
- { featureId: 'settings-view', canView: true, canEdit: false, canAdmin: false }
26
- ],
27
- token: 'demo-token',
28
- expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours for demo
33
+ // Test user (full permissions, for testing without backend)
34
+ test: {
35
+ password: 'test',
36
+ session: {
37
+ user: {
38
+ id: 'test-user',
39
+ username: 'test',
40
+ displayName: 'Test User',
41
+ email: 'test@localhost',
42
+ groups: ['testers', 'admins'],
43
+ loginTime: Date.now()
44
+ },
45
+ permissions: [
46
+ { featureId: 'map-view', canView: true, canEdit: true, canAdmin: true },
47
+ { featureId: 'coverage-view', canView: true, canEdit: true, canAdmin: true },
48
+ { featureId: 'charts-view', canView: true, canEdit: true, canAdmin: true },
49
+ { featureId: 'desktop-view', canView: true, canEdit: true, canAdmin: true },
50
+ { featureId: 'table-view', canView: true, canEdit: true, canAdmin: true },
51
+ { featureId: 'antenna-view', canView: true, canEdit: true, canAdmin: true },
52
+ { featureId: 'site-check', canView: true, canEdit: true, canAdmin: true },
53
+ { featureId: 'settings-view', canView: true, canEdit: true, canAdmin: true },
54
+ { featureId: 'admin', canView: true, canEdit: true, canAdmin: true }
55
+ ],
56
+ token: 'test-token',
57
+ expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours
58
+ }
59
+ }
29
60
  };
30
61
  // Default configuration
31
62
  const defaultConfig = {
@@ -107,22 +138,22 @@ export function createAuthState(config = {}) {
107
138
  isLoading = true;
108
139
  error = null;
109
140
  try {
110
- // Demo mode: handle authentication client-side (no server needed)
141
+ // Check for test users (works in demo mode or as fallback)
142
+ const testUser = TEST_USERS[username.toLowerCase()];
143
+ if (testUser && password === testUser.password) {
144
+ // Create fresh session with updated timestamp
145
+ const testSession = {
146
+ ...testUser.session,
147
+ user: { ...testUser.session.user, loginTime: Date.now() },
148
+ expiresAt: Date.now() + (24 * 60 * 60 * 1000)
149
+ };
150
+ saveSession(testSession);
151
+ return true;
152
+ }
153
+ // In demo mode, only allow test users
111
154
  if (DEMO_MODE) {
112
- if (username === DEMO_USERNAME && password === DEMO_PASSWORD) {
113
- // Create fresh demo session with updated timestamp
114
- const demoSession = {
115
- ...DEMO_SESSION,
116
- user: { ...DEMO_SESSION.user, loginTime: Date.now() },
117
- expiresAt: Date.now() + (24 * 60 * 60 * 1000)
118
- };
119
- saveSession(demoSession);
120
- return true;
121
- }
122
- else {
123
- error = 'Demo mode: Use username "demo" and password "demo123"';
124
- return false;
125
- }
155
+ error = 'Demo mode: Use "demo/demo123" or "test/test" to login';
156
+ return false;
126
157
  }
127
158
  // Normal mode: call server API
128
159
  const response = await fetch(`${cfg.apiEndpoint}/login`, {
@@ -141,7 +172,9 @@ export function createAuthState(config = {}) {
141
172
  }
142
173
  }
143
174
  catch (e) {
144
- error = e instanceof Error ? e.message : 'Network error';
175
+ // If server is unavailable, show helpful message about test users
176
+ const networkError = e instanceof Error ? e.message : 'Network error';
177
+ error = `${networkError}. Try "test/test" for testing without a backend.`;
145
178
  return false;
146
179
  }
147
180
  finally {
@@ -21,5 +21,6 @@ export declare const DEFAULT_DEV_PERMISSIONS: FeatureAccess[];
21
21
  export declare function getDevConfig(): DevConfig;
22
22
  /**
23
23
  * Check if we're in development mode
24
+ * Currently disabled - authentication is always required
24
25
  */
25
26
  export declare function isDevMode(): boolean;
@@ -246,11 +246,10 @@ export function getDevConfig() {
246
246
  }
247
247
  /**
248
248
  * Check if we're in development mode
249
+ * Currently disabled - authentication is always required
249
250
  */
250
251
  export function isDevMode() {
251
- // Check environment - in dev server or explicit flag
252
- if (typeof import.meta !== 'undefined' && import.meta.env?.DEV) {
253
- return true;
254
- }
252
+ // Auto-login disabled - always require authentication
253
+ // Use test/test or demo/demo123 for testing without a backend
255
254
  return false;
256
255
  }
@@ -24,7 +24,8 @@
24
24
  import {
25
25
  CustomCellsLayer,
26
26
  CustomCellSetManager,
27
- createCustomCellSetsStore
27
+ createCustomCellSetsStore,
28
+ createCustomSetsApi
28
29
  } from '../features/custom';
29
30
  // Custom Sites Feature
30
31
  import {
@@ -103,6 +104,7 @@
103
104
  <CustomCellSetManager
104
105
  position="top-left"
105
106
  setsStore={customCellSets}
107
+ apiClient={createCustomSetsApi('/api/custom-sets')}
106
108
  />
107
109
 
108
110
 
@@ -7,6 +7,7 @@
7
7
  * - Quick Add (paste cell names)
8
8
  * - Global size/opacity sliders
9
9
  * - Collapsible set sections with TreeView + color pickers
10
+ * - Server sharing (optional, when apiClient provided)
10
11
  */
11
12
  import { untrack } from 'svelte';
12
13
  import { MapControl } from '../../../shared';
@@ -14,17 +15,30 @@
14
15
  import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
15
16
  import type { CustomCellImportResult, CustomCellSet } from '../types';
16
17
  import { buildCustomCellTree } from '../logic/tree-adapter';
18
+ import type { CustomSetsApiClient } from '../db/custom-sets-api';
19
+ import ServerSetBrowser from './ServerSetBrowser.svelte';
20
+ import { extractCsvHeaders } from '../logic/csv-parser';
21
+ import {
22
+ ColumnMapper,
23
+ autoDetectImport,
24
+ getFieldsForMode,
25
+ type ColumnMapping,
26
+ type ImportMode
27
+ } from '../../../../shared/csv-import';
17
28
 
18
29
  interface Props {
19
30
  /** The custom cell sets store */
20
31
  setsStore: CustomCellSetsStore;
21
32
  /** Control position on map */
22
33
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
34
+ /** Optional API client for server sharing */
35
+ apiClient?: CustomSetsApiClient;
23
36
  }
24
37
 
25
38
  let {
26
39
  setsStore,
27
- position = 'top-left'
40
+ position = 'top-left',
41
+ apiClient
28
42
  }: Props = $props();
29
43
 
30
44
  // Global display settings - initialized from first set if available
@@ -51,12 +65,24 @@
51
65
  let importError = $state('');
52
66
  let fileInput: HTMLInputElement;
53
67
 
68
+ // Column mapping state
69
+ let showMappingModal = $state(false);
70
+ let pendingCsvContent = $state<string | null>(null);
71
+ let csvHeaders = $state<string[]>([]);
72
+ let columnMapping = $state<ColumnMapping>({});
73
+ let importMode = $state<ImportMode>('cell');
74
+
54
75
  // Quick Add state
55
76
  let showQuickAdd = $state(false);
56
77
  let quickAddText = $state('');
57
78
  let quickAddName = $state('Quick Selection');
58
79
  let quickAddResult = $state<CustomCellImportResult | null>(null);
59
80
 
81
+ // Server sharing state
82
+ let showServerBrowser = $state(false);
83
+ let savingSetId = $state<string | null>(null);
84
+ let saveError = $state('');
85
+
60
86
  // Track expanded sets
61
87
  let expandedSets = $state<Set<string>>(new Set());
62
88
 
@@ -143,8 +169,23 @@
143
169
  reader.onload = (e) => {
144
170
  try {
145
171
  const content = e.target?.result as string;
146
- importResult = setsStore.importFromCsv(content, file.name);
147
- showImportModal = true;
172
+ // Extract headers and show mapping modal
173
+ const headers = extractCsvHeaders(content);
174
+ if (headers.length === 0) {
175
+ throw new Error('CSV file appears to be empty');
176
+ }
177
+
178
+ // Store content for later parsing
179
+ pendingCsvContent = content;
180
+ csvHeaders = headers;
181
+
182
+ // Auto-detect import mode and column mapping
183
+ const detected = autoDetectImport(headers);
184
+ importMode = detected.mode;
185
+ columnMapping = detected.mapping;
186
+
187
+ // Show mapping modal
188
+ showMappingModal = true;
148
189
  } catch (err) {
149
190
  importError = err instanceof Error ? err.message : 'Failed to parse CSV';
150
191
  importResult = null;
@@ -157,6 +198,28 @@
157
198
  reader.readAsText(file);
158
199
  input.value = '';
159
200
  }
201
+
202
+ function confirmMapping() {
203
+ if (!pendingCsvContent) return;
204
+
205
+ try {
206
+ importResult = setsStore.importFromCsv(pendingCsvContent, importFileName, columnMapping);
207
+ showMappingModal = false;
208
+ showImportModal = true;
209
+ } catch (err) {
210
+ importError = err instanceof Error ? err.message : 'Failed to parse CSV';
211
+ showMappingModal = false;
212
+ importResult = null;
213
+ }
214
+ }
215
+
216
+ function closeMappingModal() {
217
+ showMappingModal = false;
218
+ pendingCsvContent = null;
219
+ csvHeaders = [];
220
+ columnMapping = {};
221
+ importMode = 'cell';
222
+ }
160
223
 
161
224
  function confirmImport() {
162
225
  if (!importResult) return;
@@ -223,6 +286,26 @@
223
286
  }
224
287
  }
225
288
 
289
+ // Server sharing functions
290
+ async function saveSetToServer(set: CustomCellSet) {
291
+ if (!apiClient) return;
292
+
293
+ savingSetId = set.id;
294
+ saveError = '';
295
+
296
+ try {
297
+ const result = await apiClient.saveSet(set);
298
+
299
+ if (!result.success) {
300
+ saveError = result.error || 'Failed to save to server';
301
+ }
302
+ } catch (err) {
303
+ saveError = err instanceof Error ? err.message : 'Failed to save';
304
+ } finally {
305
+ savingSetId = null;
306
+ }
307
+ }
308
+
226
309
  function toggleExpanded(setId: string) {
227
310
  if (expandedSets.has(setId)) {
228
311
  expandedSets.delete(setId);
@@ -252,9 +335,20 @@
252
335
 
253
336
  <MapControl {position} title="Custom Layers" icon="layers" controlWidth="320px">
254
337
  {#snippet actions()}
338
+ {#if apiClient}
339
+ <button
340
+ class="btn btn-sm btn-outline-primary border-0 p-1 px-2"
341
+ title="Load from Server"
342
+ aria-label="Load from Server"
343
+ onclick={() => showServerBrowser = true}
344
+ >
345
+ <i class="bi bi-cloud-download"></i>
346
+ </button>
347
+ {/if}
255
348
  <button
256
349
  class="btn btn-sm btn-outline-primary border-0 p-1 px-2"
257
350
  title="Quick Add (paste cell names)"
351
+ aria-label="Quick Add"
258
352
  onclick={() => showQuickAdd = true}
259
353
  >
260
354
  <i class="bi bi-pencil-square"></i>
@@ -262,6 +356,7 @@
262
356
  <button
263
357
  class="btn btn-sm btn-outline-primary border-0 p-1 px-2"
264
358
  title="Import CSV"
359
+ aria-label="Import CSV"
265
360
  onclick={triggerFileInput}
266
361
  >
267
362
  <i class="bi bi-upload"></i>
@@ -290,6 +385,22 @@
290
385
  </div>
291
386
  {/if}
292
387
 
388
+ <!-- Server save error -->
389
+ {#if saveError}
390
+ <div class="alert alert-warning py-1 px-2 mb-2 small d-flex justify-content-between align-items-center">
391
+ <span>
392
+ <i class="bi bi-cloud-slash me-1"></i>
393
+ {saveError}
394
+ </span>
395
+ <button
396
+ type="button"
397
+ class="btn-close btn-sm"
398
+ aria-label="Dismiss"
399
+ onclick={() => saveError = ''}
400
+ ></button>
401
+ </div>
402
+ {/if}
403
+
293
404
  <!-- Empty state -->
294
405
  {#if setsStore.sets.length === 0}
295
406
  <div class="text-center p-3">
@@ -297,7 +408,15 @@
297
408
  <p class="text-muted small mt-2 mb-0">
298
409
  No custom layers loaded.
299
410
  </p>
300
- <div class="d-flex gap-2 justify-content-center mt-2">
411
+ <!-- <div class="d-flex gap-2 justify-content-center mt-2 flex-wrap">
412
+ {#if apiClient}
413
+ <button
414
+ class="btn btn-sm btn-outline-success"
415
+ onclick={() => showServerBrowser = true}
416
+ >
417
+ <i class="bi bi-cloud-download me-1"></i> Load from Server
418
+ </button>
419
+ {/if}
301
420
  <button
302
421
  class="btn btn-sm btn-outline-primary"
303
422
  onclick={() => showQuickAdd = true}
@@ -310,7 +429,7 @@
310
429
  >
311
430
  <i class="bi bi-upload me-1"></i> Import CSV
312
431
  </button>
313
- </div>
432
+ </div> -->
314
433
  </div>
315
434
  {:else}
316
435
  <!-- Global Controls -->
@@ -323,8 +442,8 @@
323
442
  <input
324
443
  type="range"
325
444
  class="form-range"
326
- min="10"
327
- max="200"
445
+ min="5"
446
+ max="100"
328
447
  step="5"
329
448
  value={globalSectorSize}
330
449
  oninput={handleGlobalSectorSizeChange}
@@ -390,13 +509,31 @@
390
509
  <small class="text-muted">{getSetItemCounts(set)} · {set.groups.length} groups</small>
391
510
  </div>
392
511
  </div>
393
- <button
394
- class="btn btn-sm btn-outline-danger border-0 p-1 ms-2"
395
- title="Remove"
396
- onclick={() => removeSet(set.id)}
397
- >
398
- <i class="bi bi-trash"></i>
399
- </button>
512
+ <div class="d-flex gap-1">
513
+ {#if apiClient}
514
+ <button
515
+ class="btn btn-sm btn-outline-primary border-0 p-1"
516
+ title="Share to Server"
517
+ aria-label="Share to Server"
518
+ onclick={() => saveSetToServer(set)}
519
+ disabled={savingSetId === set.id}
520
+ >
521
+ {#if savingSetId === set.id}
522
+ <span class="spinner-border spinner-border-sm" role="status"></span>
523
+ {:else}
524
+ <i class="bi bi-cloud-upload"></i>
525
+ {/if}
526
+ </button>
527
+ {/if}
528
+ <button
529
+ class="btn btn-sm btn-outline-danger border-0 p-1"
530
+ title="Remove"
531
+ aria-label="Remove set"
532
+ onclick={() => removeSet(set.id)}
533
+ >
534
+ <i class="bi bi-trash"></i>
535
+ </button>
536
+ </div>
400
537
  </div>
401
538
 
402
539
  <!-- Set Content (TreeView) -->
@@ -438,6 +575,50 @@
438
575
  </div>
439
576
  </MapControl>
440
577
 
578
+ <!-- Column Mapping Modal -->
579
+ {#if showMappingModal && csvHeaders.length > 0}
580
+ {@const fieldsConfig = getFieldsForMode(importMode)}
581
+ {@const isValid = fieldsConfig.required.every(f =>
582
+ columnMapping[f.type] !== null && columnMapping[f.type] !== undefined
583
+ )}
584
+ <div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
585
+ <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
586
+ <div class="modal-content">
587
+ <div class="modal-header py-2">
588
+ <h6 class="modal-title">
589
+ <i class="bi bi-grid-3x3 me-2"></i>
590
+ Import: {importFileName}
591
+ </h6>
592
+ <button type="button" class="btn-close" aria-label="Close" onclick={closeMappingModal}></button>
593
+ </div>
594
+ <div class="modal-body py-2">
595
+ <ColumnMapper
596
+ headers={csvHeaders}
597
+ mode={importMode}
598
+ mapping={columnMapping}
599
+ onmodechange={(m) => importMode = m}
600
+ onchange={(m) => columnMapping = m}
601
+ />
602
+ </div>
603
+ <div class="modal-footer py-2">
604
+ <button type="button" class="btn btn-secondary btn-sm" onclick={closeMappingModal}>
605
+ Cancel
606
+ </button>
607
+ <button
608
+ type="button"
609
+ class="btn btn-primary btn-sm"
610
+ onclick={confirmMapping}
611
+ disabled={!isValid}
612
+ >
613
+ <i class="bi bi-arrow-right me-1"></i>
614
+ Continue
615
+ </button>
616
+ </div>
617
+ </div>
618
+ </div>
619
+ </div>
620
+ {/if}
621
+
441
622
  <!-- Import Result Modal -->
442
623
  {#if showImportModal && importResult}
443
624
  <div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
@@ -635,6 +816,16 @@
635
816
  </div>
636
817
  {/if}
637
818
 
819
+ <!-- Server Set Browser Modal -->
820
+ {#if apiClient}
821
+ <ServerSetBrowser
822
+ {apiClient}
823
+ {setsStore}
824
+ show={showServerBrowser}
825
+ onclose={() => showServerBrowser = false}
826
+ />
827
+ {/if}
828
+
638
829
  <style>
639
830
  .custom-layers-manager {
640
831
  font-size: 0.875rem;
@@ -1,9 +1,12 @@
1
1
  import type { CustomCellSetsStore } from '../stores/custom-cell-sets.svelte';
2
+ import type { CustomSetsApiClient } from '../db/custom-sets-api';
2
3
  interface Props {
3
4
  /** The custom cell sets store */
4
5
  setsStore: CustomCellSetsStore;
5
6
  /** Control position on map */
6
7
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
8
+ /** Optional API client for server sharing */
9
+ apiClient?: CustomSetsApiClient;
7
10
  }
8
11
  declare const CustomCellSetManager: import("svelte").Component<Props, {}, "">;
9
12
  type CustomCellSetManager = ReturnType<typeof CustomCellSetManager>;