@smartnet360/svelte-components 0.0.103 → 0.0.105

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.
@@ -0,0 +1,456 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import { TabulatorFull as Tabulator } from 'tabulator-tables';
4
+ // Import Bootstrap 5 theme for proper styling
5
+ import 'tabulator-tables/dist/css/tabulator_bootstrap5.min.css';
6
+ import type { Options, RowComponent } from 'tabulator-tables';
7
+ import type {
8
+ CellTableProps,
9
+ CellData,
10
+ RowSelectionEvent,
11
+ RowClickEvent,
12
+ RowDblClickEvent,
13
+ DataChangeEvent
14
+ } from './types';
15
+ import {
16
+ getColumnsForPreset,
17
+ getGroupHeaderFormatter,
18
+ DEFAULT_TECH_COLORS,
19
+ DEFAULT_STATUS_COLORS
20
+ } from './column-config';
21
+
22
+ interface Props extends CellTableProps {
23
+ /** Row selection change event */
24
+ onselectionchange?: (event: RowSelectionEvent) => void;
25
+ /** Row click event */
26
+ onrowclick?: (event: RowClickEvent) => void;
27
+ /** Row double-click event */
28
+ onrowdblclick?: (event: RowDblClickEvent) => void;
29
+ /** Data change event (filter, sort, etc.) */
30
+ ondatachange?: (event: DataChangeEvent) => void;
31
+ }
32
+
33
+ let {
34
+ cells = [],
35
+ groupBy = 'none',
36
+ columnPreset = 'default',
37
+ columnVisibility,
38
+ selectable = false,
39
+ multiSelect = true,
40
+ height = '100%',
41
+ virtualDom = true,
42
+ tabulatorOptions,
43
+ techColors = DEFAULT_TECH_COLORS,
44
+ statusColors = DEFAULT_STATUS_COLORS,
45
+ headerFilters = true,
46
+ resizableColumns = true,
47
+ movableColumns = true,
48
+ persistLayout = false,
49
+ storageKey = 'cell-table-layout',
50
+ onselectionchange,
51
+ onrowclick,
52
+ onrowdblclick,
53
+ ondatachange
54
+ }: Props = $props();
55
+
56
+ let tableContainer: HTMLDivElement;
57
+ let table: Tabulator | null = null;
58
+ let isInitialized = $state(false);
59
+
60
+ // Reactive column configuration
61
+ let columns = $derived(getColumnsForPreset(columnPreset, techColors, statusColors, headerFilters));
62
+
63
+ // Build Tabulator options
64
+ function buildOptions(): Options {
65
+ const baseOptions: Options = {
66
+ data: cells,
67
+ columns: columns,
68
+ layout: 'fitDataFill',
69
+ height: height,
70
+ placeholder: 'No cells to display',
71
+
72
+ // Virtual DOM for performance with large datasets
73
+ renderVertical: virtualDom ? 'virtual' : 'basic',
74
+
75
+ // Interactivity
76
+ resizableColumns: resizableColumns,
77
+ movableColumns: movableColumns,
78
+
79
+ // Sorting
80
+ initialSort: [{ column: 'siteId', dir: 'asc' }],
81
+
82
+ // Row selection
83
+ selectable: selectable ? (multiSelect ? true : 1) : false,
84
+ selectableRangeMode: 'click',
85
+
86
+ // Persistence
87
+ persistence: persistLayout ? {
88
+ sort: true,
89
+ filter: true,
90
+ columns: ['width', 'visible'],
91
+ } : false,
92
+ persistenceID: persistLayout ? storageKey : undefined,
93
+
94
+ // Grouping
95
+ ...(groupBy !== 'none' ? {
96
+ groupBy: groupBy,
97
+ groupStartOpen: true,
98
+ groupHeader: getGroupHeaderFormatter(groupBy),
99
+ groupToggleElement: 'header',
100
+ } : {}),
101
+
102
+ // Pagination (optional, disabled by default for virtual scrolling)
103
+ // pagination: true,
104
+ // paginationSize: 50,
105
+
106
+ // Clipboard
107
+ clipboard: true,
108
+ clipboardCopyRowRange: 'selected',
109
+ };
110
+
111
+ // Merge with custom options
112
+ return { ...baseOptions, ...tabulatorOptions };
113
+ }
114
+
115
+ // Initialize table
116
+ function initTable() {
117
+ if (!tableContainer) return;
118
+
119
+ const options = buildOptions();
120
+ table = new Tabulator(tableContainer, options);
121
+
122
+ // Bind events
123
+ table.on('rowClick', (e, row) => {
124
+ if (onrowclick) {
125
+ onrowclick({
126
+ row: (row as RowComponent).getData() as CellData,
127
+ event: e as MouseEvent
128
+ });
129
+ }
130
+ });
131
+
132
+ table.on('rowDblClick', (e, row) => {
133
+ if (onrowdblclick) {
134
+ onrowdblclick({
135
+ row: (row as RowComponent).getData() as CellData,
136
+ event: e as MouseEvent
137
+ });
138
+ }
139
+ });
140
+
141
+ table.on('rowSelectionChanged', (data, rows) => {
142
+ if (onselectionchange) {
143
+ const cellData = data as CellData[];
144
+ onselectionchange({
145
+ rows: cellData,
146
+ ids: cellData.map(d => d.id)
147
+ });
148
+ }
149
+ });
150
+
151
+ table.on('dataFiltered', (filters, rows) => {
152
+ if (ondatachange) {
153
+ ondatachange({
154
+ type: 'filter',
155
+ rowCount: cells.length,
156
+ filteredCount: (rows as RowComponent[]).length
157
+ });
158
+ }
159
+ });
160
+
161
+ table.on('dataSorted', () => {
162
+ if (ondatachange) {
163
+ ondatachange({
164
+ type: 'sort',
165
+ rowCount: cells.length,
166
+ filteredCount: table?.getDataCount('active') ?? 0
167
+ });
168
+ }
169
+ });
170
+
171
+ // Mark as initialized after table is ready
172
+ table.on('tableBuilt', () => {
173
+ isInitialized = true;
174
+ });
175
+ }
176
+
177
+ // Update table data when cells change
178
+ $effect(() => {
179
+ if (isInitialized && table && cells) {
180
+ table.replaceData(cells);
181
+ if (ondatachange) {
182
+ ondatachange({
183
+ type: 'load',
184
+ rowCount: cells.length,
185
+ filteredCount: cells.length
186
+ });
187
+ }
188
+ }
189
+ });
190
+
191
+ // Update grouping when groupBy changes
192
+ $effect(() => {
193
+ if (isInitialized && table) {
194
+ if (groupBy === 'none') {
195
+ table.setGroupBy(false);
196
+ } else {
197
+ table.setGroupBy(groupBy);
198
+ table.setGroupHeader(getGroupHeaderFormatter(groupBy));
199
+ }
200
+ // Force redraw after grouping change
201
+ table.redraw(true);
202
+ }
203
+ });
204
+
205
+ // Update columns when preset changes
206
+ $effect(() => {
207
+ if (isInitialized && table && columns) {
208
+ table.setColumns(columns);
209
+ table.redraw(true);
210
+ }
211
+ });
212
+
213
+ onMount(() => {
214
+ initTable();
215
+ });
216
+
217
+ onDestroy(() => {
218
+ if (table) {
219
+ table.destroy();
220
+ table = null;
221
+ }
222
+ });
223
+
224
+ // Public API methods
225
+ export function getTable(): Tabulator | null {
226
+ return table;
227
+ }
228
+
229
+ export function getSelectedRows(): CellData[] {
230
+ return table?.getSelectedData() as CellData[] ?? [];
231
+ }
232
+
233
+ export function clearSelection(): void {
234
+ table?.deselectRow();
235
+ }
236
+
237
+ export function selectRow(id: string): void {
238
+ const row = table?.getRow(id);
239
+ if (row) row.select();
240
+ }
241
+
242
+ export function scrollToRow(id: string): void {
243
+ table?.scrollToRow(id, 'top', true);
244
+ }
245
+
246
+ export function downloadCSV(filename: string = 'cells.csv'): void {
247
+ table?.download('csv', filename);
248
+ }
249
+
250
+ export function downloadJSON(filename: string = 'cells.json'): void {
251
+ table?.download('json', filename);
252
+ }
253
+
254
+ export function setFilter(field: string, type: string, value: unknown): void {
255
+ table?.setFilter(field, type as any, value);
256
+ }
257
+
258
+ export function clearFilters(): void {
259
+ table?.clearFilter();
260
+ }
261
+
262
+ export function redraw(): void {
263
+ table?.redraw(true);
264
+ }
265
+ </script>
266
+
267
+ <div class="cell-table-container">
268
+ <div bind:this={tableContainer} class="cell-table"></div>
269
+ </div>
270
+
271
+ <style>
272
+ .cell-table-container {
273
+ width: 100%;
274
+ height: 100%;
275
+ display: flex;
276
+ flex-direction: column;
277
+ }
278
+
279
+ .cell-table {
280
+ flex: 1;
281
+ min-height: 0;
282
+ }
283
+
284
+ /* Bootstrap-aligned Tabulator theme overrides */
285
+ :global(.cell-table-container .tabulator) {
286
+ font-family: var(--bs-body-font-family, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
287
+ font-size: 0.875rem;
288
+ border: 1px solid var(--bs-border-color, #dee2e6);
289
+ border-radius: var(--bs-border-radius, 0.375rem);
290
+ background-color: var(--bs-body-bg, #fff);
291
+ }
292
+
293
+ /* Header styling */
294
+ :global(.cell-table-container .tabulator-header) {
295
+ background-color: var(--bs-tertiary-bg, #f8f9fa);
296
+ border-bottom: 2px solid var(--bs-border-color, #dee2e6);
297
+ }
298
+
299
+ :global(.cell-table-container .tabulator-col) {
300
+ background-color: var(--bs-tertiary-bg, #f8f9fa);
301
+ border-right: 1px solid var(--bs-border-color, #dee2e6);
302
+ }
303
+
304
+ :global(.cell-table-container .tabulator-col-title) {
305
+ font-weight: 600;
306
+ color: var(--bs-body-color, #212529);
307
+ padding: 0.5rem 0.75rem;
308
+ }
309
+
310
+ :global(.cell-table-container .tabulator-col-sorter) {
311
+ color: var(--bs-secondary-color, #6c757d);
312
+ }
313
+
314
+ /* Header filter inputs */
315
+ :global(.cell-table-container .tabulator-header-filter input) {
316
+ font-size: 0.75rem;
317
+ padding: 0.25rem 0.5rem;
318
+ border: 1px solid var(--bs-border-color, #dee2e6);
319
+ border-radius: var(--bs-border-radius-sm, 0.25rem);
320
+ background-color: var(--bs-body-bg, #fff);
321
+ color: var(--bs-body-color, #212529);
322
+ }
323
+
324
+ :global(.cell-table-container .tabulator-header-filter input:focus) {
325
+ border-color: var(--bs-primary, #0d6efd);
326
+ outline: 0;
327
+ box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
328
+ }
329
+
330
+ /* Row styling */
331
+ :global(.cell-table-container .tabulator-row) {
332
+ border-bottom: 1px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.1));
333
+ }
334
+
335
+ :global(.cell-table-container .tabulator-row:hover) {
336
+ background-color: var(--bs-tertiary-bg, #f8f9fa);
337
+ }
338
+
339
+ :global(.cell-table-container .tabulator-row.tabulator-row-even) {
340
+ background-color: var(--bs-body-bg, #fff);
341
+ }
342
+
343
+ :global(.cell-table-container .tabulator-row.tabulator-row-odd) {
344
+ background-color: rgba(var(--bs-tertiary-bg-rgb, 248, 249, 250), 0.5);
345
+ }
346
+
347
+ :global(.cell-table-container .tabulator-row.tabulator-selected) {
348
+ background-color: rgba(var(--bs-primary-rgb, 13, 110, 253), 0.1);
349
+ }
350
+
351
+ :global(.cell-table-container .tabulator-row.tabulator-selected:hover) {
352
+ background-color: rgba(var(--bs-primary-rgb, 13, 110, 253), 0.15);
353
+ }
354
+
355
+ /* Cell styling */
356
+ :global(.cell-table-container .tabulator-cell) {
357
+ padding: 0.5rem 0.75rem;
358
+ border-right: 1px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.05));
359
+ color: var(--bs-body-color, #212529);
360
+ }
361
+
362
+ :global(.cell-table-container .tabulator-cell.tabulator-frozen) {
363
+ background-color: var(--bs-tertiary-bg, #f8f9fa);
364
+ border-right: 2px solid var(--bs-border-color, #dee2e6);
365
+ }
366
+
367
+ /* Group header styling */
368
+ :global(.cell-table-container .tabulator-row.tabulator-group) {
369
+ background-color: var(--bs-secondary-bg, #e9ecef);
370
+ border-bottom: 1px solid var(--bs-border-color, #dee2e6);
371
+ font-weight: 500;
372
+ min-height: 36px;
373
+ }
374
+
375
+ :global(.cell-table-container .tabulator-row.tabulator-group:hover) {
376
+ background-color: var(--bs-secondary-bg, #e9ecef);
377
+ cursor: pointer;
378
+ }
379
+
380
+ :global(.cell-table-container .tabulator-row.tabulator-group span) {
381
+ color: var(--bs-body-color, #212529);
382
+ }
383
+
384
+ /* Group toggle arrow */
385
+ :global(.cell-table-container .tabulator-group-toggle) {
386
+ margin-right: 0.5rem;
387
+ }
388
+
389
+ /* Scrollbar styling */
390
+ :global(.cell-table-container .tabulator-tableholder::-webkit-scrollbar) {
391
+ width: 8px;
392
+ height: 8px;
393
+ }
394
+
395
+ :global(.cell-table-container .tabulator-tableholder::-webkit-scrollbar-track) {
396
+ background: var(--bs-tertiary-bg, #f8f9fa);
397
+ }
398
+
399
+ :global(.cell-table-container .tabulator-tableholder::-webkit-scrollbar-thumb) {
400
+ background: var(--bs-secondary-color, #6c757d);
401
+ border-radius: 4px;
402
+ }
403
+
404
+ :global(.cell-table-container .tabulator-tableholder::-webkit-scrollbar-thumb:hover) {
405
+ background: var(--bs-body-color, #212529);
406
+ }
407
+
408
+ /* Placeholder */
409
+ :global(.cell-table-container .tabulator-placeholder) {
410
+ color: var(--bs-secondary-color, #6c757d);
411
+ font-style: italic;
412
+ }
413
+
414
+ /* Resize handle */
415
+ :global(.cell-table-container .tabulator-col-resize-handle) {
416
+ width: 6px;
417
+ right: 0;
418
+ }
419
+
420
+ :global(.cell-table-container .tabulator-col-resize-handle:hover) {
421
+ background-color: var(--bs-primary, #0d6efd);
422
+ }
423
+
424
+ /* Footer/pagination (if enabled) */
425
+ :global(.cell-table-container .tabulator-footer) {
426
+ background-color: var(--bs-tertiary-bg, #f8f9fa);
427
+ border-top: 1px solid var(--bs-border-color, #dee2e6);
428
+ padding: 0.5rem;
429
+ }
430
+
431
+ :global(.cell-table-container .tabulator-page) {
432
+ padding: 0.25rem 0.5rem;
433
+ margin: 0 0.125rem;
434
+ border: 1px solid var(--bs-border-color, #dee2e6);
435
+ border-radius: var(--bs-border-radius-sm, 0.25rem);
436
+ background-color: var(--bs-body-bg, #fff);
437
+ color: var(--bs-body-color, #212529);
438
+ }
439
+
440
+ :global(.cell-table-container .tabulator-page:hover) {
441
+ background-color: var(--bs-tertiary-bg, #f8f9fa);
442
+ }
443
+
444
+ :global(.cell-table-container .tabulator-page.active) {
445
+ background-color: var(--bs-primary, #0d6efd);
446
+ border-color: var(--bs-primary, #0d6efd);
447
+ color: white;
448
+ }
449
+
450
+ /* Badge styling in cells */
451
+ :global(.cell-table-container .badge) {
452
+ font-weight: 500;
453
+ padding: 0.25em 0.5em;
454
+ border-radius: var(--bs-border-radius-sm, 0.25rem);
455
+ }
456
+ </style>
@@ -0,0 +1,27 @@
1
+ import { TabulatorFull as Tabulator } from 'tabulator-tables';
2
+ import 'tabulator-tables/dist/css/tabulator_bootstrap5.min.css';
3
+ import type { CellTableProps, CellData, RowSelectionEvent, RowClickEvent, RowDblClickEvent, DataChangeEvent } from './types';
4
+ interface Props extends CellTableProps {
5
+ /** Row selection change event */
6
+ onselectionchange?: (event: RowSelectionEvent) => void;
7
+ /** Row click event */
8
+ onrowclick?: (event: RowClickEvent) => void;
9
+ /** Row double-click event */
10
+ onrowdblclick?: (event: RowDblClickEvent) => void;
11
+ /** Data change event (filter, sort, etc.) */
12
+ ondatachange?: (event: DataChangeEvent) => void;
13
+ }
14
+ declare const CellTable: import("svelte").Component<Props, {
15
+ getTable: () => Tabulator | null;
16
+ getSelectedRows: () => CellData[];
17
+ clearSelection: () => void;
18
+ selectRow: (id: string) => void;
19
+ scrollToRow: (id: string) => void;
20
+ downloadCSV: (filename?: string) => void;
21
+ downloadJSON: (filename?: string) => void;
22
+ setFilter: (field: string, type: string, value: unknown) => void;
23
+ clearFilters: () => void;
24
+ redraw: () => void;
25
+ }, "">;
26
+ type CellTable = ReturnType<typeof CellTable>;
27
+ export default CellTable;
@@ -0,0 +1,145 @@
1
+ <script lang="ts">
2
+ import CellTablePanel from './CellTablePanel.svelte';
3
+ import type { CellTableGroupField, ColumnPreset, RowClickEvent, RowSelectionEvent, CellData } from './types';
4
+ import { demoCells } from './demo-data';
5
+
6
+ let groupBy: CellTableGroupField = $state('none');
7
+ let columnPreset: ColumnPreset = $state('default');
8
+ let lastClickedCell: CellData | null = $state(null);
9
+ let selectedIds: string[] = $state([]);
10
+
11
+ function handleRowClick(event: RowClickEvent) {
12
+ lastClickedCell = event.row;
13
+ console.log('Row clicked:', event.row);
14
+ }
15
+
16
+ function handleSelectionChange(event: RowSelectionEvent) {
17
+ selectedIds = event.ids;
18
+ console.log('Selection changed:', event.ids);
19
+ }
20
+ </script>
21
+
22
+ <div class="cell-table-demo container-fluid py-3">
23
+ <div class="row mb-3">
24
+ <div class="col">
25
+ <h2 class="d-flex align-items-center gap-2">
26
+ <i class="bi bi-table text-primary"></i>
27
+ CellTable Component Demo
28
+ </h2>
29
+ <p class="text-muted">
30
+ A Bootstrap-themed Tabulator wrapper for displaying cell data with grouping, filtering, and export capabilities.
31
+ </p>
32
+ </div>
33
+ </div>
34
+
35
+ <div class="row" style="height: calc(100vh - 200px);">
36
+ <div class="col-12 col-lg-9 h-100">
37
+ <CellTablePanel
38
+ cells={demoCells}
39
+ bind:groupBy
40
+ bind:columnPreset
41
+ selectable={true}
42
+ multiSelect={true}
43
+ title="Cell Data Table"
44
+ showToolbar={true}
45
+ showExport={true}
46
+ headerFilters={true}
47
+ onrowclick={handleRowClick}
48
+ onselectionchange={handleSelectionChange}
49
+ >
50
+ {#snippet footer({ selectedRows, selectedCount })}
51
+ <div class="d-flex align-items-center justify-content-between">
52
+ <span class="text-muted small">
53
+ {#if selectedCount > 0}
54
+ {selectedCount} cell(s) selected
55
+ {:else}
56
+ Click rows to select
57
+ {/if}
58
+ </span>
59
+ <div class="btn-group">
60
+ <button
61
+ type="button"
62
+ class="btn btn-sm btn-outline-primary"
63
+ disabled={selectedCount === 0}
64
+ onclick={() => console.log('Process:', selectedRows.map(r => r.id))}
65
+ >
66
+ <i class="bi bi-gear"></i> Process Selected
67
+ </button>
68
+ <button
69
+ type="button"
70
+ class="btn btn-sm btn-outline-secondary"
71
+ disabled={selectedCount === 0}
72
+ onclick={() => console.log('View:', selectedRows.map(r => r.id))}
73
+ >
74
+ <i class="bi bi-eye"></i> View Details
75
+ </button>
76
+ </div>
77
+ </div>
78
+ {/snippet}
79
+ </CellTablePanel>
80
+ </div>
81
+
82
+ <div class="col-12 col-lg-3 h-100 overflow-auto">
83
+ <div class="card h-100">
84
+ <div class="card-header">
85
+ <h6 class="mb-0">
86
+ <i class="bi bi-info-circle"></i> Cell Details
87
+ </h6>
88
+ </div>
89
+ <div class="card-body">
90
+ {#if lastClickedCell}
91
+ <dl class="row mb-0 small">
92
+ <dt class="col-5">ID</dt>
93
+ <dd class="col-7"><code>{lastClickedCell.id}</code></dd>
94
+
95
+ <dt class="col-5">Cell Name</dt>
96
+ <dd class="col-7">{lastClickedCell.cellName}</dd>
97
+
98
+ <dt class="col-5">Site</dt>
99
+ <dd class="col-7">{lastClickedCell.siteId}</dd>
100
+
101
+ <dt class="col-5">Technology</dt>
102
+ <dd class="col-7">
103
+ <span class="badge bg-secondary">{lastClickedCell.tech}</span>
104
+ </dd>
105
+
106
+ <dt class="col-5">Band</dt>
107
+ <dd class="col-7">
108
+ <span class="badge bg-info">{lastClickedCell.fband}</span>
109
+ </dd>
110
+
111
+ <dt class="col-5">Status</dt>
112
+ <dd class="col-7">
113
+ <span class="badge bg-success">{lastClickedCell.status.replace(/_/g, ' ')}</span>
114
+ </dd>
115
+
116
+ <dt class="col-5">Azimuth</dt>
117
+ <dd class="col-7">{lastClickedCell.azimuth}°</dd>
118
+
119
+ <dt class="col-5">Height</dt>
120
+ <dd class="col-7">{lastClickedCell.height}m</dd>
121
+
122
+ <dt class="col-5">Antenna</dt>
123
+ <dd class="col-7 text-truncate" title={lastClickedCell.antenna}>
124
+ {lastClickedCell.antenna}
125
+ </dd>
126
+
127
+ <dt class="col-5">Planner</dt>
128
+ <dd class="col-7">{lastClickedCell.planner}</dd>
129
+
130
+ {#if lastClickedCell.comment}
131
+ <dt class="col-5">Comment</dt>
132
+ <dd class="col-7 text-muted">{lastClickedCell.comment}</dd>
133
+ {/if}
134
+ </dl>
135
+ {:else}
136
+ <p class="text-muted text-center">
137
+ <i class="bi bi-hand-index"></i><br>
138
+ Click a row to see details
139
+ </p>
140
+ {/if}
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const CellTableDemo: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type CellTableDemo = ReturnType<typeof CellTableDemo>;
3
+ export default CellTableDemo;