@smartnet360/svelte-components 0.0.43 → 0.0.45

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
@@ -191,30 +214,90 @@
191
214
  <div class="row flex-grow-1" style="min-height: 0;">
192
215
  <!-- Left: Tree View -->
193
216
  <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>
217
+ <!-- Collapsible Controls Toggle -->
218
+ {#if onSearch || showGroupingSelector}
219
+ <button
220
+ class="controls-toggle w-100 text-start p-2 bg-light border-bottom d-flex align-items-center flex-shrink-0"
221
+ onclick={() => controlsExpanded = !controlsExpanded}
222
+ aria-expanded={controlsExpanded}
223
+ aria-label="Toggle controls"
224
+ >
225
+ <i class="bi bi-sliders me-2"></i>
226
+ <span class="fw-semibold small">Controls</span>
227
+ <i class="bi ms-auto"
228
+ class:bi-chevron-down={controlsExpanded}
229
+ class:bi-chevron-right={!controlsExpanded}>
230
+ </i>
231
+ </button>
232
+ {/if}
233
+
234
+ <!-- Collapsible Controls Content -->
235
+ {#if controlsExpanded}
236
+ <!-- Search Box -->
237
+ {#if onSearch}
238
+ <div class="p-3 border-bottom flex-shrink-0">
239
+ <label for="searchInput" class="form-label small fw-semibold mb-2">
240
+ Search
241
+ </label>
242
+ <div class="input-group input-group-sm">
243
+ <input
244
+ type="text"
245
+ id="searchInput"
246
+ class="form-control"
247
+ placeholder={searchPlaceholder}
248
+ bind:value={searchTerm}
249
+ onkeydown={(e) => {
250
+ if (e.key === 'Enter') {
251
+ handleSearch();
252
+ }
253
+ }}
254
+ />
255
+ {#if searchTerm}
256
+ <button
257
+ class="btn btn-outline-secondary"
258
+ type="button"
259
+ onclick={handleClearSearch}
260
+ title="Clear search"
261
+ aria-label="Clear search"
262
+ >
263
+ <i class="bi bi-x-lg"></i>
264
+ </button>
265
+ {/if}
266
+ <button
267
+ class="btn btn-primary"
268
+ type="button"
269
+ onclick={handleSearch}
270
+ title="Search"
271
+ aria-label="Search"
272
+ >
273
+ <i class="bi bi-search"></i>
274
+ </button>
275
+ </div>
276
+ </div>
277
+ {/if}
278
+
279
+ <!-- Grouping Selector -->
280
+ {#if showGroupingSelector}
281
+ <div class="p-3 border-bottom flex-shrink-0">
282
+ <label for="groupingSelect" class="form-label small fw-semibold mb-2">
283
+ Tree Grouping
284
+ </label>
285
+ <select
286
+ id="groupingSelect"
287
+ class="form-select form-select-sm"
288
+ onchange={(e) => {
289
+ const index = parseInt(e.currentTarget.value);
290
+ treeGrouping = groupingPresets[index].value;
291
+ }}
292
+ >
293
+ {#each groupingPresets as preset, i}
294
+ <option value={i} selected={JSON.stringify(preset.value) === JSON.stringify(treeGrouping)}>
295
+ {preset.label}
296
+ </option>
297
+ {/each}
298
+ </select>
299
+ </div>
300
+ {/if}
218
301
  {/if}
219
302
 
220
303
  <!-- Tree View -->
@@ -247,3 +330,20 @@
247
330
  </div>
248
331
  </div>
249
332
 
333
+ <style>
334
+ .controls-toggle {
335
+ cursor: pointer;
336
+ border: none;
337
+ transition: background-color 0.2s;
338
+ }
339
+
340
+ .controls-toggle:hover {
341
+ background-color: #e9ecef !important;
342
+ }
343
+
344
+ .controls-toggle:focus {
345
+ outline: 2px solid #0d6efd;
346
+ outline-offset: -2px;
347
+ }
348
+ </style>
349
+
@@ -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,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>
@@ -341,16 +346,21 @@
341
346
  {/snippet}
342
347
 
343
348
  <div class="chart-component" bind:this={componentElement}>
344
- <!-- Global Controls Section (appears above tabs/scrollspy) -->
345
- {#if showGlobalControls && showControlsPanel}
346
- <GlobalControls controls={globalControls} onUpdate={handleControlsUpdate} />
349
+ <!-- Floating Global Controls (renders as fixed position overlay) -->
350
+ {#if showGlobalControls}
351
+ <GlobalControls
352
+ controls={globalControls}
353
+ onUpdate={handleControlsUpdate}
354
+ isExpanded={showControlsPanel}
355
+ onToggle={() => showControlsPanel = !showControlsPanel}
356
+ />
347
357
  {/if}
348
358
 
349
359
  <!-- Always render the main content (tabs or scrollspy) -->
350
360
  {#if mode === 'tabs'}
351
361
  <!-- Tab Mode with Navigation -->
352
362
  <div class="tabs-container">
353
- <!-- Tab Navigation with Controls Toggle -->
363
+ <!-- Tab Navigation -->
354
364
  <div class="nav-tabs-wrapper">
355
365
  <ul class="nav nav-tabs" role="tablist">
356
366
  {#each layout.sections as section, index}
@@ -369,21 +379,6 @@
369
379
  </li>
370
380
  {/each}
371
381
  </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
382
  </div>
388
383
 
389
384
  <!-- Tab Content -->
@@ -402,7 +397,7 @@
402
397
  {:else if mode === 'scrollspy'}
403
398
  <!-- ScrollSpy Mode with Navigation -->
404
399
  <div class="scrollspy-container">
405
- <!-- ScrollSpy Navigation with Controls Toggle -->
400
+ <!-- ScrollSpy Navigation -->
406
401
  <nav class="scrollspy-nav">
407
402
  <div class="nav-wrapper">
408
403
  <ul class="nav nav-pills">
@@ -418,21 +413,6 @@
418
413
  </li>
419
414
  {/each}
420
415
  </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
416
  </div>
437
417
  </nav>
438
418
 
@@ -722,11 +702,10 @@
722
702
  max-height: 100%; /* Constrain to slot */
723
703
  }
724
704
 
725
- /* Tab navigation wrapper with toggle button */
705
+ /* Tab navigation wrapper */
726
706
  .nav-tabs-wrapper {
727
707
  display: flex;
728
708
  align-items: center;
729
- gap: 1rem;
730
709
  border-bottom: 1px solid #dee2e6;
731
710
  }
732
711
 
@@ -736,25 +715,7 @@
736
715
  margin-bottom: 0;
737
716
  }
738
717
 
739
- .controls-toggle {
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 */
718
+ /* ScrollSpy navigation wrapper */
758
719
  .scrollspy-nav .nav-wrapper {
759
720
  display: flex;
760
721
  align-items: center;
@@ -1,14 +1,16 @@
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;
8
8
  onUpdate: (controls: GlobalChartControls) => void;
9
+ isExpanded?: boolean;
10
+ onToggle?: () => void;
9
11
  }
10
12
 
11
- let { controls, onUpdate }: Props = $props();
13
+ let { controls, onUpdate, isExpanded = false, onToggle }: Props = $props();
12
14
 
13
15
  function updateControls(updates: Partial<GlobalChartControls>) {
14
16
  onUpdate({
@@ -46,180 +48,457 @@
46
48
  }
47
49
  });
48
50
  }
51
+
52
+ function updateHoverMode(updates: Partial<NonNullable<GlobalChartControls['hoverMode']>>) {
53
+ onUpdate({
54
+ ...controls,
55
+ hoverMode: {
56
+ ...controls.hoverMode!,
57
+ ...updates
58
+ }
59
+ });
60
+ }
49
61
  </script>
50
62
 
51
- <div class="global-controls">
52
- <div class="controls-section">
53
- <!-- <span class="controls-label">Display Controls:</span> -->
54
-
55
- <!-- Moving Average Controls -->
56
- {#if controls.movingAverage}
57
- <div class="control-group">
58
- <!-- MA Enable Toggle Button -->
59
- <input
60
- type="checkbox"
61
- class="btn-check"
62
- id="maToggle"
63
- checked={controls.movingAverage.enabled}
64
- onchange={() => updateMovingAverage({ enabled: !controls.movingAverage!.enabled })}
65
- />
66
- <label class="btn btn-outline-primary btn-sm" for="maToggle">
67
- Moving Average
68
- </label>
69
-
70
- {#if controls.movingAverage.enabled}
71
- <div class="control-subgroup">
72
- <!-- MA Window Size -->
73
- <div class="btn-group btn-group-sm" role="group" aria-label="MA Window">
63
+ <!-- Floating Controls Container -->
64
+ <div class="floating-controls-wrapper">
65
+ <!-- Toggle Button (always visible) -->
66
+ <button
67
+ class="floating-toggle-btn"
68
+ onclick={onToggle}
69
+ title={isExpanded ? "Hide Controls" : "Show Controls"}
70
+ type="button"
71
+ aria-expanded={isExpanded}
72
+ aria-label="Toggle chart controls"
73
+ >
74
+ <svg width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
75
+ <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"/>
76
+ </svg>
77
+ <svg
78
+ class="chevron"
79
+ class:expanded={isExpanded}
80
+ width="12"
81
+ height="12"
82
+ fill="currentColor"
83
+ viewBox="0 0 16 16"
84
+ >
85
+ <path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
86
+ </svg>
87
+ </button>
88
+
89
+ <!-- Controls Panel (expandable) -->
90
+ {#if isExpanded}
91
+ <div class="floating-controls-panel">
92
+ <div class="controls-section">
93
+ <!-- Top Row: Markers and Legend side by side -->
94
+ <div class="controls-row">
95
+ <!-- Markers Toggle -->
96
+ {#if controls.markers}
97
+ <div class="control-group-inline">
74
98
  <input
75
- type="radio"
99
+ type="checkbox"
100
+ class="btn-check"
101
+ id="markersToggle"
102
+ checked={controls.markers.enabled}
103
+ onchange={() => updateMarkers({ enabled: !controls.markers!.enabled })}
104
+ />
105
+ <label class="btn btn-outline-primary btn-sm" for="markersToggle">
106
+ Markers
107
+ </label>
108
+ </div>
109
+ {/if}
110
+
111
+ <!-- Legend Toggle -->
112
+ {#if controls.legend}
113
+ <div class="control-group-inline">
114
+ <input
115
+ type="checkbox"
76
116
  class="btn-check"
77
- name="maWindow"
78
- id="maWindowAuto"
79
- checked={controls.movingAverage.windowOverride === undefined}
80
- onchange={() => updateMovingAverage({ windowOverride: undefined })}
117
+ id="legendToggle"
118
+ checked={controls.legend.enabled}
119
+ onchange={() => updateLegend({ enabled: !controls.legend!.enabled })}
81
120
  />
82
- <label class="btn btn-outline-primary" for="maWindowAuto">Auto</label>
121
+ <label class="btn btn-outline-primary btn-sm" for="legendToggle">
122
+ Legend
123
+ </label>
124
+ </div>
125
+ {/if}
126
+ </div>
83
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">
84
133
  <input
85
134
  type="radio"
86
135
  class="btn-check"
87
- name="maWindow"
88
- id="maWindow7"
89
- checked={controls.movingAverage.windowOverride === 7}
90
- onchange={() => updateMovingAverage({ windowOverride: 7 })}
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' })}
91
150
  />
92
- <label class="btn btn-outline-primary" for="maWindow7">7</label>
151
+ <label class="btn btn-outline-primary" for="hoverModeY">Y-Axis</label> -->
93
152
 
94
153
  <input
95
154
  type="radio"
96
155
  class="btn-check"
97
- name="maWindow"
98
- id="maWindow14"
99
- checked={controls.movingAverage.windowOverride === 14}
100
- onchange={() => updateMovingAverage({ windowOverride: 14 })}
156
+ name="hoverMode"
157
+ id="hoverModeClosest"
158
+ checked={controls.hoverMode.mode === 'closest'}
159
+ onchange={() => updateHoverMode({ mode: 'closest' })}
101
160
  />
102
- <label class="btn btn-outline-primary" for="maWindow14">14</label>
161
+ <label class="btn btn-outline-primary" for="hoverModeClosest">Closest</label>
103
162
 
104
163
  <input
105
164
  type="radio"
106
165
  class="btn-check"
107
- name="maWindow"
108
- id="maWindow24"
109
- checked={controls.movingAverage.windowOverride === 24}
110
- onchange={() => updateMovingAverage({ windowOverride: 24 })}
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' })}
111
180
  />
112
- <label class="btn btn-outline-primary" for="maWindow24">24</label>
181
+ <label class="btn btn-outline-primary" for="hoverModeYUnified">Y-Unified</label> -->
113
182
 
114
183
  <input
115
184
  type="radio"
116
185
  class="btn-check"
117
- name="maWindow"
118
- id="maWindow30"
119
- checked={controls.movingAverage.windowOverride === 30}
120
- onchange={() => updateMovingAverage({ windowOverride: 30 })}
186
+ name="hoverMode"
187
+ id="hoverModeFalse"
188
+ checked={controls.hoverMode.mode === false}
189
+ onchange={() => updateHoverMode({ mode: false })}
121
190
  />
122
- <label class="btn btn-outline-primary" for="maWindow30">30</label>
191
+ <label class="btn btn-outline-primary" for="hoverModeFalse">Off</label>
123
192
  </div>
193
+ </div>
194
+ {/if}
124
195
 
125
- <!-- Show Original Toggle Button -->
196
+ <!-- Bottom: Moving Average Controls -->
197
+ {#if controls.movingAverage}
198
+ <div class="control-group">
199
+ <!-- MA Enable Toggle Button -->
126
200
  <input
127
201
  type="checkbox"
128
202
  class="btn-check"
129
- id="showOriginal"
130
- checked={controls.movingAverage.showOriginal}
131
- onchange={() => updateMovingAverage({ showOriginal: !controls.movingAverage!.showOriginal })}
203
+ id="maToggle"
204
+ checked={controls.movingAverage.enabled}
205
+ onchange={() => updateMovingAverage({ enabled: !controls.movingAverage!.enabled })}
132
206
  />
133
- <label class="btn btn-outline-primary btn-sm ms-2" for="showOriginal">
134
- Show Original
207
+ <label class="btn btn-outline-primary btn-sm" for="maToggle">
208
+ Moving Average
135
209
  </label>
210
+
211
+ {#if controls.movingAverage.enabled}
212
+ <div class="control-subgroup">
213
+ <!-- MA Window Size -->
214
+ <div class="btn-group btn-group-sm" role="group" aria-label="MA Window">
215
+ <input
216
+ type="radio"
217
+ class="btn-check"
218
+ name="maWindow"
219
+ id="maWindowAuto"
220
+ checked={controls.movingAverage.windowOverride === undefined}
221
+ onchange={() => updateMovingAverage({ windowOverride: undefined })}
222
+ />
223
+ <label class="btn btn-outline-primary" for="maWindowAuto">Auto</label>
224
+
225
+ <input
226
+ type="radio"
227
+ class="btn-check"
228
+ name="maWindow"
229
+ id="maWindow7"
230
+ checked={controls.movingAverage.windowOverride === 7}
231
+ onchange={() => updateMovingAverage({ windowOverride: 7 })}
232
+ />
233
+ <label class="btn btn-outline-primary" for="maWindow7">7</label>
234
+
235
+ <input
236
+ type="radio"
237
+ class="btn-check"
238
+ name="maWindow"
239
+ id="maWindow14"
240
+ checked={controls.movingAverage.windowOverride === 14}
241
+ onchange={() => updateMovingAverage({ windowOverride: 14 })}
242
+ />
243
+ <label class="btn btn-outline-primary" for="maWindow14">14</label>
244
+
245
+ <input
246
+ type="radio"
247
+ class="btn-check"
248
+ name="maWindow"
249
+ id="maWindow24"
250
+ checked={controls.movingAverage.windowOverride === 24}
251
+ onchange={() => updateMovingAverage({ windowOverride: 24 })}
252
+ />
253
+ <label class="btn btn-outline-primary" for="maWindow24">24</label>
254
+
255
+ <input
256
+ type="radio"
257
+ class="btn-check"
258
+ name="maWindow"
259
+ id="maWindow30"
260
+ checked={controls.movingAverage.windowOverride === 30}
261
+ onchange={() => updateMovingAverage({ windowOverride: 30 })}
262
+ />
263
+ <label class="btn btn-outline-primary" for="maWindow30">30</label>
264
+ </div>
265
+
266
+ <!-- Show Original Toggle Button -->
267
+ <input
268
+ type="checkbox"
269
+ class="btn-check"
270
+ id="showOriginal"
271
+ checked={controls.movingAverage.showOriginal}
272
+ onchange={() => updateMovingAverage({ showOriginal: !controls.movingAverage!.showOriginal })}
273
+ />
274
+ <label class="btn btn-outline-primary btn-sm ms-2" for="showOriginal">
275
+ Show Original
276
+ </label>
277
+ </div>
278
+ {/if}
136
279
  </div>
137
280
  {/if}
138
281
  </div>
139
- {/if}
140
-
141
- <!-- Markers Toggle -->
142
- {#if controls.markers}
143
- <div class="control-group">
144
- <input
145
- type="checkbox"
146
- class="btn-check"
147
- id="markersToggle"
148
- checked={controls.markers.enabled}
149
- onchange={() => updateMarkers({ enabled: !controls.markers!.enabled })}
150
- />
151
- <label class="btn btn-outline-secondary btn-sm" for="markersToggle">
152
- Markers
153
- </label>
154
- </div>
155
- {/if}
156
-
157
- <!-- Legend Toggle -->
158
- {#if controls.legend}
159
- <div class="control-group">
160
- <input
161
- type="checkbox"
162
- class="btn-check"
163
- id="legendToggle"
164
- checked={controls.legend.enabled}
165
- onchange={() => updateLegend({ enabled: !controls.legend!.enabled })}
166
- />
167
- <label class="btn btn-outline-secondary btn-sm" for="legendToggle">
168
- Legend
169
- </label>
170
- </div>
171
- {/if}
172
-
173
- </div>
282
+ </div>
283
+ {/if}
174
284
  </div>
175
285
 
176
286
  <style>
177
- /* Global Controls Section */
178
- .global-controls {
179
- flex-shrink: 0;
180
- background-color: #f8f9fa;
181
- border-bottom: 1px solid #dee2e6;
182
- padding: 0.5rem 1rem;
287
+ /* Floating Controls Wrapper - positioned in top-right */
288
+ .floating-controls-wrapper {
289
+ position: fixed;
290
+ top: 0.3rem;
291
+ right: 0.3rem;
292
+ z-index: 1000;
293
+ display: flex;
294
+ flex-direction: column;
295
+ align-items: flex-end;
296
+ gap: 0.5rem;
297
+ }
298
+
299
+ /* Toggle Button - Always visible */
300
+ .floating-toggle-btn {
301
+ display: flex;
302
+ align-items: center;
303
+ gap: 0.375rem;
304
+ padding: 0.5rem 0.75rem;
305
+ background: rgba(255, 255, 255, 0.95);
306
+ backdrop-filter: blur(10px);
307
+ -webkit-backdrop-filter: blur(10px);
308
+ border: 1px solid rgba(0, 0, 0, 0.08);
309
+ border-radius: 8px;
310
+ box-shadow:
311
+ 0 2px 8px rgba(0, 0, 0, 0.08),
312
+ 0 4px 16px rgba(0, 0, 0, 0.04);
313
+ cursor: pointer;
314
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
183
315
  font-size: 0.875rem;
316
+ font-weight: 500;
317
+ color: #495057;
318
+ opacity: 0.5;
319
+ }
320
+
321
+ .floating-toggle-btn:hover {
322
+ background: rgba(255, 255, 255, 1);
323
+ border-color: rgba(0, 0, 0, 0.12);
324
+ box-shadow:
325
+ 0 4px 12px rgba(0, 0, 0, 0.12),
326
+ 0 8px 24px rgba(0, 0, 0, 0.08);
327
+ transform: translateY(-1px);
328
+ opacity: 1;
329
+ }
330
+
331
+ .floating-toggle-btn:active {
332
+ transform: translateY(0);
333
+ box-shadow:
334
+ 0 1px 4px rgba(0, 0, 0, 0.08),
335
+ 0 2px 8px rgba(0, 0, 0, 0.04);
336
+ }
337
+
338
+ .floating-toggle-btn .chevron {
339
+ transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
340
+ opacity: 0.6;
341
+ }
342
+
343
+ .floating-toggle-btn .chevron.expanded {
344
+ transform: rotate(180deg);
345
+ }
346
+
347
+ /* Floating Controls Panel - Glass-morphism style */
348
+ .floating-controls-panel {
349
+ background: rgba(255, 255, 255, 0.95);
350
+ backdrop-filter: blur(12px);
351
+ -webkit-backdrop-filter: blur(12px);
352
+ border: 1px solid rgba(0, 0, 0, 0.08);
353
+ border-radius: 12px;
354
+ box-shadow:
355
+ 0 4px 16px rgba(0, 0, 0, 0.1),
356
+ 0 8px 32px rgba(0, 0, 0, 0.08);
357
+ padding: 1rem;
358
+ min-width: 320px;
359
+ max-width: 480px;
360
+ opacity: 1;
361
+ animation: slideIn 200ms cubic-bezier(0.4, 0, 0.2, 1);
362
+ }
363
+
364
+ .floating-controls-panel:hover {
365
+ opacity: 1;
366
+ }
367
+
368
+ @keyframes slideIn {
369
+ from {
370
+ opacity: 0;
371
+ transform: translateY(-8px);
372
+ }
373
+ to {
374
+ opacity: 1;
375
+ transform: translateY(0);
376
+ }
184
377
  }
185
378
 
186
379
  .controls-section {
380
+ display: flex;
381
+ flex-direction: column;
382
+ gap: 1rem;
383
+ }
384
+
385
+ /* Horizontal row for Markers and Legend at top */
386
+ .controls-row {
187
387
  display: flex;
188
388
  align-items: center;
189
- gap: 1.5rem;
190
- flex-wrap: wrap;
389
+ gap: 0.75rem;
390
+ padding-bottom: 0.75rem;
391
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
191
392
  }
192
393
 
193
- .control-group {
394
+ /* Inline control group for horizontal layout */
395
+ .control-group-inline {
194
396
  display: flex;
195
397
  align-items: center;
398
+ }
399
+
400
+ /* Vertical control group for Moving Average */
401
+ .control-group {
402
+ display: flex;
403
+ flex-direction: column;
196
404
  gap: 0.75rem;
197
405
  }
198
406
 
407
+ .control-group > label:first-of-type {
408
+ font-weight: 500;
409
+ }
410
+
411
+ .control-label {
412
+ font-size: 0.8125rem;
413
+ font-weight: 600;
414
+ color: #495057;
415
+ margin-bottom: 0.25rem;
416
+ }
417
+
199
418
  .control-subgroup {
200
419
  display: flex;
420
+ flex-wrap: wrap;
201
421
  align-items: center;
202
422
  gap: 0.5rem;
203
- padding-left: 0.5rem;
204
- border-left: 2px solid #dee2e6;
423
+ padding-left: 0.75rem;
424
+ padding-top: 0.5rem;
425
+ border-left: 2px solid rgba(0, 0, 0, 0.08);
205
426
  }
206
427
 
207
- .global-controls :global(.btn-group-sm .btn) {
208
- padding: 0.25rem 0.75rem;
209
- font-size: 0.75rem;
428
+ /* Bootstrap button overrides for better integration */
429
+ .floating-controls-panel :global(.btn-sm) {
430
+ padding: 0.375rem 0.75rem;
431
+ font-size: 0.8125rem;
210
432
  font-weight: 500;
433
+ border-radius: 6px;
434
+ transition: all 150ms ease;
435
+ }
436
+
437
+ .floating-controls-panel :global(.btn-outline-primary) {
438
+ border-color: rgba(13, 110, 253, 0.3);
439
+ }
440
+
441
+ .floating-controls-panel :global(.btn-outline-primary:hover) {
442
+ background-color: rgba(13, 110, 253, 0.1);
443
+ border-color: rgba(13, 110, 253, 0.5);
444
+ }
445
+
446
+ .floating-controls-panel :global(.btn-check:checked + .btn-outline-primary) {
447
+ background-color: #0d6efd;
448
+ border-color: #0d6efd;
449
+ }
450
+
451
+ .floating-controls-panel :global(.btn-group) {
452
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
453
+ border-radius: 6px;
454
+ overflow: hidden;
455
+ }
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;
211
462
  }
212
463
 
213
464
  /* Responsive adjustments */
214
465
  @media (max-width: 768px) {
215
- .controls-section {
216
- flex-direction: column;
217
- align-items: flex-start;
218
- gap: 0.75rem;
466
+ .floating-controls-wrapper {
467
+ top: 0.5rem;
468
+ right: 0.5rem;
469
+ }
470
+
471
+ .floating-controls-panel {
472
+ min-width: 280px;
473
+ max-width: calc(100vw - 2rem);
474
+ padding: 0.875rem;
219
475
  }
220
-
476
+
477
+ .control-subgroup {
478
+ padding-left: 0.5rem;
479
+ }
480
+ }
481
+
482
+ /* Dark mode support (optional - activate if needed) */
483
+ @media (prefers-color-scheme: dark) {
484
+ .floating-toggle-btn {
485
+ background: rgba(33, 37, 41, 0.95);
486
+ border-color: rgba(255, 255, 255, 0.1);
487
+ color: #f8f9fa;
488
+ }
489
+
490
+ .floating-toggle-btn:hover {
491
+ background: rgba(33, 37, 41, 1);
492
+ border-color: rgba(255, 255, 255, 0.15);
493
+ }
494
+
495
+ .floating-controls-panel {
496
+ background: rgba(33, 37, 41, 0.95);
497
+ border-color: rgba(255, 255, 255, 0.1);
498
+ }
499
+
221
500
  .control-subgroup {
222
- flex-wrap: wrap;
501
+ border-left-color: rgba(255, 255, 255, 0.1);
223
502
  }
224
503
  }
225
504
  </style>
@@ -2,6 +2,8 @@ import type { GlobalChartControls } from './charts.model.js';
2
2
  interface Props {
3
3
  controls: GlobalChartControls;
4
4
  onUpdate: (controls: GlobalChartControls) => void;
5
+ isExpanded?: boolean;
6
+ onToggle?: () => void;
5
7
  }
6
8
  declare const GlobalControls: import("svelte").Component<Props, {}, "">;
7
9
  type GlobalControls = ReturnType<typeof GlobalControls>;
@@ -63,6 +63,9 @@ export interface GlobalChartControls {
63
63
  legend?: {
64
64
  enabled: boolean;
65
65
  };
66
+ hoverMode?: {
67
+ mode: HoverMode;
68
+ };
66
69
  }
67
70
  export interface CellStylingConfig {
68
71
  bandColors: Record<string, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",