@smartnet360/svelte-components 0.0.22 → 0.0.23
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/utils/msi-parser.js +18 -1
- package/dist/cellular/CellularChartsView.svelte +293 -0
- package/dist/cellular/CellularChartsView.svelte.d.ts +7 -0
- package/dist/cellular/HierarchicalTree.svelte +469 -0
- package/dist/cellular/HierarchicalTree.svelte.d.ts +9 -0
- package/dist/cellular/SiteTree.svelte +286 -0
- package/dist/cellular/SiteTree.svelte.d.ts +11 -0
- package/dist/cellular/cellular-transforms.d.ts +25 -0
- package/dist/cellular/cellular-transforms.js +129 -0
- package/dist/cellular/cellular.model.d.ts +63 -0
- package/dist/cellular/cellular.model.js +6 -0
- package/dist/cellular/index.d.ts +11 -0
- package/dist/cellular/index.js +11 -0
- package/dist/cellular/mock-cellular-data.d.ts +13 -0
- package/dist/cellular/mock-cellular-data.js +241 -0
- package/dist/core/TreeChartView/TreeChartView.svelte +208 -0
- package/dist/core/TreeChartView/TreeChartView.svelte.d.ts +42 -0
- package/dist/core/TreeChartView/index.d.ts +7 -0
- package/dist/core/TreeChartView/index.js +7 -0
- package/dist/core/TreeView/TreeNode.svelte +173 -0
- package/dist/core/TreeView/TreeNode.svelte.d.ts +10 -0
- package/dist/core/TreeView/TreeView.svelte +163 -0
- package/dist/core/TreeView/TreeView.svelte.d.ts +10 -0
- package/dist/core/TreeView/index.d.ts +48 -0
- package/dist/core/TreeView/index.js +50 -0
- package/dist/core/TreeView/tree-utils.d.ts +56 -0
- package/dist/core/TreeView/tree-utils.js +194 -0
- package/dist/core/TreeView/tree.model.d.ts +104 -0
- package/dist/core/TreeView/tree.model.js +5 -0
- package/dist/core/TreeView/tree.store.d.ts +10 -0
- package/dist/core/TreeView/tree.store.js +225 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/package.json +1 -1
@@ -0,0 +1,241 @@
|
|
1
|
+
/**
|
2
|
+
* Generate realistic time series data for testing
|
3
|
+
*/
|
4
|
+
function generateTimeSeries(startDate, points, baseValue, variance, intervalMinutes = 15) {
|
5
|
+
const data = [];
|
6
|
+
const current = new Date(startDate);
|
7
|
+
for (let i = 0; i < points; i++) {
|
8
|
+
const noise = (Math.random() - 0.5) * variance;
|
9
|
+
const trend = Math.sin((i / points) * Math.PI * 2) * (variance / 2); // Sinusoidal pattern
|
10
|
+
const value = Math.max(0, baseValue + noise + trend);
|
11
|
+
data.push({
|
12
|
+
timestamp: current.toISOString(),
|
13
|
+
value: Math.round(value * 100) / 100 // Round to 2 decimals
|
14
|
+
});
|
15
|
+
current.setMinutes(current.getMinutes() + intervalMinutes);
|
16
|
+
}
|
17
|
+
return data;
|
18
|
+
}
|
19
|
+
/**
|
20
|
+
* Get base throughput value for a frequency band (Mbps)
|
21
|
+
*/
|
22
|
+
function getThroughputBase(band) {
|
23
|
+
const baseValues = {
|
24
|
+
700: 30,
|
25
|
+
800: 35,
|
26
|
+
900: 40,
|
27
|
+
1800: 60,
|
28
|
+
2100: 80,
|
29
|
+
2600: 100,
|
30
|
+
3500: 150 // C-Band
|
31
|
+
};
|
32
|
+
return baseValues[band];
|
33
|
+
}
|
34
|
+
/**
|
35
|
+
* Get base timing advance value for a frequency band (microseconds)
|
36
|
+
*/
|
37
|
+
function getTimingAdvanceBase(band) {
|
38
|
+
// Lower frequency = larger cell = higher TA
|
39
|
+
const baseValues = {
|
40
|
+
700: 8.5,
|
41
|
+
800: 7.8,
|
42
|
+
900: 7.2,
|
43
|
+
1800: 5.5,
|
44
|
+
2100: 4.8,
|
45
|
+
2600: 4.2,
|
46
|
+
3500: 3.5
|
47
|
+
};
|
48
|
+
return baseValues[band];
|
49
|
+
}
|
50
|
+
/**
|
51
|
+
* Generate a single cell with KPI data
|
52
|
+
*/
|
53
|
+
function generateCell(siteId, sectorId, band, startDate, points = 96 // 24 hours at 15-min intervals
|
54
|
+
) {
|
55
|
+
const cellId = `${siteId}-${sectorId}-${band}`;
|
56
|
+
return {
|
57
|
+
cellId,
|
58
|
+
band,
|
59
|
+
kpis: {
|
60
|
+
throughput: generateTimeSeries(startDate, points, getThroughputBase(band), getThroughputBase(band) * 0.3, // 30% variance
|
61
|
+
15),
|
62
|
+
timingAdvance: generateTimeSeries(startDate, points, getTimingAdvanceBase(band), 1.5, // ±1.5 μs variance
|
63
|
+
15)
|
64
|
+
}
|
65
|
+
};
|
66
|
+
}
|
67
|
+
/**
|
68
|
+
* Generate mock cellular sites for testing
|
69
|
+
*/
|
70
|
+
export function generateMockCellularData() {
|
71
|
+
const startDate = new Date('2025-10-09T00:00:00Z');
|
72
|
+
const points = 96; // 24 hours of data
|
73
|
+
return [
|
74
|
+
// Site A - Urban, 3 sectors, multiple bands
|
75
|
+
{
|
76
|
+
siteId: 'site-a',
|
77
|
+
siteName: 'Site A (Urban)',
|
78
|
+
sectors: [
|
79
|
+
{
|
80
|
+
sectorId: 'sec-1',
|
81
|
+
sectorName: 'Sector 1',
|
82
|
+
azimuth: 0,
|
83
|
+
cells: [
|
84
|
+
generateCell('site-a', 'sec-1', 700, startDate, points),
|
85
|
+
generateCell('site-a', 'sec-1', 2100, startDate, points),
|
86
|
+
generateCell('site-a', 'sec-1', 3500, startDate, points)
|
87
|
+
]
|
88
|
+
},
|
89
|
+
{
|
90
|
+
sectorId: 'sec-2',
|
91
|
+
sectorName: 'Sector 2',
|
92
|
+
azimuth: 120,
|
93
|
+
cells: [
|
94
|
+
generateCell('site-a', 'sec-2', 700, startDate, points),
|
95
|
+
generateCell('site-a', 'sec-2', 1800, startDate, points),
|
96
|
+
generateCell('site-a', 'sec-2', 2100, startDate, points)
|
97
|
+
]
|
98
|
+
},
|
99
|
+
{
|
100
|
+
sectorId: 'sec-3',
|
101
|
+
sectorName: 'Sector 3',
|
102
|
+
azimuth: 240,
|
103
|
+
cells: [
|
104
|
+
generateCell('site-a', 'sec-3', 1800, startDate, points),
|
105
|
+
generateCell('site-a', 'sec-3', 2600, startDate, points),
|
106
|
+
generateCell('site-a', 'sec-3', 3500, startDate, points)
|
107
|
+
]
|
108
|
+
}
|
109
|
+
]
|
110
|
+
},
|
111
|
+
// Site B - Suburban, 2 sectors, fewer bands
|
112
|
+
{
|
113
|
+
siteId: 'site-b',
|
114
|
+
siteName: 'Site B (Suburban)',
|
115
|
+
sectors: [
|
116
|
+
{
|
117
|
+
sectorId: 'sec-1',
|
118
|
+
sectorName: 'Sector 1',
|
119
|
+
azimuth: 0,
|
120
|
+
cells: [
|
121
|
+
generateCell('site-b', 'sec-1', 800, startDate, points),
|
122
|
+
generateCell('site-b', 'sec-1', 2100, startDate, points)
|
123
|
+
]
|
124
|
+
},
|
125
|
+
{
|
126
|
+
sectorId: 'sec-2',
|
127
|
+
sectorName: 'Sector 2',
|
128
|
+
azimuth: 180,
|
129
|
+
cells: [
|
130
|
+
generateCell('site-b', 'sec-2', 800, startDate, points),
|
131
|
+
generateCell('site-b', 'sec-2', 1800, startDate, points),
|
132
|
+
generateCell('site-b', 'sec-2', 2600, startDate, points)
|
133
|
+
]
|
134
|
+
}
|
135
|
+
]
|
136
|
+
},
|
137
|
+
// Site C - Rural, 4 sectors, low-band focus
|
138
|
+
{
|
139
|
+
siteId: 'site-c',
|
140
|
+
siteName: 'Site C (Rural)',
|
141
|
+
sectors: [
|
142
|
+
{
|
143
|
+
sectorId: 'sec-1',
|
144
|
+
sectorName: 'Sector 1',
|
145
|
+
azimuth: 0,
|
146
|
+
cells: [
|
147
|
+
generateCell('site-c', 'sec-1', 700, startDate, points),
|
148
|
+
generateCell('site-c', 'sec-1', 900, startDate, points)
|
149
|
+
]
|
150
|
+
},
|
151
|
+
{
|
152
|
+
sectorId: 'sec-2',
|
153
|
+
sectorName: 'Sector 2',
|
154
|
+
azimuth: 90,
|
155
|
+
cells: [
|
156
|
+
generateCell('site-c', 'sec-2', 700, startDate, points),
|
157
|
+
generateCell('site-c', 'sec-2', 1800, startDate, points)
|
158
|
+
]
|
159
|
+
},
|
160
|
+
{
|
161
|
+
sectorId: 'sec-3',
|
162
|
+
sectorName: 'Sector 3',
|
163
|
+
azimuth: 180,
|
164
|
+
cells: [
|
165
|
+
generateCell('site-c', 'sec-3', 800, startDate, points),
|
166
|
+
generateCell('site-c', 'sec-3', 900, startDate, points)
|
167
|
+
]
|
168
|
+
},
|
169
|
+
{
|
170
|
+
sectorId: 'sec-4',
|
171
|
+
sectorName: 'Sector 4',
|
172
|
+
azimuth: 270,
|
173
|
+
cells: [
|
174
|
+
generateCell('site-c', 'sec-4', 700, startDate, points),
|
175
|
+
generateCell('site-c', 'sec-4', 2100, startDate, points)
|
176
|
+
]
|
177
|
+
}
|
178
|
+
]
|
179
|
+
},
|
180
|
+
// Site D - Dense Urban, 3 sectors, high-band focus
|
181
|
+
{
|
182
|
+
siteId: 'site-d',
|
183
|
+
siteName: 'Site D (Dense Urban)',
|
184
|
+
sectors: [
|
185
|
+
{
|
186
|
+
sectorId: 'sec-1',
|
187
|
+
sectorName: 'Sector 1',
|
188
|
+
azimuth: 0,
|
189
|
+
cells: [
|
190
|
+
generateCell('site-d', 'sec-1', 2100, startDate, points),
|
191
|
+
generateCell('site-d', 'sec-1', 2600, startDate, points),
|
192
|
+
generateCell('site-d', 'sec-1', 3500, startDate, points)
|
193
|
+
]
|
194
|
+
},
|
195
|
+
{
|
196
|
+
sectorId: 'sec-2',
|
197
|
+
sectorName: 'Sector 2',
|
198
|
+
azimuth: 120,
|
199
|
+
cells: [
|
200
|
+
generateCell('site-d', 'sec-2', 1800, startDate, points),
|
201
|
+
generateCell('site-d', 'sec-2', 2600, startDate, points),
|
202
|
+
generateCell('site-d', 'sec-2', 3500, startDate, points)
|
203
|
+
]
|
204
|
+
},
|
205
|
+
{
|
206
|
+
sectorId: 'sec-3',
|
207
|
+
sectorName: 'Sector 3',
|
208
|
+
azimuth: 240,
|
209
|
+
cells: [
|
210
|
+
generateCell('site-d', 'sec-3', 2100, startDate, points),
|
211
|
+
generateCell('site-d', 'sec-3', 2600, startDate, points),
|
212
|
+
generateCell('site-d', 'sec-3', 3500, startDate, points)
|
213
|
+
]
|
214
|
+
}
|
215
|
+
]
|
216
|
+
}
|
217
|
+
];
|
218
|
+
}
|
219
|
+
/**
|
220
|
+
* Get color for frequency band (consistent color scheme)
|
221
|
+
*/
|
222
|
+
export function getBandColor(band) {
|
223
|
+
const colors = {
|
224
|
+
700: '#e74c3c', // Red
|
225
|
+
800: '#e67e22', // Orange
|
226
|
+
900: '#f39c12', // Yellow-Orange
|
227
|
+
1800: '#3498db', // Blue
|
228
|
+
2100: '#9b59b6', // Purple
|
229
|
+
2600: '#1abc9c', // Teal
|
230
|
+
3500: '#2ecc71' // Green (C-Band)
|
231
|
+
};
|
232
|
+
return colors[band];
|
233
|
+
}
|
234
|
+
/**
|
235
|
+
* Get label for frequency band
|
236
|
+
*/
|
237
|
+
export function getBandLabel(band) {
|
238
|
+
if (band === 3500)
|
239
|
+
return `${band} MHz (C-Band)`;
|
240
|
+
return `${band} MHz`;
|
241
|
+
}
|
@@ -0,0 +1,208 @@
|
|
1
|
+
<svelte:options runes={true} />
|
2
|
+
|
3
|
+
<script lang="ts">
|
4
|
+
/**
|
5
|
+
* TreeChartView - Generic component combining TreeView with ChartComponent
|
6
|
+
*
|
7
|
+
* This is a layout component that provides:
|
8
|
+
* - Left sidebar with TreeView
|
9
|
+
* - Right panel with ChartComponent
|
10
|
+
* - Empty state handling
|
11
|
+
* - Responsive layout
|
12
|
+
*
|
13
|
+
* All data transformation logic should be done externally by the consumer.
|
14
|
+
*/
|
15
|
+
|
16
|
+
import type { Layout } from '../Charts/charts.model.js';
|
17
|
+
import type { TreeStoreValue } from '../TreeView/tree.model.js';
|
18
|
+
import ChartComponent from '../Charts/ChartComponent.svelte';
|
19
|
+
import { TreeView } from '../TreeView/index.js';
|
20
|
+
|
21
|
+
interface Props {
|
22
|
+
/** Tree store (created externally with createTreeStore) */
|
23
|
+
treeStore: TreeStoreValue;
|
24
|
+
|
25
|
+
/** Chart data array (consumer's responsibility to filter based on tree state) */
|
26
|
+
chartData: any[];
|
27
|
+
|
28
|
+
/** Chart layout configuration */
|
29
|
+
chartLayout: Layout;
|
30
|
+
|
31
|
+
/** Number of total selectable items */
|
32
|
+
totalItems: number;
|
33
|
+
|
34
|
+
/** Number of currently visible items */
|
35
|
+
visibleItems: number;
|
36
|
+
|
37
|
+
/** Chart area title */
|
38
|
+
title?: string;
|
39
|
+
|
40
|
+
/** Chart display mode */
|
41
|
+
mode?: 'tabs' | 'scrollspy';
|
42
|
+
|
43
|
+
/** Show global chart controls */
|
44
|
+
showGlobalControls?: boolean;
|
45
|
+
|
46
|
+
/** Enable chart adaptation */
|
47
|
+
enableAdaptation?: boolean;
|
48
|
+
|
49
|
+
/** Show tree controls (expand/collapse all, etc.) */
|
50
|
+
showTreeControls?: boolean;
|
51
|
+
|
52
|
+
/** Tree sidebar width */
|
53
|
+
treeWidth?: string;
|
54
|
+
|
55
|
+
/** Empty state message */
|
56
|
+
emptyMessage?: string;
|
57
|
+
}
|
58
|
+
|
59
|
+
let {
|
60
|
+
treeStore,
|
61
|
+
chartData,
|
62
|
+
chartLayout,
|
63
|
+
totalItems,
|
64
|
+
visibleItems,
|
65
|
+
title = 'Charts',
|
66
|
+
mode = 'tabs',
|
67
|
+
showGlobalControls = true,
|
68
|
+
enableAdaptation = true,
|
69
|
+
showTreeControls = true,
|
70
|
+
treeWidth = '300px',
|
71
|
+
emptyMessage = 'No items selected. Select items from the tree on the left to display charts.'
|
72
|
+
}: Props = $props();
|
73
|
+
</script>
|
74
|
+
|
75
|
+
<div class="tree-chart-view">
|
76
|
+
<!-- Left: Tree Selector -->
|
77
|
+
<aside class="tree-sidebar" style:width={treeWidth}>
|
78
|
+
<TreeView
|
79
|
+
store={treeStore}
|
80
|
+
showControls={showTreeControls}
|
81
|
+
height="100%"
|
82
|
+
/>
|
83
|
+
</aside>
|
84
|
+
|
85
|
+
<!-- Right: Charts Area -->
|
86
|
+
<main class="charts-area">
|
87
|
+
<div class="charts-header">
|
88
|
+
<h5 class="mb-0">{title}</h5>
|
89
|
+
<div class="stats">
|
90
|
+
<span class="badge bg-primary">
|
91
|
+
{visibleItems} of {totalItems} selected
|
92
|
+
</span>
|
93
|
+
</div>
|
94
|
+
</div>
|
95
|
+
|
96
|
+
<div class="charts-container">
|
97
|
+
{#if visibleItems === 0}
|
98
|
+
<div class="empty-state">
|
99
|
+
<div class="empty-state-content">
|
100
|
+
<i class="bi bi-info-circle" style="font-size: 3rem; color: #6c757d;"></i>
|
101
|
+
<h4 class="mt-3">No Items Selected</h4>
|
102
|
+
<p class="text-muted">
|
103
|
+
{emptyMessage}
|
104
|
+
</p>
|
105
|
+
</div>
|
106
|
+
</div>
|
107
|
+
{:else}
|
108
|
+
<ChartComponent
|
109
|
+
layout={chartLayout}
|
110
|
+
data={chartData}
|
111
|
+
{mode}
|
112
|
+
{showGlobalControls}
|
113
|
+
{enableAdaptation}
|
114
|
+
/>
|
115
|
+
{/if}
|
116
|
+
</div>
|
117
|
+
</main>
|
118
|
+
</div>
|
119
|
+
|
120
|
+
<style>
|
121
|
+
.tree-chart-view {
|
122
|
+
width: 100%;
|
123
|
+
height: 100%;
|
124
|
+
display: flex;
|
125
|
+
gap: 0;
|
126
|
+
background-color: #fff;
|
127
|
+
}
|
128
|
+
|
129
|
+
.tree-sidebar {
|
130
|
+
flex-shrink: 0;
|
131
|
+
height: 100%;
|
132
|
+
overflow: hidden;
|
133
|
+
border-right: 1px solid #dee2e6;
|
134
|
+
}
|
135
|
+
|
136
|
+
.charts-area {
|
137
|
+
flex: 1;
|
138
|
+
display: flex;
|
139
|
+
flex-direction: column;
|
140
|
+
min-width: 0;
|
141
|
+
height: 100%;
|
142
|
+
}
|
143
|
+
|
144
|
+
.charts-header {
|
145
|
+
padding: 1rem;
|
146
|
+
border-bottom: 1px solid #dee2e6;
|
147
|
+
background-color: #f8f9fa;
|
148
|
+
display: flex;
|
149
|
+
justify-content: space-between;
|
150
|
+
align-items: center;
|
151
|
+
flex-shrink: 0;
|
152
|
+
}
|
153
|
+
|
154
|
+
.charts-header h5 {
|
155
|
+
margin: 0;
|
156
|
+
color: #495057;
|
157
|
+
}
|
158
|
+
|
159
|
+
.stats {
|
160
|
+
display: flex;
|
161
|
+
gap: 0.5rem;
|
162
|
+
}
|
163
|
+
|
164
|
+
.charts-container {
|
165
|
+
flex: 1;
|
166
|
+
min-height: 0;
|
167
|
+
overflow: hidden;
|
168
|
+
}
|
169
|
+
|
170
|
+
.empty-state {
|
171
|
+
display: flex;
|
172
|
+
align-items: center;
|
173
|
+
justify-content: center;
|
174
|
+
height: 100%;
|
175
|
+
width: 100%;
|
176
|
+
}
|
177
|
+
|
178
|
+
.empty-state-content {
|
179
|
+
text-align: center;
|
180
|
+
max-width: 400px;
|
181
|
+
padding: 2rem;
|
182
|
+
}
|
183
|
+
|
184
|
+
.empty-state-content h4 {
|
185
|
+
color: #495057;
|
186
|
+
margin-bottom: 0.5rem;
|
187
|
+
}
|
188
|
+
|
189
|
+
/* Responsive layout */
|
190
|
+
@media (max-width: 992px) {
|
191
|
+
.tree-sidebar {
|
192
|
+
width: 250px !important;
|
193
|
+
}
|
194
|
+
}
|
195
|
+
|
196
|
+
@media (max-width: 768px) {
|
197
|
+
.tree-chart-view {
|
198
|
+
flex-direction: column;
|
199
|
+
}
|
200
|
+
|
201
|
+
.tree-sidebar {
|
202
|
+
width: 100% !important;
|
203
|
+
height: 300px;
|
204
|
+
border-right: none;
|
205
|
+
border-bottom: 1px solid #dee2e6;
|
206
|
+
}
|
207
|
+
}
|
208
|
+
</style>
|
@@ -0,0 +1,42 @@
|
|
1
|
+
/**
|
2
|
+
* TreeChartView - Generic component combining TreeView with ChartComponent
|
3
|
+
*
|
4
|
+
* This is a layout component that provides:
|
5
|
+
* - Left sidebar with TreeView
|
6
|
+
* - Right panel with ChartComponent
|
7
|
+
* - Empty state handling
|
8
|
+
* - Responsive layout
|
9
|
+
*
|
10
|
+
* All data transformation logic should be done externally by the consumer.
|
11
|
+
*/
|
12
|
+
import type { Layout } from '../Charts/charts.model.js';
|
13
|
+
import type { TreeStoreValue } from '../TreeView/tree.model.js';
|
14
|
+
interface Props {
|
15
|
+
/** Tree store (created externally with createTreeStore) */
|
16
|
+
treeStore: TreeStoreValue;
|
17
|
+
/** Chart data array (consumer's responsibility to filter based on tree state) */
|
18
|
+
chartData: any[];
|
19
|
+
/** Chart layout configuration */
|
20
|
+
chartLayout: Layout;
|
21
|
+
/** Number of total selectable items */
|
22
|
+
totalItems: number;
|
23
|
+
/** Number of currently visible items */
|
24
|
+
visibleItems: number;
|
25
|
+
/** Chart area title */
|
26
|
+
title?: string;
|
27
|
+
/** Chart display mode */
|
28
|
+
mode?: 'tabs' | 'scrollspy';
|
29
|
+
/** Show global chart controls */
|
30
|
+
showGlobalControls?: boolean;
|
31
|
+
/** Enable chart adaptation */
|
32
|
+
enableAdaptation?: boolean;
|
33
|
+
/** Show tree controls (expand/collapse all, etc.) */
|
34
|
+
showTreeControls?: boolean;
|
35
|
+
/** Tree sidebar width */
|
36
|
+
treeWidth?: string;
|
37
|
+
/** Empty state message */
|
38
|
+
emptyMessage?: string;
|
39
|
+
}
|
40
|
+
declare const TreeChartView: import("svelte").Component<Props, {}, "">;
|
41
|
+
type TreeChartView = ReturnType<typeof TreeChartView>;
|
42
|
+
export default TreeChartView;
|
@@ -0,0 +1,7 @@
|
|
1
|
+
/**
|
2
|
+
* TreeChartView - Combined Layout Component
|
3
|
+
*
|
4
|
+
* Provides a split-screen layout combining TreeView (left) with ChartComponent (right).
|
5
|
+
* This is a LAYOUT component - all data transformation is the consumer's responsibility.
|
6
|
+
*/
|
7
|
+
export { default as TreeChartView } from './TreeChartView.svelte';
|
@@ -0,0 +1,7 @@
|
|
1
|
+
/**
|
2
|
+
* TreeChartView - Combined Layout Component
|
3
|
+
*
|
4
|
+
* Provides a split-screen layout combining TreeView (left) with ChartComponent (right).
|
5
|
+
* This is a LAYOUT component - all data transformation is the consumer's responsibility.
|
6
|
+
*/
|
7
|
+
export { default as TreeChartView } from './TreeChartView.svelte';
|
@@ -0,0 +1,173 @@
|
|
1
|
+
<script lang="ts">
|
2
|
+
import type { NodeState, TreeStoreValue } from './tree.model';
|
3
|
+
import TreeNode from './TreeNode.svelte';
|
4
|
+
|
5
|
+
interface Props {
|
6
|
+
nodeState: NodeState;
|
7
|
+
store: TreeStoreValue;
|
8
|
+
showIndeterminate?: boolean;
|
9
|
+
}
|
10
|
+
|
11
|
+
let { nodeState, store, showIndeterminate = true }: Props = $props();
|
12
|
+
|
13
|
+
// Computed states
|
14
|
+
let isChecked = $derived(store.state.checkedPaths.has(nodeState.path));
|
15
|
+
let isIndeterminate = $derived(
|
16
|
+
showIndeterminate && store.state.indeterminatePaths.has(nodeState.path)
|
17
|
+
);
|
18
|
+
let isExpanded = $derived(store.state.expandedPaths.has(nodeState.path));
|
19
|
+
let hasChildren = $derived(nodeState.childPaths.length > 0);
|
20
|
+
|
21
|
+
// Get child node states
|
22
|
+
let childNodes = $derived(
|
23
|
+
nodeState.childPaths
|
24
|
+
.map(path => store.state.nodes.get(path))
|
25
|
+
.filter((node): node is NodeState => node !== undefined)
|
26
|
+
);
|
27
|
+
|
28
|
+
// Indentation based on level
|
29
|
+
let indentStyle = $derived(`padding-left: ${nodeState.level * 1.5}rem`);
|
30
|
+
|
31
|
+
function handleToggle() {
|
32
|
+
store.toggle(nodeState.path);
|
33
|
+
}
|
34
|
+
|
35
|
+
function handleExpandToggle() {
|
36
|
+
if (hasChildren) {
|
37
|
+
store.toggleExpand(nodeState.path);
|
38
|
+
}
|
39
|
+
}
|
40
|
+
</script>
|
41
|
+
|
42
|
+
<div class="tree-node" style={indentStyle}>
|
43
|
+
<div class="tree-node-content">
|
44
|
+
<!-- Expand/Collapse Button -->
|
45
|
+
{#if hasChildren}
|
46
|
+
<button
|
47
|
+
type="button"
|
48
|
+
class="btn btn-sm btn-link expand-toggle p-0"
|
49
|
+
onclick={handleExpandToggle}
|
50
|
+
aria-label={isExpanded ? 'Collapse' : 'Expand'}
|
51
|
+
>
|
52
|
+
{#if isExpanded}
|
53
|
+
<i class="bi bi-chevron-down"></i>
|
54
|
+
{:else}
|
55
|
+
<i class="bi bi-chevron-right"></i>
|
56
|
+
{/if}
|
57
|
+
</button>
|
58
|
+
{:else}
|
59
|
+
<span class="expand-placeholder"></span>
|
60
|
+
{/if}
|
61
|
+
|
62
|
+
<!-- Checkbox -->
|
63
|
+
<div class="form-check">
|
64
|
+
<input
|
65
|
+
type="checkbox"
|
66
|
+
class="form-check-input"
|
67
|
+
class:indeterminate={isIndeterminate}
|
68
|
+
checked={isChecked}
|
69
|
+
indeterminate={isIndeterminate}
|
70
|
+
onchange={handleToggle}
|
71
|
+
id={`checkbox-${nodeState.path}`}
|
72
|
+
/>
|
73
|
+
<label class="form-check-label" for={`checkbox-${nodeState.path}`}>
|
74
|
+
{#if nodeState.node.icon}
|
75
|
+
<span class="node-icon">{nodeState.node.icon}</span>
|
76
|
+
{/if}
|
77
|
+
<span class="node-label">{nodeState.node.label}</span>
|
78
|
+
</label>
|
79
|
+
</div>
|
80
|
+
</div>
|
81
|
+
|
82
|
+
<!-- Recursive Children -->
|
83
|
+
{#if hasChildren && isExpanded}
|
84
|
+
<div class="tree-node-children">
|
85
|
+
{#each childNodes as childNode (childNode.path)}
|
86
|
+
<TreeNode nodeState={childNode} {store} {showIndeterminate} />
|
87
|
+
{/each}
|
88
|
+
</div>
|
89
|
+
{/if}
|
90
|
+
</div>
|
91
|
+
|
92
|
+
<style>
|
93
|
+
.tree-node {
|
94
|
+
position: relative;
|
95
|
+
}
|
96
|
+
|
97
|
+
.tree-node-content {
|
98
|
+
display: flex;
|
99
|
+
align-items: center;
|
100
|
+
gap: 0.5rem;
|
101
|
+
padding: 0.25rem 0;
|
102
|
+
min-height: 2rem;
|
103
|
+
}
|
104
|
+
|
105
|
+
.expand-toggle {
|
106
|
+
width: 1.5rem;
|
107
|
+
height: 1.5rem;
|
108
|
+
display: flex;
|
109
|
+
align-items: center;
|
110
|
+
justify-content: center;
|
111
|
+
border: none;
|
112
|
+
background: none;
|
113
|
+
cursor: pointer;
|
114
|
+
color: #6c757d;
|
115
|
+
text-decoration: none;
|
116
|
+
}
|
117
|
+
|
118
|
+
.expand-toggle:hover {
|
119
|
+
color: #495057;
|
120
|
+
background-color: #f8f9fa;
|
121
|
+
border-radius: 0.25rem;
|
122
|
+
}
|
123
|
+
|
124
|
+
.expand-toggle:focus {
|
125
|
+
outline: 2px solid #0d6efd;
|
126
|
+
outline-offset: 2px;
|
127
|
+
}
|
128
|
+
|
129
|
+
.expand-placeholder {
|
130
|
+
width: 1.5rem;
|
131
|
+
height: 1.5rem;
|
132
|
+
display: inline-block;
|
133
|
+
}
|
134
|
+
|
135
|
+
.form-check {
|
136
|
+
display: flex;
|
137
|
+
align-items: center;
|
138
|
+
gap: 0.5rem;
|
139
|
+
margin: 0;
|
140
|
+
}
|
141
|
+
|
142
|
+
.form-check-input {
|
143
|
+
cursor: pointer;
|
144
|
+
margin: 0;
|
145
|
+
flex-shrink: 0;
|
146
|
+
}
|
147
|
+
|
148
|
+
/* Indeterminate checkbox styling */
|
149
|
+
.form-check-input.indeterminate {
|
150
|
+
background-color: #0d6efd;
|
151
|
+
border-color: #0d6efd;
|
152
|
+
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e");
|
153
|
+
}
|
154
|
+
|
155
|
+
.form-check-label {
|
156
|
+
cursor: pointer;
|
157
|
+
display: flex;
|
158
|
+
align-items: center;
|
159
|
+
gap: 0.5rem;
|
160
|
+
margin: 0;
|
161
|
+
user-select: none;
|
162
|
+
}
|
163
|
+
|
164
|
+
.node-icon {
|
165
|
+
font-size: 1.1rem;
|
166
|
+
line-height: 1;
|
167
|
+
}
|
168
|
+
|
169
|
+
.node-label {
|
170
|
+
font-size: 0.875rem;
|
171
|
+
line-height: 1.5;
|
172
|
+
}
|
173
|
+
</style>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import type { NodeState, TreeStoreValue } from './tree.model';
|
2
|
+
import TreeNode from './TreeNode.svelte';
|
3
|
+
interface Props {
|
4
|
+
nodeState: NodeState;
|
5
|
+
store: TreeStoreValue;
|
6
|
+
showIndeterminate?: boolean;
|
7
|
+
}
|
8
|
+
declare const TreeNode: import("svelte").Component<Props, {}, "">;
|
9
|
+
type TreeNode = ReturnType<typeof TreeNode>;
|
10
|
+
export default TreeNode;
|