@smartnet360/svelte-components 0.0.42 → 0.0.44
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/site-check/SiteCheck.svelte +133 -28
- package/dist/apps/site-check/SiteCheck.svelte.d.ts +6 -2
- package/dist/apps/site-check/data-loader.d.ts +19 -0
- package/dist/apps/site-check/data-loader.js +8 -0
- package/dist/apps/site-check/index.d.ts +1 -1
- package/dist/apps/site-check/index.js +1 -1
- package/dist/apps/site-check/transforms-old.d.ts +56 -0
- package/dist/apps/site-check/transforms-old.js +273 -0
- package/dist/apps/site-check/transforms.d.ts +14 -13
- package/dist/apps/site-check/transforms.js +178 -58
- package/dist/core/Charts/ChartComponent.svelte +12 -56
- package/dist/core/Charts/GlobalControls.svelte +327 -126
- package/dist/core/Charts/GlobalControls.svelte.d.ts +2 -0
- package/dist/core/FeatureRegistry/index.d.ts +2 -0
- package/dist/core/FeatureRegistry/index.js +10 -0
- package/dist/core/TreeView/TreeView.svelte +44 -53
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +2 -0
- package/package.json +1 -1
|
@@ -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
|
|
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., ['
|
|
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
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* @param
|
|
51
|
-
* @
|
|
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
|
|
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', {
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
76
|
-
if (!
|
|
77
|
-
|
|
88
|
+
const level1Map = level0Map.get(level0Value);
|
|
89
|
+
if (!level1Map.has(level1Value)) {
|
|
90
|
+
level1Map.set(level1Value, new Map());
|
|
78
91
|
}
|
|
79
|
-
const cellMap =
|
|
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(
|
|
88
|
-
.sort(([a], [b]) => a
|
|
89
|
-
.forEach(([
|
|
90
|
-
const
|
|
91
|
-
id:
|
|
92
|
-
label:
|
|
93
|
-
|
|
94
|
-
|
|
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(
|
|
100
|
-
.sort(([a], [b]) => a
|
|
101
|
-
.forEach(([
|
|
102
|
-
const
|
|
103
|
-
id:
|
|
104
|
-
label:
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
147
|
+
level1Node.children.push(cellNode);
|
|
129
148
|
});
|
|
130
|
-
|
|
149
|
+
level0Node.children.push(level1Node);
|
|
131
150
|
});
|
|
132
|
-
treeNodes.push(
|
|
151
|
+
treeNodes.push(level0Node);
|
|
133
152
|
});
|
|
134
153
|
log('✅ Tree nodes built', {
|
|
135
154
|
totalNodes: treeNodes.length,
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
-
//
|
|
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('✅
|
|
302
|
+
log('✅ Filtered chart data', {
|
|
174
303
|
selectedCells: Array.from(selectedCells),
|
|
175
|
-
|
|
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., ['
|
|
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
|
-
|
|
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('✅
|
|
351
|
+
log('✅ Chart data transformed', {
|
|
225
352
|
outputRows: pivotedData.length,
|
|
226
|
-
|
|
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
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
* @param
|
|
240
|
-
* @
|
|
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;
|
|
@@ -341,16 +341,21 @@
|
|
|
341
341
|
{/snippet}
|
|
342
342
|
|
|
343
343
|
<div class="chart-component" bind:this={componentElement}>
|
|
344
|
-
<!-- Global Controls
|
|
345
|
-
{#if showGlobalControls
|
|
346
|
-
<GlobalControls
|
|
344
|
+
<!-- Floating Global Controls (renders as fixed position overlay) -->
|
|
345
|
+
{#if showGlobalControls}
|
|
346
|
+
<GlobalControls
|
|
347
|
+
controls={globalControls}
|
|
348
|
+
onUpdate={handleControlsUpdate}
|
|
349
|
+
isExpanded={showControlsPanel}
|
|
350
|
+
onToggle={() => showControlsPanel = !showControlsPanel}
|
|
351
|
+
/>
|
|
347
352
|
{/if}
|
|
348
353
|
|
|
349
354
|
<!-- Always render the main content (tabs or scrollspy) -->
|
|
350
355
|
{#if mode === 'tabs'}
|
|
351
356
|
<!-- Tab Mode with Navigation -->
|
|
352
357
|
<div class="tabs-container">
|
|
353
|
-
<!-- Tab Navigation
|
|
358
|
+
<!-- Tab Navigation -->
|
|
354
359
|
<div class="nav-tabs-wrapper">
|
|
355
360
|
<ul class="nav nav-tabs" role="tablist">
|
|
356
361
|
{#each layout.sections as section, index}
|
|
@@ -369,21 +374,6 @@
|
|
|
369
374
|
</li>
|
|
370
375
|
{/each}
|
|
371
376
|
</ul>
|
|
372
|
-
|
|
373
|
-
<!-- Controls Toggle Button -->
|
|
374
|
-
{#if showGlobalControls}
|
|
375
|
-
<button
|
|
376
|
-
class="btn btn-sm btn-outline-secondary controls-toggle"
|
|
377
|
-
onclick={() => showControlsPanel = !showControlsPanel}
|
|
378
|
-
title={showControlsPanel ? "Hide Controls" : "Show Controls"}
|
|
379
|
-
type="button"
|
|
380
|
-
>
|
|
381
|
-
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
382
|
-
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
|
|
383
|
-
</svg>
|
|
384
|
-
<span class="ms-1">{showControlsPanel ? 'Hide' : 'Show'} Controls</span>
|
|
385
|
-
</button>
|
|
386
|
-
{/if}
|
|
387
377
|
</div>
|
|
388
378
|
|
|
389
379
|
<!-- Tab Content -->
|
|
@@ -402,7 +392,7 @@
|
|
|
402
392
|
{:else if mode === 'scrollspy'}
|
|
403
393
|
<!-- ScrollSpy Mode with Navigation -->
|
|
404
394
|
<div class="scrollspy-container">
|
|
405
|
-
<!-- ScrollSpy Navigation
|
|
395
|
+
<!-- ScrollSpy Navigation -->
|
|
406
396
|
<nav class="scrollspy-nav">
|
|
407
397
|
<div class="nav-wrapper">
|
|
408
398
|
<ul class="nav nav-pills">
|
|
@@ -418,21 +408,6 @@
|
|
|
418
408
|
</li>
|
|
419
409
|
{/each}
|
|
420
410
|
</ul>
|
|
421
|
-
|
|
422
|
-
<!-- Controls Toggle Button -->
|
|
423
|
-
{#if showGlobalControls}
|
|
424
|
-
<button
|
|
425
|
-
class="btn btn-sm btn-outline-secondary controls-toggle"
|
|
426
|
-
onclick={() => showControlsPanel = !showControlsPanel}
|
|
427
|
-
title={showControlsPanel ? "Hide Controls" : "Show Controls"}
|
|
428
|
-
type="button"
|
|
429
|
-
>
|
|
430
|
-
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
431
|
-
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
|
|
432
|
-
</svg>
|
|
433
|
-
<!-- <span class="ms-1">{showControlsPanel ? 'Hide' : 'Show'} Controls</span> -->
|
|
434
|
-
</button>
|
|
435
|
-
{/if}
|
|
436
411
|
</div>
|
|
437
412
|
</nav>
|
|
438
413
|
|
|
@@ -722,11 +697,10 @@
|
|
|
722
697
|
max-height: 100%; /* Constrain to slot */
|
|
723
698
|
}
|
|
724
699
|
|
|
725
|
-
/* Tab navigation wrapper
|
|
700
|
+
/* Tab navigation wrapper */
|
|
726
701
|
.nav-tabs-wrapper {
|
|
727
702
|
display: flex;
|
|
728
703
|
align-items: center;
|
|
729
|
-
gap: 1rem;
|
|
730
704
|
border-bottom: 1px solid #dee2e6;
|
|
731
705
|
}
|
|
732
706
|
|
|
@@ -736,25 +710,7 @@
|
|
|
736
710
|
margin-bottom: 0;
|
|
737
711
|
}
|
|
738
712
|
|
|
739
|
-
|
|
740
|
-
display: flex;
|
|
741
|
-
align-items: center;
|
|
742
|
-
gap: 0.25rem;
|
|
743
|
-
white-space: nowrap;
|
|
744
|
-
border-radius: 0.25rem;
|
|
745
|
-
transition: all 0.2s ease;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
.controls-toggle:hover {
|
|
749
|
-
background-color: #f8f9fa;
|
|
750
|
-
border-color: #6c757d;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
.controls-toggle svg {
|
|
754
|
-
flex-shrink: 0;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/* ScrollSpy navigation wrapper with toggle button */
|
|
713
|
+
/* ScrollSpy navigation wrapper */
|
|
758
714
|
.scrollspy-nav .nav-wrapper {
|
|
759
715
|
display: flex;
|
|
760
716
|
align-items: center;
|