@smartnet360/svelte-components 0.0.102 → 0.0.104
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/apps/antenna-pattern/index.d.ts +1 -0
- package/dist/apps/antenna-pattern/index.js +1 -0
- package/dist/apps/antenna-pattern/utils/load-static-antennas.d.ts +17 -0
- package/dist/apps/antenna-pattern/utils/load-static-antennas.js +83 -0
- package/dist/apps/site-check/SiteCheck.svelte +13 -81
- package/dist/apps/site-check/SiteCheckControls.svelte +0 -7
- package/dist/apps/site-check/helper.js +0 -33
- package/dist/apps/site-check/transforms.js +15 -65
- package/dist/core/CellTable/CellTable.svelte +456 -0
- package/dist/core/CellTable/CellTable.svelte.d.ts +27 -0
- package/dist/core/CellTable/CellTablePanel.svelte +211 -0
- package/dist/core/CellTable/CellTablePanel.svelte.d.ts +49 -0
- package/dist/core/CellTable/CellTableToolbar.svelte +218 -0
- package/dist/core/CellTable/CellTableToolbar.svelte.d.ts +32 -0
- package/dist/core/CellTable/column-config.d.ts +63 -0
- package/dist/core/CellTable/column-config.js +465 -0
- package/dist/core/CellTable/index.d.ts +10 -0
- package/dist/core/CellTable/index.js +11 -0
- package/dist/core/CellTable/types.d.ts +166 -0
- package/dist/core/CellTable/types.js +6 -0
- package/dist/core/Charts/ChartCard.svelte +118 -31
- package/dist/core/Charts/ChartCard.svelte.d.ts +2 -0
- package/dist/core/Charts/ChartComponent.svelte +8 -31
- package/dist/core/Charts/data-processor.js +1 -19
- package/dist/core/CoverageMap/ai/AITools.d.ts +117 -0
- package/dist/core/CoverageMap/ai/AITools.js +380 -0
- package/dist/core/CoverageMap/core/CoverageCalculator.d.ts +138 -0
- package/dist/core/CoverageMap/core/CoverageCalculator.js +375 -0
- package/dist/core/CoverageMap/core/GridCalculator.d.ts +115 -0
- package/dist/core/CoverageMap/core/GridCalculator.js +484 -0
- package/dist/core/CoverageMap/core/PathLossModels.d.ts +253 -0
- package/dist/core/CoverageMap/core/PathLossModels.js +380 -0
- package/dist/core/CoverageMap/core/SignalProcessor.d.ts +288 -0
- package/dist/core/CoverageMap/core/SignalProcessor.js +424 -0
- package/dist/core/CoverageMap/data/AntennaStore.d.ts +165 -0
- package/dist/core/CoverageMap/data/AntennaStore.js +327 -0
- package/dist/core/CoverageMap/data/SiteStore.d.ts +155 -0
- package/dist/core/CoverageMap/data/SiteStore.js +355 -0
- package/dist/core/CoverageMap/index.d.ts +74 -0
- package/dist/core/CoverageMap/index.js +103 -0
- package/dist/core/CoverageMap/types.d.ts +252 -0
- package/dist/core/CoverageMap/types.js +7 -0
- package/dist/core/CoverageMap/utils/geoUtils.d.ts +223 -0
- package/dist/core/CoverageMap/utils/geoUtils.js +374 -0
- package/dist/core/CoverageMap/utils/rfUtils.d.ts +329 -0
- package/dist/core/CoverageMap/utils/rfUtils.js +434 -0
- package/dist/core/CoverageMap/visualization/ColorSchemes.d.ts +149 -0
- package/dist/core/CoverageMap/visualization/ColorSchemes.js +377 -0
- package/dist/core/TreeView/index.d.ts +4 -4
- package/dist/core/TreeView/index.js +5 -5
- package/dist/core/TreeView/tree-utils.d.ts +12 -0
- package/dist/core/TreeView/tree-utils.js +115 -6
- package/dist/core/TreeView/tree.store.svelte.d.ts +94 -0
- package/dist/core/TreeView/tree.store.svelte.js +274 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +2 -0
- package/dist/map-v2/features/cells/controls/CellFilterControl.svelte +16 -27
- package/dist/map-v2/features/repeaters/controls/RepeaterFilterControl.svelte +33 -42
- package/dist/map-v2/features/sites/controls/SiteFilterControl.svelte +12 -19
- package/dist/map-v3/core/components/Map.svelte +4 -0
- package/dist/map-v3/core/stores/map.store.svelte.js +2 -0
- package/dist/map-v3/features/cells/components/CellFilterControl.svelte +24 -30
- package/dist/map-v3/features/coverage/index.d.ts +12 -0
- package/dist/map-v3/features/coverage/index.js +16 -0
- package/dist/map-v3/features/coverage/layers/CoverageLayer.svelte +198 -0
- package/dist/map-v3/features/coverage/layers/CoverageLayer.svelte.d.ts +10 -0
- package/dist/map-v3/features/coverage/logic/coloring.d.ts +28 -0
- package/dist/map-v3/features/coverage/logic/coloring.js +77 -0
- package/dist/map-v3/features/coverage/logic/geometry.d.ts +33 -0
- package/dist/map-v3/features/coverage/logic/geometry.js +112 -0
- package/dist/map-v3/features/coverage/stores/coverage.data.svelte.d.ts +46 -0
- package/dist/map-v3/features/coverage/stores/coverage.data.svelte.js +95 -0
- package/dist/map-v3/features/coverage/stores/coverage.display.svelte.d.ts +33 -0
- package/dist/map-v3/features/coverage/stores/coverage.display.svelte.js +90 -0
- package/dist/map-v3/features/coverage/types.d.ts +52 -0
- package/dist/map-v3/features/coverage/types.js +7 -0
- package/dist/map-v3/features/repeaters/components/RepeaterFilterControl.svelte +14 -20
- package/dist/map-v3/features/sites/components/SiteFilterControl.svelte +23 -33
- package/dist/map-v3/index.d.ts +4 -0
- package/dist/map-v3/index.js +5 -0
- package/package.json +4 -3
- package/dist/apps/site-check/transforms-old.d.ts +0 -56
- package/dist/apps/site-check/transforms-old.js +0 -273
- package/dist/core/TreeView/tree.store.d.ts +0 -10
- package/dist/core/TreeView/tree.store.js +0 -320
|
@@ -1,273 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tree Store
|
|
3
|
-
* Svelte writable store for managing tree state with persistence
|
|
4
|
-
*/
|
|
5
|
-
import { type Writable } from 'svelte/store';
|
|
6
|
-
import type { TreeConfig, TreeStoreValue } from './tree.model';
|
|
7
|
-
/**
|
|
8
|
-
* Create a tree store with state management and persistence
|
|
9
|
-
*/
|
|
10
|
-
export declare function createTreeStore<T = any>(config: TreeConfig<T>): Writable<TreeStoreValue>;
|
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tree Store
|
|
3
|
-
* Svelte writable store for managing tree state with persistence
|
|
4
|
-
*/
|
|
5
|
-
import { writable } from 'svelte/store';
|
|
6
|
-
import { flattenTree, buildInitialState, calculateIndeterminateStates, getDescendantPaths, getParentPath, getAncestorPaths, saveStateToStorage, loadStateFromStorage, clearStorageForNamespace } from './tree-utils';
|
|
7
|
-
import { log } from '../logger';
|
|
8
|
-
/**
|
|
9
|
-
* Create a tree store with state management and persistence
|
|
10
|
-
*/
|
|
11
|
-
export function createTreeStore(config) {
|
|
12
|
-
log('🌲 Creating TreeStore', {
|
|
13
|
-
namespace: config.namespace,
|
|
14
|
-
nodeCount: config.nodes.length,
|
|
15
|
-
persistState: config.persistState,
|
|
16
|
-
defaultExpandAll: config.defaultExpandAll
|
|
17
|
-
});
|
|
18
|
-
const separator = config.pathSeparator || ':';
|
|
19
|
-
// Flatten tree structure
|
|
20
|
-
const nodesMap = flattenTree(config.nodes, config);
|
|
21
|
-
log('📊 Tree flattened', {
|
|
22
|
-
totalNodes: nodesMap.size,
|
|
23
|
-
separator
|
|
24
|
-
});
|
|
25
|
-
// Build initial state
|
|
26
|
-
let state = buildInitialState(nodesMap, config);
|
|
27
|
-
log('🔧 Initial state built', {
|
|
28
|
-
checkedPaths: state.checkedPaths.size,
|
|
29
|
-
expandedPaths: state.expandedPaths.size
|
|
30
|
-
});
|
|
31
|
-
// Load persisted state if enabled
|
|
32
|
-
if (config.persistState && config.namespace) {
|
|
33
|
-
const persistedState = loadStateFromStorage(config.namespace, state);
|
|
34
|
-
state = { ...state, ...persistedState };
|
|
35
|
-
log('💾 Loaded persisted state', {
|
|
36
|
-
namespace: config.namespace,
|
|
37
|
-
checkedPaths: state.checkedPaths.size,
|
|
38
|
-
expandedPaths: state.expandedPaths.size
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
// Create writable store
|
|
42
|
-
const store = writable({
|
|
43
|
-
state,
|
|
44
|
-
config,
|
|
45
|
-
toggle: () => { },
|
|
46
|
-
toggleExpand: () => { },
|
|
47
|
-
expandAll: () => { },
|
|
48
|
-
collapseAll: () => { },
|
|
49
|
-
checkAll: () => { },
|
|
50
|
-
uncheckAll: () => { },
|
|
51
|
-
getCheckedPaths: () => [],
|
|
52
|
-
getCheckedLeafPaths: () => [],
|
|
53
|
-
clearStorage: () => { }
|
|
54
|
-
});
|
|
55
|
-
/**
|
|
56
|
-
* Update state and trigger reactivity
|
|
57
|
-
*/
|
|
58
|
-
function updateState(updater) {
|
|
59
|
-
store.update(current => {
|
|
60
|
-
const newState = updater(current.state);
|
|
61
|
-
// Persist if enabled
|
|
62
|
-
if (config.persistState && config.namespace) {
|
|
63
|
-
saveStateToStorage(config.namespace, newState);
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
...current,
|
|
67
|
-
state: newState
|
|
68
|
-
};
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Toggle a node's checked state (with cascading)
|
|
73
|
-
*
|
|
74
|
-
* Logic:
|
|
75
|
-
* 1. Toggle the clicked node
|
|
76
|
-
* 2. Cascade DOWN to all descendants (check/uncheck all children)
|
|
77
|
-
* 3. Propagate UP to all ancestors (update based on their children's states)
|
|
78
|
-
*/
|
|
79
|
-
function toggle(path) {
|
|
80
|
-
log('🔄 Toggling node', { path });
|
|
81
|
-
updateState(state => {
|
|
82
|
-
const nodeState = state.nodes.get(path);
|
|
83
|
-
if (!nodeState) {
|
|
84
|
-
log('⚠️ Node not found', { path });
|
|
85
|
-
return state;
|
|
86
|
-
}
|
|
87
|
-
const newChecked = !state.checkedPaths.has(path);
|
|
88
|
-
const newCheckedPaths = new Set(state.checkedPaths);
|
|
89
|
-
log('📌 Toggle action', { path, newChecked });
|
|
90
|
-
// STEP 0: If singleRootSelect mode and this is a root node being checked, uncheck all other roots
|
|
91
|
-
if (config.singleRootSelect && newChecked && nodeState.level === 0) {
|
|
92
|
-
log('🔘 Single root select mode: unchecking other roots', { path });
|
|
93
|
-
// Uncheck all root nodes and their descendants
|
|
94
|
-
state.rootPaths.forEach(rootPath => {
|
|
95
|
-
if (rootPath !== path) {
|
|
96
|
-
newCheckedPaths.delete(rootPath);
|
|
97
|
-
// Also uncheck all descendants of this root
|
|
98
|
-
const rootDescendants = getDescendantPaths(rootPath, state.nodes, separator);
|
|
99
|
-
rootDescendants.forEach(descendantPath => {
|
|
100
|
-
newCheckedPaths.delete(descendantPath);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
// STEP 0.5: If singleLevel1Select mode and this is a Level 1 node being checked, uncheck sibling Level 1 nodes
|
|
106
|
-
if (config.singleLevel1Select && newChecked && nodeState.level === 1) {
|
|
107
|
-
log('🔘 Single Level 1 select mode: unchecking sibling Level 1 nodes', { path });
|
|
108
|
-
const parentPath = nodeState.parentPath;
|
|
109
|
-
// Find and uncheck all Level 1 siblings (same parent, same level, different path)
|
|
110
|
-
state.nodes.forEach((node, nodePath) => {
|
|
111
|
-
if (node.level === 1 &&
|
|
112
|
-
node.parentPath === parentPath &&
|
|
113
|
-
nodePath !== path) {
|
|
114
|
-
newCheckedPaths.delete(nodePath);
|
|
115
|
-
// Also uncheck all descendants of this sibling
|
|
116
|
-
const siblingDescendants = getDescendantPaths(nodePath, state.nodes, separator);
|
|
117
|
-
siblingDescendants.forEach(descendantPath => {
|
|
118
|
-
newCheckedPaths.delete(descendantPath);
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
// STEP 1: Update this node
|
|
124
|
-
if (newChecked) {
|
|
125
|
-
newCheckedPaths.add(path);
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
newCheckedPaths.delete(path);
|
|
129
|
-
}
|
|
130
|
-
// STEP 2: CASCADE DOWN - Update all descendants to match
|
|
131
|
-
const descendants = getDescendantPaths(path, state.nodes, separator);
|
|
132
|
-
log('⬇️ Cascading to descendants', {
|
|
133
|
-
path,
|
|
134
|
-
descendantCount: descendants.length,
|
|
135
|
-
newChecked
|
|
136
|
-
});
|
|
137
|
-
descendants.forEach(descendantPath => {
|
|
138
|
-
if (newChecked) {
|
|
139
|
-
newCheckedPaths.add(descendantPath);
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
newCheckedPaths.delete(descendantPath);
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
// STEP 3: PROPAGATE UP - Update all ancestors based on their children
|
|
146
|
-
// We need to update from the deepest ancestor up to the root
|
|
147
|
-
const ancestorPaths = getAncestorPaths(path, separator);
|
|
148
|
-
log('⬆️ Propagating to ancestors', {
|
|
149
|
-
path,
|
|
150
|
-
ancestorCount: ancestorPaths.length,
|
|
151
|
-
ancestors: ancestorPaths
|
|
152
|
-
});
|
|
153
|
-
// Process ancestors from deepest to shallowest (reverse order)
|
|
154
|
-
// This ensures we calculate states correctly as we go up
|
|
155
|
-
for (let i = ancestorPaths.length - 1; i >= 0; i--) {
|
|
156
|
-
const ancestorPath = ancestorPaths[i];
|
|
157
|
-
const ancestor = state.nodes.get(ancestorPath);
|
|
158
|
-
if (!ancestor)
|
|
159
|
-
continue;
|
|
160
|
-
// Count how many direct children are checked
|
|
161
|
-
const checkedChildrenCount = ancestor.childPaths.filter(childPath => newCheckedPaths.has(childPath)).length;
|
|
162
|
-
const totalChildren = ancestor.childPaths.length;
|
|
163
|
-
log('👨👧👦 Checking ancestor children', {
|
|
164
|
-
ancestorPath,
|
|
165
|
-
checkedChildrenCount,
|
|
166
|
-
totalChildren
|
|
167
|
-
});
|
|
168
|
-
// Update ancestor based on children states:
|
|
169
|
-
// - All children checked → check parent
|
|
170
|
-
// - No children checked → uncheck parent
|
|
171
|
-
// - Some children checked → uncheck parent (will show indeterminate)
|
|
172
|
-
if (checkedChildrenCount === totalChildren) {
|
|
173
|
-
newCheckedPaths.add(ancestorPath);
|
|
174
|
-
log('✅ All children checked, checking parent', { ancestorPath });
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
newCheckedPaths.delete(ancestorPath);
|
|
178
|
-
if (checkedChildrenCount > 0) {
|
|
179
|
-
log('➖ Some children checked, parent will be indeterminate', { ancestorPath });
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
log('❌ No children checked, unchecking parent', { ancestorPath });
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// STEP 4: Recalculate indeterminate states
|
|
187
|
-
const newIndeterminatePaths = calculateIndeterminateStates(state.nodes, newCheckedPaths);
|
|
188
|
-
log('✅ Toggle complete', {
|
|
189
|
-
path,
|
|
190
|
-
newChecked,
|
|
191
|
-
totalChecked: newCheckedPaths.size,
|
|
192
|
-
indeterminate: newIndeterminatePaths.size
|
|
193
|
-
});
|
|
194
|
-
return {
|
|
195
|
-
...state,
|
|
196
|
-
checkedPaths: newCheckedPaths,
|
|
197
|
-
indeterminatePaths: newIndeterminatePaths
|
|
198
|
-
};
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Toggle expanded/collapsed state
|
|
203
|
-
*/
|
|
204
|
-
function toggleExpand(path) {
|
|
205
|
-
updateState(state => {
|
|
206
|
-
const newExpandedPaths = new Set(state.expandedPaths);
|
|
207
|
-
if (newExpandedPaths.has(path)) {
|
|
208
|
-
newExpandedPaths.delete(path);
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
newExpandedPaths.add(path);
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
...state,
|
|
215
|
-
expandedPaths: newExpandedPaths
|
|
216
|
-
};
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Expand all nodes
|
|
221
|
-
*/
|
|
222
|
-
function expandAll() {
|
|
223
|
-
updateState(state => {
|
|
224
|
-
const newExpandedPaths = new Set();
|
|
225
|
-
// Add all nodes with children
|
|
226
|
-
state.nodes.forEach((nodeState, path) => {
|
|
227
|
-
if (nodeState.childPaths.length > 0) {
|
|
228
|
-
newExpandedPaths.add(path);
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
return {
|
|
232
|
-
...state,
|
|
233
|
-
expandedPaths: newExpandedPaths
|
|
234
|
-
};
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Collapse all nodes
|
|
239
|
-
*/
|
|
240
|
-
function collapseAll() {
|
|
241
|
-
updateState(state => ({
|
|
242
|
-
...state,
|
|
243
|
-
expandedPaths: new Set()
|
|
244
|
-
}));
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Check all nodes
|
|
248
|
-
*/
|
|
249
|
-
function checkAll() {
|
|
250
|
-
log('✅ Check all nodes');
|
|
251
|
-
updateState(state => {
|
|
252
|
-
const newCheckedPaths = new Set();
|
|
253
|
-
state.nodes.forEach((_, path) => {
|
|
254
|
-
newCheckedPaths.add(path);
|
|
255
|
-
});
|
|
256
|
-
log('✅ All nodes checked', { totalChecked: newCheckedPaths.size });
|
|
257
|
-
return {
|
|
258
|
-
...state,
|
|
259
|
-
checkedPaths: newCheckedPaths,
|
|
260
|
-
indeterminatePaths: new Set() // No indeterminate when all checked
|
|
261
|
-
};
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Uncheck all nodes
|
|
266
|
-
*/
|
|
267
|
-
function uncheckAll() {
|
|
268
|
-
log('❌ Uncheck all nodes');
|
|
269
|
-
updateState(state => ({
|
|
270
|
-
...state,
|
|
271
|
-
checkedPaths: new Set(),
|
|
272
|
-
indeterminatePaths: new Set()
|
|
273
|
-
}));
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Get all checked paths
|
|
277
|
-
*/
|
|
278
|
-
function getCheckedPaths() {
|
|
279
|
-
let result = [];
|
|
280
|
-
store.subscribe(value => {
|
|
281
|
-
result = Array.from(value.state.checkedPaths);
|
|
282
|
-
})();
|
|
283
|
-
return result;
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Get only checked leaf paths (nodes without children)
|
|
287
|
-
*/
|
|
288
|
-
function getCheckedLeafPaths() {
|
|
289
|
-
let result = [];
|
|
290
|
-
store.subscribe(value => {
|
|
291
|
-
result = Array.from(value.state.checkedPaths).filter(path => {
|
|
292
|
-
const node = value.state.nodes.get(path);
|
|
293
|
-
return node && node.childPaths.length === 0;
|
|
294
|
-
});
|
|
295
|
-
})();
|
|
296
|
-
return result;
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Clear localStorage
|
|
300
|
-
*/
|
|
301
|
-
function clearStorage() {
|
|
302
|
-
if (config.namespace) {
|
|
303
|
-
clearStorageForNamespace(config.namespace);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
// Initialize store with methods
|
|
307
|
-
store.update(current => ({
|
|
308
|
-
...current,
|
|
309
|
-
toggle,
|
|
310
|
-
toggleExpand,
|
|
311
|
-
expandAll,
|
|
312
|
-
collapseAll,
|
|
313
|
-
checkAll,
|
|
314
|
-
uncheckAll,
|
|
315
|
-
getCheckedPaths,
|
|
316
|
-
getCheckedLeafPaths,
|
|
317
|
-
clearStorage
|
|
318
|
-
}));
|
|
319
|
-
return store;
|
|
320
|
-
}
|