@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.
Files changed (40) hide show
  1. package/dist/core/CellTable/CellHistoryDemo.svelte +106 -65
  2. package/dist/core/CellTable/column-config.d.ts +12 -0
  3. package/dist/core/CellTable/column-config.js +79 -2
  4. package/dist/core/CellTable/history-api-helper.d.ts +79 -0
  5. package/dist/core/CellTable/history-api-helper.js +83 -0
  6. package/dist/core/CellTable/index.d.ts +3 -2
  7. package/dist/core/CellTable/index.js +3 -1
  8. package/dist/core/CellTable/types.d.ts +26 -0
  9. package/dist/map-v3/demo/DemoMap.svelte +1 -6
  10. package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellFilterControl.svelte +11 -4
  11. package/dist/map-v3/features/custom/components/CustomCellSetManager.svelte +692 -0
  12. package/dist/map-v3/features/{cells/custom → custom}/index.d.ts +1 -1
  13. package/dist/map-v3/features/{cells/custom → custom}/index.js +1 -1
  14. package/dist/map-v3/features/custom/layers/CustomCellsLayer.svelte +399 -0
  15. package/dist/map-v3/features/{cells/custom → custom}/logic/csv-parser.d.ts +11 -7
  16. package/dist/map-v3/features/{cells/custom → custom}/logic/csv-parser.js +64 -20
  17. package/dist/map-v3/features/{cells/custom → custom}/logic/tree-adapter.d.ts +1 -1
  18. package/dist/map-v3/features/{cells/custom → custom}/stores/custom-cell-sets.svelte.d.ts +4 -3
  19. package/dist/map-v3/features/{cells/custom → custom}/stores/custom-cell-sets.svelte.js +30 -10
  20. package/dist/map-v3/features/{cells/custom → custom}/types.d.ts +32 -12
  21. package/dist/map-v3/features/{cells/custom → custom}/types.js +5 -3
  22. package/dist/map-v3/index.d.ts +1 -1
  23. package/dist/map-v3/index.js +1 -1
  24. package/dist/map-v3/shared/controls/MapControl.svelte +43 -15
  25. package/dist/map-v3/shared/controls/MapControl.svelte.d.ts +3 -1
  26. package/package.json +1 -1
  27. package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte +0 -306
  28. package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte +0 -262
  29. /package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellFilterControl.svelte.d.ts +0 -0
  30. /package/dist/map-v3/features/{cells/custom → custom}/components/CustomCellSetManager.svelte.d.ts +0 -0
  31. /package/dist/map-v3/features/{cells/custom → custom}/components/index.d.ts +0 -0
  32. /package/dist/map-v3/features/{cells/custom → custom}/components/index.js +0 -0
  33. /package/dist/map-v3/features/{cells/custom → custom}/layers/CustomCellsLayer.svelte.d.ts +0 -0
  34. /package/dist/map-v3/features/{cells/custom → custom}/layers/index.d.ts +0 -0
  35. /package/dist/map-v3/features/{cells/custom → custom}/layers/index.js +0 -0
  36. /package/dist/map-v3/features/{cells/custom → custom}/logic/index.d.ts +0 -0
  37. /package/dist/map-v3/features/{cells/custom → custom}/logic/index.js +0 -0
  38. /package/dist/map-v3/features/{cells/custom → custom}/logic/tree-adapter.js +0 -0
  39. /package/dist/map-v3/features/{cells/custom → custom}/stores/index.d.ts +0 -0
  40. /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
- if (cellCount > 0 && this.sets.length > 0) {
50
- // Check if any cells need resolution
51
- const needsResolution = this.sets.some(set => set.cells.some(c => !c.resolvedCell));
52
- if (needsResolution) {
53
- console.log('[CustomCellSetsStore] Re-resolving cells after data loaded');
54
- this.refreshResolutions();
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 => c.resolvedCell && set.visibleGroups.has(c.customGroup));
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.resolvedCell = cellLookup.get(cell.txId);
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
- txId: c.txId,
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 Cells Feature - Type Definitions
2
+ * Custom Feature - Type Definitions
3
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.
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 '../../../../shared/demo';
9
+ import type { Cell } from '../../../shared/demo';
8
10
  /**
9
- * A custom cell entry from CSV import
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
- /** Required: Reference to cell in main data store */
13
- txId: string;
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 arc radius (defaults to 1) */
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
- /** Resolved cell data from main store (populated after import) */
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 matched cells */
41
+ /** Successfully parsed items (cells and/or points) */
28
42
  cells: CustomCell[];
29
- /** Cell IDs that weren't found in main data store */
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 Cells Feature - Type Definitions
2
+ * Custom Feature - Type Definitions
3
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.
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
@@ -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/cells/custom';
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';
@@ -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/cells/custom';
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 class="map-control-header" title={title}>
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 actions}
159
- {@render actions()}
160
- {/if}
161
- {#if collapsible}
162
- <button
163
- class="map-control-toggle"
164
- onclick={toggleCollapse}
165
- aria-label={collapsed ? 'Expand' : 'Collapse'}
166
- title={collapsed ? 'Expand' : 'Collapse'}
167
- >
168
- <i class="bi bi-chevron-{collapsed ? 'down' : 'up'}"></i>
169
- </button>
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.130",
3
+ "version": "0.0.132",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -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}