@aquera/nile-visualization 0.7.0 → 0.8.0

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.
@@ -100,3 +100,5 @@ export { NileWidgetViewer } from './nile-widget-viewer/index.js';
100
100
  export type { NileWidgetConfig, NileWidgetChartConfig, NileWidgetKpiConfig, WidgetLayout, } from './nile-widget-viewer/index.js';
101
101
  export { NileDashboardViewer } from './nile-dashboard-viewer/index.js';
102
102
  export type { NileDashboardConfig } from './nile-dashboard-viewer/index.js';
103
+ export { NileExecutiveSummary } from './nile-executive-summary/index.js';
104
+ export type { NileExecutiveSummaryConfig } from './nile-executive-summary/index.js';
package/dist/src/index.js CHANGED
@@ -54,4 +54,5 @@ export { NileAiSender } from './nile-ai-sender/index.js';
54
54
  export { NileAiPanel } from './nile-ai-panel/index.js';
55
55
  export { NileWidgetViewer } from './nile-widget-viewer/index.js';
56
56
  export { NileDashboardViewer } from './nile-dashboard-viewer/index.js';
57
+ export { NileExecutiveSummary } from './nile-executive-summary/index.js';
57
58
  //# sourceMappingURL=index.js.map
@@ -333,5 +333,103 @@ export const styles = css `
333
333
  .ai-panel-overlay[data-open] {
334
334
  transform: translateY(0);
335
335
  }
336
+
337
+ /* ── Skeleton loader (matches nile-skeleton-loader animation style) ── */
338
+
339
+ :host([loading]) .chart-card {
340
+ pointer-events: none;
341
+ }
342
+
343
+ .chart-skeleton {
344
+ display: flex;
345
+ flex-direction: column;
346
+ gap: 0;
347
+ padding: var(--nile-spacing-3xl, 24px) var(--nile-spacing-3xl, 24px) var(--nile-spacing-xl, 16px);
348
+ min-height: var(--nile-chart-skeleton-height, 300px);
349
+ }
350
+
351
+ .chart-skeleton-body {
352
+ display: flex;
353
+ flex-direction: column;
354
+ justify-content: space-around;
355
+ flex: 1;
356
+ gap: var(--nile-spacing-xl, 14px);
357
+ padding-left: 44px;
358
+ position: relative;
359
+ }
360
+
361
+ /* Vertical y-axis rule */
362
+ .chart-skeleton-body::before {
363
+ content: '';
364
+ position: absolute;
365
+ left: 34px;
366
+ top: 4px;
367
+ bottom: 4px;
368
+ width: 2px;
369
+ border-radius: 1px;
370
+ background: var(--nile-colors-neutral-400, #e5e7eb);
371
+ }
372
+
373
+ .chart-skeleton-row {
374
+ display: flex;
375
+ align-items: center;
376
+ gap: var(--nile-spacing-md, 8px);
377
+ height: 24px;
378
+ }
379
+
380
+ .chart-skeleton-ylabel {
381
+ width: 26px;
382
+ height: 10px;
383
+ border-radius: 3px;
384
+ flex-shrink: 0;
385
+ background: var(--nile-colors-neutral-400, #e5e7eb);
386
+ animation: nile-skeleton-blink 1.2s ease-in-out infinite;
387
+ }
388
+
389
+ .chart-skeleton-bar {
390
+ height: 20px;
391
+ width: var(--w, 60%);
392
+ border-radius: 0 3px 3px 0;
393
+ background: var(--nile-colors-neutral-400, #e5e7eb);
394
+ animation: nile-skeleton-blink 1.2s ease-in-out infinite;
395
+ }
396
+
397
+ /* Staggered wave across the bars */
398
+ .chart-skeleton-row:nth-child(1) .chart-skeleton-bar { animation-delay: 0ms; }
399
+ .chart-skeleton-row:nth-child(2) .chart-skeleton-bar { animation-delay: 100ms; }
400
+ .chart-skeleton-row:nth-child(3) .chart-skeleton-bar { animation-delay: 200ms; }
401
+ .chart-skeleton-row:nth-child(4) .chart-skeleton-bar { animation-delay: 300ms; }
402
+ .chart-skeleton-row:nth-child(5) .chart-skeleton-bar { animation-delay: 400ms; }
403
+
404
+ /* Horizontal x-axis labels row */
405
+ .chart-skeleton-xaxis-row {
406
+ display: flex;
407
+ justify-content: space-around;
408
+ padding-left: 44px;
409
+ margin-top: var(--nile-spacing-xl, 14px);
410
+ }
411
+
412
+ .chart-skeleton-xlabel {
413
+ height: 10px;
414
+ width: 30px;
415
+ border-radius: 3px;
416
+ background: var(--nile-colors-neutral-400, #e5e7eb);
417
+ animation: nile-skeleton-blink 1.2s ease-in-out infinite;
418
+ animation-delay: var(--d, 0ms);
419
+ }
420
+
421
+ @keyframes nile-skeleton-blink {
422
+ 0%, 100% { opacity: 0.5; }
423
+ 50% { opacity: 1; }
424
+ }
425
+
426
+ @media (prefers-reduced-motion: reduce) {
427
+ .chart-skeleton-bar,
428
+ .chart-skeleton-ylabel,
429
+ .chart-skeleton-xlabel {
430
+ animation: none;
431
+ opacity: 0.7;
432
+ }
433
+ }
336
434
  `;
337
435
  //# sourceMappingURL=nile-chart.css.js.map
@@ -53,6 +53,17 @@ export declare class NileChart extends NileElement {
53
53
  static styles: CSSResultGroup;
54
54
  /** Full chart configuration. Accepts flat NileChartConfig or separated { chart, aq } input. */
55
55
  config: NileChartConfig | NileChartConfigInputType | null;
56
+ /**
57
+ * When true, hides the chart and shows a skeleton bar-chart loader.
58
+ * Set to false once data is ready to reveal the chart.
59
+ *
60
+ * @example
61
+ * ```js
62
+ * chart.loading = true;
63
+ * fetchData().then(data => { chart.config = buildConfig(data); chart.loading = false; });
64
+ * ```
65
+ */
66
+ loading: boolean;
56
67
  /**
57
68
  * When set, fills `chart.type` if the config omits it (same values as `chart.type`, e.g. `stacked`, `pie`).
58
69
  * Usage: `<nile-chart chart-type="pie" />` plus `config.chart` with series data only.
@@ -108,6 +119,7 @@ export declare class NileChart extends NileElement {
108
119
  private renderHeader;
109
120
  private renderAiPanel;
110
121
  private renderChartContent;
122
+ private renderSkeleton;
111
123
  render(): TemplateResult;
112
124
  }
113
125
  declare global {
@@ -103,6 +103,17 @@ let NileChart = class NileChart extends NileElement {
103
103
  super(...arguments);
104
104
  /** Full chart configuration. Accepts flat NileChartConfig or separated { chart, aq } input. */
105
105
  this.config = null;
106
+ /**
107
+ * When true, hides the chart and shows a skeleton bar-chart loader.
108
+ * Set to false once data is ready to reveal the chart.
109
+ *
110
+ * @example
111
+ * ```js
112
+ * chart.loading = true;
113
+ * fetchData().then(data => { chart.config = buildConfig(data); chart.loading = false; });
114
+ * ```
115
+ */
116
+ this.loading = false;
106
117
  /**
107
118
  * When set, fills `chart.type` if the config omits it (same values as `chart.type`, e.g. `stacked`, `pie`).
108
119
  * Usage: `<nile-chart chart-type="pie" />` plus `config.chart` with series data only.
@@ -887,16 +898,35 @@ let NileChart = class NileChart extends NileElement {
887
898
  }
888
899
  }
889
900
  }
901
+ renderSkeleton() {
902
+ return html `
903
+ <div class="chart-skeleton" aria-busy="true" aria-label="Loading chart">
904
+ <div class="chart-skeleton-body">
905
+ ${[78, 55, 91, 42, 68].map(w => html `
906
+ <div class="chart-skeleton-row">
907
+ <div class="chart-skeleton-ylabel"></div>
908
+ <div class="chart-skeleton-bar" style="--w: ${w}%"></div>
909
+ </div>
910
+ `)}
911
+ </div>
912
+ <div class="chart-skeleton-xaxis-row">
913
+ ${[0, 1, 2, 3, 4].map(i => html `<div class="chart-skeleton-xlabel" style="--d: ${i * 80}ms"></div>`)}
914
+ </div>
915
+ </div>
916
+ `;
917
+ }
890
918
  render() {
919
+ const isLoading = this.loading || (this.activeConfig?.loading ?? false);
891
920
  return html `
892
921
  <div class="chart-card">
893
922
  ${this.renderHeader()}
894
923
  <div class="chart-wrapper">
895
- <div class="chart-inner">
896
- <div
897
- class="chart-inner ${this.activeConfig?.type === 'kpi' ? 'chart-inner--kpi' : ''}"
898
- >
899
- ${this.activeConfig ? this.renderChartContent() : html `<slot></slot>`}
924
+ <div class="chart-inner ${this.activeConfig?.type === 'kpi' ? 'chart-inner--kpi' : ''}">
925
+ ${isLoading
926
+ ? this.renderSkeleton()
927
+ : this.activeConfig
928
+ ? this.renderChartContent()
929
+ : html `<slot></slot>`}
900
930
  ${this.renderAiPanel()}
901
931
  </div>
902
932
  </div>
@@ -909,6 +939,9 @@ NileChart.styles = styles;
909
939
  __decorate([
910
940
  property({ type: Object })
911
941
  ], NileChart.prototype, "config", void 0);
942
+ __decorate([
943
+ property({ type: Boolean, reflect: true })
944
+ ], NileChart.prototype, "loading", void 0);
912
945
  __decorate([
913
946
  property({ type: String, attribute: 'chart-type' })
914
947
  ], NileChart.prototype, "chartTypeAttr", void 0);
@@ -0,0 +1,2 @@
1
+ export { NileExecutiveSummary } from './nile-executive-summary.js';
2
+ export type { NileExecutiveSummaryConfig } from './nile-executive-summary-config.js';
@@ -0,0 +1,2 @@
1
+ export { NileExecutiveSummary } from './nile-executive-summary.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Copyright Aquera Inc 2023
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ /**
8
+ * Configuration interface for the nile-executive-summary component.
9
+ * Assign to the component's `config` property via JavaScript.
10
+ *
11
+ * @example
12
+ * ```js
13
+ * const el = document.querySelector('nile-executive-summary');
14
+ * el.config = {
15
+ * summary: '<p>Q1 revenue grew <strong>23%</strong> YoY...</p>',
16
+ * buttonLabel: 'View Summary',
17
+ * drawerLabel: 'AI Executive Summary',
18
+ * };
19
+ * ```
20
+ */
21
+ export interface NileExecutiveSummaryConfig {
22
+ /** Text or HTML string displayed with the typewriter effect inside the drawer. */
23
+ summary: string;
24
+ /** Label rendered on the built-in trigger nile-button. Default: 'Executive Summary' */
25
+ buttonLabel?: string;
26
+ /**
27
+ * Variant forwarded to the internal nile-button. Default: 'primary'
28
+ * Mirrors nile-button's `variant` property.
29
+ */
30
+ buttonVariant?: 'primary' | 'secondary' | 'tertiary' | 'caution' | 'ghost' | 'destructive' | 'secondary-grey' | 'secondary-blue';
31
+ /** Title displayed in the drawer header. Default: 'Executive Summary' */
32
+ drawerLabel?: string;
33
+ /** Milliseconds per character for the typing animation. Default: 30 */
34
+ typingSpeed?: number;
35
+ /** Close the drawer when the user presses Escape. Default: true */
36
+ closeOnEscape?: boolean;
37
+ /** Prevent the drawer from closing when the overlay is clicked. Default: false */
38
+ preventOverlayClose?: boolean;
39
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright Aquera Inc 2023
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=nile-executive-summary-config.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copyright Aquera Inc 2023
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ export declare const styles: import("lit").CSSResult;
8
+ declare const _default: import("lit").CSSResult[];
9
+ export default _default;
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Copyright Aquera Inc 2023
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import { css } from 'lit';
8
+ export const styles = css `
9
+ :host {
10
+ box-sizing: border-box;
11
+ -webkit-font-smoothing: antialiased;
12
+ -moz-osx-font-smoothing: grayscale;
13
+ text-rendering: optimizeLegibility;
14
+ display: inline-block;
15
+ position: relative;
16
+ }
17
+
18
+ :host *,
19
+ :host *::before,
20
+ :host *::after {
21
+ box-sizing: inherit;
22
+ }
23
+
24
+ [hidden] {
25
+ display: none !important;
26
+ }
27
+
28
+ /* ─── Trigger button ───────────────────────────────────────────────────── */
29
+
30
+ .es-trigger {
31
+ display: inline-flex;
32
+ align-items: center;
33
+ gap: 6px;
34
+ padding: var(--executive-summary-btn-padding, 8px 14px);
35
+ border: none;
36
+ border-radius: var(--executive-summary-btn-radius, var(--nile-radius-base-standard, 4px));
37
+ background-color: var(--executive-summary-btn-bg, var(--nile-colors-primary-600, #0052a3));
38
+ color: var(--executive-summary-btn-color, var(--nile-colors-white-base, #fff));
39
+ font-family: var(--nile-font-family-body, sans-serif);
40
+ font-size: var(--nile-type-scale-3, 14px);
41
+ font-weight: 500;
42
+ line-height: 1.4;
43
+ cursor: pointer;
44
+ transition: background-color 0.2s ease, box-shadow 0.2s ease;
45
+ outline: none;
46
+ white-space: nowrap;
47
+ user-select: none;
48
+ }
49
+
50
+ .es-trigger:hover {
51
+ background-color: var(--executive-summary-btn-bg-hover, var(--nile-colors-primary-700, #003d7a));
52
+ }
53
+
54
+ .es-trigger:focus-visible {
55
+ box-shadow: 0 0 0 3px var(--nile-colors-primary-200, rgba(0, 82, 163, 0.3));
56
+ }
57
+
58
+ .es-trigger:active {
59
+ background-color: var(--executive-summary-btn-bg-active, var(--nile-colors-primary-800, #002e5b));
60
+ }
61
+
62
+ /* ─── Backdrop wrapper (fixed, full viewport) ──────────────────────────── */
63
+
64
+ .es-backdrop {
65
+ position: fixed;
66
+ top: 0;
67
+ left: 0;
68
+ width: 100%;
69
+ height: 100%;
70
+ pointer-events: none;
71
+ overflow: hidden;
72
+ z-index: var(--executive-summary-z-index, 700);
73
+ }
74
+
75
+ /* ─── Overlay ──────────────────────────────────────────────────────────── */
76
+
77
+ .es-overlay {
78
+ position: fixed;
79
+ top: 0;
80
+ right: 0;
81
+ bottom: 0;
82
+ left: 0;
83
+ background-color: var(--executive-summary-overlay-color, hsl(240 3.8% 46.1% / 33%));
84
+ pointer-events: all;
85
+ }
86
+
87
+ /* ─── Drawer panel ─────────────────────────────────────────────────────── */
88
+
89
+ .es-panel {
90
+ position: absolute;
91
+ top: 0;
92
+ right: 0;
93
+ bottom: 0;
94
+ width: var(--executive-summary-size, 480px);
95
+ max-width: 100%;
96
+ display: flex;
97
+ flex-direction: column;
98
+ z-index: 2;
99
+ background-color: var(--nile-colors-white-base, #fff);
100
+ box-shadow: var(
101
+ --executive-summary-shadow,
102
+ var(
103
+ --nile-box-shadow-2,
104
+ 0px 20px 24px -4px rgba(16, 24, 40, 0.08),
105
+ 0px 8px 8px -4px rgba(16, 24, 40, 0.03)
106
+ )
107
+ );
108
+ pointer-events: all;
109
+ outline: none;
110
+ }
111
+
112
+ /* ─── Header ───────────────────────────────────────────────────────────── */
113
+
114
+ .es-header {
115
+ display: flex;
116
+ align-items: center;
117
+ flex-shrink: 0;
118
+ padding: var(--executive-summary-header-spacing, 16px 20px);
119
+ border-bottom: 1px solid var(--nile-colors-neutral-100, #e5e7eb);
120
+ }
121
+
122
+ .es-title {
123
+ flex: 1 1 auto;
124
+ margin: 0;
125
+ font-family: var(--nile-font-family-body, sans-serif);
126
+ font-size: var(--nile-font-size-rem-small, 16px);
127
+ font-weight: 600;
128
+ line-height: 1.4;
129
+ color: var(--nile-colors-dark-900, #111827);
130
+ }
131
+
132
+ /* ─── Close button ─────────────────────────────────────────────────────── */
133
+
134
+ .es-close {
135
+ flex-shrink: 0;
136
+ display: inline-flex;
137
+ align-items: center;
138
+ justify-content: center;
139
+ width: 32px;
140
+ height: 32px;
141
+ padding: 0;
142
+ border: none;
143
+ background: none;
144
+ border-radius: var(--nile-radius-sm, 4px);
145
+ cursor: pointer;
146
+ color: var(--nile-colors-dark-900, #374151);
147
+ transition: background-color 0.15s ease;
148
+ margin-left: 8px;
149
+ }
150
+
151
+ .es-close:hover {
152
+ background-color: var(--nile-colors-neutral-50, #f3f4f6);
153
+ }
154
+
155
+ .es-close:focus-visible {
156
+ outline: 2px solid var(--nile-colors-primary-600, #0052a3);
157
+ outline-offset: 1px;
158
+ }
159
+
160
+ .es-close svg {
161
+ width: 18px;
162
+ height: 18px;
163
+ stroke: currentColor;
164
+ fill: none;
165
+ stroke-width: 2;
166
+ stroke-linecap: round;
167
+ stroke-linejoin: round;
168
+ }
169
+
170
+ /* ─── Body ─────────────────────────────────────────────────────────────── */
171
+
172
+ .es-body {
173
+ flex: 1 1 auto;
174
+ overflow-y: auto;
175
+ padding: var(--executive-summary-body-spacing, 20px);
176
+ -webkit-overflow-scrolling: touch;
177
+ }
178
+
179
+ /* ─── Typed text ───────────────────────────────────────────────────────── */
180
+
181
+ .es-typed-text {
182
+ font-family: var(--nile-font-family-body, sans-serif);
183
+ font-size: var(--nile-type-scale-3, 14px);
184
+ line-height: 1.7;
185
+ color: var(--nile-colors-dark-900, #111827);
186
+ word-break: break-word;
187
+ }
188
+
189
+ @media (forced-colors: active) {
190
+ .es-panel {
191
+ border: 1px solid ButtonText;
192
+ }
193
+ }
194
+ `;
195
+ export default [styles];
196
+ //# sourceMappingURL=nile-executive-summary.css.js.map
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Copyright Aquera Inc 2023
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import type { CSSResultGroup, TemplateResult } from 'lit';
8
+ import NileElement from '../internal/nile-element.js';
9
+ import type { NileExecutiveSummaryConfig } from './nile-executive-summary-config.js';
10
+ /**
11
+ * Nile Executive Summary component.
12
+ *
13
+ * @tag nile-executive-summary
14
+ *
15
+ * @summary A self-contained drawer component with a built-in trigger button.
16
+ * Place it anywhere — clicking the button slides in a right-side drawer that
17
+ * displays summary content (plain text or HTML) with a typewriter animation.
18
+ * Configured entirely via the `config` JS property.
19
+ *
20
+ * @event nile-show - Emitted when the drawer starts opening.
21
+ * @event nile-after-show - Emitted when the drawer is fully open and typing begins.
22
+ * @event nile-hide - Emitted when the drawer starts closing.
23
+ * @event nile-after-hide - Emitted when the drawer is fully closed.
24
+ * @event nile-typing-complete - Emitted when the typewriter animation finishes.
25
+ * @event nile-init - Emitted when the component connects to the DOM.
26
+ * @event nile-destroy - Emitted when the component disconnects from the DOM.
27
+ *
28
+ * @csspart base - Root wrapper (contains trigger + drawer).
29
+ * @csspart trigger - The built-in button that opens the drawer.
30
+ * @csspart overlay - The dim overlay behind the panel.
31
+ * @csspart panel - The drawer panel.
32
+ * @csspart header - The drawer header.
33
+ * @csspart title - The drawer title.
34
+ * @csspart close - The close button.
35
+ * @csspart body - The drawer body containing the typed summary.
36
+ *
37
+ * @cssproperty --executive-summary-size - Drawer width. Default: 480px.
38
+ * @cssproperty --executive-summary-header-spacing - Header padding. Default: 16px 20px.
39
+ * @cssproperty --executive-summary-body-spacing - Body padding. Default: 20px.
40
+ * @cssproperty --executive-summary-z-index - Z-index of the drawer. Default: 700.
41
+ * @cssproperty --executive-summary-overlay-color - Overlay background colour.
42
+ * @cssproperty --executive-summary-btn-bg - Trigger button background.
43
+ * @cssproperty --executive-summary-btn-color - Trigger button text colour.
44
+ * @cssproperty --executive-summary-btn-radius - Trigger button border-radius.
45
+ */
46
+ export declare class NileExecutiveSummary extends NileElement {
47
+ static styles: CSSResultGroup;
48
+ private typingTimer;
49
+ private keyDownHandler;
50
+ private originalTrigger;
51
+ private scrollLockActive;
52
+ private backdrop;
53
+ private panel;
54
+ private overlay;
55
+ /**
56
+ * Full configuration. Assign via JavaScript — all fields are optional except `summary`.
57
+ *
58
+ * @example
59
+ * ```js
60
+ * el.config = {
61
+ * summary: '<p>Q1 revenue grew <strong>23%</strong>.</p>',
62
+ * buttonLabel: 'View Summary',
63
+ * drawerLabel: 'AI Executive Summary',
64
+ * typingSpeed: 20,
65
+ * };
66
+ * ```
67
+ */
68
+ config: NileExecutiveSummaryConfig | null;
69
+ /**
70
+ * Whether the drawer is currently open.
71
+ * Can be set programmatically; use `show()` / `hide()` for animated transitions.
72
+ */
73
+ open: boolean;
74
+ /** @internal Characters revealed so far during typing. */
75
+ private displayedText;
76
+ /** @internal Whether typing is still in progress (drives cursor visibility). */
77
+ private isTyping;
78
+ connectedCallback(): void;
79
+ firstUpdated(): void;
80
+ disconnectedCallback(): void;
81
+ private addKeyListener;
82
+ private removeKeyListener;
83
+ private handleKeyDown;
84
+ private lockScroll;
85
+ private unlockScroll;
86
+ private animatePanel;
87
+ private animateOverlay;
88
+ private requestClose;
89
+ private startTyping;
90
+ private stopTyping;
91
+ /** Opens the drawer with animation. Resolves when the animation completes. */
92
+ show(): Promise<void>;
93
+ /** Closes the drawer with animation. Resolves when the animation completes. */
94
+ hide(): Promise<void>;
95
+ render(): TemplateResult;
96
+ }
97
+ declare global {
98
+ interface HTMLElementTagNameMap {
99
+ 'nile-executive-summary': NileExecutiveSummary;
100
+ }
101
+ }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Copyright Aquera Inc 2023
3
+ *
4
+ * This source code is licensed under the BSD-3-Clause license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import { __decorate } from "tslib";
8
+ import { customElement, property, query, state } from 'lit/decorators.js';
9
+ import { classMap } from 'lit/directives/class-map.js';
10
+ import { html } from 'lit';
11
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
12
+ import NileElement from '../internal/nile-element.js';
13
+ import { styles } from './nile-executive-summary.css.js';
14
+ /**
15
+ * Nile Executive Summary component.
16
+ *
17
+ * @tag nile-executive-summary
18
+ *
19
+ * @summary A self-contained drawer component with a built-in trigger button.
20
+ * Place it anywhere — clicking the button slides in a right-side drawer that
21
+ * displays summary content (plain text or HTML) with a typewriter animation.
22
+ * Configured entirely via the `config` JS property.
23
+ *
24
+ * @event nile-show - Emitted when the drawer starts opening.
25
+ * @event nile-after-show - Emitted when the drawer is fully open and typing begins.
26
+ * @event nile-hide - Emitted when the drawer starts closing.
27
+ * @event nile-after-hide - Emitted when the drawer is fully closed.
28
+ * @event nile-typing-complete - Emitted when the typewriter animation finishes.
29
+ * @event nile-init - Emitted when the component connects to the DOM.
30
+ * @event nile-destroy - Emitted when the component disconnects from the DOM.
31
+ *
32
+ * @csspart base - Root wrapper (contains trigger + drawer).
33
+ * @csspart trigger - The built-in button that opens the drawer.
34
+ * @csspart overlay - The dim overlay behind the panel.
35
+ * @csspart panel - The drawer panel.
36
+ * @csspart header - The drawer header.
37
+ * @csspart title - The drawer title.
38
+ * @csspart close - The close button.
39
+ * @csspart body - The drawer body containing the typed summary.
40
+ *
41
+ * @cssproperty --executive-summary-size - Drawer width. Default: 480px.
42
+ * @cssproperty --executive-summary-header-spacing - Header padding. Default: 16px 20px.
43
+ * @cssproperty --executive-summary-body-spacing - Body padding. Default: 20px.
44
+ * @cssproperty --executive-summary-z-index - Z-index of the drawer. Default: 700.
45
+ * @cssproperty --executive-summary-overlay-color - Overlay background colour.
46
+ * @cssproperty --executive-summary-btn-bg - Trigger button background.
47
+ * @cssproperty --executive-summary-btn-color - Trigger button text colour.
48
+ * @cssproperty --executive-summary-btn-radius - Trigger button border-radius.
49
+ */
50
+ let NileExecutiveSummary = class NileExecutiveSummary extends NileElement {
51
+ constructor() {
52
+ super(...arguments);
53
+ this.typingTimer = null;
54
+ this.originalTrigger = null;
55
+ this.scrollLockActive = false;
56
+ /**
57
+ * Full configuration. Assign via JavaScript — all fields are optional except `summary`.
58
+ *
59
+ * @example
60
+ * ```js
61
+ * el.config = {
62
+ * summary: '<p>Q1 revenue grew <strong>23%</strong>.</p>',
63
+ * buttonLabel: 'View Summary',
64
+ * drawerLabel: 'AI Executive Summary',
65
+ * typingSpeed: 20,
66
+ * };
67
+ * ```
68
+ */
69
+ this.config = null;
70
+ /**
71
+ * Whether the drawer is currently open.
72
+ * Can be set programmatically; use `show()` / `hide()` for animated transitions.
73
+ */
74
+ this.open = false;
75
+ /** @internal Characters revealed so far during typing. */
76
+ this.displayedText = '';
77
+ /** @internal Whether typing is still in progress (drives cursor visibility). */
78
+ this.isTyping = false;
79
+ }
80
+ // ─── Lifecycle ─────────────────────────────────────────────────────────────
81
+ connectedCallback() {
82
+ super.connectedCallback();
83
+ this.keyDownHandler = this.handleKeyDown.bind(this);
84
+ this.emit('nile-init');
85
+ }
86
+ firstUpdated() {
87
+ this.backdrop.hidden = !this.open;
88
+ }
89
+ disconnectedCallback() {
90
+ super.disconnectedCallback();
91
+ this.stopTyping();
92
+ this.removeKeyListener();
93
+ this.unlockScroll();
94
+ this.emit('nile-destroy');
95
+ }
96
+ // ─── Keyboard / scroll ─────────────────────────────────────────────────────
97
+ addKeyListener() {
98
+ if (this.config?.closeOnEscape ?? true) {
99
+ document.addEventListener('keydown', this.keyDownHandler);
100
+ }
101
+ }
102
+ removeKeyListener() {
103
+ document.removeEventListener('keydown', this.keyDownHandler);
104
+ }
105
+ handleKeyDown(e) {
106
+ if (this.open && e.key === 'Escape') {
107
+ e.stopPropagation();
108
+ this.requestClose('keyboard');
109
+ }
110
+ }
111
+ lockScroll() {
112
+ if (!this.scrollLockActive) {
113
+ document.body.style.overflow = 'hidden';
114
+ this.scrollLockActive = true;
115
+ }
116
+ }
117
+ unlockScroll() {
118
+ if (this.scrollLockActive) {
119
+ document.body.style.overflow = '';
120
+ this.scrollLockActive = false;
121
+ }
122
+ }
123
+ // ─── Animation helpers ──────────────────────────────────────────────────────
124
+ async animatePanel(show) {
125
+ const from = show ? { opacity: '0', translate: '100%' } : { opacity: '1', translate: '0' };
126
+ const to = show ? { opacity: '1', translate: '0' } : { opacity: '0', translate: '100%' };
127
+ await this.panel.animate([from, to], { duration: 250, easing: 'ease', fill: 'forwards' }).finished;
128
+ }
129
+ async animateOverlay(show) {
130
+ const from = show ? { opacity: '0' } : { opacity: '1' };
131
+ const to = show ? { opacity: '1' } : { opacity: '0' };
132
+ await this.overlay.animate([from, to], { duration: 250, fill: 'forwards' }).finished;
133
+ }
134
+ // ─── Close request ──────────────────────────────────────────────────────────
135
+ requestClose(source) {
136
+ const preventOverlayClose = this.config?.preventOverlayClose ?? false;
137
+ if ((source === 'overlay' || source === 'keyboard') && preventOverlayClose)
138
+ return;
139
+ const ev = this.emit('nile-request-close', { cancelable: true, detail: { source } });
140
+ if (ev.defaultPrevented) {
141
+ // Deny-close jiggle
142
+ this.panel.animate([{ scale: '1' }, { scale: '1.01' }, { scale: '1' }], { duration: 250 });
143
+ return;
144
+ }
145
+ this.hide();
146
+ }
147
+ // ─── Typing animation ───────────────────────────────────────────────────────
148
+ startTyping() {
149
+ this.stopTyping();
150
+ this.displayedText = '';
151
+ this.isTyping = true;
152
+ const text = this.config?.summary ?? '';
153
+ const speed = this.config?.typingSpeed ?? 30;
154
+ let index = 0;
155
+ const tick = () => {
156
+ if (index < text.length) {
157
+ this.displayedText += text[index++];
158
+ this.typingTimer = setTimeout(tick, speed);
159
+ }
160
+ else {
161
+ this.isTyping = false;
162
+ this.emit('nile-typing-complete');
163
+ }
164
+ };
165
+ tick();
166
+ }
167
+ stopTyping() {
168
+ if (this.typingTimer !== null) {
169
+ clearTimeout(this.typingTimer);
170
+ this.typingTimer = null;
171
+ }
172
+ this.isTyping = false;
173
+ }
174
+ // ─── Open / close ───────────────────────────────────────────────────────────
175
+ /** Opens the drawer with animation. Resolves when the animation completes. */
176
+ async show() {
177
+ if (this.open)
178
+ return;
179
+ this.open = true;
180
+ this.emit('nile-show');
181
+ this.addKeyListener();
182
+ this.lockScroll();
183
+ this.originalTrigger = document.activeElement;
184
+ this.backdrop.hidden = false;
185
+ await Promise.all([this.animatePanel(true), this.animateOverlay(true)]);
186
+ requestAnimationFrame(() => this.panel.focus({ preventScroll: true }));
187
+ this.emit('nile-after-show');
188
+ this.startTyping();
189
+ }
190
+ /** Closes the drawer with animation. Resolves when the animation completes. */
191
+ async hide() {
192
+ if (!this.open)
193
+ return;
194
+ this.open = false;
195
+ this.emit('nile-hide');
196
+ this.removeKeyListener();
197
+ this.stopTyping();
198
+ this.displayedText = '';
199
+ this.unlockScroll();
200
+ await Promise.all([this.animatePanel(false), this.animateOverlay(false)]);
201
+ this.backdrop.hidden = true;
202
+ // Reset animation fill state for next open
203
+ this.panel.getAnimations().forEach(a => a.cancel());
204
+ this.overlay.getAnimations().forEach(a => a.cancel());
205
+ // Restore focus to original trigger
206
+ const trigger = this.originalTrigger;
207
+ if (typeof trigger?.focus === 'function')
208
+ setTimeout(() => trigger.focus());
209
+ this.emit('nile-after-hide');
210
+ }
211
+ // ─── Render ─────────────────────────────────────────────────────────────────
212
+ render() {
213
+ const buttonLabel = this.config?.buttonLabel ?? 'Executive Summary';
214
+ const buttonVariant = this.config?.buttonVariant ?? 'primary';
215
+ const drawerLabel = this.config?.drawerLabel ?? 'Executive Summary';
216
+ return html `
217
+ <div part="base">
218
+
219
+ <!-- Built-in trigger — renders wherever the element is placed -->
220
+ <nile-button
221
+ part="trigger"
222
+ type="button"
223
+ variant=${buttonVariant}
224
+ aria-haspopup="dialog"
225
+ aria-expanded=${this.open ? 'true' : 'false'}
226
+ @click=${this.show}
227
+ >${buttonLabel}</nile-button>
228
+
229
+ <!-- Fixed backdrop: overlay + panel -->
230
+ <div class="es-backdrop">
231
+
232
+ <div
233
+ part="overlay"
234
+ class="es-overlay"
235
+ @click=${() => this.requestClose('overlay')}
236
+ tabindex="-1"
237
+ ></div>
238
+
239
+ <div
240
+ part="panel"
241
+ class="es-panel"
242
+ role="dialog"
243
+ aria-modal="true"
244
+ aria-hidden=${this.open ? 'false' : 'true'}
245
+ aria-labelledby="es-title"
246
+ tabindex="0"
247
+ >
248
+ <header part="header" class="es-header">
249
+ <h2 part="title" class="es-title" id="es-title">${drawerLabel}</h2>
250
+
251
+ <button
252
+ part="close"
253
+ class="es-close"
254
+ type="button"
255
+ aria-label="Close"
256
+ @click=${() => this.requestClose('close-button')}
257
+ >
258
+ <!-- X icon (inline SVG — no icon lib dependency) -->
259
+ <svg viewBox="0 0 24 24" aria-hidden="true">
260
+ <line x1="18" y1="6" x2="6" y2="18"/>
261
+ <line x1="6" y1="6" x2="18" y2="18"/>
262
+ </svg>
263
+ </button>
264
+ </header>
265
+
266
+ <div part="body" class="es-body">
267
+ <div
268
+ class=${classMap({
269
+ 'es-typed-text': true,
270
+ 'es-typed-text--typing': this.isTyping,
271
+ })}
272
+ >${unsafeHTML(this.displayedText)}</div>
273
+ <slot></slot>
274
+ </div>
275
+
276
+ </div>
277
+ </div>
278
+ </div>
279
+ `;
280
+ }
281
+ };
282
+ NileExecutiveSummary.styles = styles;
283
+ __decorate([
284
+ query('.es-backdrop')
285
+ ], NileExecutiveSummary.prototype, "backdrop", void 0);
286
+ __decorate([
287
+ query('.es-panel')
288
+ ], NileExecutiveSummary.prototype, "panel", void 0);
289
+ __decorate([
290
+ query('.es-overlay')
291
+ ], NileExecutiveSummary.prototype, "overlay", void 0);
292
+ __decorate([
293
+ property({ type: Object })
294
+ ], NileExecutiveSummary.prototype, "config", void 0);
295
+ __decorate([
296
+ property({ type: Boolean, reflect: true })
297
+ ], NileExecutiveSummary.prototype, "open", void 0);
298
+ __decorate([
299
+ state()
300
+ ], NileExecutiveSummary.prototype, "displayedText", void 0);
301
+ __decorate([
302
+ state()
303
+ ], NileExecutiveSummary.prototype, "isTyping", void 0);
304
+ NileExecutiveSummary = __decorate([
305
+ customElement('nile-executive-summary')
306
+ ], NileExecutiveSummary);
307
+ export { NileExecutiveSummary };
308
+ //# sourceMappingURL=nile-executive-summary.js.map
@@ -1 +1,6 @@
1
+ /**
2
+ * Global tooltip styles — injected once into document.head so the tooltip
3
+ * element on document.body is styled independently of the shadow DOM.
4
+ */
5
+ export declare const tooltipCss = "\n .nile-kpi-tooltip {\n position: fixed;\n display: none;\n transform: translate(-50%, calc(-100% - 10px));\n background: #1D2939;\n color: #fff;\n font-family: system-ui, sans-serif;\n font-size: 12px;\n font-weight: 500;\n line-height: 1.4;\n padding: 5px 10px;\n border-radius: 6px;\n pointer-events: none;\n white-space: nowrap;\n z-index: 2147483647;\n box-shadow: 0 4px 12px rgba(16, 24, 40, 0.22);\n }\n\n .nile-kpi-tooltip::after {\n content: '';\n position: absolute;\n top: 100%;\n left: 50%;\n transform: translateX(-50%);\n border: 5px solid transparent;\n border-top-color:rgb(54, 74, 99);\n }\n";
1
6
  export declare const styles: import("lit").CSSResult;
@@ -1,4 +1,37 @@
1
1
  import { css } from 'lit';
2
+ /**
3
+ * Global tooltip styles — injected once into document.head so the tooltip
4
+ * element on document.body is styled independently of the shadow DOM.
5
+ */
6
+ export const tooltipCss = `
7
+ .nile-kpi-tooltip {
8
+ position: fixed;
9
+ display: none;
10
+ transform: translate(-50%, calc(-100% - 10px));
11
+ background: #1D2939;
12
+ color: #fff;
13
+ font-family: system-ui, sans-serif;
14
+ font-size: 12px;
15
+ font-weight: 500;
16
+ line-height: 1.4;
17
+ padding: 5px 10px;
18
+ border-radius: 6px;
19
+ pointer-events: none;
20
+ white-space: nowrap;
21
+ z-index: 2147483647;
22
+ box-shadow: 0 4px 12px rgba(16, 24, 40, 0.22);
23
+ }
24
+
25
+ .nile-kpi-tooltip::after {
26
+ content: '';
27
+ position: absolute;
28
+ top: 100%;
29
+ left: 50%;
30
+ transform: translateX(-50%);
31
+ border: 5px solid transparent;
32
+ border-top-color:rgb(54, 74, 99);
33
+ }
34
+ `;
2
35
  export const styles = css `
3
36
  :host {
4
37
  display: flex;
@@ -29,7 +62,6 @@ export const styles = css `
29
62
 
30
63
  .kpi {
31
64
  flex: 1 1 auto;
32
-
33
65
  display: flex;
34
66
  flex-direction: column;
35
67
  gap: var(--nile-spacing-md, var(--ng-spacing-md));
@@ -59,6 +91,7 @@ export const styles = css `
59
91
  font-weight: var(--nile-font-weight-semi-bold, var(--ng-font-weight-semibold));
60
92
  color: var(--nile-colors-dark-900, var(--ng-colors-text-primary-900));
61
93
  line-height: 1.2;
94
+ cursor: default;
62
95
  }
63
96
 
64
97
  .kpi-prefix,
@@ -51,6 +51,8 @@ export declare class NileKpiChart extends NileElement {
51
51
  private sparklineChart;
52
52
  private gaugeChart;
53
53
  private resizeObserver;
54
+ /** Tooltip element on document.body — outside shadow DOM, never clipped. */
55
+ private _tipEl;
54
56
  private sparklineContainer;
55
57
  private gaugeContainer;
56
58
  /** Full configuration: `{ chart, aq }` (same convention as other Nile charts). */
@@ -93,8 +95,14 @@ export declare class NileKpiChart extends NileElement {
93
95
  * Set by nile-chart: skip host border/shadow (variant card/gauge) so the parent chart-card is the only frame.
94
96
  */
95
97
  embedInNileChart: boolean;
98
+ private _createTipEl;
99
+ private _showTip;
100
+ private _hideTip;
96
101
  private formatCssLength;
97
- /** Apply `{ chart, aq }` to individual properties. */
102
+ private parseNumericValue;
103
+ private formatTooltipNumber;
104
+ private inferSparklineTooltipScale;
105
+ private getTooltipContent;
98
106
  private applyConfig;
99
107
  connectedCallback(): void;
100
108
  disconnectedCallback(): void;
@@ -102,6 +110,8 @@ export declare class NileKpiChart extends NileElement {
102
110
  protected updated(changedProperties: PropertyValues): void;
103
111
  private syncSparklineChartSize;
104
112
  private setupResizeObserver;
113
+ private _onSparklineMouseMove;
114
+ private _onSparklineMouseLeave;
105
115
  private buildSparklineOptions;
106
116
  private buildGaugeOptions;
107
117
  private initSparkline;
@@ -109,8 +119,10 @@ export declare class NileKpiChart extends NileElement {
109
119
  private destroySparkline;
110
120
  private destroyGauge;
111
121
  private destroyCharts;
122
+ private _onValueEnter;
123
+ private _onGaugeEnter;
124
+ private _onTipLeave;
112
125
  private renderTrend;
113
- private renderContent;
114
126
  render(): TemplateResult;
115
127
  }
116
128
  declare global {
@@ -3,7 +3,18 @@ import { customElement, property, query } from 'lit/decorators.js';
3
3
  import { html, nothing } from 'lit';
4
4
  import NileElement from '../internal/nile-element.js';
5
5
  import { getHighcharts } from '../internal/highcharts-provider.js';
6
- import { styles } from './nile-kpi-chart.css.js';
6
+ import { styles, tooltipCss } from './nile-kpi-chart.css.js';
7
+ // Inject tooltip styles into document.head once per page load.
8
+ let _tooltipStylesInjected = false;
9
+ function ensureTooltipStyles() {
10
+ if (_tooltipStylesInjected || typeof document === 'undefined')
11
+ return;
12
+ const style = document.createElement('style');
13
+ style.dataset['nilekpichart'] = '';
14
+ style.textContent = tooltipCss;
15
+ document.head.appendChild(style);
16
+ _tooltipStylesInjected = true;
17
+ }
7
18
  let NileKpiChart = class NileKpiChart extends NileElement {
8
19
  constructor() {
9
20
  super(...arguments);
@@ -11,6 +22,8 @@ let NileKpiChart = class NileKpiChart extends NileElement {
11
22
  this.sparklineChart = null;
12
23
  this.gaugeChart = null;
13
24
  this.resizeObserver = null;
25
+ /** Tooltip element on document.body — outside shadow DOM, never clipped. */
26
+ this._tipEl = null;
14
27
  /** Full configuration: `{ chart, aq }` (same convention as other Nile charts). */
15
28
  this.config = null;
16
29
  /** Display variant: default (flat), card (bordered container), gauge (Highcharts solid gauge). */
@@ -51,7 +64,70 @@ let NileKpiChart = class NileKpiChart extends NileElement {
51
64
  * Set by nile-chart: skip host border/shadow (variant card/gauge) so the parent chart-card is the only frame.
52
65
  */
53
66
  this.embedInNileChart = false;
67
+ // ── Sparkline mousemove tooltip ──────────────────────────────────────────
68
+ this._onSparklineMouseMove = (e) => {
69
+ const chart = this.sparklineChart;
70
+ if (!chart || !this.sparklineContainer)
71
+ return;
72
+ const series = chart.series[0];
73
+ if (!series?.points?.length)
74
+ return;
75
+ const rect = this.sparklineContainer.getBoundingClientRect();
76
+ const mouseXInPlot = e.clientX - rect.left - (chart.plotLeft ?? 0);
77
+ // Snap to nearest point by plotX
78
+ let nearest = series.points[0];
79
+ let minDist = Infinity;
80
+ for (const p of series.points) {
81
+ const dist = Math.abs((p.plotX ?? 0) - mouseXInPlot);
82
+ if (dist < minDist) {
83
+ minDist = dist;
84
+ nearest = p;
85
+ }
86
+ }
87
+ const scale = this.inferSparklineTooltipScale();
88
+ const text = this.getTooltipContent((nearest.y ?? 0) * scale);
89
+ const tipX = rect.left + (chart.plotLeft ?? 0) + (nearest.plotX ?? 0);
90
+ const tipY = rect.top + (chart.plotTop ?? 0) + (nearest.plotY ?? 0);
91
+ this._showTip(text, tipX, tipY);
92
+ };
93
+ this._onSparklineMouseLeave = () => {
94
+ this._hideTip();
95
+ };
96
+ // ── Value / Gauge hover handlers ─────────────────────────────────────────
97
+ this._onValueEnter = (e) => {
98
+ const rect = e.currentTarget.getBoundingClientRect();
99
+ this._showTip(this.getTooltipContent(), rect.left + rect.width / 2, rect.top);
100
+ };
101
+ this._onGaugeEnter = (e) => {
102
+ const rect = e.currentTarget.getBoundingClientRect();
103
+ this._showTip(this.getTooltipContent(), rect.left + rect.width / 2, rect.top + rect.height / 2);
104
+ };
105
+ this._onTipLeave = () => {
106
+ this._hideTip();
107
+ };
108
+ }
109
+ // ── Tooltip ──────────────────────────────────────────────────────────────
110
+ _createTipEl() {
111
+ if (this._tipEl)
112
+ return;
113
+ const el = document.createElement('div');
114
+ el.className = 'nile-kpi-tooltip';
115
+ document.body.appendChild(el);
116
+ this._tipEl = el;
117
+ }
118
+ _showTip(text, x, y) {
119
+ if (!this._tipEl)
120
+ return;
121
+ this._tipEl.textContent = text;
122
+ this._tipEl.style.left = `${x}px`;
123
+ this._tipEl.style.top = `${y}px`;
124
+ this._tipEl.style.display = 'block';
125
+ }
126
+ _hideTip() {
127
+ if (this._tipEl)
128
+ this._tipEl.style.display = 'none';
54
129
  }
130
+ // ── Formatting helpers ───────────────────────────────────────────────────
55
131
  formatCssLength(value) {
56
132
  if (value == null)
57
133
  return null;
@@ -65,7 +141,52 @@ let NileKpiChart = class NileKpiChart extends NileElement {
65
141
  return `${s}px`;
66
142
  return s;
67
143
  }
68
- /** Apply `{ chart, aq }` to individual properties. */
144
+ parseNumericValue(v) {
145
+ if (typeof v === 'number')
146
+ return Number.isFinite(v) ? v : null;
147
+ if (typeof v !== 'string')
148
+ return null;
149
+ const cleaned = v.replace(/,/g, '').trim();
150
+ if (!cleaned)
151
+ return null;
152
+ const n = Number(cleaned);
153
+ return Number.isFinite(n) ? n : null;
154
+ }
155
+ formatTooltipNumber(n) {
156
+ const maxFractionDigits = Number.isInteger(n) ? 0 : 6;
157
+ return new Intl.NumberFormat(undefined, {
158
+ useGrouping: true,
159
+ minimumFractionDigits: 0,
160
+ maximumFractionDigits: maxFractionDigits,
161
+ }).format(n);
162
+ }
163
+ inferSparklineTooltipScale() {
164
+ if (!this.sparkline?.length)
165
+ return 1;
166
+ const main = this.parseNumericValue(this.value);
167
+ const last = this.sparkline[this.sparkline.length - 1];
168
+ if (main == null || !Number.isFinite(last) || last === 0)
169
+ return 1;
170
+ const ratio = Math.abs(main / last);
171
+ const candidates = [1000, 1000000];
172
+ for (const c of candidates) {
173
+ if (Math.abs(ratio - c) / c < 0.02)
174
+ return c;
175
+ }
176
+ return 1;
177
+ }
178
+ getTooltipContent(overrideNumeric) {
179
+ const prefix = this.prefix ?? '';
180
+ const suffix = this.suffix ?? '';
181
+ const numeric = overrideNumeric ?? this.parseNumericValue(this.value) ??
182
+ (this.variant === 'gauge' ? this.gaugeValue : null) ??
183
+ (this.sparkline.length ? this.sparkline[this.sparkline.length - 1] : null);
184
+ const valueText = numeric == null
185
+ ? String(this.value ?? '').trim()
186
+ : this.formatTooltipNumber(numeric);
187
+ return `${prefix}${valueText}${suffix}`.trim();
188
+ }
189
+ // ── Config ───────────────────────────────────────────────────────────────
69
190
  applyConfig(cfg) {
70
191
  const { chart: c, aq } = cfg;
71
192
  if (c) {
@@ -106,7 +227,6 @@ let NileKpiChart = class NileKpiChart extends NileElement {
106
227
  if ('height' in c) {
107
228
  const h = this.formatCssLength(c.height);
108
229
  if (h) {
109
- /* min-height only: host can grow with content; sparkline shrinks via flex + setSize */
110
230
  this.style.minHeight = h;
111
231
  this.style.removeProperty('height');
112
232
  this.style.removeProperty('max-height');
@@ -125,13 +245,19 @@ let NileKpiChart = class NileKpiChart extends NileElement {
125
245
  this.description = aq.chartSubtitle;
126
246
  }
127
247
  }
248
+ // ── Lifecycle ────────────────────────────────────────────────────────────
128
249
  connectedCallback() {
129
250
  super.connectedCallback();
251
+ ensureTooltipStyles();
252
+ this._createTipEl();
130
253
  if (this.config)
131
254
  this.applyConfig(this.config);
132
255
  }
133
256
  disconnectedCallback() {
134
257
  super.disconnectedCallback();
258
+ this._hideTip();
259
+ this._tipEl?.remove();
260
+ this._tipEl = null;
135
261
  this.destroyCharts();
136
262
  this.resizeObserver?.disconnect();
137
263
  this.resizeObserver = null;
@@ -166,6 +292,7 @@ let NileKpiChart = class NileKpiChart extends NileElement {
166
292
  }
167
293
  }
168
294
  }
295
+ // ── Chart sizing ─────────────────────────────────────────────────────────
169
296
  syncSparklineChartSize() {
170
297
  if (!this.sparklineChart || !this.sparklineContainer)
171
298
  return;
@@ -183,6 +310,7 @@ let NileKpiChart = class NileKpiChart extends NileElement {
183
310
  if (this.gaugeContainer)
184
311
  this.resizeObserver.observe(this.gaugeContainer);
185
312
  }
313
+ // ── Chart options ────────────────────────────────────────────────────────
186
314
  buildSparklineOptions() {
187
315
  const brandColor = this.sparklineColor || '#005EA6';
188
316
  const defaults = {
@@ -278,6 +406,7 @@ let NileKpiChart = class NileKpiChart extends NileElement {
278
406
  ...this.options,
279
407
  };
280
408
  }
409
+ // ── Chart init/destroy ───────────────────────────────────────────────────
281
410
  async initSparkline() {
282
411
  if (!this.sparkline.length || !this.sparklineContainer)
283
412
  return;
@@ -286,6 +415,8 @@ let NileKpiChart = class NileKpiChart extends NileElement {
286
415
  this.destroySparkline();
287
416
  this.sparklineChart = this._hc.chart(this.sparklineContainer, this.buildSparklineOptions());
288
417
  requestAnimationFrame(() => this.syncSparklineChartSize());
418
+ this.sparklineContainer.addEventListener('mousemove', this._onSparklineMouseMove);
419
+ this.sparklineContainer.addEventListener('mouseleave', this._onSparklineMouseLeave);
289
420
  this.emit('nile-chart-ready', { chart: this.sparklineChart });
290
421
  }
291
422
  async initGauge() {
@@ -298,6 +429,10 @@ let NileKpiChart = class NileKpiChart extends NileElement {
298
429
  this.emit('nile-chart-ready', { chart: this.gaugeChart });
299
430
  }
300
431
  destroySparkline() {
432
+ if (this.sparklineContainer) {
433
+ this.sparklineContainer.removeEventListener('mousemove', this._onSparklineMouseMove);
434
+ this.sparklineContainer.removeEventListener('mouseleave', this._onSparklineMouseLeave);
435
+ }
301
436
  if (this.sparklineChart) {
302
437
  this.sparklineChart.destroy();
303
438
  this.sparklineChart = null;
@@ -313,6 +448,7 @@ let NileKpiChart = class NileKpiChart extends NileElement {
313
448
  this.destroySparkline();
314
449
  this.destroyGauge();
315
450
  }
451
+ // ── Render ───────────────────────────────────────────────────────────────
316
452
  renderTrend() {
317
453
  if (this.trendValue === null)
318
454
  return nothing;
@@ -331,18 +467,37 @@ let NileKpiChart = class NileKpiChart extends NileElement {
331
467
  </span>
332
468
  `;
333
469
  }
334
- renderContent() {
470
+ render() {
471
+ if (this.loading) {
472
+ return html `<div class="chart-loading">Loading...</div>`;
473
+ }
335
474
  const isGauge = this.variant === 'gauge';
336
475
  return html `
337
476
  <div class="kpi ${isGauge ? 'kpi--gauge' : ''}">
338
477
  ${this.label ? html `<p class="kpi-label">${this.label}</p>` : nothing}
339
478
 
340
- ${isGauge ? html `<div class="kpi-gauge-container"></div>` : nothing}
479
+ ${isGauge
480
+ ? html `
481
+ <div
482
+ class="kpi-gauge-container"
483
+ @mouseenter=${this._onGaugeEnter}
484
+ @mouseleave=${this._onTipLeave}
485
+ ></div>
486
+ `
487
+ : nothing}
341
488
 
342
489
  <div class="kpi-value-row">
343
- <h2 class="kpi-value">
344
- ${this.prefix ? html `<span class="kpi-prefix">${this.prefix}</span>` : nothing}${this.value}${this.suffix ? html `<span class="kpi-suffix">${this.suffix}</span>` : nothing}
345
- </h2>
490
+ ${!isGauge
491
+ ? html `
492
+ <h2
493
+ class="kpi-value"
494
+ @mouseenter=${this._onValueEnter}
495
+ @mouseleave=${this._onTipLeave}
496
+ >
497
+ ${this.prefix ? html `<span class="kpi-prefix">${this.prefix}</span>` : nothing}${this.value}${this.suffix ? html `<span class="kpi-suffix">${this.suffix}</span>` : nothing}
498
+ </h2>
499
+ `
500
+ : nothing}
346
501
  ${!isGauge ? this.renderTrend() : nothing}
347
502
  </div>
348
503
 
@@ -356,13 +511,6 @@ let NileKpiChart = class NileKpiChart extends NileElement {
356
511
  </div>
357
512
  `;
358
513
  }
359
- render() {
360
- if (this.loading) {
361
- return html `<div class="chart-loading">Loading...</div>`;
362
- }
363
- /* Same DOM as inside nile-chart: one surface; card/gauge chrome is on :host when standalone. */
364
- return this.renderContent();
365
- }
366
514
  };
367
515
  NileKpiChart.styles = styles;
368
516
  __decorate([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aquera/nile-visualization",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "A visualization Library for the Nile Design System",
5
5
  "license": "MIT",
6
6
  "author": "Aquera Inc",
@@ -42,7 +42,8 @@
42
42
  "./nile-kpi-chart": "./dist/src/nile-kpi-chart/index.js",
43
43
  "./nile-chart": "./dist/src/nile-chart/index.js",
44
44
  "./nile-widget-viewer": "./dist/src/nile-widget-viewer/index.js",
45
- "./nile-dashboard-viewer": "./dist/src/nile-dashboard-viewer/index.js"
45
+ "./nile-dashboard-viewer": "./dist/src/nile-dashboard-viewer/index.js",
46
+ "./nile-executive-summary": "./dist/src/nile-executive-summary/index.js"
46
47
  },
47
48
  "files": [
48
49
  "dist/src/**/*.js",