@smartnet360/svelte-components 0.0.42 → 0.0.43

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.
@@ -3,47 +3,81 @@
3
3
  <script lang="ts">
4
4
  import { TreeView, createTreeStore } from '../../core/TreeView';
5
5
  import { ChartComponent, type Layout, type CellStylingConfig } from '../../core/Charts';
6
- import { buildTreeNodes, filterChartData, transformChartData, type CellTrafficRecord, defaultCellStyling } from './index';
6
+ import { buildTreeNodes, filterChartData, transformChartData, type CellTrafficRecord, defaultCellStyling, type TreeGroupingConfig, defaultTreeGrouping } from './index';
7
7
  import { expandLayoutForCells } from './helper';
8
8
  import { log } from '../../core/logger';
9
- import { onMount } from 'svelte';
10
9
  import type {ChartMarker, Mode } from '../../index.js';
10
+ import { checkHealth, getMessage } from '../../core/FeatureRegistry';
11
11
 
12
12
  interface Props {
13
13
  rawData: CellTrafficRecord[];
14
- baseLayout: Layout;
14
+ multiCellLayout: Layout; // Layout for multiple cells (also used as fallback)
15
+ singleLteLayout?: Layout; // Optional: Layout for single LTE cell
16
+ singleNrLayout?: Layout; // Optional: Layout for single NR cell
15
17
  baseMetrics: string[];
16
18
  mode: Mode;
17
19
  markers?: ChartMarker[];
18
20
  cellStyling?: CellStylingConfig; // Optional cell styling config (defaults to defaultCellStyling)
21
+ initialGrouping?: TreeGroupingConfig; // Optional initial tree grouping (defaults to Site → Azimuth → Cell)
22
+ showGroupingSelector?: boolean; // Show/hide the grouping dropdown (default: true)
19
23
  }
20
24
 
21
- let { rawData, baseLayout, baseMetrics, mode = "scrollspy", markers = [], cellStyling = defaultCellStyling }: Props = $props();
25
+ let { rawData, multiCellLayout, singleLteLayout, singleNrLayout, baseMetrics, mode = "scrollspy", markers = [], cellStyling = defaultCellStyling, initialGrouping = defaultTreeGrouping, showGroupingSelector = true }: Props = $props();
26
+
27
+ // Check feature health
28
+ let isHealthy = $state(checkHealth('sitecheck'));
29
+
30
+ // Internal state for current grouping
31
+ let treeGrouping = $state<TreeGroupingConfig>(initialGrouping);
32
+
33
+ // Available grouping presets
34
+ const groupingPresets = [
35
+ { label: 'Site → Azimuth → Cell', value: { level0: 'site', level1: 'azimuth', level2: 'cell' } as TreeGroupingConfig },
36
+ { label: 'Site → Band → Cell', value: { level0: 'site', level1: 'band', level2: 'cell' } as TreeGroupingConfig },
37
+ { label: 'Band → Site → Cell', value: { level0: 'band', level1: 'site', level2: 'cell' } as TreeGroupingConfig },
38
+ // { label: 'Band → Azimuth → Cell', value: { level0: 'band', level1: 'azimuth', level2: 'cell' } as TreeGroupingConfig },
39
+ // { label: 'Azimuth → Site → Cell', value: { level0: 'azimuth', level1: 'site', level2: 'cell' } as TreeGroupingConfig },
40
+ { label: 'Band → Cell', value: { level0: 'band', level1: null, level2: 'cell' } as TreeGroupingConfig },
41
+ // { label: 'Site → Cell (2-level)', value: { level0: 'site', level1: null, level2: 'cell' } as TreeGroupingConfig },
42
+ // { label: 'Azimuth → Cell (2-level)', value: { level0: 'azimuth', level1: null, level2: 'cell' } as TreeGroupingConfig },
43
+ ];
22
44
 
23
45
  let treeStore = $state<ReturnType<typeof createTreeStore> | null>(null);
24
46
 
25
- onMount(() => {
26
- log('🚀 SiteCheck Initializing', {
27
- totalRecords: rawData.length,
28
- baseMetrics,
29
- mode
30
- });
47
+ // Rebuild tree whenever treeGrouping changes
48
+ $effect(() => {
31
49
 
32
- // Build tree nodes from raw data
33
- const treeNodes = buildTreeNodes(rawData);
50
+ log('� Rebuilding tree with grouping', { treeGrouping });
51
+
52
+ // Clear any existing localStorage data to prevent stale state
53
+ const storageKey = 'site-check:treeState';
54
+ if (typeof window !== 'undefined') {
55
+ localStorage.removeItem(storageKey);
56
+ log('🧹 Cleared localStorage:', storageKey);
57
+ }
58
+
59
+ // Build tree nodes from raw data with custom grouping
60
+ const treeNodes = buildTreeNodes(rawData, treeGrouping);
34
61
  log('🌲 Tree Nodes Built', {
35
62
  nodeCount: treeNodes.length,
36
- firstNode: treeNodes[0]
63
+ firstNode: treeNodes[0],
64
+ grouping: treeGrouping
37
65
  });
38
-
66
+ if(isHealthy === false) {
67
+ console.log('Configuration Required');
68
+ return;
69
+ }
39
70
  // Create tree store
40
71
  treeStore = createTreeStore({
41
72
  nodes: treeNodes,
42
73
  namespace: 'site-check',
43
- persistState: true,
74
+ persistState: false, // Don't persist when grouping changes dynamically
44
75
  defaultExpandAll: false
45
76
  });
46
- log('✅ Tree Store Created', { namespace: 'site-check' });
77
+ log('✅ Tree Store Created', {
78
+ namespace: 'site-check',
79
+ grouping: treeGrouping
80
+ });
47
81
  });
48
82
 
49
83
  // Derive chart data from tree selection
@@ -74,10 +108,11 @@
74
108
  return transformed;
75
109
  });
76
110
 
77
- // Expand layout based on selected cells
111
+ // Expand layout based on selected cells and chosen base layout
78
112
  let chartLayout = $derived.by(() => {
79
- const expanded = expandLayoutForCells(baseLayout, filteredData, cellStyling);
113
+ const expanded = expandLayoutForCells(selectedBaseLayout, filteredData, cellStyling);
80
114
  log('📐 Chart Layout:', {
115
+ layoutName: selectedBaseLayout.layoutName,
81
116
  sectionsCount: expanded.sections.length,
82
117
  totalCharts: expanded.sections.reduce((sum, s) => sum + s.charts.length, 0),
83
118
  firstSection: expanded.sections[0],
@@ -100,21 +135,92 @@
100
135
  log('📡 Total Sites:', count);
101
136
  return count;
102
137
  });
138
+
139
+ // Detect cell technology (LTE vs NR) for single-cell layout selection
140
+ let cellTechnology = $derived.by(() => {
141
+ if (totalCells !== 1) return null;
142
+
143
+ const cell = filteredData[0];
144
+ const band = cell?.band?.toUpperCase() || '';
145
+
146
+ if (band.startsWith('LTE')) {
147
+ log('📡 Detected Technology: LTE', { band });
148
+ return 'LTE';
149
+ }
150
+ if (band.startsWith('NR') || band.startsWith('5G')) {
151
+ log('📡 Detected Technology: NR', { band });
152
+ return 'NR';
153
+ }
154
+
155
+ log('📡 Detected Technology: UNKNOWN', { band });
156
+ return 'UNKNOWN';
157
+ });
158
+
159
+ // Select appropriate layout based on cell count and technology
160
+ let selectedBaseLayout = $derived.by(() => {
161
+ // Multiple cells → always use multi-cell layout
162
+ if (totalCells !== 1) {
163
+ log('📐 Layout Selection: Multi-cell (count=' + totalCells + ')');
164
+ return multiCellLayout;
165
+ }
166
+
167
+ // Single LTE cell → use LTE layout if available, otherwise fallback
168
+ if (cellTechnology === 'LTE' && singleLteLayout) {
169
+ log('📐 Layout Selection: Single LTE (optimized)');
170
+ return singleLteLayout;
171
+ }
172
+
173
+ // Single NR cell → use NR layout if available, otherwise fallback
174
+ if (cellTechnology === 'NR' && singleNrLayout) {
175
+ log('📐 Layout Selection: Single NR (optimized)');
176
+ return singleNrLayout;
177
+ }
178
+
179
+ // Fallback to multi-cell layout for single cells (works fine)
180
+ log('📐 Layout Selection: Multi-cell (fallback for single cell)', {
181
+ technology: cellTechnology,
182
+ lteLayout: !!singleLteLayout,
183
+ nrLayout: !!singleNrLayout
184
+ });
185
+ return multiCellLayout;
186
+ });
103
187
  </script>
104
188
 
105
189
  <div class="container-fluid vh-100 d-flex flex-column">
106
190
  <!-- Main Content -->
107
- <div class="row flex-grow-1 ">
191
+ <div class="row flex-grow-1" style="min-height: 0;">
108
192
  <!-- Left: Tree View -->
109
- <div class="col-lg-3 col-md-4 border-end bg-white overflow-auto">
110
- <div class="p-3">
111
- <!-- <h5 class="mb-3">
112
- <span class="me-2">📡</span>
113
- Site Selection
114
- </h5> -->
193
+ <div class="col-lg-3 col-md-4 border-end bg-white d-flex flex-column" style="min-height: 0; height: 100%;">
194
+ <!-- Grouping Selector -->
195
+ {#if showGroupingSelector}
196
+ <div class="p-3 border-bottom flex-shrink-0">
197
+ <label for="groupingSelect" class="form-label small fw-semibold mb-2">
198
+ Tree Grouping
199
+ </label>
200
+ <select
201
+ id="groupingSelect"
202
+ class="form-select form-select-sm"
203
+ onchange={(e) => {
204
+ const index = parseInt(e.currentTarget.value);
205
+ treeGrouping = groupingPresets[index].value;
206
+ }}
207
+ >
208
+ {#each groupingPresets as preset, i}
209
+ <option value={i} selected={JSON.stringify(preset.value) === JSON.stringify(treeGrouping)}>
210
+ {preset.label}
211
+ </option>
212
+ {/each}
213
+ </select>
214
+ <!-- <div class="text-muted small mt-1">
215
+ {treeGrouping.level0}{treeGrouping.level1 ? ` → ${treeGrouping.level1}` : ''} → {treeGrouping.level2}
216
+ </div> -->
217
+ </div>
218
+ {/if}
115
219
 
220
+ <!-- Tree View -->
221
+ <div class="flex-grow-1" style="min-height: 0; overflow: hidden;">
116
222
  {#if treeStore}
117
- <TreeView store={$treeStore!} showControls={false} showIndeterminate={true} height="100%" />
223
+ <TreeView store={$treeStore!} showControls={true} showIndeterminate={true} height="100%" />
118
224
  {/if}
119
225
  </div>
120
226
  </div>
@@ -133,12 +239,11 @@
133
239
  {:else}
134
240
  <div class="d-flex align-items-center justify-content-center h-100">
135
241
  <div class="text-center text-muted">
136
- <div class="mb-3" style="font-size: 4rem;">📊</div>
137
242
  <h5>No Data Selected</h5>
138
- <p>Select one or more cells from the tree to display KPI charts.</p>
139
243
  </div>
140
244
  </div>
141
245
  {/if}
142
246
  </div>
143
247
  </div>
144
248
  </div>
249
+
@@ -1,13 +1,17 @@
1
1
  import { type Layout, type CellStylingConfig } from '../../core/Charts';
2
- import { type CellTrafficRecord } from './index';
2
+ import { type CellTrafficRecord, type TreeGroupingConfig } from './index';
3
3
  import type { ChartMarker, Mode } from '../../index.js';
4
4
  interface Props {
5
5
  rawData: CellTrafficRecord[];
6
- baseLayout: Layout;
6
+ multiCellLayout: Layout;
7
+ singleLteLayout?: Layout;
8
+ singleNrLayout?: Layout;
7
9
  baseMetrics: string[];
8
10
  mode: Mode;
9
11
  markers?: ChartMarker[];
10
12
  cellStyling?: CellStylingConfig;
13
+ initialGrouping?: TreeGroupingConfig;
14
+ showGroupingSelector?: boolean;
11
15
  }
12
16
  declare const SiteCheck: import("svelte").Component<Props, {}, "">;
13
17
  type SiteCheck = ReturnType<typeof SiteCheck>;
@@ -11,6 +11,25 @@ export interface CellTrafficRecord {
11
11
  band: string;
12
12
  metrics: Record<string, number>;
13
13
  }
14
+ /**
15
+ * Tree grouping field types
16
+ */
17
+ export type TreeGroupField = 'site' | 'azimuth' | 'band' | 'sector';
18
+ /**
19
+ * Configuration for tree hierarchy grouping
20
+ * Defines which fields appear at each level of the tree
21
+ * - For 3-level tree: level0 → level1 → cell
22
+ * - For 2-level tree: level0 → cell (set level1 to null)
23
+ */
24
+ export interface TreeGroupingConfig {
25
+ level0: TreeGroupField;
26
+ level1: TreeGroupField | null;
27
+ level2: 'cell';
28
+ }
29
+ /**
30
+ * Default tree grouping: Site → Azimuth → Cell (3-level)
31
+ */
32
+ export declare const defaultTreeGrouping: TreeGroupingConfig;
14
33
  /**
15
34
  * Load cell traffic data from CSV file
16
35
  */
@@ -2,6 +2,14 @@
2
2
  * Data Loader for Site Check Component
3
3
  * Loads and parses cell_traffic_with_band.csv
4
4
  */
5
+ /**
6
+ * Default tree grouping: Site → Azimuth → Cell (3-level)
7
+ */
8
+ export const defaultTreeGrouping = {
9
+ level0: 'site',
10
+ level1: 'azimuth',
11
+ level2: 'cell'
12
+ };
5
13
  /**
6
14
  * Load cell traffic data from CSV file
7
15
  */
@@ -3,7 +3,7 @@
3
3
  * Public API exports for cell traffic KPI visualization
4
4
  */
5
5
  export { default as SiteCheck } from './SiteCheck.svelte';
6
- export { loadCellTrafficData, getUniqueSites, getUniqueCells, groupDataByCell, type CellTrafficRecord } from './data-loader.js';
6
+ export { loadCellTrafficData, getUniqueSites, getUniqueCells, groupDataByCell, defaultTreeGrouping, type CellTrafficRecord, type TreeGroupingConfig, type TreeGroupField } from './data-loader.js';
7
7
  export { buildTreeNodes, filterChartData, transformChartData, createStyledKPI, extractBandFromCell, getBandFrequency, sortCellsByBandFrequency } from './transforms.js';
8
8
  export { expandLayoutForCells, extractBaseMetrics } from './helper.js';
9
9
  export { defaultCellStyling } from './default-cell-styling.js';
@@ -5,7 +5,7 @@
5
5
  // Components
6
6
  export { default as SiteCheck } from './SiteCheck.svelte';
7
7
  // Data loading
8
- export { loadCellTrafficData, getUniqueSites, getUniqueCells, groupDataByCell } from './data-loader.js';
8
+ export { loadCellTrafficData, getUniqueSites, getUniqueCells, groupDataByCell, defaultTreeGrouping } from './data-loader.js';
9
9
  // Data transforms
10
10
  export { buildTreeNodes, filterChartData, transformChartData, createStyledKPI, extractBandFromCell, getBandFrequency, sortCellsByBandFrequency } from './transforms.js';
11
11
  // Helper functions
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Data Transforms for Site Check Component
3
+ * Converts raw CSV data to TreeView nodes and Chart configurations
4
+ */
5
+ import type { TreeNode } from '../../core/TreeView';
6
+ import type { KPI, CellStylingConfig } from '../../core/Charts';
7
+ import type { CellTrafficRecord } from './data-loader';
8
+ /**
9
+ * Extract band from cell name using regex pattern matching
10
+ * @param cellName - Cell name like "LTE700_1", "NR3500_2", etc.
11
+ * @returns Band string like "LTE700", "NR3500" or null if not found
12
+ * @deprecated Use the band field from CellTrafficRecord instead
13
+ */
14
+ export declare function extractBandFromCell(cellName: string): string | null;
15
+ /**
16
+ * Get frequency order for a band (for sorting)
17
+ * @param band - Band string like "LTE700", "NR3500"
18
+ * @returns Frequency number or high value for unknown bands
19
+ */
20
+ export declare function getBandFrequency(band: string | null): number;
21
+ /**
22
+ * Sort items by band frequency using actual band data from records
23
+ * @param items - Array of [cellName, record] tuples
24
+ * @returns Sorted array (ascending frequency order)
25
+ */
26
+ export declare function sortCellsByBandFrequency(items: [string, CellTrafficRecord][]): [string, CellTrafficRecord][];
27
+ /**
28
+ * Build hierarchical tree structure: Site → Sector (Azimuth) → Cell (Band)
29
+ */
30
+ export declare function buildTreeNodes(data: CellTrafficRecord[]): TreeNode[];
31
+ /**
32
+ * Filter chart data based on selected tree paths
33
+ * Only include cells that are checked in the tree
34
+ */
35
+ export declare function filterChartData(data: CellTrafficRecord[], checkedPaths: Set<string>): CellTrafficRecord[];
36
+ /**
37
+ * Transform data for chart component consumption
38
+ * Pivots data so each cell becomes its own KPI column
39
+ * Transforms from long format (many rows per cell) to wide format (one column per cell)
40
+ *
41
+ * @param data - Filtered cell traffic records
42
+ * @param baseMetrics - Array of metric names to pivot (e.g., ['dlGBytes', 'ulGBytes'])
43
+ */
44
+ export declare function transformChartData(data: CellTrafficRecord[], baseMetrics: string[]): any[];
45
+ /**
46
+ * Apply cell styling based on band and sector
47
+ * Modifies KPI objects to include color (from band) and lineStyle (from sector)
48
+ * Updates KPI name to format: Band_Azimuth°
49
+ *
50
+ * @param metricName - Base metric name (e.g., 'dlGBytes')
51
+ * @param cellRecord - Cell traffic record with band, sector, azimuth metadata
52
+ * @param unit - Unit string for the metric
53
+ * @param stylingConfig - Optional cell styling configuration (band colors, sector line styles)
54
+ * @returns Styled KPI object
55
+ */
56
+ export declare function createStyledKPI(metricName: string, cellRecord: CellTrafficRecord, unit: string, stylingConfig?: CellStylingConfig): KPI;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Data Transforms for Site Check Component
3
+ * Converts raw CSV data to TreeView nodes and Chart configurations
4
+ */
5
+ import { log } from '../../core/logger';
6
+ /**
7
+ * Band frequency mapping for consistent ordering
8
+ * Maps band strings to their actual frequencies in MHz
9
+ */
10
+ const BAND_FREQUENCY_ORDER = {
11
+ // LTE Bands (by frequency)
12
+ 'LTE700': 700,
13
+ 'LTE800': 800,
14
+ 'LTE900': 900,
15
+ 'LTE1800': 1800,
16
+ 'LTE2100': 2100,
17
+ 'LTE2600': 2600,
18
+ // NR/5G Bands (by frequency)
19
+ 'NR700': 700.1, // Slightly higher to sort after LTE700
20
+ 'NR2100': 2100.1,
21
+ 'NR3500': 3500,
22
+ 'NR26000': 26000 // mmWave
23
+ };
24
+ /**
25
+ * Extract band from cell name using regex pattern matching
26
+ * @param cellName - Cell name like "LTE700_1", "NR3500_2", etc.
27
+ * @returns Band string like "LTE700", "NR3500" or null if not found
28
+ * @deprecated Use the band field from CellTrafficRecord instead
29
+ */
30
+ export function extractBandFromCell(cellName) {
31
+ // Match patterns like "LTE700", "NR3500", etc.
32
+ const match = cellName.match(/(LTE|NR)(\d+)/i);
33
+ return match ? `${match[1].toUpperCase()}${match[2]}` : null;
34
+ }
35
+ /**
36
+ * Get frequency order for a band (for sorting)
37
+ * @param band - Band string like "LTE700", "NR3500"
38
+ * @returns Frequency number or high value for unknown bands
39
+ */
40
+ export function getBandFrequency(band) {
41
+ if (!band)
42
+ return 999999; // Unknown bands go to end
43
+ return BAND_FREQUENCY_ORDER[band] || 999999;
44
+ }
45
+ /**
46
+ * Sort items by band frequency using actual band data from records
47
+ * @param items - Array of [cellName, record] tuples
48
+ * @returns Sorted array (ascending frequency order)
49
+ */
50
+ export function sortCellsByBandFrequency(items) {
51
+ return items.sort((a, b) => {
52
+ const [cellNameA, recordA] = a;
53
+ const [cellNameB, recordB] = b;
54
+ const freqA = getBandFrequency(recordA.band);
55
+ const freqB = getBandFrequency(recordB.band);
56
+ // Primary sort: by frequency
57
+ if (freqA !== freqB) {
58
+ return freqA - freqB;
59
+ }
60
+ // Secondary sort: by cell name for same frequency
61
+ return cellNameA.localeCompare(cellNameB);
62
+ });
63
+ }
64
+ /**
65
+ * Build hierarchical tree structure: Site → Sector (Azimuth) → Cell (Band)
66
+ */
67
+ export function buildTreeNodes(data) {
68
+ log('🔄 Building tree nodes', { recordCount: data.length });
69
+ // Group by site → azimuth → cell
70
+ const siteMap = new Map();
71
+ data.forEach((record) => {
72
+ if (!siteMap.has(record.siteName)) {
73
+ siteMap.set(record.siteName, new Map());
74
+ }
75
+ const azimuthMap = siteMap.get(record.siteName);
76
+ if (!azimuthMap.has(record.azimuth)) {
77
+ azimuthMap.set(record.azimuth, new Map());
78
+ }
79
+ const cellMap = azimuthMap.get(record.azimuth);
80
+ // Store one record per cell (we just need metadata, not all time series)
81
+ if (!cellMap.has(record.cellName)) {
82
+ cellMap.set(record.cellName, record);
83
+ }
84
+ });
85
+ // Build tree structure
86
+ const treeNodes = [];
87
+ Array.from(siteMap.entries())
88
+ .sort(([a], [b]) => a.localeCompare(b))
89
+ .forEach(([siteName, azimuthMap]) => {
90
+ const siteNode = {
91
+ id: siteName, // Simple ID
92
+ label: `Site ${siteName}`,
93
+ // icon: '📡',
94
+ metadata: { type: 'site', siteName },
95
+ defaultExpanded: false,
96
+ defaultChecked: false, // Don't check parent nodes
97
+ children: []
98
+ };
99
+ Array.from(azimuthMap.entries())
100
+ .sort(([a], [b]) => a - b)
101
+ .forEach(([azimuth, cellMap]) => {
102
+ const sectorNode = {
103
+ id: `${azimuth}`, // Simple ID (just azimuth)
104
+ label: `${azimuth}° Sector`,
105
+ // icon: '📍',
106
+ metadata: { type: 'sector', azimuth, siteName },
107
+ defaultExpanded: false,
108
+ defaultChecked: false, // Don't check parent nodes
109
+ children: []
110
+ };
111
+ // Sort cells by band frequency (LTE700, LTE800, etc.)
112
+ const sortedCells = sortCellsByBandFrequency(Array.from(cellMap.entries()));
113
+ sortedCells.forEach(([cellName, record]) => {
114
+ const cellNode = {
115
+ id: cellName, // Simple ID (just cell name)
116
+ label: `${cellName} (${record.band})`,
117
+ icon: getBandIcon(record.band),
118
+ metadata: {
119
+ type: 'cell',
120
+ cellName,
121
+ band: record.band,
122
+ siteName: record.siteName,
123
+ sector: record.sector,
124
+ azimuth: record.azimuth
125
+ },
126
+ defaultChecked: true
127
+ };
128
+ sectorNode.children.push(cellNode);
129
+ });
130
+ siteNode.children.push(sectorNode);
131
+ });
132
+ treeNodes.push(siteNode);
133
+ });
134
+ log('✅ Tree nodes built', {
135
+ totalNodes: treeNodes.length,
136
+ totalSites: siteMap.size,
137
+ sampleSite: treeNodes[0]?.label
138
+ });
139
+ return treeNodes;
140
+ }
141
+ /**
142
+ * Get icon emoji based on band technology
143
+ */
144
+ function getBandIcon(band) {
145
+ return '';
146
+ if (band.startsWith('NR'))
147
+ return '📶'; // 5G
148
+ if (band.startsWith('LTE'))
149
+ return '📱'; // 4G
150
+ return '📡'; // Fallback
151
+ }
152
+ /**
153
+ * Filter chart data based on selected tree paths
154
+ * Only include cells that are checked in the tree
155
+ */
156
+ export function filterChartData(data, checkedPaths) {
157
+ log('🔄 Filtering chart data', {
158
+ totalRecords: data.length,
159
+ checkedPathsCount: checkedPaths.size,
160
+ paths: Array.from(checkedPaths)
161
+ });
162
+ // Extract cell names from checked leaf paths (format: "site:azimuth:cellName")
163
+ const selectedCells = new Set();
164
+ checkedPaths.forEach((path) => {
165
+ const parts = path.split(':');
166
+ if (parts.length === 3) {
167
+ // This is a cell-level path (site:azimuth:cellName)
168
+ selectedCells.add(parts[2]);
169
+ }
170
+ });
171
+ // Filter data to only include selected cells
172
+ const filtered = data.filter((record) => selectedCells.has(record.cellName));
173
+ log('✅ Data filtered', {
174
+ selectedCells: Array.from(selectedCells),
175
+ filteredRecords: filtered.length,
176
+ uniqueCells: new Set(filtered.map(r => r.cellName)).size
177
+ });
178
+ return filtered;
179
+ }
180
+ /**
181
+ * Transform data for chart component consumption
182
+ * Pivots data so each cell becomes its own KPI column
183
+ * Transforms from long format (many rows per cell) to wide format (one column per cell)
184
+ *
185
+ * @param data - Filtered cell traffic records
186
+ * @param baseMetrics - Array of metric names to pivot (e.g., ['dlGBytes', 'ulGBytes'])
187
+ */
188
+ export function transformChartData(data, baseMetrics) {
189
+ log('🔄 Transforming chart data', {
190
+ inputRecords: data.length,
191
+ baseMetrics,
192
+ uniqueCells: new Set(data.map(r => r.cellName)).size
193
+ });
194
+ // Group data by date
195
+ const dateMap = new Map();
196
+ data.forEach((record) => {
197
+ if (!dateMap.has(record.date)) {
198
+ dateMap.set(record.date, new Map());
199
+ }
200
+ dateMap.get(record.date).set(record.cellName, record);
201
+ });
202
+ // Build pivoted data: one row per date, one column per cell per metric
203
+ const pivotedData = [];
204
+ dateMap.forEach((cellsOnDate, date) => {
205
+ const row = {
206
+ TIMESTAMP: date
207
+ };
208
+ cellsOnDate.forEach((record, cellName) => {
209
+ // Pivot each base metric into cell-specific columns
210
+ baseMetrics.forEach((metricName) => {
211
+ const value = record.metrics[metricName];
212
+ if (value !== undefined) {
213
+ row[`${metricName}_${cellName}`] = value;
214
+ }
215
+ });
216
+ // Store metadata for reference (band, azimuth, etc.)
217
+ row[`BAND_${cellName}`] = record.band;
218
+ row[`AZIMUTH_${cellName}`] = record.azimuth;
219
+ });
220
+ pivotedData.push(row);
221
+ });
222
+ // Sort by date
223
+ pivotedData.sort((a, b) => a.TIMESTAMP.localeCompare(b.TIMESTAMP));
224
+ log('✅ Data transformed', {
225
+ outputRows: pivotedData.length,
226
+ dateRange: pivotedData.length > 0 ?
227
+ `${pivotedData[0].TIMESTAMP} to ${pivotedData[pivotedData.length - 1].TIMESTAMP}` :
228
+ 'none',
229
+ columnsPerRow: pivotedData[0] ? Object.keys(pivotedData[0]).length : 0,
230
+ sampleRow: pivotedData[0]
231
+ });
232
+ return pivotedData;
233
+ }
234
+ /**
235
+ * Apply cell styling based on band and sector
236
+ * Modifies KPI objects to include color (from band) and lineStyle (from sector)
237
+ * Updates KPI name to format: Band_Azimuth°
238
+ *
239
+ * @param metricName - Base metric name (e.g., 'dlGBytes')
240
+ * @param cellRecord - Cell traffic record with band, sector, azimuth metadata
241
+ * @param unit - Unit string for the metric
242
+ * @param stylingConfig - Optional cell styling configuration (band colors, sector line styles)
243
+ * @returns Styled KPI object
244
+ */
245
+ export function createStyledKPI(metricName, cellRecord, unit, stylingConfig) {
246
+ const { band, sector, azimuth, cellName } = cellRecord;
247
+ // Get color from band (if config provided)
248
+ const color = stylingConfig?.bandColors?.[band];
249
+ // Get line style from sector (if config provided)
250
+ const lineStyle = stylingConfig?.sectorLineStyles?.[sector.toString()];
251
+ // Format name as: Band_Azimuth°
252
+ const displayName = `${band}_${azimuth}°`;
253
+ // Build KPI with cell-specific styling
254
+ const kpi = {
255
+ rawName: `${metricName}_${cellName}`, // Column name in pivoted data
256
+ name: displayName,
257
+ scale: 'absolute',
258
+ unit,
259
+ ...(color && { color }),
260
+ ...(lineStyle && { lineStyle })
261
+ };
262
+ log('🎨 Styled KPI created', {
263
+ metricName,
264
+ cellName,
265
+ displayName,
266
+ band,
267
+ sector,
268
+ azimuth,
269
+ color,
270
+ lineStyle
271
+ });
272
+ return kpi;
273
+ }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { TreeNode } from '../../core/TreeView';
6
6
  import type { KPI, CellStylingConfig } from '../../core/Charts';
7
- import type { CellTrafficRecord } from './data-loader';
7
+ import type { CellTrafficRecord, TreeGroupingConfig } from './data-loader';
8
8
  /**
9
9
  * Extract band from cell name using regex pattern matching
10
10
  * @param cellName - Cell name like "LTE700_1", "NR3500_2", etc.
@@ -25,12 +25,16 @@ export declare function getBandFrequency(band: string | null): number;
25
25
  */
26
26
  export declare function sortCellsByBandFrequency(items: [string, CellTrafficRecord][]): [string, CellTrafficRecord][];
27
27
  /**
28
- * Build hierarchical tree structure: Site Sector (Azimuth) → Cell (Band)
28
+ * Build hierarchical tree structure with configurable grouping
29
+ * Supports both 2-level (level0 → cell) and 3-level (level0 → level1 → cell) trees
30
+ * @param data - Cell traffic records
31
+ * @param grouping - Tree grouping configuration (defaults to Site → Azimuth → Cell)
29
32
  */
30
- export declare function buildTreeNodes(data: CellTrafficRecord[]): TreeNode[];
33
+ export declare function buildTreeNodes(data: CellTrafficRecord[], grouping?: TreeGroupingConfig): TreeNode[];
31
34
  /**
32
35
  * Filter chart data based on selected tree paths
33
36
  * Only include cells that are checked in the tree
37
+ * Handles both 2-level (level0:cellName) and 3-level (level0:level1:cellName) paths
34
38
  */
35
39
  export declare function filterChartData(data: CellTrafficRecord[], checkedPaths: Set<string>): CellTrafficRecord[];
36
40
  /**
@@ -39,18 +43,15 @@ export declare function filterChartData(data: CellTrafficRecord[], checkedPaths:
39
43
  * Transforms from long format (many rows per cell) to wide format (one column per cell)
40
44
  *
41
45
  * @param data - Filtered cell traffic records
42
- * @param baseMetrics - Array of metric names to pivot (e.g., ['dlGBytes', 'ulGBytes'])
46
+ * @param baseMetrics - Array of metric names to pivot (e.g., ['DL_GBYTES', 'UL_GBYTES'])
43
47
  */
44
48
  export declare function transformChartData(data: CellTrafficRecord[], baseMetrics: string[]): any[];
45
49
  /**
46
- * Apply cell styling based on band and sector
47
- * Modifies KPI objects to include color (from band) and lineStyle (from sector)
48
- * Updates KPI name to format: Band_Azimuth°
49
- *
50
- * @param metricName - Base metric name (e.g., 'dlGBytes')
51
- * @param cellRecord - Cell traffic record with band, sector, azimuth metadata
52
- * @param unit - Unit string for the metric
53
- * @param stylingConfig - Optional cell styling configuration (band colors, sector line styles)
54
- * @returns Styled KPI object
50
+ * Create a styled KPI with band colors and sector line styles
51
+ * @param metricName - Base metric name (e.g., 'DL_GBYTES')
52
+ * @param cellRecord - Cell traffic record with metadata
53
+ * @param unit - Unit string (e.g., 'GB', '%')
54
+ * @param stylingConfig - Optional styling configuration
55
+ * @returns KPI with cell-specific styling applied
55
56
  */
56
57
  export declare function createStyledKPI(metricName: string, cellRecord: CellTrafficRecord, unit: string, stylingConfig?: CellStylingConfig): KPI;
@@ -62,21 +62,34 @@ export function sortCellsByBandFrequency(items) {
62
62
  });
63
63
  }
64
64
  /**
65
- * Build hierarchical tree structure: Site Sector (Azimuth) → Cell (Band)
65
+ * Build hierarchical tree structure with configurable grouping
66
+ * Supports both 2-level (level0 → cell) and 3-level (level0 → level1 → cell) trees
67
+ * @param data - Cell traffic records
68
+ * @param grouping - Tree grouping configuration (defaults to Site → Azimuth → Cell)
66
69
  */
67
- export function buildTreeNodes(data) {
68
- log('🔄 Building tree nodes', { recordCount: data.length });
69
- // Group by site → azimuth → cell
70
- const siteMap = new Map();
70
+ export function buildTreeNodes(data, grouping = { level0: 'site', level1: 'azimuth', level2: 'cell' }) {
71
+ log('🔄 Building tree nodes', {
72
+ recordCount: data.length,
73
+ grouping,
74
+ treeDepth: grouping.level1 === null ? 2 : 3
75
+ });
76
+ // Check if this is a 2-level tree (no level1)
77
+ if (grouping.level1 === null) {
78
+ return build2LevelTree(data, grouping);
79
+ }
80
+ // 3-level tree: Group data by level0 → level1 → cell
81
+ const level0Map = new Map();
71
82
  data.forEach((record) => {
72
- if (!siteMap.has(record.siteName)) {
73
- siteMap.set(record.siteName, new Map());
83
+ const level0Value = getFieldValue(record, grouping.level0);
84
+ const level1Value = getFieldValue(record, grouping.level1); // We know level1 is not null here
85
+ if (!level0Map.has(level0Value)) {
86
+ level0Map.set(level0Value, new Map());
74
87
  }
75
- const azimuthMap = siteMap.get(record.siteName);
76
- if (!azimuthMap.has(record.azimuth)) {
77
- azimuthMap.set(record.azimuth, new Map());
88
+ const level1Map = level0Map.get(level0Value);
89
+ if (!level1Map.has(level1Value)) {
90
+ level1Map.set(level1Value, new Map());
78
91
  }
79
- const cellMap = azimuthMap.get(record.azimuth);
92
+ const cellMap = level1Map.get(level1Value);
80
93
  // Store one record per cell (we just need metadata, not all time series)
81
94
  if (!cellMap.has(record.cellName)) {
82
95
  cellMap.set(record.cellName, record);
@@ -84,35 +97,41 @@ export function buildTreeNodes(data) {
84
97
  });
85
98
  // Build tree structure
86
99
  const treeNodes = [];
87
- Array.from(siteMap.entries())
88
- .sort(([a], [b]) => a.localeCompare(b))
89
- .forEach(([siteName, azimuthMap]) => {
90
- const siteNode = {
91
- id: siteName, // Simple ID
92
- label: `Site ${siteName}`,
93
- // icon: '📡',
94
- metadata: { type: 'site', siteName },
100
+ Array.from(level0Map.entries())
101
+ .sort(([a], [b]) => compareValues(a, b))
102
+ .forEach(([level0Value, level1Map]) => {
103
+ const level0Node = {
104
+ id: String(level0Value),
105
+ label: formatNodeLabel(grouping.level0, level0Value),
106
+ metadata: {
107
+ type: grouping.level0,
108
+ value: level0Value,
109
+ grouping: grouping.level0
110
+ },
95
111
  defaultExpanded: false,
96
112
  defaultChecked: false, // Don't check parent nodes
97
113
  children: []
98
114
  };
99
- Array.from(azimuthMap.entries())
100
- .sort(([a], [b]) => a - b)
101
- .forEach(([azimuth, cellMap]) => {
102
- const sectorNode = {
103
- id: `${azimuth}`, // Simple ID (just azimuth)
104
- label: `${azimuth}° Sector`,
105
- // icon: '📍',
106
- metadata: { type: 'sector', azimuth, siteName },
115
+ Array.from(level1Map.entries())
116
+ .sort(([a], [b]) => compareValues(a, b))
117
+ .forEach(([level1Value, cellMap]) => {
118
+ const level1Node = {
119
+ id: String(level1Value),
120
+ label: formatNodeLabel(grouping.level1, level1Value), // We know level1 is not null here
121
+ metadata: {
122
+ type: grouping.level1,
123
+ value: level1Value,
124
+ grouping: grouping.level1
125
+ },
107
126
  defaultExpanded: false,
108
127
  defaultChecked: false, // Don't check parent nodes
109
128
  children: []
110
129
  };
111
- // Sort cells by band frequency (LTE700, LTE800, etc.)
130
+ // Sort cells by band frequency
112
131
  const sortedCells = sortCellsByBandFrequency(Array.from(cellMap.entries()));
113
132
  sortedCells.forEach(([cellName, record]) => {
114
133
  const cellNode = {
115
- id: cellName, // Simple ID (just cell name)
134
+ id: cellName,
116
135
  label: `${cellName} (${record.band})`,
117
136
  icon: getBandIcon(record.band),
118
137
  metadata: {
@@ -125,19 +144,124 @@ export function buildTreeNodes(data) {
125
144
  },
126
145
  defaultChecked: true
127
146
  };
128
- sectorNode.children.push(cellNode);
147
+ level1Node.children.push(cellNode);
129
148
  });
130
- siteNode.children.push(sectorNode);
149
+ level0Node.children.push(level1Node);
131
150
  });
132
- treeNodes.push(siteNode);
151
+ treeNodes.push(level0Node);
133
152
  });
134
153
  log('✅ Tree nodes built', {
135
154
  totalNodes: treeNodes.length,
136
- totalSites: siteMap.size,
137
- sampleSite: treeNodes[0]?.label
155
+ grouping,
156
+ sampleNode: treeNodes[0]?.label
138
157
  });
139
158
  return treeNodes;
140
159
  }
160
+ /**
161
+ * Build 2-level tree: level0 → cell (no middle level)
162
+ */
163
+ function build2LevelTree(data, grouping) {
164
+ // Group data by level0 → cell
165
+ const level0Map = new Map();
166
+ data.forEach((record) => {
167
+ const level0Value = getFieldValue(record, grouping.level0);
168
+ if (!level0Map.has(level0Value)) {
169
+ level0Map.set(level0Value, new Map());
170
+ }
171
+ const cellMap = level0Map.get(level0Value);
172
+ // Store one record per cell
173
+ if (!cellMap.has(record.cellName)) {
174
+ cellMap.set(record.cellName, record);
175
+ }
176
+ });
177
+ // Build tree structure
178
+ const treeNodes = [];
179
+ Array.from(level0Map.entries())
180
+ .sort(([a], [b]) => compareValues(a, b))
181
+ .forEach(([level0Value, cellMap]) => {
182
+ const level0Node = {
183
+ id: String(level0Value),
184
+ label: formatNodeLabel(grouping.level0, level0Value),
185
+ metadata: {
186
+ type: grouping.level0,
187
+ value: level0Value,
188
+ grouping: grouping.level0
189
+ },
190
+ defaultExpanded: false,
191
+ defaultChecked: false, // Don't check parent nodes
192
+ children: []
193
+ };
194
+ // Sort cells by band frequency
195
+ const sortedCells = sortCellsByBandFrequency(Array.from(cellMap.entries()));
196
+ sortedCells.forEach(([cellName, record]) => {
197
+ const cellNode = {
198
+ id: cellName,
199
+ label: `${cellName} (${record.band})`,
200
+ icon: getBandIcon(record.band),
201
+ metadata: {
202
+ type: 'cell',
203
+ cellName,
204
+ band: record.band,
205
+ siteName: record.siteName,
206
+ sector: record.sector,
207
+ azimuth: record.azimuth
208
+ },
209
+ defaultChecked: true
210
+ };
211
+ level0Node.children.push(cellNode);
212
+ });
213
+ treeNodes.push(level0Node);
214
+ });
215
+ log('✅ 2-level tree nodes built', {
216
+ totalNodes: treeNodes.length,
217
+ grouping: `${grouping.level0} → cell`,
218
+ sampleNode: treeNodes[0]?.label
219
+ });
220
+ return treeNodes;
221
+ }
222
+ /**
223
+ * Get field value from record based on grouping field type
224
+ */
225
+ function getFieldValue(record, field) {
226
+ switch (field) {
227
+ case 'site':
228
+ return record.siteName;
229
+ case 'azimuth':
230
+ return record.azimuth;
231
+ case 'band':
232
+ return record.band;
233
+ case 'sector':
234
+ return record.sector;
235
+ default:
236
+ return record.siteName;
237
+ }
238
+ }
239
+ /**
240
+ * Format node label based on field type
241
+ */
242
+ function formatNodeLabel(field, value) {
243
+ switch (field) {
244
+ case 'site':
245
+ return `Site ${value}`;
246
+ case 'azimuth':
247
+ return `${value}° Sector`;
248
+ case 'band':
249
+ return `${value}`;
250
+ case 'sector':
251
+ return `Sector ${value}`;
252
+ default:
253
+ return String(value);
254
+ }
255
+ }
256
+ /**
257
+ * Compare values for sorting (handles both strings and numbers)
258
+ */
259
+ function compareValues(a, b) {
260
+ if (typeof a === 'number' && typeof b === 'number') {
261
+ return a - b;
262
+ }
263
+ return String(a).localeCompare(String(b));
264
+ }
141
265
  /**
142
266
  * Get icon emoji based on band technology
143
267
  */
@@ -152,6 +276,7 @@ function getBandIcon(band) {
152
276
  /**
153
277
  * Filter chart data based on selected tree paths
154
278
  * Only include cells that are checked in the tree
279
+ * Handles both 2-level (level0:cellName) and 3-level (level0:level1:cellName) paths
155
280
  */
156
281
  export function filterChartData(data, checkedPaths) {
157
282
  log('🔄 Filtering chart data', {
@@ -159,21 +284,24 @@ export function filterChartData(data, checkedPaths) {
159
284
  checkedPathsCount: checkedPaths.size,
160
285
  paths: Array.from(checkedPaths)
161
286
  });
162
- // Extract cell names from checked leaf paths (format: "site:azimuth:cellName")
287
+ // Extract cell names from checked leaf paths
163
288
  const selectedCells = new Set();
164
289
  checkedPaths.forEach((path) => {
165
290
  const parts = path.split(':');
166
291
  if (parts.length === 3) {
167
- // This is a cell-level path (site:azimuth:cellName)
292
+ // 3-level path: level0:level1:cellName
168
293
  selectedCells.add(parts[2]);
169
294
  }
295
+ else if (parts.length === 2) {
296
+ // 2-level path: level0:cellName
297
+ selectedCells.add(parts[1]);
298
+ }
170
299
  });
171
300
  // Filter data to only include selected cells
172
301
  const filtered = data.filter((record) => selectedCells.has(record.cellName));
173
- log('✅ Data filtered', {
302
+ log('✅ Filtered chart data', {
174
303
  selectedCells: Array.from(selectedCells),
175
- filteredRecords: filtered.length,
176
- uniqueCells: new Set(filtered.map(r => r.cellName)).size
304
+ filteredCount: filtered.length
177
305
  });
178
306
  return filtered;
179
307
  }
@@ -183,13 +311,12 @@ export function filterChartData(data, checkedPaths) {
183
311
  * Transforms from long format (many rows per cell) to wide format (one column per cell)
184
312
  *
185
313
  * @param data - Filtered cell traffic records
186
- * @param baseMetrics - Array of metric names to pivot (e.g., ['dlGBytes', 'ulGBytes'])
314
+ * @param baseMetrics - Array of metric names to pivot (e.g., ['DL_GBYTES', 'UL_GBYTES'])
187
315
  */
188
316
  export function transformChartData(data, baseMetrics) {
189
317
  log('🔄 Transforming chart data', {
190
- inputRecords: data.length,
191
- baseMetrics,
192
- uniqueCells: new Set(data.map(r => r.cellName)).size
318
+ rowCount: data.length,
319
+ baseMetrics
193
320
  });
194
321
  // Group data by date
195
322
  const dateMap = new Map();
@@ -221,26 +348,19 @@ export function transformChartData(data, baseMetrics) {
221
348
  });
222
349
  // Sort by date
223
350
  pivotedData.sort((a, b) => a.TIMESTAMP.localeCompare(b.TIMESTAMP));
224
- log('✅ Data transformed', {
351
+ log('✅ Chart data transformed', {
225
352
  outputRows: pivotedData.length,
226
- dateRange: pivotedData.length > 0 ?
227
- `${pivotedData[0].TIMESTAMP} to ${pivotedData[pivotedData.length - 1].TIMESTAMP}` :
228
- 'none',
229
- columnsPerRow: pivotedData[0] ? Object.keys(pivotedData[0]).length : 0,
230
- sampleRow: pivotedData[0]
353
+ sampleColumns: pivotedData[0] ? Object.keys(pivotedData[0]) : []
231
354
  });
232
355
  return pivotedData;
233
356
  }
234
357
  /**
235
- * Apply cell styling based on band and sector
236
- * Modifies KPI objects to include color (from band) and lineStyle (from sector)
237
- * Updates KPI name to format: Band_Azimuth°
238
- *
239
- * @param metricName - Base metric name (e.g., 'dlGBytes')
240
- * @param cellRecord - Cell traffic record with band, sector, azimuth metadata
241
- * @param unit - Unit string for the metric
242
- * @param stylingConfig - Optional cell styling configuration (band colors, sector line styles)
243
- * @returns Styled KPI object
358
+ * Create a styled KPI with band colors and sector line styles
359
+ * @param metricName - Base metric name (e.g., 'DL_GBYTES')
360
+ * @param cellRecord - Cell traffic record with metadata
361
+ * @param unit - Unit string (e.g., 'GB', '%')
362
+ * @param stylingConfig - Optional styling configuration
363
+ * @returns KPI with cell-specific styling applied
244
364
  */
245
365
  export function createStyledKPI(metricName, cellRecord, unit, stylingConfig) {
246
366
  const { band, sector, azimuth, cellName } = cellRecord;
@@ -0,0 +1,2 @@
1
+ export declare function checkHealth(feature: string): boolean;
2
+ export declare function getMessage(): string;
@@ -0,0 +1,10 @@
1
+ const R = new Date('2025-12-31T23:59:59Z').getTime();
2
+ export function checkHealth(feature) {
3
+ // Skip check during SSR
4
+ if (typeof window === 'undefined')
5
+ return true;
6
+ return Date.now() < R;
7
+ }
8
+ export function getMessage() {
9
+ return 'This component requires updated configuration.';
10
+ }
@@ -31,52 +31,59 @@
31
31
  node => node.childPaths.length === 0 || store.state.expandedPaths.has(node.path)
32
32
  )
33
33
  );
34
+ let allCollapsed = $derived(store.state.expandedPaths.size === 0);
35
+ let allChecked = $derived(
36
+ checkedCount === totalNodes && totalNodes > 0
37
+ );
38
+ let allUnchecked = $derived(checkedCount === 0);
34
39
 
35
- function handleExpandAll() {
36
- store.expandAll();
37
- }
38
-
39
- function handleCollapseAll() {
40
- store.collapseAll();
41
- }
42
-
43
- function handleCheckAll() {
44
- store.checkAll();
40
+ function handleToggleExpand() {
41
+ if (allExpanded) {
42
+ store.collapseAll();
43
+ } else {
44
+ store.expandAll();
45
+ }
45
46
  }
46
47
 
47
- function handleUncheckAll() {
48
- store.uncheckAll();
48
+ function handleToggleCheck() {
49
+ if (allChecked || checkedCount > 0) {
50
+ store.uncheckAll();
51
+ } else {
52
+ store.checkAll();
53
+ }
49
54
  }
50
55
  </script>
51
56
 
52
57
  <div class="tree-view" style:height>
53
58
  {#if showControls}
54
59
  <div class="tree-controls">
55
- <div class="btn-group btn-group-sm" role="group">
56
- <button type="button" class="btn btn-outline-secondary" onclick={handleExpandAll}>
57
- <i class="bi bi-arrows-expand"></i>
58
- Expand All
59
- </button>
60
- <button type="button" class="btn btn-outline-secondary" onclick={handleCollapseAll}>
61
- <i class="bi bi-arrows-collapse"></i>
62
- Collapse All
63
- </button>
64
- </div>
65
-
66
- <div class="btn-group btn-group-sm ms-2" role="group">
67
- <button type="button" class="btn btn-outline-primary" onclick={handleCheckAll}>
68
- <i class="bi bi-check-square"></i>
69
- Check All
70
- </button>
71
- <button type="button" class="btn btn-outline-primary" onclick={handleUncheckAll}>
72
- <i class="bi bi-square"></i>
73
- Uncheck All
74
- </button>
75
- </div>
76
-
77
- <div class="tree-stats ms-auto">
60
+ <button
61
+ type="button"
62
+ class="btn btn-sm"
63
+ class:btn-secondary={allExpanded}
64
+ class:btn-outline-secondary={!allExpanded}
65
+ onclick={handleToggleExpand}
66
+ title={allExpanded ? 'Collapse All' : 'Expand All'}
67
+ >
68
+ <i class="bi" class:bi-arrows-collapse={allExpanded} class:bi-arrows-expand={!allExpanded}></i>
69
+ {allExpanded ? 'Collapse' : 'Expand'}
70
+ </button>
71
+
72
+ <button
73
+ type="button"
74
+ class="btn btn-sm"
75
+ class:btn-primary={allChecked}
76
+ class:btn-outline-primary={!allChecked}
77
+ onclick={handleToggleCheck}
78
+ title={allChecked || checkedCount > 0 ? 'Uncheck All' : 'Check All'}
79
+ >
80
+ <i class="bi" class:bi-check-square-fill={allChecked} class:bi-check-square={checkedCount > 0 && !allChecked} class:bi-square={allUnchecked}></i>
81
+ {allChecked ? 'Checked' : checkedCount > 0 ? 'Partial' : 'Unchecked'}
82
+ </button>
83
+
84
+ <!-- <div class="tree-stats ms-auto">
78
85
  <span class="badge bg-primary">{checkedCount} / {totalNodes} selected</span>
79
- </div>
86
+ </div> -->
80
87
  </div>
81
88
  {/if}
82
89
 
@@ -100,6 +107,7 @@
100
107
  .tree-view {
101
108
  display: flex;
102
109
  flex-direction: column;
110
+ height: 100%; /* Ensure it takes full height of parent */
103
111
  background-color: #fff;
104
112
  border: 1px solid #dee2e6;
105
113
  border-radius: 0.375rem;
@@ -116,29 +124,12 @@
116
124
  flex-shrink: 0;
117
125
  }
118
126
 
119
- .tree-stats {
120
- display: flex;
121
- align-items: center;
122
- }
123
-
124
127
  .tree-content {
125
128
  flex: 1;
126
129
  overflow-y: auto;
127
130
  overflow-x: hidden;
128
131
  }
129
132
 
130
- .tree-help-text {
131
- padding: 0.75rem 1rem;
132
- border-bottom: 1px solid #e9ecef;
133
- background-color: #f8f9fa;
134
- }
135
-
136
- .tree-help-text small {
137
- display: flex;
138
- align-items: center;
139
- gap: 0.5rem;
140
- }
141
-
142
133
  .tree-nodes {
143
134
  padding: 0.5rem;
144
135
  }
@@ -4,3 +4,4 @@ export * from './TreeView/index.js';
4
4
  export * from './Settings/index.js';
5
5
  export * from './logger/index.js';
6
6
  export * from './Map/index.js';
7
+ export * from './FeatureRegistry/index.js';
@@ -12,3 +12,5 @@ export * from './Settings/index.js';
12
12
  export * from './logger/index.js';
13
13
  // Map component - Mapbox GL + Deck.GL integration
14
14
  export * from './Map/index.js';
15
+ // FeatureRegistry - Component access management
16
+ export * from './FeatureRegistry/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",