@smartnet360/svelte-components 0.0.112 → 0.0.114

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.
@@ -20,6 +20,13 @@
20
20
  cellDataSorter
21
21
  } from './column-config';
22
22
 
23
+ // Type for Tabulator group component (not exported from tabulator-tables)
24
+ interface TabulatorGroup {
25
+ getKey(): string | number | boolean;
26
+ getRows(): RowComponent[];
27
+ scrollTo(): Promise<void>;
28
+ }
29
+
23
30
  interface Props extends CellTableProps {
24
31
  /** Row selection change event */
25
32
  onselectionchange?: (event: RowSelectionEvent) => void;
@@ -335,6 +342,34 @@
335
342
  .map(col => col.getField())
336
343
  .filter((field): field is string => !!field);
337
344
  }
345
+
346
+ /** Get all group keys (when grouping is active) */
347
+ export function getGroups(): { key: string; count: number }[] {
348
+ if (!table || groupBy === 'none') return [];
349
+ try {
350
+ const groups = table.getGroups() as TabulatorGroup[];
351
+ return groups.map(g => ({
352
+ key: String(g.getKey()),
353
+ count: g.getRows().length
354
+ }));
355
+ } catch {
356
+ return [];
357
+ }
358
+ }
359
+
360
+ /** Scroll to a specific group by key */
361
+ export function scrollToGroup(key: string): void {
362
+ if (!table || groupBy === 'none') return;
363
+ try {
364
+ const groups = table.getGroups() as TabulatorGroup[];
365
+ const group = groups.find(g => String(g.getKey()) === key);
366
+ if (group) {
367
+ group.scrollTo();
368
+ }
369
+ } catch (e) {
370
+ console.warn('Failed to scroll to group:', e);
371
+ }
372
+ }
338
373
  </script>
339
374
 
340
375
  <div class="cell-table-container">
@@ -28,6 +28,11 @@ declare const CellTable: import("svelte").Component<Props, {
28
28
  showColumn: (field: string) => void;
29
29
  hideColumn: (field: string) => void;
30
30
  getVisibleColumns: () => string[];
31
+ getGroups: () => {
32
+ key: string;
33
+ count: number;
34
+ }[];
35
+ scrollToGroup: (key: string) => void;
31
36
  }, "">;
32
37
  type CellTable = ReturnType<typeof CellTable>;
33
38
  export default CellTable;
@@ -10,6 +10,33 @@
10
10
  let groupBy: CellTableGroupField = $state('none');
11
11
  let columnPreset: ColumnPreset = $state('default');
12
12
  let searchTerm = $state('');
13
+
14
+ // Loading states
15
+ let isCreating = $state(false);
16
+ let isReloading = $state(false);
17
+ let isLoading = $derived(isCreating || isReloading);
18
+
19
+ // Check if 5G-3500 already exists for the searched site
20
+ let has5G3500 = $derived.by(() => {
21
+ const siteId = searchTerm.trim();
22
+ if (!siteId) return false;
23
+ return demoCells.some(cell =>
24
+ cell.siteId.toLowerCase() === siteId.toLowerCase() &&
25
+ cell.fband === '5G-3500'
26
+ );
27
+ });
28
+
29
+ // Create button disabled state
30
+ let createDisabled = $derived(!searchTerm.trim() || isLoading || has5G3500);
31
+
32
+ // Tooltip for create button
33
+ let createTooltip = $derived.by(() => {
34
+ if (!searchTerm.trim()) return 'Enter a site ID first';
35
+ if (has5G3500) return '5G-3500 already exists for this site';
36
+ if (isCreating) return 'Creating...';
37
+ if (isReloading) return 'Reloading...';
38
+ return 'Create 5G-3500 cell for this site';
39
+ });
13
40
 
14
41
  function handleSelectionChange(event: RowSelectionEvent) {
15
42
  console.log('Selection changed:', event.ids);
@@ -40,6 +67,64 @@
40
67
  handleSearch();
41
68
  }
42
69
  }
70
+
71
+ // ============================================
72
+ // PLACEHOLDER FUNCTIONS - Replace with your API calls
73
+ // ============================================
74
+
75
+ /**
76
+ * Create a new 5G-3500 cell for the given site
77
+ * TODO: Replace with your actual API call
78
+ */
79
+ async function createCellForSite(siteId: string): Promise<void> {
80
+ // Simulate API call - replace with your implementation
81
+ console.log(`Creating 5G-3500 cell for site: ${siteId}`);
82
+ await new Promise(resolve => setTimeout(resolve, 1000));
83
+ console.log(`Cell created for site: ${siteId}`);
84
+ }
85
+
86
+ /**
87
+ * Reload site data after creation
88
+ * TODO: Replace with your actual API call
89
+ */
90
+ async function reloadSiteData(siteId: string): Promise<typeof demoCells> {
91
+ // Simulate API call - replace with your implementation
92
+ console.log(`Reloading data for site: ${siteId}`);
93
+ await new Promise(resolve => setTimeout(resolve, 500));
94
+ // For demo, just return filtered data
95
+ const allCells = generateCellsFromPreset(datasetSize);
96
+ return allCells.filter(cell =>
97
+ cell.siteId.toLowerCase().includes(siteId.toLowerCase())
98
+ );
99
+ }
100
+
101
+ // ============================================
102
+
103
+ /**
104
+ * Handle create cell button click
105
+ */
106
+ async function handleCreateCell() {
107
+ const siteId = searchTerm.trim();
108
+ if (!siteId || isLoading || has5G3500) return;
109
+
110
+ try {
111
+ // Step 1: Create the cell
112
+ isCreating = true;
113
+ await createCellForSite(siteId);
114
+ isCreating = false;
115
+
116
+ // Step 2: Reload the data
117
+ isReloading = true;
118
+ demoCells = await reloadSiteData(siteId);
119
+ isReloading = false;
120
+
121
+ console.log('Cell created and data reloaded successfully');
122
+ } catch (error) {
123
+ console.error('Error creating cell:', error);
124
+ isCreating = false;
125
+ isReloading = false;
126
+ }
127
+ }
43
128
  </script>
44
129
 
45
130
  <div class="cell-table-page vh-100 d-flex flex-column">
@@ -75,26 +160,53 @@
75
160
  headerFilters={true}
76
161
  showDetailsSidebar={true}
77
162
  sidebarWidth={320}
163
+ showScrollSpy={true}
78
164
  title="Cell Data"
79
165
  onselectionchange={handleSelectionChange}
80
166
  >
81
167
  {#snippet headerSearch()}
82
- <div class="input-group input-group-sm" style="width: 180px;">
83
- <input
84
- type="text"
85
- class="form-control"
86
- placeholder="Search site or cell..."
87
- bind:value={searchTerm}
88
- onkeydown={handleSearchKeydown}
89
- />
168
+ <div class="d-flex align-items-center gap-2">
169
+ <div class="input-group input-group-sm" style="width: 180px;">
170
+ <input
171
+ type="text"
172
+ class="form-control"
173
+ placeholder="Search site or cell..."
174
+ bind:value={searchTerm}
175
+ onkeydown={handleSearchKeydown}
176
+ disabled={isLoading}
177
+ />
178
+ <button
179
+ class="btn btn-outline-primary"
180
+ type="button"
181
+ onclick={handleSearch}
182
+ title="Search"
183
+ aria-label="Search"
184
+ disabled={isLoading}
185
+ >
186
+ {#if isReloading}
187
+ <span class="spinner-border spinner-border-sm" role="status"></span>
188
+ {:else}
189
+ <i class="bi bi-search"></i>
190
+ {/if}
191
+ </button>
192
+ </div>
90
193
  <button
91
- class="btn btn-outline-primary"
194
+ class="btn btn-sm"
195
+ class:btn-outline-success={!has5G3500}
196
+ class:btn-success={has5G3500}
92
197
  type="button"
93
- onclick={handleSearch}
94
- title="Search"
95
- aria-label="Search"
198
+ onclick={handleCreateCell}
199
+ title={createTooltip}
200
+ aria-label="Create 5G-3500 cell"
201
+ disabled={createDisabled}
96
202
  >
97
- <i class="bi bi-search"></i>
203
+ {#if isCreating}
204
+ <span class="spinner-border spinner-border-sm" role="status"></span>
205
+ {:else if has5G3500}
206
+ <i class="bi bi-check-lg"></i>
207
+ {:else}
208
+ <i class="bi bi-plus-lg"></i>
209
+ {/if}
98
210
  </button>
99
211
  </div>
100
212
  {/snippet}
@@ -32,6 +32,8 @@
32
32
  showToolbar?: boolean;
33
33
  /** Show export buttons */
34
34
  showExport?: boolean;
35
+ /** Show JSON export button (requires showExport=true) */
36
+ showJsonExport?: boolean;
35
37
  /** Technology color mapping */
36
38
  techColors?: TechColorMap;
37
39
  /** Status color mapping */
@@ -48,6 +50,8 @@
48
50
  persistSettings?: boolean;
49
51
  /** Storage key prefix for persisted settings */
50
52
  storageKey?: string;
53
+ /** Show scrollspy navigation bar for quick group navigation */
54
+ showScrollSpy?: boolean;
51
55
  /** Bindable reference to table methods */
52
56
  tableRef?: { redraw: () => void } | null;
53
57
  /** Row selection change event */
@@ -75,6 +79,7 @@
75
79
  height = '100%',
76
80
  showToolbar = true,
77
81
  showExport = true,
82
+ showJsonExport = false,
78
83
  techColors,
79
84
  statusColors,
80
85
  headerFilters = true,
@@ -83,6 +88,7 @@
83
88
  sidebarWidth = 320,
84
89
  persistSettings = true,
85
90
  storageKey = 'cell-table',
91
+ showScrollSpy = false,
86
92
  tableRef = $bindable(null),
87
93
  onselectionchange,
88
94
  onrowclick,
@@ -97,13 +103,15 @@
97
103
  const STORAGE_KEY_GROUP = `${storageKey}-groupBy`;
98
104
  const STORAGE_KEY_COLUMNS = `${storageKey}-visibleColumns`;
99
105
  const STORAGE_KEY_FILTERS = `${storageKey}-filtersVisible`;
106
+ const STORAGE_KEY_SCROLLSPY = `${storageKey}-scrollSpyEnabled`;
100
107
 
101
108
  // Load persisted settings
102
109
  function loadPersistedSettings() {
103
- if (!persistSettings || typeof localStorage === 'undefined') return { columns: null, filtersVisible: true };
110
+ if (!persistSettings || typeof localStorage === 'undefined') return { columns: null, filtersVisible: true, scrollSpyEnabled: showScrollSpy };
104
111
 
105
112
  let columns: string[] | null = null;
106
113
  let filters = true;
114
+ let scrollSpy = showScrollSpy;
107
115
 
108
116
  try {
109
117
  const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
@@ -120,10 +128,15 @@
120
128
  if (savedFilters !== null) {
121
129
  filters = savedFilters === 'true';
122
130
  }
131
+
132
+ const savedScrollSpy = localStorage.getItem(STORAGE_KEY_SCROLLSPY);
133
+ if (savedScrollSpy !== null) {
134
+ scrollSpy = savedScrollSpy === 'true';
135
+ }
123
136
  } catch (e) {
124
137
  console.warn('Failed to load CellTable settings:', e);
125
138
  }
126
- return { columns, filtersVisible: filters };
139
+ return { columns, filtersVisible: filters, scrollSpyEnabled: scrollSpy };
127
140
  }
128
141
 
129
142
  // Save group setting
@@ -156,6 +169,16 @@
156
169
  }
157
170
  }
158
171
 
172
+ // Save scrollspy state
173
+ function saveScrollSpyState(enabled: boolean) {
174
+ if (!persistSettings || typeof localStorage === 'undefined') return;
175
+ try {
176
+ localStorage.setItem(STORAGE_KEY_SCROLLSPY, String(enabled));
177
+ } catch (e) {
178
+ console.warn('Failed to save scrollspy state:', e);
179
+ }
180
+ }
181
+
159
182
  let cellTable: CellTable;
160
183
  let selectedCount = $state(0);
161
184
  let selectedRows = $state<CellData[]>([]);
@@ -171,6 +194,10 @@
171
194
  const persistedSettings = loadPersistedSettings();
172
195
  let filtersVisible = $state(persistedSettings.filtersVisible);
173
196
  let visibleColumns = $state<string[]>(persistedSettings.columns ?? getPresetVisibleFields(columnPreset));
197
+
198
+ // ScrollSpy state - initialize from persisted settings
199
+ let scrollSpyGroups = $state<{ key: string; count: number }[]>([]);
200
+ let scrollSpyEnabled = $state(persistedSettings.scrollSpyEnabled);
174
201
 
175
202
  // Update visible columns when preset changes (but not from storage load)
176
203
  $effect(() => {
@@ -208,11 +235,42 @@
208
235
 
209
236
  function handleDataChange(event: DataChangeEvent) {
210
237
  filteredCount = event.filteredCount;
238
+ // Update scrollspy groups when data changes
239
+ updateScrollSpyGroups();
240
+ }
241
+
242
+ function updateScrollSpyGroups() {
243
+ if (scrollSpyEnabled && groupBy !== 'none') {
244
+ // Small delay to ensure table has updated
245
+ setTimeout(() => {
246
+ scrollSpyGroups = cellTable?.getGroups() ?? [];
247
+ }, 50);
248
+ } else {
249
+ scrollSpyGroups = [];
250
+ }
251
+ }
252
+
253
+ function handleScrollToGroup(key: string) {
254
+ cellTable?.scrollToGroup(key);
255
+ }
256
+
257
+ function handleToggleScrollSpy() {
258
+ scrollSpyEnabled = !scrollSpyEnabled;
259
+ saveScrollSpyState(scrollSpyEnabled);
260
+ if (scrollSpyEnabled && groupBy !== 'none') {
261
+ updateScrollSpyGroups();
262
+ } else {
263
+ scrollSpyGroups = [];
264
+ }
211
265
  }
212
266
 
213
267
  function handleGroupChange(group: CellTableGroupField) {
214
268
  groupBy = group;
215
269
  saveGroupSetting(group);
270
+ // Update scrollspy groups after grouping changes
271
+ if (scrollSpyEnabled) {
272
+ setTimeout(() => updateScrollSpyGroups(), 100);
273
+ }
216
274
  }
217
275
 
218
276
  function handlePresetChange(preset: ColumnPreset) {
@@ -353,6 +411,7 @@
353
411
  {filteredCount}
354
412
  {selectedCount}
355
413
  {showExport}
414
+ {showJsonExport}
356
415
  ongroupchange={handleGroupChange}
357
416
  onpresetchange={handlePresetChange}
358
417
  onexportcsv={handleExportCSV}
@@ -366,9 +425,40 @@
366
425
  {visibleColumns}
367
426
  oncolumnvisibilitychange={handleColumnVisibilityChange}
368
427
  onresetcolumns={handleResetColumns}
428
+ {scrollSpyEnabled}
429
+ showScrollSpyToggle={showScrollSpy}
430
+ ontogglescrollspy={handleToggleScrollSpy}
369
431
  />
370
432
  {/if}
371
433
 
434
+ <!-- ScrollSpy Navigation Bar -->
435
+ {#if scrollSpyEnabled && groupBy !== 'none' && scrollSpyGroups.length > 0}
436
+ <div class="scrollspy-bar d-flex align-items-center gap-2 px-3 py-2 bg-body-tertiary border-bottom overflow-auto">
437
+ <span class="text-muted small me-1">
438
+ <i class="bi bi-signpost-split"></i> Jump to:
439
+ </span>
440
+ {#each scrollSpyGroups as group (group.key)}
441
+ {@const bgColor = groupBy === 'tech'
442
+ ? (techColors?.[group.key] ?? DEFAULT_TECH_COLORS[group.key] ?? '#6c757d')
443
+ : groupBy === 'fband'
444
+ ? (FBAND_COLORS[group.key] ?? '#6c757d')
445
+ : groupBy === 'status'
446
+ ? (statusColors?.[group.key] ?? DEFAULT_STATUS_COLORS[group.key] ?? '#6c757d')
447
+ : '#6c757d'}
448
+ <button
449
+ type="button"
450
+ class="btn btn-sm scrollspy-badge"
451
+ style="background-color: {bgColor}; border-color: {bgColor}; color: white;"
452
+ onclick={() => handleScrollToGroup(group.key)}
453
+ title="Scroll to {group.key} ({group.count} cells)"
454
+ >
455
+ <span class="badge rounded-pill bg-light text-dark me-1">{group.count}</span>
456
+ {group.key}
457
+ </button>
458
+ {/each}
459
+ </div>
460
+ {/if}
461
+
372
462
  <!-- Main content with optional sidebar -->
373
463
  <div class="content-area d-flex flex-grow-1 overflow-hidden">
374
464
  <!-- Table -->
@@ -571,4 +661,37 @@
571
661
  .panel-footer {
572
662
  min-height: 48px;
573
663
  }
664
+
665
+ /* ScrollSpy bar styling */
666
+ .scrollspy-bar {
667
+ min-height: 40px;
668
+ flex-wrap: nowrap;
669
+ scrollbar-width: thin;
670
+ }
671
+
672
+ .scrollspy-bar::-webkit-scrollbar {
673
+ height: 4px;
674
+ }
675
+
676
+ .scrollspy-bar::-webkit-scrollbar-thumb {
677
+ background: var(--bs-secondary-color, #6c757d);
678
+ border-radius: 2px;
679
+ }
680
+
681
+ .scrollspy-badge {
682
+ white-space: nowrap;
683
+ font-size: 0.75rem;
684
+ padding: 0.25rem 0.5rem;
685
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
686
+ }
687
+
688
+ .scrollspy-badge:hover {
689
+ transform: translateY(-1px);
690
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
691
+ filter: brightness(1.1);
692
+ }
693
+
694
+ .scrollspy-badge:active {
695
+ transform: translateY(0);
696
+ }
574
697
  </style>
@@ -17,6 +17,8 @@ interface Props {
17
17
  showToolbar?: boolean;
18
18
  /** Show export buttons */
19
19
  showExport?: boolean;
20
+ /** Show JSON export button (requires showExport=true) */
21
+ showJsonExport?: boolean;
20
22
  /** Technology color mapping */
21
23
  techColors?: TechColorMap;
22
24
  /** Status color mapping */
@@ -33,6 +35,8 @@ interface Props {
33
35
  persistSettings?: boolean;
34
36
  /** Storage key prefix for persisted settings */
35
37
  storageKey?: string;
38
+ /** Show scrollspy navigation bar for quick group navigation */
39
+ showScrollSpy?: boolean;
36
40
  /** Bindable reference to table methods */
37
41
  tableRef?: {
38
42
  redraw: () => void;
@@ -16,6 +16,8 @@
16
16
  selectedCount?: number;
17
17
  /** Show export buttons */
18
18
  showExport?: boolean;
19
+ /** Show JSON export button (requires showExport=true) */
20
+ showJsonExport?: boolean;
19
21
  /** Show grouping dropdown */
20
22
  showGrouping?: boolean;
21
23
  /** Show preset dropdown */
@@ -46,6 +48,12 @@
46
48
  oncolumnvisibilitychange?: (field: string, visible: boolean) => void;
47
49
  /** Reset columns to preset default */
48
50
  onresetcolumns?: () => void;
51
+ /** Whether scrollspy is enabled */
52
+ scrollSpyEnabled?: boolean;
53
+ /** Show scrollspy toggle button */
54
+ showScrollSpyToggle?: boolean;
55
+ /** Toggle scrollspy event */
56
+ ontogglescrollspy?: () => void;
49
57
  }
50
58
 
51
59
  let {
@@ -55,6 +63,7 @@
55
63
  filteredCount = 0,
56
64
  selectedCount = 0,
57
65
  showExport = true,
66
+ showJsonExport = false,
58
67
  showGrouping = true,
59
68
  showPresets = true,
60
69
  ongroupchange,
@@ -69,7 +78,10 @@
69
78
  columnMeta = [],
70
79
  visibleColumns = [],
71
80
  oncolumnvisibilitychange,
72
- onresetcolumns
81
+ onresetcolumns,
82
+ scrollSpyEnabled = false,
83
+ showScrollSpyToggle = false,
84
+ ontogglescrollspy
73
85
  }: Props = $props();
74
86
 
75
87
  const groupOptions: { value: CellTableGroupField; label: string }[] = [
@@ -192,6 +204,19 @@
192
204
 
193
205
  <!-- Actions -->
194
206
  <div class="toolbar-actions d-flex align-items-center gap-2">
207
+ {#if showScrollSpyToggle}
208
+ <button
209
+ type="button"
210
+ class="btn btn-sm"
211
+ class:btn-outline-secondary={!scrollSpyEnabled}
212
+ class:btn-secondary={scrollSpyEnabled}
213
+ onclick={ontogglescrollspy}
214
+ title={scrollSpyEnabled ? 'Hide quick navigation' : 'Show quick navigation'}
215
+ aria-label={scrollSpyEnabled ? 'Hide quick navigation' : 'Show quick navigation'}
216
+ >
217
+ <i class="bi bi-signpost-split"></i>
218
+ </button>
219
+ {/if}
195
220
  {#if ontogglefilters}
196
221
  <button
197
222
  type="button"
@@ -228,15 +253,17 @@
228
253
  <i class="bi bi-filetype-csv"></i>
229
254
  <span class="d-none d-md-inline ms-1">CSV</span>
230
255
  </button>
231
- <button
232
- type="button"
233
- class="btn btn-sm btn-outline-primary"
234
- onclick={onexportjson}
235
- title="Export to JSON"
236
- >
237
- <i class="bi bi-filetype-json"></i>
238
- <span class="d-none d-md-inline ms-1">JSON</span>
239
- </button>
256
+ {#if showJsonExport}
257
+ <button
258
+ type="button"
259
+ class="btn btn-sm btn-outline-primary"
260
+ onclick={onexportjson}
261
+ title="Export to JSON"
262
+ >
263
+ <i class="bi bi-filetype-json"></i>
264
+ <span class="d-none d-md-inline ms-1">JSON</span>
265
+ </button>
266
+ {/if}
240
267
  </div>
241
268
  {/if}
242
269
  </div>
@@ -13,6 +13,8 @@ interface Props {
13
13
  selectedCount?: number;
14
14
  /** Show export buttons */
15
15
  showExport?: boolean;
16
+ /** Show JSON export button (requires showExport=true) */
17
+ showJsonExport?: boolean;
16
18
  /** Show grouping dropdown */
17
19
  showGrouping?: boolean;
18
20
  /** Show preset dropdown */
@@ -43,6 +45,12 @@ interface Props {
43
45
  oncolumnvisibilitychange?: (field: string, visible: boolean) => void;
44
46
  /** Reset columns to preset default */
45
47
  onresetcolumns?: () => void;
48
+ /** Whether scrollspy is enabled */
49
+ scrollSpyEnabled?: boolean;
50
+ /** Show scrollspy toggle button */
51
+ showScrollSpyToggle?: boolean;
52
+ /** Toggle scrollspy event */
53
+ ontogglescrollspy?: () => void;
46
54
  }
47
55
  declare const CellTableToolbar: import("svelte").Component<Props, {}, "">;
48
56
  type CellTableToolbar = ReturnType<typeof CellTableToolbar>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.112",
3
+ "version": "0.0.114",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",