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