@smartnet360/svelte-components 0.0.105 → 0.0.107

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.
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import CellTable from './CellTable.svelte';
4
4
  import CellTableToolbar from './CellTableToolbar.svelte';
5
+ import { getColumnMetadata, getPresetVisibleFields } from './column-config';
5
6
  import type {
6
7
  CellData,
7
8
  CellTableGroupField,
@@ -39,6 +40,12 @@
39
40
  headerFilters?: boolean;
40
41
  /** Panel title */
41
42
  title?: string;
43
+ /** Show details sidebar */
44
+ showDetailsSidebar?: boolean;
45
+ /** Sidebar width in pixels */
46
+ sidebarWidth?: number;
47
+ /** Bindable reference to table methods */
48
+ tableRef?: { redraw: () => void } | null;
42
49
  /** Row selection change event */
43
50
  onselectionchange?: (event: RowSelectionEvent) => void;
44
51
  /** Row click event */
@@ -49,6 +56,8 @@
49
56
  headerActions?: Snippet;
50
57
  /** Custom footer slot */
51
58
  footer?: Snippet<[{ selectedRows: CellData[]; selectedCount: number }]>;
59
+ /** Custom details sidebar content */
60
+ detailsContent?: Snippet<[{ cell: CellData | null; closeSidebar: () => void }]>;
52
61
  }
53
62
 
54
63
  let {
@@ -64,17 +73,44 @@
64
73
  statusColors,
65
74
  headerFilters = true,
66
75
  title = 'Cell Data',
76
+ showDetailsSidebar = false,
77
+ sidebarWidth = 320,
78
+ tableRef = $bindable(null),
67
79
  onselectionchange,
68
80
  onrowclick,
69
81
  onrowdblclick,
70
82
  headerActions,
71
- footer
83
+ footer,
84
+ detailsContent
72
85
  }: Props = $props();
73
86
 
74
87
  let cellTable: CellTable;
75
88
  let selectedCount = $state(0);
76
89
  let selectedRows = $state<CellData[]>([]);
77
90
  let filteredCount = $state(cells.length);
91
+ let sidebarOpen = $state(false);
92
+ let clickedCell: CellData | null = $state(null);
93
+ let tableRefSet = false;
94
+ let filtersVisible = $state(true);
95
+
96
+ // Column visibility management
97
+ const columnMeta = getColumnMetadata();
98
+ let visibleColumns = $state<string[]>(getPresetVisibleFields(columnPreset));
99
+
100
+ // Update visible columns when preset changes
101
+ $effect(() => {
102
+ visibleColumns = getPresetVisibleFields(columnPreset);
103
+ });
104
+
105
+ // Expose table methods via tableRef - only set once
106
+ $effect(() => {
107
+ if (cellTable && !tableRefSet) {
108
+ tableRefSet = true;
109
+ tableRef = {
110
+ redraw: () => cellTable?.redraw()
111
+ };
112
+ }
113
+ });
78
114
 
79
115
  function handleSelectionChange(event: RowSelectionEvent) {
80
116
  selectedCount = event.rows.length;
@@ -82,6 +118,15 @@
82
118
  onselectionchange?.(event);
83
119
  }
84
120
 
121
+ function handleRowClick(event: RowClickEvent) {
122
+ if (showDetailsSidebar) {
123
+ clickedCell = event.row;
124
+ sidebarOpen = true;
125
+ setTimeout(() => cellTable?.redraw(), 320);
126
+ }
127
+ onrowclick?.(event);
128
+ }
129
+
85
130
  function handleDataChange(event: DataChangeEvent) {
86
131
  filteredCount = event.filteredCount;
87
132
  }
@@ -106,6 +151,54 @@
106
151
  cellTable?.clearFilters();
107
152
  }
108
153
 
154
+ function handleCollapseAll() {
155
+ cellTable?.collapseAll();
156
+ }
157
+
158
+ function handleExpandAll() {
159
+ cellTable?.expandAll();
160
+ }
161
+
162
+ function handleToggleFilters() {
163
+ filtersVisible = !filtersVisible;
164
+ cellTable?.toggleHeaderFilters(filtersVisible);
165
+ }
166
+
167
+ function handleColumnVisibilityChange(field: string, visible: boolean) {
168
+ if (visible) {
169
+ if (!visibleColumns.includes(field)) {
170
+ visibleColumns = [...visibleColumns, field];
171
+ }
172
+ cellTable?.showColumn(field);
173
+ } else {
174
+ visibleColumns = visibleColumns.filter(f => f !== field);
175
+ cellTable?.hideColumn(field);
176
+ }
177
+ }
178
+
179
+ function handleResetColumns() {
180
+ const defaultFields = getPresetVisibleFields(columnPreset);
181
+ visibleColumns = defaultFields;
182
+ // Show/hide columns to match preset
183
+ columnMeta.forEach(col => {
184
+ if (defaultFields.includes(col.field)) {
185
+ cellTable?.showColumn(col.field);
186
+ } else {
187
+ cellTable?.hideColumn(col.field);
188
+ }
189
+ });
190
+ }
191
+
192
+ function toggleSidebar() {
193
+ sidebarOpen = !sidebarOpen;
194
+ setTimeout(() => cellTable?.redraw(), 320);
195
+ }
196
+
197
+ function closeSidebar() {
198
+ sidebarOpen = false;
199
+ setTimeout(() => cellTable?.redraw(), 320);
200
+ }
201
+
109
202
  // Expose table methods
110
203
  export function getSelectedRows(): CellData[] {
111
204
  return cellTable?.getSelectedRows() ?? [];
@@ -122,6 +215,11 @@
122
215
  export function redraw(): void {
123
216
  cellTable?.redraw();
124
217
  }
218
+
219
+ export function openSidebar(): void {
220
+ sidebarOpen = true;
221
+ setTimeout(() => cellTable?.redraw(), 320);
222
+ }
125
223
  </script>
126
224
 
127
225
  <div class="cell-table-panel d-flex flex-column" style:height>
@@ -131,11 +229,24 @@
131
229
  <i class="bi bi-table text-primary"></i>
132
230
  {title}
133
231
  </h6>
134
- {#if headerActions}
135
- <div class="header-actions">
136
- {@render headerActions()}
137
- </div>
138
- {/if}
232
+ <div class="d-flex align-items-center gap-2">
233
+ {#if headerActions}
234
+ <div class="header-actions">
235
+ {@render headerActions()}
236
+ </div>
237
+ {/if}
238
+ {#if showDetailsSidebar}
239
+ <button
240
+ class="btn btn-sm"
241
+ class:btn-outline-secondary={!sidebarOpen}
242
+ class:btn-secondary={sidebarOpen}
243
+ onclick={toggleSidebar}
244
+ title={sidebarOpen ? 'Hide details' : 'Show details'}
245
+ >
246
+ <i class="bi" class:bi-layout-sidebar-reverse={!sidebarOpen} class:bi-x-lg={sidebarOpen}></i>
247
+ </button>
248
+ {/if}
249
+ </div>
139
250
  </div>
140
251
 
141
252
  <!-- Toolbar -->
@@ -152,27 +263,167 @@
152
263
  onexportcsv={handleExportCSV}
153
264
  onexportjson={handleExportJSON}
154
265
  onclearfilters={handleClearFilters}
266
+ oncollapseall={handleCollapseAll}
267
+ onexpandall={handleExpandAll}
268
+ {filtersVisible}
269
+ ontogglefilters={handleToggleFilters}
270
+ {columnMeta}
271
+ {visibleColumns}
272
+ oncolumnvisibilitychange={handleColumnVisibilityChange}
273
+ onresetcolumns={handleResetColumns}
155
274
  />
156
275
  {/if}
157
276
 
158
- <!-- Table -->
159
- <div class="table-wrapper flex-grow-1">
160
- <CellTable
161
- bind:this={cellTable}
162
- {cells}
163
- {groupBy}
164
- {columnPreset}
165
- {selectable}
166
- {multiSelect}
167
- height="100%"
168
- {techColors}
169
- {statusColors}
170
- {headerFilters}
171
- onselectionchange={handleSelectionChange}
172
- ondatachange={handleDataChange}
173
- {onrowclick}
174
- {onrowdblclick}
175
- />
277
+ <!-- Main content with optional sidebar -->
278
+ <div class="content-area d-flex flex-grow-1 overflow-hidden">
279
+ <!-- Table -->
280
+ <div class="table-wrapper flex-grow-1 overflow-hidden">
281
+ <CellTable
282
+ bind:this={cellTable}
283
+ {cells}
284
+ {groupBy}
285
+ {columnPreset}
286
+ {selectable}
287
+ {multiSelect}
288
+ height="100%"
289
+ {techColors}
290
+ {statusColors}
291
+ {headerFilters}
292
+ onselectionchange={handleSelectionChange}
293
+ ondatachange={handleDataChange}
294
+ onrowclick={handleRowClick}
295
+ {onrowdblclick}
296
+ />
297
+ </div>
298
+
299
+ <!-- Details Sidebar -->
300
+ {#if showDetailsSidebar}
301
+ <aside
302
+ class="details-sidebar border-start bg-white overflow-auto"
303
+ class:open={sidebarOpen}
304
+ style:--sidebar-width="{sidebarWidth}px"
305
+ >
306
+ <div class="sidebar-content" style:width="{sidebarWidth}px">
307
+ <div class="d-flex align-items-center justify-content-between p-3 border-bottom bg-light sticky-top">
308
+ <h6 class="mb-0">
309
+ <i class="bi bi-info-circle text-primary"></i> Details
310
+ </h6>
311
+ <button
312
+ class="btn btn-sm btn-outline-secondary"
313
+ onclick={closeSidebar}
314
+ title="Close"
315
+ >
316
+ <i class="bi bi-x-lg"></i>
317
+ </button>
318
+ </div>
319
+
320
+ <div class="p-3">
321
+ {#if detailsContent}
322
+ {@render detailsContent({ cell: clickedCell, closeSidebar })}
323
+ {:else if clickedCell}
324
+ <!-- Default details view -->
325
+ <dl class="row mb-0 small">
326
+ <dt class="col-5 text-muted">ID</dt>
327
+ <dd class="col-7"><code class="text-primary">{clickedCell.id}</code></dd>
328
+
329
+ <dt class="col-5 text-muted">Cell Name</dt>
330
+ <dd class="col-7 fw-medium">{clickedCell.cellName}</dd>
331
+
332
+ <dt class="col-5 text-muted">Site</dt>
333
+ <dd class="col-7">{clickedCell.siteId}</dd>
334
+
335
+ <dt class="col-5 text-muted">Technology</dt>
336
+ <dd class="col-7">
337
+ <span class="badge bg-secondary">{clickedCell.tech}</span>
338
+ </dd>
339
+
340
+ <dt class="col-5 text-muted">Band</dt>
341
+ <dd class="col-7">
342
+ <span class="badge bg-info">{clickedCell.fband}</span>
343
+ </dd>
344
+
345
+ <dt class="col-5 text-muted">Frequency</dt>
346
+ <dd class="col-7">{clickedCell.frq} MHz</dd>
347
+
348
+ <dt class="col-5 text-muted">Status</dt>
349
+ <dd class="col-7">
350
+ <span class="badge bg-success">{clickedCell.status.replace(/_/g, ' ')}</span>
351
+ </dd>
352
+
353
+ <dt class="col-12 text-muted mt-2 mb-1 border-top pt-2">Physical</dt>
354
+
355
+ <dt class="col-5 text-muted">Azimuth</dt>
356
+ <dd class="col-7">{clickedCell.azimuth}°</dd>
357
+
358
+ <dt class="col-5 text-muted">Height</dt>
359
+ <dd class="col-7">{clickedCell.height}m</dd>
360
+
361
+ <dt class="col-5 text-muted">Beamwidth</dt>
362
+ <dd class="col-7">{clickedCell.beamwidth}°</dd>
363
+
364
+ <dt class="col-5 text-muted">E-Tilt</dt>
365
+ <dd class="col-7">{clickedCell.electricalTilt}°</dd>
366
+
367
+ <dt class="col-5 text-muted">Antenna</dt>
368
+ <dd class="col-7 text-truncate" title={clickedCell.antenna}>
369
+ {clickedCell.antenna}
370
+ </dd>
371
+
372
+ <dt class="col-12 text-muted mt-2 mb-1 border-top pt-2">Location</dt>
373
+
374
+ <dt class="col-5 text-muted">Latitude</dt>
375
+ <dd class="col-7">{clickedCell.latitude.toFixed(6)}</dd>
376
+
377
+ <dt class="col-5 text-muted">Longitude</dt>
378
+ <dd class="col-7">{clickedCell.longitude.toFixed(6)}</dd>
379
+
380
+ <dt class="col-12 text-muted mt-2 mb-1 border-top pt-2">Planning</dt>
381
+
382
+ <dt class="col-5 text-muted">Planner</dt>
383
+ <dd class="col-7">{clickedCell.planner}</dd>
384
+
385
+ <dt class="col-5 text-muted">On Air</dt>
386
+ <dd class="col-7">{clickedCell.onAirDate}</dd>
387
+
388
+ {#if clickedCell.comment}
389
+ <dt class="col-5 text-muted">Comment</dt>
390
+ <dd class="col-7 fst-italic">{clickedCell.comment}</dd>
391
+ {/if}
392
+
393
+ <!-- Dynamic Other Properties -->
394
+ {#if clickedCell.other && Object.keys(clickedCell.other).length > 0}
395
+ <dt class="col-12 text-muted mt-2 mb-1 border-top pt-2">Other</dt>
396
+
397
+ {#each Object.entries(clickedCell.other) as [key, value]}
398
+ <dt class="col-5 text-muted text-capitalize">{key.replace(/_/g, ' ')}</dt>
399
+ <dd class="col-7">
400
+ {#if value === null || value === undefined}
401
+ <span class="text-muted fst-italic">—</span>
402
+ {:else if typeof value === 'boolean'}
403
+ <span class="badge" class:bg-success={value} class:bg-secondary={!value}>
404
+ {value ? 'Yes' : 'No'}
405
+ </span>
406
+ {:else if typeof value === 'number'}
407
+ <code>{value}</code>
408
+ {:else if typeof value === 'object'}
409
+ <code class="small text-break">{JSON.stringify(value)}</code>
410
+ {:else}
411
+ {String(value)}
412
+ {/if}
413
+ </dd>
414
+ {/each}
415
+ {/if}
416
+ </dl>
417
+ {:else}
418
+ <div class="text-center text-muted py-5">
419
+ <i class="bi bi-hand-index fs-1 opacity-50"></i>
420
+ <p class="mt-2 mb-0">Click a row to see details</p>
421
+ </div>
422
+ {/if}
423
+ </div>
424
+ </div>
425
+ </aside>
426
+ {/if}
176
427
  </div>
177
428
 
178
429
  <!-- Footer -->
@@ -200,9 +451,26 @@
200
451
  color: var(--bs-body-color, #212529);
201
452
  }
202
453
 
454
+ .content-area {
455
+ min-height: 0;
456
+ }
457
+
203
458
  .table-wrapper {
204
459
  min-height: 0;
205
- overflow: hidden;
460
+ transition: all 0.3s ease;
461
+ }
462
+
463
+ .details-sidebar {
464
+ width: 0;
465
+ min-width: 0;
466
+ opacity: 0;
467
+ transition: width 0.3s ease, opacity 0.3s ease, min-width 0.3s ease;
468
+ }
469
+
470
+ .details-sidebar.open {
471
+ width: var(--sidebar-width, 320px);
472
+ min-width: var(--sidebar-width, 320px);
473
+ opacity: 1;
206
474
  }
207
475
 
208
476
  .panel-footer {
@@ -25,6 +25,14 @@ interface Props {
25
25
  headerFilters?: boolean;
26
26
  /** Panel title */
27
27
  title?: string;
28
+ /** Show details sidebar */
29
+ showDetailsSidebar?: boolean;
30
+ /** Sidebar width in pixels */
31
+ sidebarWidth?: number;
32
+ /** Bindable reference to table methods */
33
+ tableRef?: {
34
+ redraw: () => void;
35
+ } | null;
28
36
  /** Row selection change event */
29
37
  onselectionchange?: (event: RowSelectionEvent) => void;
30
38
  /** Row click event */
@@ -38,12 +46,18 @@ interface Props {
38
46
  selectedRows: CellData[];
39
47
  selectedCount: number;
40
48
  }]>;
49
+ /** Custom details sidebar content */
50
+ detailsContent?: Snippet<[{
51
+ cell: CellData | null;
52
+ closeSidebar: () => void;
53
+ }]>;
41
54
  }
42
55
  declare const CellTablePanel: import("svelte").Component<Props, {
43
56
  getSelectedRows: () => CellData[];
44
57
  clearSelection: () => void;
45
58
  scrollToRow: (id: string) => void;
46
59
  redraw: () => void;
47
- }, "groupBy" | "columnPreset">;
60
+ openSidebar: () => void;
61
+ }, "groupBy" | "columnPreset" | "tableRef">;
48
62
  type CellTablePanel = ReturnType<typeof CellTablePanel>;
49
63
  export default CellTablePanel;
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { CellTableGroupField, ColumnPreset } from './types';
3
+ import type { ColumnMeta } from './column-config';
4
+ import ColumnPicker from './ColumnPicker.svelte';
3
5
 
4
6
  interface Props {
5
7
  /** Current grouping field */
@@ -28,6 +30,22 @@
28
30
  onexportjson?: () => void;
29
31
  /** Clear filters event */
30
32
  onclearfilters?: () => void;
33
+ /** Collapse all groups event */
34
+ oncollapseall?: () => void;
35
+ /** Expand all groups event */
36
+ onexpandall?: () => void;
37
+ /** Whether header filters are visible */
38
+ filtersVisible?: boolean;
39
+ /** Toggle filters event */
40
+ ontogglefilters?: () => void;
41
+ /** All available columns metadata */
42
+ columnMeta?: ColumnMeta[];
43
+ /** Currently visible column fields */
44
+ visibleColumns?: string[];
45
+ /** Column visibility change event */
46
+ oncolumnvisibilitychange?: (field: string, visible: boolean) => void;
47
+ /** Reset columns to preset default */
48
+ onresetcolumns?: () => void;
31
49
  }
32
50
 
33
51
  let {
@@ -43,7 +61,15 @@
43
61
  onpresetchange,
44
62
  onexportcsv,
45
63
  onexportjson,
46
- onclearfilters
64
+ onclearfilters,
65
+ oncollapseall,
66
+ onexpandall,
67
+ filtersVisible = true,
68
+ ontogglefilters,
69
+ columnMeta = [],
70
+ visibleColumns = [],
71
+ oncolumnvisibilitychange,
72
+ onresetcolumns
47
73
  }: Props = $props();
48
74
 
49
75
  const groupOptions: { value: CellTableGroupField; label: string }[] = [
@@ -107,6 +133,28 @@
107
133
  <option value={option.value}>{option.label}</option>
108
134
  {/each}
109
135
  </select>
136
+ {#if groupBy !== 'none'}
137
+ <div class="btn-group ms-2">
138
+ <button
139
+ type="button"
140
+ class="btn btn-sm btn-outline-secondary"
141
+ onclick={oncollapseall}
142
+ title="Collapse all groups"
143
+ aria-label="Collapse all groups"
144
+ >
145
+ <i class="bi bi-chevron-contract"></i>
146
+ </button>
147
+ <button
148
+ type="button"
149
+ class="btn btn-sm btn-outline-secondary"
150
+ onclick={onexpandall}
151
+ title="Expand all groups"
152
+ aria-label="Expand all groups"
153
+ >
154
+ <i class="bi bi-chevron-expand"></i>
155
+ </button>
156
+ </div>
157
+ {/if}
110
158
  </div>
111
159
  {/if}
112
160
 
@@ -127,6 +175,15 @@
127
175
  <option value={option.value}>{option.label}</option>
128
176
  {/each}
129
177
  </select>
178
+ {#if columnMeta.length > 0}
179
+ <ColumnPicker
180
+ columns={columnMeta}
181
+ {visibleColumns}
182
+ presetName={presetOptions.find(p => p.value === columnPreset)?.label ?? 'Default'}
183
+ onchange={oncolumnvisibilitychange}
184
+ onreset={onresetcolumns}
185
+ />
186
+ {/if}
130
187
  </div>
131
188
  {/if}
132
189
 
@@ -134,6 +191,19 @@
134
191
 
135
192
  <!-- Actions -->
136
193
  <div class="toolbar-actions d-flex align-items-center gap-2">
194
+ {#if ontogglefilters}
195
+ <button
196
+ type="button"
197
+ class="btn btn-sm"
198
+ class:btn-outline-secondary={!filtersVisible}
199
+ class:btn-secondary={filtersVisible}
200
+ onclick={ontogglefilters}
201
+ title={filtersVisible ? 'Hide filters' : 'Show filters'}
202
+ aria-label={filtersVisible ? 'Hide filters' : 'Show filters'}
203
+ >
204
+ <i class="bi bi-funnel"></i>
205
+ </button>
206
+ {/if}
137
207
  {#if onclearfilters}
138
208
  <button
139
209
  type="button"
@@ -1,4 +1,5 @@
1
1
  import type { CellTableGroupField, ColumnPreset } from './types';
2
+ import type { ColumnMeta } from './column-config';
2
3
  interface Props {
3
4
  /** Current grouping field */
4
5
  groupBy?: CellTableGroupField;
@@ -26,6 +27,22 @@ interface Props {
26
27
  onexportjson?: () => void;
27
28
  /** Clear filters event */
28
29
  onclearfilters?: () => void;
30
+ /** Collapse all groups event */
31
+ oncollapseall?: () => void;
32
+ /** Expand all groups event */
33
+ onexpandall?: () => void;
34
+ /** Whether header filters are visible */
35
+ filtersVisible?: boolean;
36
+ /** Toggle filters event */
37
+ ontogglefilters?: () => void;
38
+ /** All available columns metadata */
39
+ columnMeta?: ColumnMeta[];
40
+ /** Currently visible column fields */
41
+ visibleColumns?: string[];
42
+ /** Column visibility change event */
43
+ oncolumnvisibilitychange?: (field: string, visible: boolean) => void;
44
+ /** Reset columns to preset default */
45
+ onresetcolumns?: () => void;
29
46
  }
30
47
  declare const CellTableToolbar: import("svelte").Component<Props, {}, "">;
31
48
  type CellTableToolbar = ReturnType<typeof CellTableToolbar>;