@aquera/nile-visualization 1.8.0 → 2.0.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.
@@ -2,9 +2,17 @@ import { __decorate } from "tslib";
2
2
  import { customElement, property, query, state } from 'lit/decorators.js';
3
3
  import { html, nothing } from 'lit';
4
4
  import NileElement from '../internal/nile-element.js';
5
- import { styles } from './nile-chart.css.js';
5
+ import { styles, tooltipCss } from './nile-chart.css.js';
6
6
  import { nileChartConfig } from './nile-chart-config-builder.js';
7
7
  import { convertConfig } from '../internal/chart-adapters.js';
8
+ import { deepMerge } from '../internal/utils.js';
9
+ import { initNileChartExporting, getHighcharts } from '../internal/highcharts-provider.js';
10
+ // Start loading exporting/offline-exporting/export-data at module import time.
11
+ // `chart.fullscreen` is attached via a `beforeRender` hook installed by the exporting module's
12
+ // compose step, so if a chart is constructed before the module applies, that instance will have
13
+ // no `fullscreen`. Kicking the load off here gives it a head start; handlers still await it
14
+ // before invoking methods, and viewFullscreen falls back to the DOM Fullscreen API.
15
+ initNileChartExporting();
8
16
  import '../nile-bar-chart/index.js';
9
17
  import '../nile-pie-chart/index.js';
10
18
  import '../nile-trendline-chart/index.js';
@@ -100,9 +108,20 @@ function chartTypeLabel(type) {
100
108
  .replace(/-/g, ' ')
101
109
  .replace(/\b\w/g, ch => ch.toUpperCase());
102
110
  }
111
+ let _headerTooltipStylesInjected = false;
112
+ function ensureHeaderTooltipStyles() {
113
+ if (_headerTooltipStylesInjected || typeof document === 'undefined')
114
+ return;
115
+ const style = document.createElement('style');
116
+ style.dataset['nilechart'] = '';
117
+ style.textContent = tooltipCss;
118
+ document.head.appendChild(style);
119
+ _headerTooltipStylesInjected = true;
120
+ }
103
121
  let NileChart = class NileChart extends NileElement {
104
122
  constructor() {
105
123
  super(...arguments);
124
+ this._headerTipEl = null;
106
125
  /** Full chart configuration. Accepts flat NileChartConfig or separated { chart, aq } input. */
107
126
  this.config = null;
108
127
  /**
@@ -122,27 +141,60 @@ let NileChart = class NileChart extends NileElement {
122
141
  */
123
142
  this.chartTypeAttr = '';
124
143
  /** Summary/insight text — shown as the AI panel's opening message when the chat is opened. */
144
+ this.summary = '';
125
145
  /**
126
- * When set, fills `chart.type` if the config omits it (same values as `chart.type`, e.g. `stacked`, `pie`).
127
- * Usage: `<nile-chart chart-type="pie" />` plus `config.chart` with series data only.
146
+ * Controls which items appear in the actions menu. All items are opt-in
147
+ * only items explicitly set to `true` are shown. Merged with (and takes
148
+ * priority over) `config.menu`.
149
+ *
150
+ * @example
151
+ * // PNG + CSV only
152
+ * chart.menu = { enabled: true, downloadPng: true, downloadCsv: true }
128
153
  */
129
- /** Summary/insight text — shown as the AI panel's opening message when the chat is opened. */
130
- this.summary = '';
154
+ this.menu = null;
131
155
  this.activeType = null;
132
156
  this.activeConfig = null;
133
157
  this.menuOpen = false;
134
158
  this.chatOpen = false;
159
+ this._hcChart = null;
135
160
  /** True when elements are projected into the `header` slot (default title/subtitle hidden). */
136
161
  this.hasHeaderSlotContent = false;
137
162
  /** True when elements are projected into `header-actions` (used to show the header row). */
138
163
  this.hasHeaderActionsSlot = false;
139
164
  this.handleOutsideClick = (e) => {
140
- if (!e.composedPath().includes(this)) {
165
+ const path = e.composedPath();
166
+ if (!path.includes(this)) {
141
167
  this.menuOpen = false;
142
168
  this.chatOpen = false;
169
+ return;
143
170
  }
171
+ if (this.chatOpen) {
172
+ const root = this.renderRoot;
173
+ const panel = root.querySelector('.nile-ai-panel-overlay');
174
+ const trigger = root.querySelector('.nile-ai-trigger');
175
+ if (!path.some((n) => n === panel || n === trigger)) {
176
+ this.chatOpen = false;
177
+ }
178
+ }
179
+ };
180
+ this.handleChartReady = (e) => {
181
+ this._hcChart = e.detail?.chart ?? null;
144
182
  };
145
183
  this.resolvedConfig = null;
184
+ this._onTitleEnter = (e) => {
185
+ const el = e.currentTarget;
186
+ if (el.scrollWidth <= el.clientWidth)
187
+ return;
188
+ const rect = el.getBoundingClientRect();
189
+ this._showHeaderTip(el.textContent ?? '', rect.left + rect.width / 2, rect.top);
190
+ };
191
+ this._onSubtitleEnter = (e) => {
192
+ const el = e.currentTarget;
193
+ if (el.scrollWidth <= el.clientWidth)
194
+ return;
195
+ const rect = el.getBoundingClientRect();
196
+ this._showHeaderTip(el.textContent ?? '', rect.left + rect.width / 2, rect.top);
197
+ };
146
198
  }
147
199
  get effectiveSummary() {
148
200
  return this.resolvedConfig?.summary ?? this.summary;
@@ -153,7 +205,14 @@ let NileChart = class NileChart extends NileElement {
153
205
  }
154
206
  connectedCallback() {
155
207
  super.connectedCallback();
208
+ ensureHeaderTooltipStyles();
209
+ const tip = document.createElement('div');
210
+ tip.className = 'nile-chart-header-tooltip';
211
+ document.body.appendChild(tip);
212
+ this._headerTipEl = tip;
156
213
  document.addEventListener('click', this.handleOutsideClick);
214
+ this.addEventListener('nile-chart-ready', this.handleChartReady);
215
+ initNileChartExporting();
157
216
  // Pick up config set before element upgrade (e.g. Angular ngAfterViewInit)
158
217
  if (this.config && !this.resolvedConfig) {
159
218
  this.resolvedConfig = this.resolveConfig(this.config);
@@ -164,6 +223,9 @@ let NileChart = class NileChart extends NileElement {
164
223
  disconnectedCallback() {
165
224
  super.disconnectedCallback();
166
225
  document.removeEventListener('click', this.handleOutsideClick);
226
+ this.removeEventListener('nile-chart-ready', this.handleChartReady);
227
+ this._headerTipEl?.remove();
228
+ this._headerTipEl = null;
167
229
  }
168
230
  mergeChartTypeFromAttr(chart) {
169
231
  const t = this.chartTypeAttr?.trim();
@@ -278,7 +340,8 @@ let NileChart = class NileChart extends NileElement {
278
340
  }
279
341
  shouldShowHeader() {
280
342
  const hasTitles = !!(this.headerTitle || this.headerSubtitle);
281
- const hasBuiltinActions = this.aiEnabled || (this.resolvedConfig?.switchableTypes?.length ?? 0) > 0;
343
+ const menuEnabled = this.resolvedConfig?.menu?.enabled === true || this.menu?.enabled === true;
344
+ const hasBuiltinActions = this.aiEnabled || (this.resolvedConfig?.switchableTypes?.length ?? 0) > 0 || menuEnabled;
282
345
  return (hasTitles ||
283
346
  this.hasHeaderSlotContent ||
284
347
  this.hasHeaderActionsSlot ||
@@ -286,36 +349,102 @@ let NileChart = class NileChart extends NileElement {
286
349
  this.lightDomHasSlot('header-actions') ||
287
350
  hasBuiltinActions);
288
351
  }
289
- renderTypeSwitcher() {
290
- const types = this.resolvedConfig?.switchableTypes;
291
- if (!types || types.length === 0)
352
+ buildExportingOptions() {
353
+ return { exporting: { enabled: true, buttons: { contextButton: { enabled: false } } } };
354
+ }
355
+ /** Ensures exporting modules are loaded and the chart's exporting/fullscreen instances exist. */
356
+ async ensureExporting() {
357
+ await initNileChartExporting();
358
+ const chart = this._hcChart;
359
+ if (chart && !chart.exporting) {
360
+ const HC = await getHighcharts();
361
+ HC.fireEvent(chart, 'afterInit');
362
+ }
363
+ }
364
+ async viewFullscreen() {
365
+ this.menuOpen = false;
366
+ await this.ensureExporting();
367
+ if (this._hcChart?.fullscreen?.open) {
368
+ this._hcChart.fullscreen.open();
369
+ return;
370
+ }
371
+ const target = this.shadowRoot?.querySelector('.nile-chart-card') ?? this;
372
+ target.requestFullscreen?.();
373
+ }
374
+ async printChart() {
375
+ this.menuOpen = false;
376
+ await this.ensureExporting();
377
+ this._hcChart?.print?.();
378
+ }
379
+ async exportChart(type) {
380
+ this.menuOpen = false;
381
+ await this.ensureExporting();
382
+ const filename = (this.headerTitle || 'chart').replace(/[^a-z0-9_-]+/gi, '_');
383
+ this._hcChart?.exportChartLocal?.({ type, filename, local: true });
384
+ }
385
+ async downloadCsv() {
386
+ this.menuOpen = false;
387
+ await this.ensureExporting();
388
+ this._hcChart?.downloadCSV?.();
389
+ }
390
+ renderActionsMenu() {
391
+ if (!this.resolvedConfig)
292
392
  return nothing;
393
+ const menuCfg = { ...(this.resolvedConfig.menu ?? {}), ...(this.menu ?? {}) };
394
+ if (menuCfg.enabled !== true)
395
+ return nothing;
396
+ const types = this.resolvedConfig.switchableTypes;
397
+ const allItems = [
398
+ ...(this.resolvedConfig.menuItems ?? []),
399
+ ...(menuCfg.items ?? []),
400
+ ];
401
+ const hasChart = !!this._hcChart;
402
+ const hasAnyItems = !!(types?.length || allItems.length);
293
403
  return html `
294
- <div class="chart-menu-anchor">
404
+ <div class="nile-chart-menu-anchor">
295
405
  <button
296
- class="chart-menu-trigger"
406
+ type="button"
407
+ class="nile-chart-menu-trigger"
297
408
  aria-haspopup="true"
298
409
  aria-expanded=${this.menuOpen ? 'true' : 'false'}
299
- aria-label="Change chart type"
300
- @click=${this.toggleMenu}
410
+ aria-label="Chart actions"
411
+ @click=${(e) => this.toggleMenu(e)}
301
412
  >
302
413
  <nile-glyph name="options" size="16"></nile-glyph>
303
414
  </button>
304
- ${this.menuOpen
305
- ? html `
306
- <div class="chart-menu-dropdown" role="menu" aria-label="Chart type">
307
- ${types.map(type => html `
308
- <button
309
- class="chart-menu-item ${type === this.activeType ? 'active' : ''}"
310
- role="menuitem"
311
- @click=${() => this.switchType(type)}
312
- >
313
- ${chartTypeLabel(type)} chart
314
- </button>
315
- `)}
316
- </div>
317
- `
318
- : nothing}
415
+ ${this.menuOpen && hasAnyItems ? html `
416
+ <div class="nile-chart-menu-dropdown" role="menu">
417
+ ${types?.length ? html `
418
+ ${types.map(type => html `
419
+ <button type="button" class="nile-chart-menu-item ${type === this.activeType ? 'active' : ''}" role="menuitem" @click=${() => this.switchType(type)}>
420
+ ${chartTypeLabel(type)}
421
+ </button>
422
+ `)}
423
+ ${allItems.length ? html `<div class="nile-chart-menu-separator"></div>` : nothing}
424
+ ` : nothing}
425
+ ${allItems.map(item => {
426
+ if (item.type === 'custom') {
427
+ return html `
428
+ ${item.divider ? html `<div class="nile-chart-menu-separator"></div>` : nothing}
429
+ <button type="button" class="nile-chart-menu-item" role="menuitem"
430
+ @click=${() => { this.menuOpen = false; this.emit('nile-menu-change', { id: item.id }); }}>
431
+ ${item.label}
432
+ </button>
433
+ `;
434
+ }
435
+ if (!hasChart)
436
+ return nothing;
437
+ return html `
438
+ ${item.divider ? html `<div class="nile-chart-menu-separator"></div>` : nothing}
439
+ ${item.fullscreen ? html `<button type="button" class="nile-chart-menu-item" role="menuitem" @click=${() => this.viewFullscreen()}>${item.label ?? 'Fullscreen'}</button>` : nothing}
440
+ ${item.print ? html `<button type="button" class="nile-chart-menu-item" role="menuitem" @click=${() => this.printChart()}>${item.label ?? 'Print'}</button>` : nothing}
441
+ ${item.downloadPng ? html `<button type="button" class="nile-chart-menu-item" role="menuitem" @click=${() => this.exportChart('image/png')}>${item.label ?? 'Download PNG'}</button>` : nothing}
442
+ ${item.downloadSvg ? html `<button type="button" class="nile-chart-menu-item" role="menuitem" @click=${() => this.exportChart('image/svg+xml')}>${item.label ?? 'Download SVG'}</button>` : nothing}
443
+ ${item.downloadCsv ? html `<button type="button" class="nile-chart-menu-item" role="menuitem" @click=${() => this.downloadCsv()}>${item.label ?? 'Download CSV'}</button>` : nothing}
444
+ `;
445
+ })}
446
+ </div>
447
+ ` : nothing}
319
448
  </div>
320
449
  `;
321
450
  }
@@ -324,16 +453,29 @@ let NileChart = class NileChart extends NileElement {
324
453
  return nothing;
325
454
  return html `
326
455
  <button
327
- class="ai-trigger ${this.chatOpen ? 'active' : ''}"
456
+ class="nile-ai-trigger ${this.chatOpen ? 'active' : ''}"
328
457
  aria-label="Ask AI about this chart"
329
458
  aria-expanded=${this.chatOpen ? 'true' : 'false'}
330
459
  @click=${this.toggleChat}
460
+ part="ai-trigger"
331
461
  >
332
462
  <nile-glyph name="smart-code" size="16"></nile-glyph>
333
463
  </button>
334
- <slot name="ai-trigger-after"></slot>
464
+ <slot name="nile-ai-trigger-after"></slot>
335
465
  `;
336
466
  }
467
+ _showHeaderTip(text, x, y) {
468
+ if (!this._headerTipEl)
469
+ return;
470
+ this._headerTipEl.textContent = text;
471
+ this._headerTipEl.style.left = `${x}px`;
472
+ this._headerTipEl.style.top = `${y}px`;
473
+ this._headerTipEl.style.display = 'block';
474
+ }
475
+ _hideHeaderTip() {
476
+ if (this._headerTipEl)
477
+ this._headerTipEl.style.display = 'none';
478
+ }
337
479
  renderHeader() {
338
480
  if (!this.shouldShowHeader())
339
481
  return nothing;
@@ -342,20 +484,20 @@ let NileChart = class NileChart extends NileElement {
342
484
  const showDefaultTitles = !this.hasHeaderSlotContent && !!(title || subtitle);
343
485
  const headerCompact = !subtitle?.trim();
344
486
  return html `
345
- <div class="chart-header ${headerCompact ? 'chart-header--compact' : ''}">
346
- <div class="chart-header-titles">
487
+ <div class="nile-chart-header ${headerCompact ? 'nile-chart-header--compact' : ''}">
488
+ <div class="nile-chart-header-titles">
347
489
  <slot name="header" @slotchange=${this.onHeaderSlotChange}></slot>
348
490
  ${showDefaultTitles
349
491
  ? html `
350
- ${title ? html `<p class="chart-header-title">${title}</p>` : nothing}
351
- ${subtitle ? html `<p class="chart-header-subtitle">${subtitle}</p>` : nothing}
492
+ ${title ? html `<p class="nile-chart-header-title" @mouseenter=${this._onTitleEnter} @mouseleave=${() => this._hideHeaderTip()}>${title}</p>` : nothing}
493
+ ${subtitle ? html `<p class="nile-chart-header-subtitle" @mouseenter=${this._onSubtitleEnter} @mouseleave=${() => this._hideHeaderTip()}>${subtitle}</p>` : nothing}
352
494
  `
353
495
  : nothing}
354
496
  </div>
355
- <div class="chart-header-actions">
497
+ <div class="nile-chart-header-actions">
356
498
  <slot name="header-actions" @slotchange=${this.onHeaderActionsSlotChange}></slot>
357
499
  ${this.renderAiTrigger()}
358
- ${this.renderTypeSwitcher()}
500
+ ${this.renderActionsMenu()}
359
501
  </div>
360
502
  </div>
361
503
  `;
@@ -371,7 +513,7 @@ let NileChart = class NileChart extends NileElement {
371
513
  const summaryMessage = summary;
372
514
  const welcomeMessage = summary ? '' : (aiConfig?.welcomeMessage ?? '');
373
515
  return html `
374
- <div class="ai-panel-overlay" ?data-open=${this.chatOpen}>
516
+ <div class="nile-ai-panel-overlay" ?data-open=${this.chatOpen}>
375
517
  <nile-ai-panel
376
518
  .placeholder=${aiConfig?.placeholder ?? 'Ask about this chart...'}
377
519
  .welcomeMessage=${welcomeMessage}
@@ -385,7 +527,7 @@ let NileChart = class NileChart extends NileElement {
385
527
  const config = this.activeConfig;
386
528
  // Suppress inner Highcharts title/subtitle — the card header shows them
387
529
  const noTitle = { title: { text: undefined }, subtitle: { text: undefined } };
388
- const mergedOptions = { ...(config.options ?? {}), ...noTitle };
530
+ const mergedOptions = deepMerge(deepMerge(config.options ?? {}, noTitle), this.buildExportingOptions());
389
531
  switch (config.type) {
390
532
  case 'bar':
391
533
  return html `<nile-bar-chart
@@ -542,7 +684,6 @@ let NileChart = class NileChart extends NileElement {
542
684
  .height=${this.hasAttribute('fit') ? null : (config.height ?? '100%')}
543
685
  .innerSize=${config.innerSize ?? '50%'}
544
686
  .semiCircle=${config.semiCircle ?? false}
545
- .semiCircle=${config.semiCircle ?? false}
546
687
  .showDataLabels=${config.showDataLabels ?? true}
547
688
  .showLegend=${config.showLegend ?? true}
548
689
  .options=${mergedOptions}
@@ -1370,6 +1511,7 @@ let NileChart = class NileChart extends NileElement {
1370
1511
  ></nile-xrange-chart>`;
1371
1512
  case 'kpi': {
1372
1513
  const k = config;
1514
+ const kpiOptions = deepMerge(k.options ?? {}, this.buildExportingOptions());
1373
1515
  return html `<nile-kpi-chart
1374
1516
  embed-in-nile-chart
1375
1517
  .config=${{
@@ -1429,8 +1571,11 @@ let NileChart = class NileChart extends NileElement {
1429
1571
  contentGap: k.contentGap,
1430
1572
  tooltipEnabled: k.tooltipEnabled,
1431
1573
  loading: k.loading,
1432
- options: k.options,
1574
+ options: kpiOptions,
1433
1575
  height: k.height,
1576
+ valueFormat: k.valueFormat,
1577
+ precision: k.precision,
1578
+ unit: k.unit,
1434
1579
  },
1435
1580
  aq: {
1436
1581
  chartSubtitle: k.chartSubtitle,
@@ -1442,9 +1587,6 @@ let NileChart = class NileChart extends NileElement {
1442
1587
  const gridChrome = '--nile-data-grid-radius:0;' +
1443
1588
  '--nile-data-grid-border-color:transparent;' +
1444
1589
  '--nile-data-grid-shadow:none;';
1445
- const gridStyle = config.height
1446
- ? `${gridChrome}height:${config.height};`
1447
- : gridChrome;
1448
1590
  return html `<nile-data-grid
1449
1591
  class="nile-chart-grid"
1450
1592
  .data=${config.data}
@@ -1454,9 +1596,9 @@ let NileChart = class NileChart extends NileElement {
1454
1596
  .hoverable=${config.hoverable ?? false}
1455
1597
  .stickyHeader=${config.stickyHeader ?? false}
1456
1598
  .emptyMessage=${config.emptyMessage ?? 'No data'}
1457
- .loadingMessage=${config.loadingMessage ?? 'Loading\u2026'}
1599
+ .loadingMessage=${config.loadingMessage ?? 'Loading'}
1458
1600
  .noMatchMessage=${config.noMatchMessage ?? 'No matching rows'}
1459
- style=${gridStyle}
1601
+ style=${gridChrome}
1460
1602
  ></nile-data-grid>`;
1461
1603
  }
1462
1604
  default: {
@@ -1467,28 +1609,32 @@ let NileChart = class NileChart extends NileElement {
1467
1609
  }
1468
1610
  renderSkeleton() {
1469
1611
  return html `
1470
- <div class="chart-skeleton" aria-busy="true" aria-label="Loading chart">
1471
- <div class="chart-skeleton-body">
1612
+ <div class="nile-chart-skeleton" aria-busy="true" aria-label="Loading chart">
1613
+ <div class="nile-chart-skeleton-body">
1472
1614
  ${[78, 55, 91, 42, 68].map(w => html `
1473
- <div class="chart-skeleton-row">
1474
- <div class="chart-skeleton-ylabel"></div>
1475
- <div class="chart-skeleton-bar" style="--w: ${w}%"></div>
1615
+ <div class="nile-chart-skeleton-row">
1616
+ <div class="nile-chart-skeleton-ylabel"></div>
1617
+ <div class="nile-chart-skeleton-bar" style="--w: ${w}%"></div>
1476
1618
  </div>
1477
1619
  `)}
1478
1620
  </div>
1479
- <div class="chart-skeleton-xaxis-row">
1480
- ${[0, 1, 2, 3, 4].map(i => html `<div class="chart-skeleton-xlabel" style="--d: ${i * 80}ms"></div>`)}
1621
+ <div class="nile-chart-skeleton-xaxis-row">
1622
+ ${[0, 1, 2, 3, 4].map(i => html `<div class="nile-chart-skeleton-xlabel" style="--d: ${i * 80}ms"></div>`)}
1481
1623
  </div>
1482
1624
  </div>
1483
1625
  `;
1484
1626
  }
1485
1627
  render() {
1486
1628
  const isLoading = this.loading || (this.activeConfig?.loading ?? false);
1629
+ const isGrid = this.activeConfig?.type === 'grid';
1630
+ const cardStyle = isGrid && this.activeConfig?.height
1631
+ ? `height:${this.activeConfig.height}`
1632
+ : '';
1487
1633
  return html `
1488
- <div class="chart-card">
1634
+ <div class="nile-chart-card ${isGrid ? 'nile-chart-card--grid' : ''}" style=${cardStyle}>
1489
1635
  ${this.renderHeader()}
1490
- <div class="chart-wrapper">
1491
- <div class="chart-inner ${this.activeConfig?.type === 'kpi' ? 'chart-inner--kpi' : ''}">
1636
+ <div class="nile-chart-wrapper">
1637
+ <div class="nile-chart-inner ${this.activeConfig?.type === 'kpi' ? 'nile-chart-inner--kpi' : ''}">
1492
1638
  ${isLoading
1493
1639
  ? this.renderSkeleton()
1494
1640
  : this.activeConfig
@@ -1515,6 +1661,9 @@ __decorate([
1515
1661
  __decorate([
1516
1662
  property({ type: String })
1517
1663
  ], NileChart.prototype, "summary", void 0);
1664
+ __decorate([
1665
+ property({ type: Object })
1666
+ ], NileChart.prototype, "menu", void 0);
1518
1667
  __decorate([
1519
1668
  state()
1520
1669
  ], NileChart.prototype, "activeType", void 0);
@@ -1527,6 +1676,9 @@ __decorate([
1527
1676
  __decorate([
1528
1677
  state()
1529
1678
  ], NileChart.prototype, "chatOpen", void 0);
1679
+ __decorate([
1680
+ state()
1681
+ ], NileChart.prototype, "_hcChart", void 0);
1530
1682
  __decorate([
1531
1683
  state()
1532
1684
  ], NileChart.prototype, "hasHeaderSlotContent", void 0);
@@ -47,7 +47,7 @@ export const styles = css `
47
47
  --nile-kpi-label-color: var(--nile-colors-neutral-700, var(--ng-colors-text-secondary-700));
48
48
  --nile-kpi-label-font-size: var(--nile-type-scale-3, var(--ng-font-size-text-sm));
49
49
  --nile-kpi-label-font-weight: var(--nile-font-weight-medium, var(--ng-font-weight-medium));
50
- --nile-kpi-value-font-size: clamp(1.25rem, 2.5vw + 0.75rem, 36px);
50
+ --nile-kpi-value-font-size: clamp(1.25rem, 5cqi + 0.5rem, 36px);
51
51
  --nile-kpi-value-color: var(--nile-colors-dark-900, var(--ng-colors-text-primary-900));
52
52
  --nile-kpi-prefix-suffix-font-size: var(--nile-type-scale-6, var(--ng-font-size-text-xl));
53
53
  --nile-kpi-prefix-suffix-color: var(--nile-colors-neutral-700, var(--ng-colors-text-secondary-700));
@@ -59,8 +59,11 @@ export const styles = css `
59
59
  display: flex;
60
60
  flex-direction: column;
61
61
  width: 100%;
62
+ min-width: 160px;
62
63
  position: relative;
63
64
  box-sizing: border-box;
65
+ overflow: hidden;
66
+ container-type: inline-size;
64
67
  }
65
68
 
66
69
  :host([hidden]) {
@@ -89,12 +92,29 @@ export const styles = css `
89
92
  box-shadow: var(--nile-kpi-card-shadow-hover);
90
93
  }
91
94
 
95
+ /* Accent variant — card chrome with a prominent left colour bar. */
96
+ :host([variant='accent']:not([embed-in-nile-chart])) {
97
+ --nile-kpi-accent-color: #005EA6;
98
+ background: var(--nile-kpi-card-bg);
99
+ border: var(--nile-kpi-card-border-width) solid var(--nile-kpi-card-border-color);
100
+ border-left: 4px solid var(--nile-kpi-accent-color);
101
+ border-radius: var(--nile-kpi-card-border-radius);
102
+ box-shadow: var(--nile-kpi-card-shadow);
103
+ transition: box-shadow var(--nile-transition-duration-default, var(--ng-transition-duration-default)) ease;
104
+ }
105
+
106
+ :host([variant='accent']:not([embed-in-nile-chart]):hover) {
107
+ box-shadow: var(--nile-kpi-card-shadow-hover);
108
+ }
109
+
92
110
  .kpi {
93
111
  flex: 1 1 auto;
94
112
  display: flex;
95
113
  flex-direction: column;
96
114
  gap: var(--nile-kpi-content-gap);
97
115
  padding: var(--nile-kpi-padding-v) var(--nile-kpi-padding-h);
116
+ min-width: 0;
117
+ overflow: hidden;
98
118
  }
99
119
 
100
120
  .kpi-label {
@@ -104,13 +124,20 @@ export const styles = css `
104
124
  font-weight: var(--nile-kpi-label-font-weight);
105
125
  color: var(--nile-kpi-label-color);
106
126
  line-height: 1.4;
127
+ white-space: nowrap;
128
+ overflow: hidden;
129
+ text-overflow: ellipsis;
130
+ min-width: 0;
131
+ width: 100%;
107
132
  }
108
133
 
109
134
  .kpi-value-row {
110
135
  display: flex;
111
- align-items: baseline;
136
+ align-items: center;
112
137
  gap: var(--nile-spacing-md, var(--ng-spacing-md));
113
- flex-wrap: wrap;
138
+ flex-wrap: nowrap;
139
+ min-width: 0;
140
+ overflow: hidden;
114
141
  }
115
142
 
116
143
  .kpi-value {
@@ -121,6 +148,8 @@ export const styles = css `
121
148
  color: var(--nile-kpi-value-color);
122
149
  line-height: 1.2;
123
150
  cursor: default;
151
+ white-space: nowrap;
152
+ flex-shrink: 0;
124
153
  }
125
154
 
126
155
  .kpi-prefix,
@@ -141,6 +170,16 @@ export const styles = css `
141
170
  font-size: var(--nile-type-scale-2, var(--ng-font-size-text-xs));
142
171
  font-weight: var(--nile-font-weight-medium, var(--ng-font-weight-medium));
143
172
  line-height: 1;
173
+ flex-shrink: 1;
174
+ min-width: 0;
175
+ overflow: hidden;
176
+ white-space: nowrap;
177
+ }
178
+
179
+ @container (max-width: 240px) {
180
+ .kpi-trend {
181
+ display: none;
182
+ }
144
183
  }
145
184
 
146
185
  .kpi-trend--up {
@@ -176,6 +215,11 @@ export const styles = css `
176
215
  font-size: var(--nile-kpi-description-font-size);
177
216
  color: var(--nile-kpi-description-color);
178
217
  line-height: 1.5;
218
+ white-space: nowrap;
219
+ overflow: hidden;
220
+ text-overflow: ellipsis;
221
+ min-width: 0;
222
+ width: 100%;
179
223
  }
180
224
 
181
225
  .kpi-sparkline {
@@ -3,8 +3,9 @@ import type Highcharts from 'highcharts';
3
3
  import NileElement from '../internal/nile-element.js';
4
4
  import type { AqConfigType } from '../internal/types/aq-config.type.js';
5
5
  export type TrendDirection = 'up' | 'down' | 'neutral';
6
- export type KpiVariant = 'default' | 'card' | 'gauge';
6
+ export type KpiVariant = 'default' | 'card' | 'gauge' | 'accent';
7
7
  export type SparklineType = 'area' | 'line';
8
+ export type KpiValueFormat = 'auto' | 'K' | 'M' | 'B' | 'T' | 'none';
8
9
  /** `chart` slice for `<nile-kpi-chart>.config` (discriminated by `type: 'kpi'`). */
9
10
  export interface ChartKpiSeparatedPayload {
10
11
  type: 'kpi';
@@ -63,6 +64,8 @@ export interface ChartKpiSeparatedPayload {
63
64
  loadingText?: string;
64
65
  /** Whether the hover tooltip is shown on value / sparkline / gauge (default: true). */
65
66
  tooltipEnabled?: boolean;
67
+ /** Accent bar colour for the 'accent' variant (default: brand blue). */
68
+ accentColor?: string;
66
69
  cardBackground?: string;
67
70
  cardBorderColor?: string;
68
71
  cardBorderWidth?: string | number;
@@ -86,6 +89,18 @@ export interface ChartKpiSeparatedPayload {
86
89
  options?: Highcharts.Options;
87
90
  /** Box size when a height is set (host min-height and height). */
88
91
  height?: string | number;
92
+ /** How to abbreviate the display value. 'auto' picks K/M/B/T by magnitude. Default: 'auto'. */
93
+ valueFormat?: KpiValueFormat;
94
+ /** Decimal places for the abbreviated value. Auto-selects 0–2 when omitted. */
95
+ precision?: number;
96
+ /** BCP 47 locale for number formatting, e.g. 'en-IN'. Defaults to browser locale. */
97
+ locale?: string;
98
+ /**
99
+ * Base unit appended after the magnitude prefix — e.g. `'L'` + `valueFormat: 'auto'` on 1500
100
+ * renders `"1.5KL"` on the card and `"1,500L"` in the tooltip. Use for physical units
101
+ * (L, g, m, Pa, Hz, B, etc.). For non-unit suffix text (%, /day), use `suffix` instead.
102
+ */
103
+ unit?: string;
89
104
  }
90
105
  /** Separated `{ chart, aq }` input for `<nile-kpi-chart>`. */
91
106
  export interface NileKpiConfigInputType {
@@ -196,6 +211,8 @@ export declare class NileKpiChart extends NileElement {
196
211
  cardPaddingVertical: string | number;
197
212
  cardPaddingHorizontal: string | number;
198
213
  contentGap: string | number;
214
+ /** Accent bar colour for the 'accent' variant. */
215
+ accentColor: string;
199
216
  labelColor: string;
200
217
  labelFontSize: string | number;
201
218
  labelFontWeight: string | number;
@@ -210,6 +227,14 @@ export declare class NileKpiChart extends NileElement {
210
227
  loading: boolean;
211
228
  /** Highcharts options override for the sparkline or gauge. */
212
229
  options: Highcharts.Options;
230
+ /** How to abbreviate the numeric value. 'auto' picks K/M/B/T by magnitude. Default: 'auto'. */
231
+ valueFormat: KpiValueFormat;
232
+ /** Base unit combined with the magnitude prefix (e.g. 'L' → "1.5KL"). */
233
+ unit: string;
234
+ /** Decimal places for the abbreviated value. null = auto (0–2 by magnitude). */
235
+ precision: number | null;
236
+ /** BCP 47 locale for number formatting, e.g. 'en-IN'. Defaults to browser locale. */
237
+ locale: string;
213
238
  /**
214
239
  * Set by nile-chart: skip host border/shadow (variant card/gauge) so the parent chart-card is the only frame.
215
240
  */
@@ -223,6 +248,7 @@ export declare class NileKpiChart extends NileElement {
223
248
  private formatTooltipNumber;
224
249
  private inferSparklineTooltipScale;
225
250
  private getTooltipContent;
251
+ private formatValue;
226
252
  private applyConfig;
227
253
  connectedCallback(): void;
228
254
  disconnectedCallback(): void;
@@ -240,6 +266,8 @@ export declare class NileKpiChart extends NileElement {
240
266
  private destroyGauge;
241
267
  private destroyCharts;
242
268
  private _onValueEnter;
269
+ private _onDescEnter;
270
+ private _onLabelEnter;
243
271
  private _onGaugeEnter;
244
272
  private _onTipLeave;
245
273
  private renderTrend;