@smartnet360/svelte-components 0.0.106 → 0.0.108

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.
@@ -16,7 +16,8 @@
16
16
  getColumnsForPreset,
17
17
  getGroupHeaderFormatter,
18
18
  DEFAULT_TECH_COLORS,
19
- DEFAULT_STATUS_COLORS
19
+ DEFAULT_STATUS_COLORS,
20
+ cellDataSorter
20
21
  } from './column-config';
21
22
 
22
23
  interface Props extends CellTableProps {
@@ -57,13 +58,21 @@
57
58
  let table: Tabulator | null = null;
58
59
  let isInitialized = $state(false);
59
60
 
60
- // Reactive column configuration
61
- let columns = $derived(getColumnsForPreset(columnPreset, techColors, statusColors, headerFilters));
61
+ // Reactive column configuration - only changes when preset changes
62
+ let columns = $derived.by(() => {
63
+ // Only depend on columnPreset to avoid unnecessary recalculations
64
+ return getColumnsForPreset(columnPreset, techColors, statusColors, headerFilters);
65
+ });
66
+
67
+ // Pre-sort data using our custom multi-level sorter
68
+ let sortedCells = $derived.by(() => {
69
+ return [...cells].sort(cellDataSorter);
70
+ });
62
71
 
63
72
  // Build Tabulator options
64
73
  function buildOptions(): Options {
65
74
  const baseOptions: Options = {
66
- data: cells,
75
+ data: sortedCells,
67
76
  columns: columns,
68
77
  layout: 'fitDataFill',
69
78
  height: height,
@@ -76,8 +85,12 @@
76
85
  resizableColumns: resizableColumns,
77
86
  movableColumns: movableColumns,
78
87
 
79
- // Sorting
80
- initialSort: [{ column: 'siteId', dir: 'asc' }],
88
+ // No initial sort - data is pre-sorted
89
+ initialSort: [
90
+ { column: 'tech', dir: 'asc' },
91
+ { column: 'fband', dir: 'asc' },
92
+ { column: 'cellName', dir: 'asc' }
93
+ ],
81
94
 
82
95
  // Row selection
83
96
  selectable: selectable ? (multiSelect ? true : 1) : false,
@@ -171,13 +184,7 @@
171
184
  // Mark as initialized after table is ready
172
185
  table.on('tableBuilt', () => {
173
186
  isInitialized = true;
174
- });
175
- }
176
-
177
- // Update table data when cells change
178
- $effect(() => {
179
- if (isInitialized && table && cells) {
180
- table.replaceData(cells);
187
+ // Fire initial data change event
181
188
  if (ondatachange) {
182
189
  ondatachange({
183
190
  type: 'load',
@@ -185,28 +192,52 @@
185
192
  filteredCount: cells.length
186
193
  });
187
194
  }
195
+ });
196
+ }
197
+
198
+ // Track previous values to avoid unnecessary updates
199
+ let prevCellsLength = 0;
200
+ let prevCellsFirstId: string | null = null;
201
+ let prevGroupBy: string | null = null;
202
+ let prevColumnPreset: string | null = null;
203
+
204
+ // Update table data when cells actually change (not just reference)
205
+ $effect(() => {
206
+ const currentLength = sortedCells?.length ?? 0;
207
+ const currentFirstId = sortedCells?.[0]?.id ?? null;
208
+
209
+ // Only update if length or first item changed (rough equality check)
210
+ if (isInitialized && table &&
211
+ (currentLength !== prevCellsLength || currentFirstId !== prevCellsFirstId)) {
212
+ prevCellsLength = currentLength;
213
+ prevCellsFirstId = currentFirstId;
214
+ table.replaceData(sortedCells);
215
+ ondatachange?.({
216
+ type: 'load',
217
+ rowCount: sortedCells.length,
218
+ filteredCount: sortedCells.length
219
+ });
188
220
  }
189
221
  });
190
222
 
191
223
  // Update grouping when groupBy changes
192
224
  $effect(() => {
193
- if (isInitialized && table) {
225
+ if (isInitialized && table && groupBy !== prevGroupBy) {
226
+ prevGroupBy = groupBy;
194
227
  if (groupBy === 'none') {
195
228
  table.setGroupBy(false);
196
229
  } else {
197
230
  table.setGroupBy(groupBy);
198
231
  table.setGroupHeader(getGroupHeaderFormatter(groupBy));
199
232
  }
200
- // Force redraw after grouping change
201
- table.redraw(true);
202
233
  }
203
234
  });
204
235
 
205
236
  // Update columns when preset changes
206
237
  $effect(() => {
207
- if (isInitialized && table && columns) {
238
+ if (isInitialized && table && columnPreset !== prevColumnPreset) {
239
+ prevColumnPreset = columnPreset;
208
240
  table.setColumns(columns);
209
- table.redraw(true);
210
241
  }
211
242
  });
212
243
 
@@ -256,12 +287,63 @@
256
287
  }
257
288
 
258
289
  export function clearFilters(): void {
259
- table?.clearFilter();
290
+ if (!table) return;
291
+ // Clear programmatic filters
292
+ table.clearFilter();
293
+ // Clear header filter inputs
294
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
295
+ (table as any).clearHeaderFilter();
260
296
  }
261
297
 
262
298
  export function redraw(): void {
263
299
  table?.redraw(true);
264
300
  }
301
+
302
+ export function collapseAll(): void {
303
+ if (!table) return;
304
+ // Use setGroupStartOpen to collapse all groups, then refresh data to apply
305
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
306
+ (table as any).setGroupStartOpen(false);
307
+ table.setData(table.getData());
308
+ }
309
+
310
+ export function expandAll(): void {
311
+ if (!table) return;
312
+ // Use setGroupStartOpen to expand all groups, then refresh data to apply
313
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
314
+ (table as any).setGroupStartOpen(true);
315
+ table.setData(table.getData());
316
+ }
317
+
318
+ export function toggleHeaderFilters(visible: boolean): void {
319
+ if (!table) return;
320
+ const headerFiltersElement = tableContainer.querySelector('.tabulator-header-filter');
321
+ if (headerFiltersElement) {
322
+ // Toggle all header filter rows
323
+ const filterRows = tableContainer.querySelectorAll('.tabulator-col .tabulator-header-filter');
324
+ filterRows.forEach(el => {
325
+ (el as HTMLElement).style.display = visible ? '' : 'none';
326
+ });
327
+ table.redraw();
328
+ }
329
+ }
330
+
331
+ export function showColumn(field: string): void {
332
+ table?.showColumn(field);
333
+ }
334
+
335
+ export function hideColumn(field: string): void {
336
+ table?.hideColumn(field);
337
+ }
338
+
339
+ export function getVisibleColumns(): string[] {
340
+ if (!table) return [];
341
+ const columns = table.getColumns();
342
+ return columns
343
+ .filter(col => col.isVisible())
344
+ .map(col => col.getField())
345
+ .filter((field): field is string => !!field);
346
+ }
265
347
  </script>
266
348
 
267
349
  <div class="cell-table-container">
@@ -22,6 +22,12 @@ declare const CellTable: import("svelte").Component<Props, {
22
22
  setFilter: (field: string, type: string, value: unknown) => void;
23
23
  clearFilters: () => void;
24
24
  redraw: () => void;
25
+ collapseAll: () => void;
26
+ expandAll: () => void;
27
+ toggleHeaderFilters: (visible: boolean) => void;
28
+ showColumn: (field: string) => void;
29
+ hideColumn: (field: string) => void;
30
+ getVisibleColumns: () => string[];
25
31
  }, "">;
26
32
  type CellTable = ReturnType<typeof CellTable>;
27
33
  export default CellTable;
@@ -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,
@@ -89,10 +90,22 @@
89
90
  let filteredCount = $state(cells.length);
90
91
  let sidebarOpen = $state(false);
91
92
  let clickedCell: CellData | null = $state(null);
93
+ let tableRefSet = false;
94
+ let filtersVisible = $state(true);
92
95
 
93
- // Expose table methods via tableRef
96
+ // Column visibility management
97
+ const columnMeta = getColumnMetadata();
98
+ let visibleColumns = $state<string[]>(getPresetVisibleFields(columnPreset));
99
+
100
+ // Update visible columns when preset changes
94
101
  $effect(() => {
95
- if (cellTable) {
102
+ visibleColumns = getPresetVisibleFields(columnPreset);
103
+ });
104
+
105
+ // Expose table methods via tableRef - only set once
106
+ $effect(() => {
107
+ if (cellTable && !tableRefSet) {
108
+ tableRefSet = true;
96
109
  tableRef = {
97
110
  redraw: () => cellTable?.redraw()
98
111
  };
@@ -138,6 +151,44 @@
138
151
  cellTable?.clearFilters();
139
152
  }
140
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
+
141
192
  function toggleSidebar() {
142
193
  sidebarOpen = !sidebarOpen;
143
194
  setTimeout(() => cellTable?.redraw(), 320);
@@ -212,6 +263,14 @@
212
263
  onexportcsv={handleExportCSV}
213
264
  onexportjson={handleExportJSON}
214
265
  onclearfilters={handleClearFilters}
266
+ oncollapseall={handleCollapseAll}
267
+ onexpandall={handleExpandAll}
268
+ {filtersVisible}
269
+ ontogglefilters={handleToggleFilters}
270
+ {columnMeta}
271
+ {visibleColumns}
272
+ oncolumnvisibilitychange={handleColumnVisibilityChange}
273
+ onresetcolumns={handleResetColumns}
215
274
  />
216
275
  {/if}
217
276
 
@@ -330,6 +389,30 @@
330
389
  <dt class="col-5 text-muted">Comment</dt>
331
390
  <dd class="col-7 fst-italic">{clickedCell.comment}</dd>
332
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}
333
416
  </dl>
334
417
  {:else}
335
418
  <div class="text-center text-muted py-5">
@@ -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>;
@@ -0,0 +1,214 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ColumnPicker - Column visibility customization dropdown
4
+ *
5
+ * Shows a dropdown panel with checkboxes to toggle individual column visibility.
6
+ * Works alongside presets - user can customize which columns are visible.
7
+ */
8
+
9
+ interface ColumnInfo {
10
+ field: string;
11
+ title: string;
12
+ group: string;
13
+ }
14
+
15
+ interface Props {
16
+ /** All available columns with their metadata */
17
+ columns: ColumnInfo[];
18
+ /** Currently visible column fields */
19
+ visibleColumns: string[];
20
+ /** Callback when column visibility changes */
21
+ onchange?: (field: string, visible: boolean) => void;
22
+ /** Callback to reset to preset defaults */
23
+ onreset?: () => void;
24
+ /** Current preset name for display */
25
+ presetName?: string;
26
+ }
27
+
28
+ let {
29
+ columns = [],
30
+ visibleColumns = [],
31
+ onchange,
32
+ onreset,
33
+ presetName = 'Default'
34
+ }: Props = $props();
35
+
36
+ let isOpen = $state(false);
37
+ let dropdownRef: HTMLDivElement;
38
+
39
+ // Group columns by category
40
+ const groupedColumns = $derived.by(() => {
41
+ const groups: Record<string, ColumnInfo[]> = {};
42
+ for (const col of columns) {
43
+ if (!groups[col.group]) {
44
+ groups[col.group] = [];
45
+ }
46
+ groups[col.group].push(col);
47
+ }
48
+ return groups;
49
+ });
50
+
51
+ const groupOrder = ['Core', 'Physical', 'Network', 'Atoll', 'Position', 'Planning'];
52
+
53
+ function toggleDropdown() {
54
+ isOpen = !isOpen;
55
+ }
56
+
57
+ function handleCheckboxChange(field: string, event: Event) {
58
+ const checked = (event.target as HTMLInputElement).checked;
59
+ onchange?.(field, checked);
60
+ }
61
+
62
+ function handleReset() {
63
+ onreset?.();
64
+ }
65
+
66
+ function handleSelectAll() {
67
+ columns.forEach(col => onchange?.(col.field, true));
68
+ }
69
+
70
+ function handleDeselectAll() {
71
+ // Keep at least core columns visible
72
+ columns.forEach(col => {
73
+ const isCore = col.group === 'Core';
74
+ onchange?.(col.field, isCore);
75
+ });
76
+ }
77
+
78
+ // Close dropdown when clicking outside
79
+ function handleClickOutside(event: MouseEvent) {
80
+ if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
81
+ isOpen = false;
82
+ }
83
+ }
84
+
85
+ $effect(() => {
86
+ if (isOpen) {
87
+ document.addEventListener('click', handleClickOutside);
88
+ return () => document.removeEventListener('click', handleClickOutside);
89
+ }
90
+ });
91
+ </script>
92
+
93
+ <div class="column-picker position-relative" bind:this={dropdownRef}>
94
+ <button
95
+ type="button"
96
+ class="btn btn-sm btn-outline-secondary"
97
+ onclick={toggleDropdown}
98
+ title="Customize columns"
99
+ aria-label="Customize columns"
100
+ aria-expanded={isOpen}
101
+ >
102
+ <i class="bi bi-pencil"></i>
103
+ </button>
104
+
105
+ {#if isOpen}
106
+ <div class="column-picker-dropdown position-absolute end-0 mt-1 bg-white border rounded shadow-sm"
107
+ style="z-index: 1050; width: 280px; max-height: 400px;">
108
+
109
+ <!-- Header -->
110
+ <div class="d-flex align-items-center justify-content-between p-2 border-bottom bg-light">
111
+ <span class="small fw-medium">
112
+ <i class="bi bi-layout-three-columns text-primary"></i>
113
+ Customize "{presetName}"
114
+ </span>
115
+ <button
116
+ type="button"
117
+ class="btn-close btn-close-sm"
118
+ onclick={() => isOpen = false}
119
+ aria-label="Close"
120
+ ></button>
121
+ </div>
122
+
123
+ <!-- Quick actions -->
124
+ <div class="d-flex gap-2 p-2 border-bottom">
125
+ <button
126
+ type="button"
127
+ class="btn btn-sm btn-outline-secondary flex-grow-1"
128
+ onclick={handleSelectAll}
129
+ >
130
+ Select All
131
+ </button>
132
+ <button
133
+ type="button"
134
+ class="btn btn-sm btn-outline-secondary flex-grow-1"
135
+ onclick={handleDeselectAll}
136
+ >
137
+ Core Only
138
+ </button>
139
+ </div>
140
+
141
+ <!-- Scrollable column list -->
142
+ <div class="column-list overflow-auto" style="max-height: 280px;">
143
+ {#each groupOrder as groupName}
144
+ {#if groupedColumns[groupName]}
145
+ <div class="column-group">
146
+ <div class="px-2 py-1 bg-body-tertiary small fw-medium text-muted border-bottom">
147
+ {groupName}
148
+ </div>
149
+ {#each groupedColumns[groupName] as col}
150
+ <label class="d-flex align-items-center px-2 py-1 column-item">
151
+ <input
152
+ type="checkbox"
153
+ class="form-check-input me-2 mt-0"
154
+ checked={visibleColumns.includes(col.field)}
155
+ onchange={(e) => handleCheckboxChange(col.field, e)}
156
+ />
157
+ <span class="small">{col.title}</span>
158
+ <code class="ms-auto small text-muted">{col.field}</code>
159
+ </label>
160
+ {/each}
161
+ </div>
162
+ {/if}
163
+ {/each}
164
+ </div>
165
+
166
+ <!-- Footer with reset -->
167
+ <div class="p-2 border-top bg-light">
168
+ <button
169
+ type="button"
170
+ class="btn btn-sm btn-outline-primary w-100"
171
+ onclick={handleReset}
172
+ >
173
+ <i class="bi bi-arrow-counterclockwise"></i>
174
+ Reset to "{presetName}" Default
175
+ </button>
176
+ </div>
177
+ </div>
178
+ {/if}
179
+ </div>
180
+
181
+ <style>
182
+ .column-picker-dropdown {
183
+ animation: fadeIn 0.15s ease-out;
184
+ }
185
+
186
+ @keyframes fadeIn {
187
+ from {
188
+ opacity: 0;
189
+ transform: translateY(-4px);
190
+ }
191
+ to {
192
+ opacity: 1;
193
+ transform: translateY(0);
194
+ }
195
+ }
196
+
197
+ .column-item {
198
+ cursor: pointer;
199
+ transition: background-color 0.1s;
200
+ }
201
+
202
+ .column-item:hover {
203
+ background-color: var(--bs-tertiary-bg, #f8f9fa);
204
+ }
205
+
206
+ .form-check-input {
207
+ cursor: pointer;
208
+ }
209
+
210
+ .btn-close-sm {
211
+ font-size: 0.65rem;
212
+ padding: 0.25rem;
213
+ }
214
+ </style>
@@ -0,0 +1,26 @@
1
+ /**
2
+ * ColumnPicker - Column visibility customization dropdown
3
+ *
4
+ * Shows a dropdown panel with checkboxes to toggle individual column visibility.
5
+ * Works alongside presets - user can customize which columns are visible.
6
+ */
7
+ interface ColumnInfo {
8
+ field: string;
9
+ title: string;
10
+ group: string;
11
+ }
12
+ interface Props {
13
+ /** All available columns with their metadata */
14
+ columns: ColumnInfo[];
15
+ /** Currently visible column fields */
16
+ visibleColumns: string[];
17
+ /** Callback when column visibility changes */
18
+ onchange?: (field: string, visible: boolean) => void;
19
+ /** Callback to reset to preset defaults */
20
+ onreset?: () => void;
21
+ /** Current preset name for display */
22
+ presetName?: string;
23
+ }
24
+ declare const ColumnPicker: import("svelte").Component<Props, {}, "">;
25
+ type ColumnPicker = ReturnType<typeof ColumnPicker>;
26
+ export default ColumnPicker;
@@ -49,6 +49,25 @@ export declare function azimuthFormatter(cell: CellComponent): string;
49
49
  * Height formatter with meter unit
50
50
  */
51
51
  export declare function heightFormatter(cell: CellComponent): string;
52
+ /**
53
+ * Custom sorter for fband - extracts numeric portion and sorts numerically
54
+ * Examples: LTE700 → 700, GSM900 → 900, LTE1800 → 1800, 5G-3500 → 3500
55
+ */
56
+ export declare function fbandSorter(a: string, b: string): number;
57
+ /**
58
+ * Custom sorter for cellName - sorts by the 5th character (sector digit)
59
+ * Example: 10001 → '1', 10002 → '2', 10003 → '3'
60
+ */
61
+ export declare function cellNameSectorSorter(a: string, b: string): number;
62
+ /**
63
+ * Combined multi-level sorter for cell data
64
+ * Sort order: tech (asc) → fband by numeric value (asc) → cellName by 5th digit (asc)
65
+ */
66
+ export declare function cellDataSorter<T extends {
67
+ tech?: string;
68
+ fband?: string;
69
+ cellName?: string;
70
+ }>(a: T, b: T): number;
52
71
  /**
53
72
  * Get all column definitions
54
73
  */
@@ -61,3 +80,19 @@ export declare function getColumnsForPreset(preset: ColumnPreset, techColors?: T
61
80
  * Get group header formatter for a specific field
62
81
  */
63
82
  export declare function getGroupHeaderFormatter(groupField: string): (value: unknown, count: number) => string;
83
+ /**
84
+ * Column metadata for the column picker
85
+ */
86
+ export interface ColumnMeta {
87
+ field: string;
88
+ title: string;
89
+ group: string;
90
+ }
91
+ /**
92
+ * Get column metadata for the column picker UI
93
+ */
94
+ export declare function getColumnMetadata(): ColumnMeta[];
95
+ /**
96
+ * Get default visible columns for a preset
97
+ */
98
+ export declare function getPresetVisibleFields(preset: ColumnPreset): string[];
@@ -123,6 +123,46 @@ export function heightFormatter(cell) {
123
123
  return '';
124
124
  return `${value}m`;
125
125
  }
126
+ /**
127
+ * Custom sorter for fband - extracts numeric portion and sorts numerically
128
+ * Examples: LTE700 → 700, GSM900 → 900, LTE1800 → 1800, 5G-3500 → 3500
129
+ */
130
+ export function fbandSorter(a, b) {
131
+ const numA = parseInt(a.replace(/\D/g, ''), 10) || 0;
132
+ const numB = parseInt(b.replace(/\D/g, ''), 10) || 0;
133
+ return numA - numB;
134
+ }
135
+ /**
136
+ * Custom sorter for cellName - sorts by the 5th character (sector digit)
137
+ * Example: 10001 → '1', 10002 → '2', 10003 → '3'
138
+ */
139
+ export function cellNameSectorSorter(a, b) {
140
+ const charA = a.charAt(4) || '0';
141
+ const charB = b.charAt(4) || '0';
142
+ return charA.localeCompare(charB);
143
+ }
144
+ /**
145
+ * Combined multi-level sorter for cell data
146
+ * Sort order: tech (asc) → fband by numeric value (asc) → cellName by 5th digit (asc)
147
+ */
148
+ export function cellDataSorter(a, b) {
149
+ // 1. Sort by tech (string comparison)
150
+ const techA = String(a.tech || '');
151
+ const techB = String(b.tech || '');
152
+ const techCompare = techA.localeCompare(techB);
153
+ if (techCompare !== 0)
154
+ return techCompare;
155
+ // 2. Sort by fband (numeric extraction)
156
+ const fbandA = String(a.fband || '');
157
+ const fbandB = String(b.fband || '');
158
+ const fbandCompare = fbandSorter(fbandA, fbandB);
159
+ if (fbandCompare !== 0)
160
+ return fbandCompare;
161
+ // 3. Sort by cellName 5th character (sector)
162
+ const cellNameA = String(a.cellName || '');
163
+ const cellNameB = String(b.cellName || '');
164
+ return cellNameSectorSorter(cellNameA, cellNameB);
165
+ }
126
166
  /**
127
167
  * Get all column definitions
128
168
  */
@@ -463,3 +503,75 @@ export function getGroupHeaderFormatter(groupField) {
463
503
  <span class="text-muted">(${count} cell${count !== 1 ? 's' : ''})</span>`;
464
504
  };
465
505
  }
506
+ /**
507
+ * Get column metadata for the column picker UI
508
+ */
509
+ export function getColumnMetadata() {
510
+ return [
511
+ // Core
512
+ { field: 'id', title: 'ID', group: 'Core' },
513
+ { field: 'cellName', title: 'Cell Name', group: 'Core' },
514
+ { field: 'siteId', title: 'Site ID', group: 'Core' },
515
+ { field: 'tech', title: 'Technology', group: 'Core' },
516
+ { field: 'fband', title: 'Band', group: 'Core' },
517
+ { field: 'frq', title: 'Frequency', group: 'Core' },
518
+ { field: 'status', title: 'Status', group: 'Core' },
519
+ { field: 'type', title: 'Type', group: 'Core' },
520
+ { field: 'onAirDate', title: 'On Air Date', group: 'Core' },
521
+ // Physical
522
+ { field: 'antenna', title: 'Antenna', group: 'Physical' },
523
+ { field: 'azimuth', title: 'Azimuth', group: 'Physical' },
524
+ { field: 'height', title: 'Height', group: 'Physical' },
525
+ { field: 'electricalTilt', title: 'E-Tilt', group: 'Physical' },
526
+ { field: 'beamwidth', title: 'Beamwidth', group: 'Physical' },
527
+ // Network
528
+ { field: 'dlEarfn', title: 'DL EARFCN', group: 'Network' },
529
+ { field: 'bcch', title: 'BCCH', group: 'Network' },
530
+ { field: 'pci', title: 'PCI', group: 'Network' },
531
+ { field: 'rru', title: 'RRU', group: 'Network' },
532
+ { field: 'cellID', title: 'Cell ID', group: 'Network' },
533
+ { field: 'cellId2G', title: 'Cell ID 2G', group: 'Network' },
534
+ { field: 'txId', title: 'TX ID', group: 'Network' },
535
+ { field: 'ctrlid', title: 'Ctrl ID', group: 'Network' },
536
+ { field: 'nwtET', title: 'NWT ET', group: 'Network' },
537
+ { field: 'nwtPW', title: 'NWT PW', group: 'Network' },
538
+ { field: 'nwtRS', title: 'NWT RS', group: 'Network' },
539
+ { field: 'nwtBW', title: 'NWT BW', group: 'Network' },
540
+ // Atoll
541
+ { field: 'atollET', title: 'Atoll ET', group: 'Atoll' },
542
+ { field: 'atollPW', title: 'Atoll PW', group: 'Atoll' },
543
+ { field: 'atollRS', title: 'Atoll RS', group: 'Atoll' },
544
+ { field: 'atollBW', title: 'Atoll BW', group: 'Atoll' },
545
+ // Position
546
+ { field: 'latitude', title: 'Latitude', group: 'Position' },
547
+ { field: 'longitude', title: 'Longitude', group: 'Position' },
548
+ { field: 'siteLatitude', title: 'Site Latitude', group: 'Position' },
549
+ { field: 'siteLongitude', title: 'Site Longitude', group: 'Position' },
550
+ { field: 'dx', title: 'DX', group: 'Position' },
551
+ { field: 'dy', title: 'DY', group: 'Position' },
552
+ // Planning
553
+ { field: 'planner', title: 'Planner', group: 'Planning' },
554
+ { field: 'comment', title: 'Comment', group: 'Planning' },
555
+ { field: 'customSubgroup', title: 'Subgroup', group: 'Planning' },
556
+ ];
557
+ }
558
+ /**
559
+ * Get default visible columns for a preset
560
+ */
561
+ export function getPresetVisibleFields(preset) {
562
+ switch (preset) {
563
+ case 'compact':
564
+ return ['cellName', 'siteId', 'tech', 'fband', 'status'];
565
+ case 'full':
566
+ return getColumnMetadata().map(c => c.field);
567
+ case 'physical':
568
+ return [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.physical];
569
+ case 'network':
570
+ return [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.network];
571
+ case 'planning':
572
+ return [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.planning];
573
+ case 'default':
574
+ default:
575
+ return ['id', 'cellName', 'siteId', 'tech', 'fband', 'frq', 'status', 'azimuth', 'height', 'antenna'];
576
+ }
577
+ }
@@ -7,6 +7,7 @@ export { default as CellTable } from './CellTable.svelte';
7
7
  export { default as CellTableToolbar } from './CellTableToolbar.svelte';
8
8
  export { default as CellTablePanel } from './CellTablePanel.svelte';
9
9
  export { default as CellTableDemo } from './CellTableDemo.svelte';
10
+ export { default as ColumnPicker } from './ColumnPicker.svelte';
10
11
  export { generateCells, generateCellsFromPreset, getGeneratorInfo, GENERATOR_PRESETS, type CellGeneratorConfig, type GeneratorPreset } from '../../shared/demo';
11
12
  export type { CellData, CellTableGroupField, ColumnPreset, ColumnVisibility, TechColorMap, StatusColorMap, CellTableProps, RowSelectionEvent, RowClickEvent, RowDblClickEvent, DataChangeEvent, CellTableColumn, ColumnGroups } from './types';
12
- export { DEFAULT_TECH_COLORS, DEFAULT_STATUS_COLORS, FBAND_COLORS, COLUMN_GROUPS, getAllColumns, getColumnsForPreset, getGroupHeaderFormatter, createTechFormatter, createStatusFormatter, createFbandFormatter, numberFormatter, coordinateFormatter, azimuthFormatter, heightFormatter } from './column-config';
13
+ export { DEFAULT_TECH_COLORS, DEFAULT_STATUS_COLORS, FBAND_COLORS, COLUMN_GROUPS, getAllColumns, getColumnsForPreset, getGroupHeaderFormatter, getColumnMetadata, getPresetVisibleFields, fbandSorter, cellNameSectorSorter, cellDataSorter, createTechFormatter, createStatusFormatter, createFbandFormatter, numberFormatter, coordinateFormatter, azimuthFormatter, heightFormatter, type ColumnMeta } from './column-config';
@@ -8,9 +8,10 @@ export { default as CellTable } from './CellTable.svelte';
8
8
  export { default as CellTableToolbar } from './CellTableToolbar.svelte';
9
9
  export { default as CellTablePanel } from './CellTablePanel.svelte';
10
10
  export { default as CellTableDemo } from './CellTableDemo.svelte';
11
+ export { default as ColumnPicker } from './ColumnPicker.svelte';
11
12
  // Re-export shared demo data utilities for convenience
12
13
  // Note: Cell type is NOT re-exported here to avoid conflicts with map-v2
13
14
  // Import Cell from '$lib/shared/demo' or '@smartnet360/svelte-components' directly
14
15
  export { generateCells, generateCellsFromPreset, getGeneratorInfo, GENERATOR_PRESETS } from '../../shared/demo';
15
16
  // Configuration utilities
16
- export { DEFAULT_TECH_COLORS, DEFAULT_STATUS_COLORS, FBAND_COLORS, COLUMN_GROUPS, getAllColumns, getColumnsForPreset, getGroupHeaderFormatter, createTechFormatter, createStatusFormatter, createFbandFormatter, numberFormatter, coordinateFormatter, azimuthFormatter, heightFormatter } from './column-config';
17
+ export { DEFAULT_TECH_COLORS, DEFAULT_STATUS_COLORS, FBAND_COLORS, COLUMN_GROUPS, getAllColumns, getColumnsForPreset, getGroupHeaderFormatter, getColumnMetadata, getPresetVisibleFields, fbandSorter, cellNameSectorSorter, cellDataSorter, createTechFormatter, createStatusFormatter, createFbandFormatter, numberFormatter, coordinateFormatter, azimuthFormatter, heightFormatter } from './column-config';
@@ -116,8 +116,8 @@ export class SiteStore {
116
116
  availableTilts: ['0']
117
117
  };
118
118
  }
119
- // Determine TX power (priority: atollPW > nwtP1 > default)
120
- const txPower = cell.atollPW || cell.nwtP1 || 43; // Default to 43 dBm (20W)
119
+ // Determine TX power (priority: atollPW > nwtPW > default)
120
+ const txPower = cell.atollPW || cell.nwtPW || 43; // Default to 43 dBm (20W)
121
121
  // Determine frequency from band
122
122
  const frequency = antennaPattern.frequency || this.parseFrequency(cell.fband);
123
123
  // Get mechanical tilt (might need to parse from string)
@@ -176,7 +176,7 @@ export function generateCells(config) {
176
176
  id: cellName,
177
177
  txId: cellName,
178
178
  cellID: cellName,
179
- cellID2G: techBand.tech === '2G' ? cellName : '',
179
+ cellId2G: techBand.tech === '2G' ? cellName : '',
180
180
  cellName: cellName,
181
181
  siteId: siteId,
182
182
  tech: techBand.tech,
@@ -201,22 +201,26 @@ export function generateCells(config) {
201
201
  siteLongitude: siteLng,
202
202
  comment: `Demo ${techBand.tech} ${techBand.band} cell at azimuth ${sector.azimuth}°`,
203
203
  planner: 'Demo User',
204
- atollETP: 43.0,
204
+ atollET: 43.0,
205
205
  atollPW: 20.0,
206
206
  atollRS: 500.0 + (techBand.band === '700' ? 200 : 0),
207
207
  atollBW: parseFloat(techBand.band) / 100,
208
- cellId3: `${cellName}-3G`,
209
- nwtP1: 20,
210
- nwtP2: 40,
211
- pci1: cellCounter % 504,
208
+ rru: `RRU-${siteId}-${sector.sectorNum}`,
209
+ nwtET: 40.0,
210
+ nwtPW: 20,
211
+ pci: cellCounter % 504,
212
212
  nwtRS: 450.0,
213
213
  nwtBW: 10.0,
214
214
  other: {
215
+ city: ['Tehran', 'Shiraz', 'Isfahan', 'Mashhad', 'Tabriz'][siteNum % 5],
216
+ bcc: bandIndex % 8,
217
+ ncc: Math.floor(bandIndex / 8) % 8,
218
+ mall: random() < 0.1 ? 'Shopping Center' : undefined,
219
+ hsn: techBand.tech === '2G' ? Math.floor(random() * 64) : undefined,
215
220
  demoCell: true,
216
221
  siteNumber: actualSiteIndex,
217
222
  sector: sector.sectorNum,
218
223
  techBandKey: `${techBand.tech}_${techBand.band}`,
219
- radius: normalizedRadius,
220
224
  densityZone: zone.name
221
225
  },
222
226
  customSubgroup: `Sector-${sector.sectorNum}`
@@ -10,7 +10,7 @@ export interface Cell {
10
10
  id: string;
11
11
  txId: string;
12
12
  cellID: string;
13
- cellID2G: string;
13
+ cellId2G: string;
14
14
  cellName: string;
15
15
  siteId: string;
16
16
  tech: string;
@@ -35,16 +35,16 @@ export interface Cell {
35
35
  siteLongitude: number;
36
36
  comment: string;
37
37
  planner: string;
38
- atollETP: number;
39
- atollPW: number;
40
- atollRS: number;
41
- atollBW: number;
42
- cellId3: string;
43
- nwtP1: number;
44
- nwtP2: number;
45
- pci1: number;
46
- nwtRS: number;
47
- nwtBW: number;
38
+ atollET?: number;
39
+ atollPW?: number;
40
+ atollRS?: number;
41
+ atollBW?: number;
42
+ rru: string;
43
+ nwtET?: number;
44
+ nwtPW?: number;
45
+ pci?: number;
46
+ nwtRS?: number;
47
+ nwtBW?: number;
48
48
  other?: Record<string, unknown>;
49
49
  customSubgroup: string;
50
50
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.106",
3
+ "version": "0.0.108",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",