@smartnet360/svelte-components 0.0.135 → 0.0.137

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.
@@ -1,833 +0,0 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import CellTable from './CellTable.svelte';
4
- import CellTableToolbar from './CellTableToolbar.svelte';
5
- import { getColumnMetadata, getPresetVisibleFields, DEFAULT_TECH_COLORS, DEFAULT_STATUS_COLORS, FBAND_COLORS } from './column-config';
6
- import type {
7
- CellData,
8
- CellTableGroupField,
9
- ColumnPreset,
10
- RowSelectionEvent,
11
- RowClickEvent,
12
- RowDblClickEvent,
13
- RowContextMenuEvent,
14
- DataChangeEvent,
15
- TechColorMap,
16
- StatusColorMap,
17
- GroupOption
18
- } from './types';
19
-
20
- interface Props {
21
- /** Cell data array to display */
22
- cells: CellData[];
23
- /** Initial grouping field */
24
- groupBy?: CellTableGroupField;
25
- /** Custom grouping options (overrides default tech/fband/status options) */
26
- groupOptions?: GroupOption[];
27
- /** Initial column preset */
28
- columnPreset?: ColumnPreset;
29
- /** Enable row selection */
30
- selectable?: boolean;
31
- /** Enable multi-row selection */
32
- multiSelect?: boolean;
33
- /** Panel height (CSS value) */
34
- height?: string;
35
- /** Show toolbar */
36
- showToolbar?: boolean;
37
- /** Show column presets dropdown and column picker (set false for simple tables) */
38
- showColumnPresets?: boolean;
39
- /** Show export buttons */
40
- showExport?: boolean;
41
- /** Show JSON export button (requires showExport=true) */
42
- showJsonExport?: boolean;
43
- /** Technology color mapping */
44
- techColors?: TechColorMap;
45
- /** Status color mapping */
46
- statusColors?: StatusColorMap;
47
- /** Enable header filters */
48
- headerFilters?: boolean;
49
- /** Panel title */
50
- title?: string;
51
- /** Show details sidebar */
52
- showDetailsSidebar?: boolean;
53
- /** Sidebar width in pixels */
54
- sidebarWidth?: number;
55
- /** Persist settings (groupBy, visibleColumns) to localStorage */
56
- persistSettings?: boolean;
57
- /** Storage key prefix for persisted settings */
58
- storageKey?: string;
59
- /** Enable Tabulator persistence for column widths, order, visibility */
60
- persistLayout?: boolean;
61
- /** Show scrollspy navigation bar for quick group navigation */
62
- showScrollSpy?: boolean;
63
- /** Bindable reference to table methods */
64
- tableRef?: { redraw: () => void } | null;
65
- /** Row selection change event */
66
- onselectionchange?: (event: RowSelectionEvent) => void;
67
- /** Row click event */
68
- onrowclick?: (event: RowClickEvent) => void;
69
- /** Row double-click event */
70
- onrowdblclick?: (event: RowDblClickEvent) => void;
71
- /** Custom header search slot (appears next to title) */
72
- headerSearch?: Snippet;
73
- /** Custom header actions slot */
74
- headerActions?: Snippet;
75
- /** Custom footer slot */
76
- footer?: Snippet<[{ selectedRows: CellData[]; selectedCount: number }]>;
77
- /** Custom details sidebar content */
78
- detailsContent?: Snippet<[{ cell: CellData | null; closeSidebar: () => void }]>;
79
- /** Custom context menu content (right-click on row) */
80
- contextMenu?: Snippet<[{ row: CellData; closeMenu: () => void }]>;
81
- }
82
-
83
- let {
84
- cells = [],
85
- groupBy = $bindable('none'),
86
- groupOptions,
87
- columnPreset = $bindable('default'),
88
- selectable = true,
89
- multiSelect = true,
90
- height = '100%',
91
- showToolbar = true,
92
- showColumnPresets = true,
93
- showExport = true,
94
- showJsonExport = false,
95
- techColors,
96
- statusColors,
97
- headerFilters = true,
98
- title = 'Cell Data',
99
- showDetailsSidebar = false,
100
- sidebarWidth = 320,
101
- persistSettings = true,
102
- storageKey = 'cell-table',
103
- persistLayout = true,
104
- showScrollSpy = false,
105
- tableRef = $bindable(null),
106
- onselectionchange,
107
- onrowclick,
108
- onrowdblclick,
109
- headerSearch,
110
- headerActions,
111
- footer,
112
- detailsContent,
113
- contextMenu
114
- }: Props = $props();
115
-
116
- // Context menu state
117
- let contextMenuVisible = $state(false);
118
- let contextMenuRow: CellData | null = $state(null);
119
- let contextMenuPosition = $state({ x: 0, y: 0 });
120
-
121
- // Storage keys
122
- const STORAGE_KEY_GROUP = `${storageKey}-groupBy`;
123
- const STORAGE_KEY_FILTERS = `${storageKey}-filtersVisible`;
124
- const STORAGE_KEY_SCROLLSPY = `${storageKey}-scrollSpyEnabled`;
125
- const STORAGE_KEY_PRESET = `${storageKey}-columnPreset`;
126
-
127
- // Per-preset column visibility storage key
128
- function getColumnsStorageKey(preset: ColumnPreset): string {
129
- return `${storageKey}-visibleColumns-${preset}`;
130
- }
131
-
132
- // Load persisted settings
133
- function loadPersistedSettings() {
134
- if (!persistSettings || typeof localStorage === 'undefined') return { filtersVisible: true, scrollSpyEnabled: showScrollSpy, preset: null };
135
-
136
- let filters = true;
137
- let scrollSpy = showScrollSpy;
138
- let preset: ColumnPreset | null = null;
139
-
140
- try {
141
- const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
142
- if (savedGroup) {
143
- groupBy = savedGroup as CellTableGroupField;
144
- }
145
-
146
- const savedPreset = localStorage.getItem(STORAGE_KEY_PRESET);
147
- if (savedPreset) {
148
- preset = savedPreset as ColumnPreset;
149
- }
150
-
151
- const savedFilters = localStorage.getItem(STORAGE_KEY_FILTERS);
152
- if (savedFilters !== null) {
153
- filters = savedFilters === 'true';
154
- }
155
-
156
- const savedScrollSpy = localStorage.getItem(STORAGE_KEY_SCROLLSPY);
157
- if (savedScrollSpy !== null) {
158
- scrollSpy = savedScrollSpy === 'true';
159
- }
160
- } catch (e) {
161
- console.warn('Failed to load CellTable settings:', e);
162
- }
163
- return { filtersVisible: filters, scrollSpyEnabled: scrollSpy, preset };
164
- }
165
-
166
- // Load columns for a specific preset
167
- function loadColumnsForPreset(preset: ColumnPreset): string[] {
168
- if (!persistSettings || typeof localStorage === 'undefined') {
169
- return getPresetVisibleFields(preset);
170
- }
171
- try {
172
- const saved = localStorage.getItem(getColumnsStorageKey(preset));
173
- if (saved) {
174
- return JSON.parse(saved) as string[];
175
- }
176
- } catch (e) {
177
- console.warn('Failed to load columns for preset:', e);
178
- }
179
- return getPresetVisibleFields(preset);
180
- }
181
-
182
- // Save group setting
183
- function saveGroupSetting(group: CellTableGroupField) {
184
- if (!persistSettings || typeof localStorage === 'undefined') return;
185
- try {
186
- localStorage.setItem(STORAGE_KEY_GROUP, group);
187
- } catch (e) {
188
- console.warn('Failed to save group setting:', e);
189
- }
190
- }
191
-
192
- // Save column visibility for current preset
193
- function saveColumnVisibility(columns: string[]) {
194
- if (!persistSettings || typeof localStorage === 'undefined') return;
195
- try {
196
- localStorage.setItem(getColumnsStorageKey(columnPreset), JSON.stringify(columns));
197
- } catch (e) {
198
- console.warn('Failed to save column visibility:', e);
199
- }
200
- }
201
-
202
- // Save filter visibility
203
- function saveFilterVisibility(visible: boolean) {
204
- if (!persistSettings || typeof localStorage === 'undefined') return;
205
- try {
206
- localStorage.setItem(STORAGE_KEY_FILTERS, String(visible));
207
- } catch (e) {
208
- console.warn('Failed to save filter visibility:', e);
209
- }
210
- }
211
-
212
- // Save scrollspy state
213
- function saveScrollSpyState(enabled: boolean) {
214
- if (!persistSettings || typeof localStorage === 'undefined') return;
215
- try {
216
- localStorage.setItem(STORAGE_KEY_SCROLLSPY, String(enabled));
217
- } catch (e) {
218
- console.warn('Failed to save scrollspy state:', e);
219
- }
220
- }
221
-
222
- // Save column preset
223
- function savePreset(preset: ColumnPreset) {
224
- if (!persistSettings || typeof localStorage === 'undefined') return;
225
- try {
226
- localStorage.setItem(STORAGE_KEY_PRESET, preset);
227
- } catch (e) {
228
- console.warn('Failed to save preset:', e);
229
- }
230
- }
231
-
232
- let cellTable: CellTable;
233
- let selectedCount = $state(0);
234
- let selectedRows = $state<CellData[]>([]);
235
- let filteredCount = $state(cells.length);
236
- let sidebarOpen = $state(false);
237
- let clickedCell: CellData | null = $state(null);
238
- let tableRefSet = false;
239
-
240
- // Column visibility management
241
- const columnMeta = getColumnMetadata();
242
-
243
- // Initialize from storage or defaults
244
- const persistedSettings = loadPersistedSettings();
245
- let filtersVisible = $state(persistedSettings.filtersVisible);
246
-
247
- // Apply persisted preset if available
248
- if (persistedSettings.preset) {
249
- columnPreset = persistedSettings.preset;
250
- }
251
-
252
- // Load columns for the current preset (with any saved customizations)
253
- let visibleColumns = $state<string[]>(loadColumnsForPreset(columnPreset));
254
-
255
- // ScrollSpy state - initialize from persisted settings
256
- let scrollSpyGroups = $state<{ key: string; count: number }[]>([]);
257
- let scrollSpyEnabled = $state(persistedSettings.scrollSpyEnabled);
258
-
259
- // Track preset changes to load per-preset columns
260
- let prevPreset: ColumnPreset | null = columnPreset;
261
-
262
- // Apply column visibility to Tabulator
263
- function applyColumnVisibility(columns: string[]) {
264
- if (!cellTable) return;
265
- columnMeta.forEach(col => {
266
- if (columns.includes(col.field)) {
267
- cellTable.showColumn(col.field);
268
- } else {
269
- cellTable.hideColumn(col.field);
270
- }
271
- });
272
- }
273
-
274
- // Update visible columns when preset changes - load saved customizations for that preset
275
- $effect(() => {
276
- if (columnPreset !== prevPreset) {
277
- prevPreset = columnPreset;
278
- // Load saved columns for this preset (or preset defaults if none saved)
279
- const newColumns = loadColumnsForPreset(columnPreset);
280
- visibleColumns = newColumns;
281
- // Apply to Tabulator
282
- applyColumnVisibility(newColumns);
283
- }
284
- });
285
-
286
- // Expose table methods via tableRef - only set once
287
- $effect(() => {
288
- if (cellTable && !tableRefSet) {
289
- tableRefSet = true;
290
- tableRef = {
291
- redraw: () => cellTable?.redraw()
292
- };
293
- // Apply persisted column visibility after table is ready
294
- setTimeout(() => applyColumnVisibility(visibleColumns), 100);
295
- // Apply persisted filter visibility after table is ready
296
- if (!filtersVisible) {
297
- setTimeout(() => cellTable?.toggleHeaderFilters(false), 100);
298
- }
299
- }
300
- });
301
-
302
- function handleSelectionChange(event: RowSelectionEvent) {
303
- selectedCount = event.rows.length;
304
- selectedRows = event.rows;
305
- onselectionchange?.(event);
306
- }
307
-
308
- function handleRowClick(event: RowClickEvent) {
309
- if (showDetailsSidebar) {
310
- clickedCell = event.row;
311
- sidebarOpen = true;
312
- setTimeout(() => cellTable?.redraw(), 320);
313
- }
314
- onrowclick?.(event);
315
- }
316
-
317
- function handleDataChange(event: DataChangeEvent) {
318
- filteredCount = event.filteredCount;
319
- // Update scrollspy groups when data changes
320
- updateScrollSpyGroups();
321
- }
322
-
323
- function handleRowContextMenu(event: RowContextMenuEvent) {
324
- if (contextMenu) {
325
- contextMenuRow = event.row;
326
- contextMenuPosition = { x: event.event.clientX, y: event.event.clientY };
327
- contextMenuVisible = true;
328
- }
329
- }
330
-
331
- function closeContextMenu() {
332
- contextMenuVisible = false;
333
- contextMenuRow = null;
334
- }
335
-
336
- function handleClickOutside(event: MouseEvent) {
337
- const target = event.target as HTMLElement;
338
- if (!target.closest('.context-menu-container')) {
339
- closeContextMenu();
340
- }
341
- }
342
-
343
- function updateScrollSpyGroups() {
344
- if (scrollSpyEnabled && groupBy !== 'none') {
345
- // Small delay to ensure table has updated
346
- setTimeout(() => {
347
- scrollSpyGroups = cellTable?.getGroups() ?? [];
348
- }, 50);
349
- } else {
350
- scrollSpyGroups = [];
351
- }
352
- }
353
-
354
- function handleScrollToGroup(key: string) {
355
- cellTable?.scrollToGroup(key);
356
- }
357
-
358
- function handleToggleScrollSpy() {
359
- scrollSpyEnabled = !scrollSpyEnabled;
360
- saveScrollSpyState(scrollSpyEnabled);
361
- if (scrollSpyEnabled && groupBy !== 'none') {
362
- updateScrollSpyGroups();
363
- } else {
364
- scrollSpyGroups = [];
365
- }
366
- }
367
-
368
- function handleGroupChange(group: CellTableGroupField) {
369
- groupBy = group;
370
- saveGroupSetting(group);
371
- // Update scrollspy groups after grouping changes
372
- if (scrollSpyEnabled) {
373
- setTimeout(() => updateScrollSpyGroups(), 100);
374
- }
375
- }
376
-
377
- function handlePresetChange(preset: ColumnPreset) {
378
- columnPreset = preset;
379
- savePreset(preset);
380
- }
381
-
382
- function handleExportCSV() {
383
- cellTable?.downloadCSV(`cells-${new Date().toISOString().slice(0, 10)}.csv`);
384
- }
385
-
386
- function handleExportJSON() {
387
- cellTable?.downloadJSON(`cells-${new Date().toISOString().slice(0, 10)}.json`);
388
- }
389
-
390
- function handleClearFilters() {
391
- cellTable?.clearFilters();
392
- }
393
-
394
- function handleCollapseAll() {
395
- cellTable?.collapseAll();
396
- }
397
-
398
- function handleExpandAll() {
399
- cellTable?.expandAll();
400
- }
401
-
402
- function handleToggleFilters() {
403
- filtersVisible = !filtersVisible;
404
- cellTable?.toggleHeaderFilters(filtersVisible);
405
- saveFilterVisibility(filtersVisible);
406
- }
407
-
408
- function handleColumnVisibilityChange(field: string, visible: boolean) {
409
- if (visible) {
410
- if (!visibleColumns.includes(field)) {
411
- visibleColumns = [...visibleColumns, field];
412
- }
413
- cellTable?.showColumn(field);
414
- } else {
415
- visibleColumns = visibleColumns.filter(f => f !== field);
416
- cellTable?.hideColumn(field);
417
- }
418
- saveColumnVisibility(visibleColumns);
419
- }
420
-
421
- function handleResetColumns() {
422
- const defaultFields = getPresetVisibleFields(columnPreset);
423
- visibleColumns = defaultFields;
424
- // Show/hide columns to match preset
425
- columnMeta.forEach(col => {
426
- if (defaultFields.includes(col.field)) {
427
- cellTable?.showColumn(col.field);
428
- } else {
429
- cellTable?.hideColumn(col.field);
430
- }
431
- });
432
- // Clear persisted column visibility for this preset (use preset defaults)
433
- if (persistSettings && typeof localStorage !== 'undefined') {
434
- try {
435
- localStorage.removeItem(getColumnsStorageKey(columnPreset));
436
- } catch (e) {
437
- console.warn('Failed to clear column visibility:', e);
438
- }
439
- }
440
- }
441
-
442
- function toggleSidebar() {
443
- sidebarOpen = !sidebarOpen;
444
- setTimeout(() => cellTable?.redraw(), 320);
445
- }
446
-
447
- function closeSidebar() {
448
- sidebarOpen = false;
449
- setTimeout(() => cellTable?.redraw(), 320);
450
- }
451
-
452
- // Expose table methods
453
- export function getSelectedRows(): CellData[] {
454
- return cellTable?.getSelectedRows() ?? [];
455
- }
456
-
457
- export function clearSelection(): void {
458
- cellTable?.clearSelection();
459
- }
460
-
461
- export function scrollToRow(id: string): void {
462
- cellTable?.scrollToRow(id);
463
- }
464
-
465
- export function redraw(): void {
466
- cellTable?.redraw();
467
- }
468
-
469
- export function openSidebar(): void {
470
- sidebarOpen = true;
471
- setTimeout(() => cellTable?.redraw(), 320);
472
- }
473
- </script>
474
-
475
- <div class="cell-table-panel d-flex flex-column" style:height>
476
- <!-- Header -->
477
- <div class="panel-header d-flex align-items-center justify-content-between px-3 py-2 bg-body-secondary border-bottom">
478
- <div class="d-flex align-items-center gap-3">
479
- <h6 class="mb-0 d-flex align-items-center gap-2">
480
- <i class="bi bi-table text-primary"></i>
481
- {title}
482
- </h6>
483
- {#if headerSearch}
484
- {@render headerSearch()}
485
- {/if}
486
- </div>
487
- <div class="d-flex align-items-center gap-2">
488
- {#if headerActions}
489
- <div class="header-actions">
490
- {@render headerActions()}
491
- </div>
492
- {/if}
493
- {#if showDetailsSidebar}
494
- <button
495
- class="btn btn-sm"
496
- class:btn-outline-secondary={!sidebarOpen}
497
- class:btn-secondary={sidebarOpen}
498
- onclick={toggleSidebar}
499
- title={sidebarOpen ? 'Hide details' : 'Show details'}
500
- >
501
- <i class="bi" class:bi-layout-sidebar-reverse={!sidebarOpen} class:bi-x-lg={sidebarOpen}></i>
502
- </button>
503
- {/if}
504
- </div>
505
- </div>
506
-
507
- <!-- Toolbar -->
508
- {#if showToolbar}
509
- <CellTableToolbar
510
- {groupBy}
511
- {groupOptions}
512
- {columnPreset}
513
- totalCount={cells.length}
514
- {filteredCount}
515
- {selectedCount}
516
- {showExport}
517
- {showJsonExport}
518
- showPresets={showColumnPresets}
519
- ongroupchange={handleGroupChange}
520
- onpresetchange={handlePresetChange}
521
- onexportcsv={handleExportCSV}
522
- onexportjson={handleExportJSON}
523
- onclearfilters={handleClearFilters}
524
- oncollapseall={handleCollapseAll}
525
- onexpandall={handleExpandAll}
526
- {filtersVisible}
527
- ontogglefilters={handleToggleFilters}
528
- {columnMeta}
529
- {visibleColumns}
530
- oncolumnvisibilitychange={handleColumnVisibilityChange}
531
- onresetcolumns={handleResetColumns}
532
- {scrollSpyEnabled}
533
- showScrollSpyToggle={showScrollSpy}
534
- ontogglescrollspy={handleToggleScrollSpy}
535
- />
536
- {/if}
537
-
538
- <!-- ScrollSpy Navigation Bar -->
539
- {#if scrollSpyEnabled && groupBy !== 'none' && scrollSpyGroups.length > 0}
540
- <div class="scrollspy-bar d-flex align-items-center gap-2 px-3 py-2 bg-body-tertiary border-bottom overflow-auto">
541
- <span class="text-muted small me-1">
542
- <i class="bi bi-signpost-split"></i> Jump to:
543
- </span>
544
- {#each scrollSpyGroups as group (group.key)}
545
- {@const bgColor = groupBy === 'tech'
546
- ? (techColors?.[group.key] ?? DEFAULT_TECH_COLORS[group.key] ?? '#6c757d')
547
- : groupBy === 'fband'
548
- ? (FBAND_COLORS[group.key] ?? '#6c757d')
549
- : groupBy === 'status'
550
- ? (statusColors?.[group.key] ?? DEFAULT_STATUS_COLORS[group.key] ?? '#6c757d')
551
- : '#6c757d'}
552
- <button
553
- type="button"
554
- class="btn btn-sm scrollspy-badge"
555
- style="background-color: {bgColor}; border-color: {bgColor}; color: white;"
556
- onclick={() => handleScrollToGroup(group.key)}
557
- title="Scroll to {group.key} ({group.count} cells)"
558
- >
559
- <span class="badge rounded-pill bg-light text-dark me-1">{group.count}</span>
560
- {group.key}
561
- </button>
562
- {/each}
563
- </div>
564
- {/if}
565
-
566
- <!-- Main content with optional sidebar -->
567
- <div class="content-area d-flex flex-grow-1 overflow-hidden">
568
- <!-- Table -->
569
- <div class="table-wrapper flex-grow-1 overflow-hidden">
570
- <CellTable
571
- bind:this={cellTable}
572
- {cells}
573
- {groupBy}
574
- {columnPreset}
575
- {selectable}
576
- {multiSelect}
577
- height="100%"
578
- {techColors}
579
- {statusColors}
580
- {headerFilters}
581
- {persistLayout}
582
- storageKey="{storageKey}-tabulator"
583
- onselectionchange={handleSelectionChange}
584
- ondatachange={handleDataChange}
585
- onrowclick={handleRowClick}
586
- onrowcontextmenu={handleRowContextMenu}
587
- {onrowdblclick}
588
- />
589
- </div>
590
-
591
- <!-- Details Sidebar -->
592
- {#if showDetailsSidebar}
593
- <aside
594
- class="details-sidebar border-start bg-white overflow-auto"
595
- class:open={sidebarOpen}
596
- style:--sidebar-width="{sidebarWidth}px"
597
- >
598
- <div class="sidebar-content" style:width="{sidebarWidth}px">
599
- <div class="d-flex align-items-center justify-content-between p-3 border-bottom bg-light sticky-top">
600
- <h6 class="mb-0">
601
- <i class="bi bi-info-circle text-primary"></i> Details
602
- </h6>
603
- <button
604
- class="btn btn-sm btn-outline-secondary"
605
- onclick={closeSidebar}
606
- title="Close"
607
- >
608
- <i class="bi bi-x-lg"></i>
609
- </button>
610
- </div>
611
-
612
- <div class="p-3">
613
- {#if detailsContent}
614
- {@render detailsContent({ cell: clickedCell, closeSidebar })}
615
- {:else if clickedCell}
616
- <!-- Default details view -->
617
- <dl class="row mb-0 small">
618
- <dt class="col-5 text-muted">ID</dt>
619
- <dd class="col-7"><code class="text-primary">{clickedCell.id}</code></dd>
620
-
621
- <dt class="col-5 text-muted">Cell Name</dt>
622
- <dd class="col-7 fw-medium">{clickedCell.cellName}</dd>
623
-
624
- <dt class="col-5 text-muted">Site</dt>
625
- <dd class="col-7">{clickedCell.siteId}</dd>
626
-
627
- <dt class="col-5 text-muted">Technology</dt>
628
- <dd class="col-7">
629
- <span class="badge" style="background-color: {techColors?.[clickedCell.tech] ?? DEFAULT_TECH_COLORS[clickedCell.tech] ?? '#6c757d'}; color: white;">{clickedCell.tech}</span>
630
- </dd>
631
-
632
- <dt class="col-5 text-muted">Band</dt>
633
- <dd class="col-7">
634
- <span class="badge" style="background-color: {FBAND_COLORS[clickedCell.fband] ?? '#6c757d'}; color: white;">{clickedCell.fband}</span>
635
- </dd>
636
-
637
- <dt class="col-5 text-muted">Frequency</dt>
638
- <dd class="col-7">{clickedCell.frq} MHz</dd>
639
-
640
- <dt class="col-5 text-muted">Status</dt>
641
- <dd class="col-7">
642
- <span class="badge" style="background-color: {statusColors?.[clickedCell.status] ?? DEFAULT_STATUS_COLORS[clickedCell.status] ?? '#6c757d'}; color: white;">{clickedCell.status.replace(/_/g, ' ')}</span>
643
- </dd>
644
-
645
- <dt class="col-12 text-muted mt-2 mb-1 border-top pt-2">Physical</dt>
646
-
647
- <dt class="col-5 text-muted">Azimuth</dt>
648
- <dd class="col-7">{clickedCell.azimuth}°</dd>
649
-
650
- <dt class="col-5 text-muted">Height</dt>
651
- <dd class="col-7">{clickedCell.height}m</dd>
652
-
653
- <dt class="col-5 text-muted">Beamwidth</dt>
654
- <dd class="col-7">{clickedCell.beamwidth}°</dd>
655
-
656
- <dt class="col-5 text-muted">E-Tilt</dt>
657
- <dd class="col-7">{clickedCell.electricalTilt}°</dd>
658
-
659
- <dt class="col-5 text-muted">Antenna</dt>
660
- <dd class="col-7 text-truncate" title={clickedCell.antenna}>
661
- {clickedCell.antenna}
662
- </dd>
663
-
664
- <dt class="col-12 text-muted mt-2 mb-1 border-top pt-2">Location</dt>
665
-
666
- <dt class="col-5 text-muted">Latitude</dt>
667
- <dd class="col-7">{clickedCell.latitude.toFixed(6)}</dd>
668
-
669
- <dt class="col-5 text-muted">Longitude</dt>
670
- <dd class="col-7">{clickedCell.longitude.toFixed(6)}</dd>
671
-
672
- <dt class="col-12 text-muted mt-2 mb-1 border-top pt-2">Planning</dt>
673
-
674
- <dt class="col-5 text-muted">Planner</dt>
675
- <dd class="col-7">{clickedCell.planner}</dd>
676
-
677
- <dt class="col-5 text-muted">On Air</dt>
678
- <dd class="col-7">{clickedCell.onAirDate}</dd>
679
-
680
- {#if clickedCell.comment}
681
- <dt class="col-5 text-muted">Comment</dt>
682
- <dd class="col-7 fst-italic">{clickedCell.comment}</dd>
683
- {/if}
684
-
685
- <!-- Dynamic Other Properties -->
686
- {#if clickedCell.other && Object.keys(clickedCell.other).length > 0}
687
- <dt class="col-12 text-muted mt-2 mb-1 border-top pt-2">Other</dt>
688
-
689
- {#each Object.entries(clickedCell.other) as [key, value]}
690
- <dt class="col-5 text-muted text-capitalize">{key.replace(/_/g, ' ')}</dt>
691
- <dd class="col-7">
692
- {#if value === null || value === undefined}
693
- <span class="text-muted fst-italic">—</span>
694
- {:else if typeof value === 'boolean'}
695
- <span class="badge" class:bg-success={value} class:bg-secondary={!value}>
696
- {value ? 'Yes' : 'No'}
697
- </span>
698
- {:else if typeof value === 'number'}
699
- <code>{value}</code>
700
- {:else if typeof value === 'object'}
701
- <code class="small text-break">{JSON.stringify(value)}</code>
702
- {:else}
703
- {String(value)}
704
- {/if}
705
- </dd>
706
- {/each}
707
- {/if}
708
- </dl>
709
- {:else}
710
- <div class="text-center text-muted py-5">
711
- <i class="bi bi-hand-index fs-1 opacity-50"></i>
712
- <p class="mt-2 mb-0">Click a row to see details</p>
713
- </div>
714
- {/if}
715
- </div>
716
- </div>
717
- </aside>
718
- {/if}
719
- </div>
720
-
721
- <!-- Footer -->
722
- {#if footer}
723
- <div class="panel-footer border-top p-2 bg-body-tertiary">
724
- {@render footer({ selectedRows, selectedCount })}
725
- </div>
726
- {/if}
727
-
728
- <!-- Context Menu (portal-like, fixed position) -->
729
- {#if contextMenuVisible && contextMenuRow && contextMenu}
730
- <div
731
- class="context-menu-container"
732
- style="position: fixed; left: {contextMenuPosition.x}px; top: {contextMenuPosition.y}px; z-index: 1050;"
733
- >
734
- {@render contextMenu({ row: contextMenuRow, closeMenu: closeContextMenu })}
735
- </div>
736
- {/if}
737
- </div>
738
-
739
- <!-- Click outside handler -->
740
- <svelte:window onclick={contextMenuVisible ? handleClickOutside : undefined} />
741
-
742
- <style>
743
- .cell-table-panel {
744
- background-color: var(--bs-body-bg, #fff);
745
- border: 1px solid var(--bs-border-color, #dee2e6);
746
- border-radius: var(--bs-border-radius, 0.375rem);
747
- overflow: hidden;
748
- }
749
-
750
- .panel-header {
751
- min-height: 48px;
752
- }
753
-
754
- .panel-header h6 {
755
- font-weight: 600;
756
- color: var(--bs-body-color, #212529);
757
- }
758
-
759
- .content-area {
760
- min-height: 0;
761
- }
762
-
763
- .table-wrapper {
764
- min-height: 0;
765
- transition: all 0.3s ease;
766
- }
767
-
768
- .details-sidebar {
769
- width: 0;
770
- min-width: 0;
771
- opacity: 0;
772
- transition: width 0.3s ease, opacity 0.3s ease, min-width 0.3s ease;
773
- }
774
-
775
- .details-sidebar.open {
776
- width: var(--sidebar-width, 320px);
777
- min-width: var(--sidebar-width, 320px);
778
- opacity: 1;
779
- }
780
-
781
- .panel-footer {
782
- min-height: 48px;
783
- }
784
-
785
- /* ScrollSpy bar styling */
786
- .scrollspy-bar {
787
- min-height: 40px;
788
- flex-wrap: nowrap;
789
- scrollbar-width: thin;
790
- }
791
-
792
- .scrollspy-bar::-webkit-scrollbar {
793
- height: 4px;
794
- }
795
-
796
- .scrollspy-bar::-webkit-scrollbar-thumb {
797
- background: var(--bs-secondary-color, #6c757d);
798
- border-radius: 2px;
799
- }
800
-
801
- .scrollspy-badge {
802
- white-space: nowrap;
803
- font-size: 0.75rem;
804
- padding: 0.25rem 0.5rem;
805
- transition: transform 0.15s ease, box-shadow 0.15s ease;
806
- }
807
-
808
- .scrollspy-badge:hover {
809
- transform: translateY(-1px);
810
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
811
- filter: brightness(1.1);
812
- }
813
-
814
- .scrollspy-badge:active {
815
- transform: translateY(0);
816
- }
817
-
818
- /* Context menu styling */
819
- .context-menu-container {
820
- animation: contextMenuFadeIn 0.1s ease-out;
821
- }
822
-
823
- @keyframes contextMenuFadeIn {
824
- from {
825
- opacity: 0;
826
- transform: scale(0.95);
827
- }
828
- to {
829
- opacity: 1;
830
- transform: scale(1);
831
- }
832
- }
833
- </style>