@smartnet360/svelte-components 0.0.44 → 0.0.46

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.
@@ -20,9 +20,33 @@
20
20
  cellStyling?: CellStylingConfig; // Optional cell styling config (defaults to defaultCellStyling)
21
21
  initialGrouping?: TreeGroupingConfig; // Optional initial tree grouping (defaults to Site → Azimuth → Cell)
22
22
  showGroupingSelector?: boolean; // Show/hide the grouping dropdown (default: true)
23
+ onSearch?: (searchTerm: string) => void; // Optional: Search callback (if provided, shows search box)
24
+ searchPlaceholder?: string; // Optional: Search box placeholder text (default: "Search...")
23
25
  }
24
26
 
25
- let { rawData, multiCellLayout, singleLteLayout, singleNrLayout, baseMetrics, mode = "scrollspy", markers = [], cellStyling = defaultCellStyling, initialGrouping = defaultTreeGrouping, showGroupingSelector = true }: Props = $props();
27
+ let { rawData, multiCellLayout, singleLteLayout, singleNrLayout, baseMetrics, mode = "scrollspy", markers = [], cellStyling = defaultCellStyling, initialGrouping = defaultTreeGrouping, showGroupingSelector = true, onSearch, searchPlaceholder = "Search..." }: Props = $props();
28
+
29
+ // Search state
30
+ let searchTerm = $state('');
31
+
32
+ // Controls visibility state (starts expanded)
33
+ let controlsExpanded = $state(true);
34
+
35
+ // Handlers
36
+ function handleSearch() {
37
+ if (onSearch) {
38
+ onSearch(searchTerm);
39
+ log('🔍 Search triggered:', searchTerm);
40
+ }
41
+ }
42
+
43
+ function handleClearSearch() {
44
+ searchTerm = '';
45
+ if (onSearch) {
46
+ onSearch('');
47
+ log('🧹 Search cleared');
48
+ }
49
+ }
26
50
 
27
51
  // Check feature health
28
52
  let isHealthy = $state(checkHealth('sitecheck'));
@@ -63,8 +87,7 @@
63
87
  firstNode: treeNodes[0],
64
88
  grouping: treeGrouping
65
89
  });
66
- if(isHealthy === false) {
67
- console.log('Configuration Required');
90
+ if(!isHealthy) {
68
91
  return;
69
92
  }
70
93
  // Create tree store
@@ -110,13 +133,14 @@
110
133
 
111
134
  // Expand layout based on selected cells and chosen base layout
112
135
  let chartLayout = $derived.by(() => {
136
+ // Pass cellStyling - helper will decide per-section whether to use it
113
137
  const expanded = expandLayoutForCells(selectedBaseLayout, filteredData, cellStyling);
114
138
  log('📐 Chart Layout:', {
115
139
  layoutName: selectedBaseLayout.layoutName,
140
+ layoutDefaultColors: selectedBaseLayout.useDefaultChartColors ?? false,
116
141
  sectionsCount: expanded.sections.length,
117
142
  totalCharts: expanded.sections.reduce((sum, s) => sum + s.charts.length, 0),
118
- firstSection: expanded.sections[0],
119
- cellStylingEnabled: !!cellStyling
143
+ firstSection: expanded.sections[0]
120
144
  });
121
145
  return expanded;
122
146
  });
@@ -191,30 +215,90 @@
191
215
  <div class="row flex-grow-1" style="min-height: 0;">
192
216
  <!-- Left: Tree View -->
193
217
  <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
+ <!-- Collapsible Controls Toggle -->
219
+ {#if onSearch || showGroupingSelector}
220
+ <button
221
+ class="controls-toggle w-100 text-start p-2 bg-light border-bottom d-flex align-items-center flex-shrink-0"
222
+ onclick={() => controlsExpanded = !controlsExpanded}
223
+ aria-expanded={controlsExpanded}
224
+ aria-label="Toggle controls"
225
+ >
226
+ <i class="bi bi-sliders me-2"></i>
227
+ <span class="fw-semibold small">Controls</span>
228
+ <i class="bi ms-auto"
229
+ class:bi-chevron-down={controlsExpanded}
230
+ class:bi-chevron-right={!controlsExpanded}>
231
+ </i>
232
+ </button>
233
+ {/if}
234
+
235
+ <!-- Collapsible Controls Content -->
236
+ {#if controlsExpanded}
237
+ <!-- Search Box -->
238
+ {#if onSearch}
239
+ <div class="p-3 border-bottom flex-shrink-0">
240
+ <label for="searchInput" class="form-label small fw-semibold mb-2">
241
+ Search
242
+ </label>
243
+ <div class="input-group input-group-sm">
244
+ <input
245
+ type="text"
246
+ id="searchInput"
247
+ class="form-control"
248
+ placeholder={searchPlaceholder}
249
+ bind:value={searchTerm}
250
+ onkeydown={(e) => {
251
+ if (e.key === 'Enter') {
252
+ handleSearch();
253
+ }
254
+ }}
255
+ />
256
+ {#if searchTerm}
257
+ <button
258
+ class="btn btn-outline-secondary"
259
+ type="button"
260
+ onclick={handleClearSearch}
261
+ title="Clear search"
262
+ aria-label="Clear search"
263
+ >
264
+ <i class="bi bi-x-lg"></i>
265
+ </button>
266
+ {/if}
267
+ <button
268
+ class="btn btn-primary"
269
+ type="button"
270
+ onclick={handleSearch}
271
+ title="Search"
272
+ aria-label="Search"
273
+ >
274
+ <i class="bi bi-search"></i>
275
+ </button>
276
+ </div>
277
+ </div>
278
+ {/if}
279
+
280
+ <!-- Grouping Selector -->
281
+ {#if showGroupingSelector}
282
+ <div class="p-3 border-bottom flex-shrink-0">
283
+ <label for="groupingSelect" class="form-label small fw-semibold mb-2">
284
+ Tree Grouping
285
+ </label>
286
+ <select
287
+ id="groupingSelect"
288
+ class="form-select form-select-sm"
289
+ onchange={(e) => {
290
+ const index = parseInt(e.currentTarget.value);
291
+ treeGrouping = groupingPresets[index].value;
292
+ }}
293
+ >
294
+ {#each groupingPresets as preset, i}
295
+ <option value={i} selected={JSON.stringify(preset.value) === JSON.stringify(treeGrouping)}>
296
+ {preset.label}
297
+ </option>
298
+ {/each}
299
+ </select>
300
+ </div>
301
+ {/if}
218
302
  {/if}
219
303
 
220
304
  <!-- Tree View -->
@@ -247,3 +331,20 @@
247
331
  </div>
248
332
  </div>
249
333
 
334
+ <style>
335
+ .controls-toggle {
336
+ cursor: pointer;
337
+ border: none;
338
+ transition: background-color 0.2s;
339
+ }
340
+
341
+ .controls-toggle:hover {
342
+ background-color: #e9ecef !important;
343
+ }
344
+
345
+ .controls-toggle:focus {
346
+ outline: 2px solid #0d6efd;
347
+ outline-offset: -2px;
348
+ }
349
+ </style>
350
+
@@ -12,6 +12,8 @@ interface Props {
12
12
  cellStyling?: CellStylingConfig;
13
13
  initialGrouping?: TreeGroupingConfig;
14
14
  showGroupingSelector?: boolean;
15
+ onSearch?: (searchTerm: string) => void;
16
+ searchPlaceholder?: string;
15
17
  }
16
18
  declare const SiteCheck: import("svelte").Component<Props, {}, "">;
17
19
  type SiteCheck = ReturnType<typeof SiteCheck>;
@@ -24,14 +24,23 @@ export function expandLayoutForCells(baseLayout, data, stylingConfig) {
24
24
  hoverMode: baseLayout.hoverMode, // Preserve hover mode from base layout
25
25
  coloredHover: baseLayout.coloredHover, // Preserve colored hover setting
26
26
  movingAverage: baseLayout.movingAverage, // Preserve moving average config
27
- sections: baseLayout.sections.map((section) => ({
28
- ...section,
29
- charts: section.charts.map((chart) => ({
30
- ...chart,
31
- yLeft: expandKPIs(chart.yLeft, cells, stylingConfig),
32
- yRight: expandKPIs(chart.yRight, cells, stylingConfig)
33
- }))
34
- }))
27
+ useDefaultChartColors: baseLayout.useDefaultChartColors, // Preserve layout-level default
28
+ sections: baseLayout.sections.map((section) => {
29
+ // Check per-section flag with fallback to layout-level, defaulting to false
30
+ const useDefaults = section.useDefaultChartColors ??
31
+ baseLayout.useDefaultChartColors ??
32
+ false;
33
+ // Decide styling for THIS section
34
+ const effectiveStyling = useDefaults ? undefined : stylingConfig;
35
+ return {
36
+ ...section,
37
+ charts: section.charts.map((chart) => ({
38
+ ...chart,
39
+ yLeft: expandKPIs(chart.yLeft, cells, effectiveStyling),
40
+ yRight: expandKPIs(chart.yRight, cells, effectiveStyling)
41
+ }))
42
+ };
43
+ })
35
44
  };
36
45
  return expandedLayout;
37
46
  }
@@ -24,6 +24,7 @@
24
24
  runtimeShowOriginal?: boolean; // Runtime control for showing original lines
25
25
  runtimeShowMarkers?: boolean; // Runtime control for showing markers (default: true)
26
26
  runtimeShowLegend?: boolean; // Runtime control for showing legend (default: true)
27
+ runtimeHoverMode?: HoverMode; // Runtime override for hover mode from global controls
27
28
  onchartcontextmenu?: (detail: {
28
29
  chart: ChartModel;
29
30
  sectionId?: string;
@@ -32,7 +33,7 @@
32
33
  }) => void;
33
34
  }
34
35
 
35
- let { chart, processedData, markers, plotlyLayout, enableAdaptation = true, sectionId, sectionMovingAverage, layoutMovingAverage, layoutHoverMode, layoutColoredHover = true, runtimeMAOverride, runtimeShowOriginal, runtimeShowMarkers = true, runtimeShowLegend = true, onchartcontextmenu }: Props = $props();
36
+ let { chart, processedData, markers, plotlyLayout, enableAdaptation = true, sectionId, sectionMovingAverage, layoutMovingAverage, layoutHoverMode, layoutColoredHover = true, runtimeMAOverride, runtimeShowOriginal, runtimeShowMarkers = true, runtimeShowLegend = true, runtimeHoverMode, onchartcontextmenu }: Props = $props();
36
37
 
37
38
  // Chart container div and state
38
39
  let chartDiv: HTMLElement;
@@ -174,8 +175,11 @@
174
175
  colorIndex++;
175
176
  });
176
177
 
178
+ // Determine effective hover mode: runtime override takes precedence over layout config
179
+ const effectiveHoverMode = runtimeHoverMode !== undefined ? runtimeHoverMode : layoutHoverMode;
180
+
177
181
  // Create default modern layout using the centralized function
178
- const defaultLayout: any = createDefaultPlotlyLayout(chart.title, layoutHoverMode, layoutColoredHover);
182
+ const defaultLayout: any = createDefaultPlotlyLayout(chart.title, effectiveHoverMode, layoutColoredHover);
179
183
 
180
184
  // Override specific properties for this chart
181
185
  defaultLayout.yaxis.title = {
@@ -235,7 +239,11 @@
235
239
  // Use Plotly.react() for updates (preserves zoom/pan) or newPlot for initial render
236
240
  if (chartInitialized) {
237
241
  // Update existing chart - much faster, preserves user interactions
238
- log('🔄 Updating chart with Plotly.react', { chartTitle: chart.title });
242
+ log('🔄 Updating chart with Plotly.react', {
243
+ chartTitle: chart.title,
244
+ hoverMode: effectiveHoverMode,
245
+ layoutHoverMode: finalLayout.hovermode
246
+ });
239
247
  Plotly.react(chartDiv, traces, finalLayout, config);
240
248
  } else {
241
249
  // Initial chart creation
@@ -243,7 +251,9 @@
243
251
  chartTitle: chart.title,
244
252
  traces: traces.length,
245
253
  leftKPIs: chart.yLeft.length,
246
- rightKPIs: chart.yRight.length
254
+ rightKPIs: chart.yRight.length,
255
+ hoverMode: effectiveHoverMode,
256
+ layoutHoverMode: finalLayout.hovermode
247
257
  });
248
258
  Plotly.newPlot(chartDiv, traces, finalLayout, config);
249
259
  chartInitialized = true;
@@ -340,6 +350,7 @@
340
350
  const currentShowOriginal = runtimeShowOriginal;
341
351
  const currentShowMarkers = runtimeShowMarkers;
342
352
  const currentShowLegend = runtimeShowLegend;
353
+ const currentHoverMode = runtimeHoverMode;
343
354
 
344
355
  // Only re-render if chartDiv is already initialized
345
356
  if (chartDiv && chartDiv.children.length > 0) {
@@ -15,6 +15,7 @@ interface Props {
15
15
  runtimeShowOriginal?: boolean;
16
16
  runtimeShowMarkers?: boolean;
17
17
  runtimeShowLegend?: boolean;
18
+ runtimeHoverMode?: HoverMode;
18
19
  onchartcontextmenu?: (detail: {
19
20
  chart: ChartModel;
20
21
  sectionId?: string;
@@ -85,6 +85,9 @@
85
85
  },
86
86
  legend: {
87
87
  enabled: true // Default to showing legend
88
+ },
89
+ hoverMode: {
90
+ mode: layout.hoverMode ?? 'x' // Default to 'x' if not specified
88
91
  }
89
92
  });
90
93
 
@@ -94,7 +97,8 @@
94
97
  movingAverageEnabled: updatedControls.movingAverage?.enabled,
95
98
  windowOverride: updatedControls.movingAverage?.windowOverride,
96
99
  markersEnabled: updatedControls.markers?.enabled,
97
- legendEnabled: updatedControls.legend?.enabled
100
+ legendEnabled: updatedControls.legend?.enabled,
101
+ hoverMode: updatedControls.hoverMode?.mode
98
102
  });
99
103
  globalControls = updatedControls;
100
104
  }
@@ -333,6 +337,7 @@
333
337
  runtimeShowOriginal={globalControls.movingAverage?.showOriginal}
334
338
  runtimeShowMarkers={globalControls.markers?.enabled}
335
339
  runtimeShowLegend={globalControls.legend?.enabled}
340
+ runtimeHoverMode={globalControls.hoverMode?.mode}
336
341
  onchartcontextmenu={(detail) => handleChartContextMenu(detail, section)}
337
342
  />
338
343
  </div>
@@ -1,7 +1,7 @@
1
1
  <svelte:options runes={true} />
2
2
 
3
3
  <script lang="ts">
4
- import type { GlobalChartControls } from './charts.model.js';
4
+ import type { GlobalChartControls, HoverMode } from './charts.model.js';
5
5
 
6
6
  interface Props {
7
7
  controls: GlobalChartControls;
@@ -48,6 +48,16 @@
48
48
  }
49
49
  });
50
50
  }
51
+
52
+ function updateHoverMode(updates: Partial<NonNullable<GlobalChartControls['hoverMode']>>) {
53
+ onUpdate({
54
+ ...controls,
55
+ hoverMode: {
56
+ ...controls.hoverMode!,
57
+ ...updates
58
+ }
59
+ });
60
+ }
51
61
  </script>
52
62
 
53
63
  <!-- Floating Controls Container -->
@@ -92,7 +102,7 @@
92
102
  checked={controls.markers.enabled}
93
103
  onchange={() => updateMarkers({ enabled: !controls.markers!.enabled })}
94
104
  />
95
- <label class="btn btn-outline-secondary btn-sm" for="markersToggle">
105
+ <label class="btn btn-outline-primary btn-sm" for="markersToggle">
96
106
  Markers
97
107
  </label>
98
108
  </div>
@@ -108,13 +118,81 @@
108
118
  checked={controls.legend.enabled}
109
119
  onchange={() => updateLegend({ enabled: !controls.legend!.enabled })}
110
120
  />
111
- <label class="btn btn-outline-secondary btn-sm" for="legendToggle">
121
+ <label class="btn btn-outline-primary btn-sm" for="legendToggle">
112
122
  Legend
113
123
  </label>
114
124
  </div>
115
125
  {/if}
116
126
  </div>
117
127
 
128
+ <!-- Hover Mode Controls -->
129
+ {#if controls.hoverMode}
130
+ <div class="control-group">
131
+ <div class="control-label">Hover Mode</div>
132
+ <div class="btn-group btn-group-sm" role="group" aria-label="Hover Mode">
133
+ <input
134
+ type="radio"
135
+ class="btn-check"
136
+ name="hoverMode"
137
+ id="hoverModeX"
138
+ checked={controls.hoverMode.mode === 'x'}
139
+ onchange={() => updateHoverMode({ mode: 'x' })}
140
+ />
141
+ <label class="btn btn-outline-primary" for="hoverModeX">X-Axis</label>
142
+
143
+ <!-- <input
144
+ type="radio"
145
+ class="btn-check"
146
+ name="hoverMode"
147
+ id="hoverModeY"
148
+ checked={controls.hoverMode.mode === 'y'}
149
+ onchange={() => updateHoverMode({ mode: 'y' })}
150
+ />
151
+ <label class="btn btn-outline-primary" for="hoverModeY">Y-Axis</label> -->
152
+
153
+ <input
154
+ type="radio"
155
+ class="btn-check"
156
+ name="hoverMode"
157
+ id="hoverModeClosest"
158
+ checked={controls.hoverMode.mode === 'closest'}
159
+ onchange={() => updateHoverMode({ mode: 'closest' })}
160
+ />
161
+ <label class="btn btn-outline-primary" for="hoverModeClosest">Closest</label>
162
+
163
+ <input
164
+ type="radio"
165
+ class="btn-check"
166
+ name="hoverMode"
167
+ id="hoverModeXUnified"
168
+ checked={controls.hoverMode.mode === 'x unified'}
169
+ onchange={() => updateHoverMode({ mode: 'x unified' })}
170
+ />
171
+ <label class="btn btn-outline-primary" for="hoverModeXUnified">X-Unified</label>
172
+
173
+ <!-- <input
174
+ type="radio"
175
+ class="btn-check"
176
+ name="hoverMode"
177
+ id="hoverModeYUnified"
178
+ checked={controls.hoverMode.mode === 'y unified'}
179
+ onchange={() => updateHoverMode({ mode: 'y unified' })}
180
+ />
181
+ <label class="btn btn-outline-primary" for="hoverModeYUnified">Y-Unified</label> -->
182
+
183
+ <input
184
+ type="radio"
185
+ class="btn-check"
186
+ name="hoverMode"
187
+ id="hoverModeFalse"
188
+ checked={controls.hoverMode.mode === false}
189
+ onchange={() => updateHoverMode({ mode: false })}
190
+ />
191
+ <label class="btn btn-outline-primary" for="hoverModeFalse">Off</label>
192
+ </div>
193
+ </div>
194
+ {/if}
195
+
118
196
  <!-- Bottom: Moving Average Controls -->
119
197
  {#if controls.movingAverage}
120
198
  <div class="control-group">
@@ -209,8 +287,8 @@
209
287
  /* Floating Controls Wrapper - positioned in top-right */
210
288
  .floating-controls-wrapper {
211
289
  position: fixed;
212
- top: 0.5rem;
213
- right: 0.5rem;
290
+ top: 0.3rem;
291
+ right: 0.3rem;
214
292
  z-index: 1000;
215
293
  display: flex;
216
294
  flex-direction: column;
@@ -330,6 +408,13 @@
330
408
  font-weight: 500;
331
409
  }
332
410
 
411
+ .control-label {
412
+ font-size: 0.8125rem;
413
+ font-weight: 600;
414
+ color: #495057;
415
+ margin-bottom: 0.25rem;
416
+ }
417
+
333
418
  .control-subgroup {
334
419
  display: flex;
335
420
  flex-wrap: wrap;
@@ -363,26 +448,19 @@
363
448
  border-color: #0d6efd;
364
449
  }
365
450
 
366
- .floating-controls-panel :global(.btn-outline-secondary) {
367
- border-color: rgba(108, 117, 125, 0.3);
368
- }
369
-
370
- .floating-controls-panel :global(.btn-outline-secondary:hover) {
371
- background-color: rgba(108, 117, 125, 0.1);
372
- border-color: rgba(108, 117, 125, 0.5);
373
- }
374
-
375
- .floating-controls-panel :global(.btn-check:checked + .btn-outline-secondary) {
376
- background-color: #6c757d;
377
- border-color: #6c757d;
378
- }
379
-
380
451
  .floating-controls-panel :global(.btn-group) {
381
452
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
382
453
  border-radius: 6px;
383
454
  overflow: hidden;
384
455
  }
385
456
 
457
+ .floating-controls-panel :global(.form-label) {
458
+ font-size: 0.8125rem;
459
+ font-weight: 600;
460
+ color: #495057;
461
+ margin-bottom: 0.5rem;
462
+ }
463
+
386
464
  /* Responsive adjustments */
387
465
  @media (max-width: 768px) {
388
466
  .floating-controls-wrapper {
@@ -33,6 +33,7 @@ export interface Section {
33
33
  charts: Chart[];
34
34
  grid?: ChartGrid;
35
35
  movingAverage?: MovingAverageConfig;
36
+ useDefaultChartColors?: boolean;
36
37
  }
37
38
  export type Mode = "tabs" | "scrollspy";
38
39
  export type HoverMode = 'x' | 'y' | 'closest' | 'x unified' | 'y unified' | false;
@@ -42,6 +43,7 @@ export interface Layout {
42
43
  movingAverage?: MovingAverageConfig;
43
44
  hoverMode?: HoverMode;
44
45
  coloredHover?: boolean;
46
+ useDefaultChartColors?: boolean;
45
47
  }
46
48
  export interface ChartMarker {
47
49
  date: string | Date;
@@ -63,6 +65,9 @@ export interface GlobalChartControls {
63
65
  legend?: {
64
66
  enabled: boolean;
65
67
  };
68
+ hoverMode?: {
69
+ mode: HoverMode;
70
+ };
66
71
  }
67
72
  export interface CellStylingConfig {
68
73
  bandColors: Record<string, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.44",
3
+ "version": "0.0.46",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",