@aquera/nile-visualization 1.8.0 → 1.9.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,16 +141,22 @@ 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). */
@@ -142,7 +167,24 @@ let NileChart = class NileChart extends NileElement {
142
167
  this.chatOpen = false;
143
168
  }
144
169
  };
170
+ this.handleChartReady = (e) => {
171
+ this._hcChart = e.detail?.chart ?? null;
172
+ };
145
173
  this.resolvedConfig = null;
174
+ this._onTitleEnter = (e) => {
175
+ const el = e.currentTarget;
176
+ if (el.scrollWidth <= el.clientWidth)
177
+ return;
178
+ const rect = el.getBoundingClientRect();
179
+ this._showHeaderTip(el.textContent ?? '', rect.left + rect.width / 2, rect.top);
180
+ };
181
+ this._onSubtitleEnter = (e) => {
182
+ const el = e.currentTarget;
183
+ if (el.scrollWidth <= el.clientWidth)
184
+ return;
185
+ const rect = el.getBoundingClientRect();
186
+ this._showHeaderTip(el.textContent ?? '', rect.left + rect.width / 2, rect.top);
187
+ };
146
188
  }
147
189
  get effectiveSummary() {
148
190
  return this.resolvedConfig?.summary ?? this.summary;
@@ -153,7 +195,14 @@ let NileChart = class NileChart extends NileElement {
153
195
  }
154
196
  connectedCallback() {
155
197
  super.connectedCallback();
198
+ ensureHeaderTooltipStyles();
199
+ const tip = document.createElement('div');
200
+ tip.className = 'nile-chart-header-tooltip';
201
+ document.body.appendChild(tip);
202
+ this._headerTipEl = tip;
156
203
  document.addEventListener('click', this.handleOutsideClick);
204
+ this.addEventListener('nile-chart-ready', this.handleChartReady);
205
+ initNileChartExporting();
157
206
  // Pick up config set before element upgrade (e.g. Angular ngAfterViewInit)
158
207
  if (this.config && !this.resolvedConfig) {
159
208
  this.resolvedConfig = this.resolveConfig(this.config);
@@ -164,6 +213,9 @@ let NileChart = class NileChart extends NileElement {
164
213
  disconnectedCallback() {
165
214
  super.disconnectedCallback();
166
215
  document.removeEventListener('click', this.handleOutsideClick);
216
+ this.removeEventListener('nile-chart-ready', this.handleChartReady);
217
+ this._headerTipEl?.remove();
218
+ this._headerTipEl = null;
167
219
  }
168
220
  mergeChartTypeFromAttr(chart) {
169
221
  const t = this.chartTypeAttr?.trim();
@@ -278,7 +330,8 @@ let NileChart = class NileChart extends NileElement {
278
330
  }
279
331
  shouldShowHeader() {
280
332
  const hasTitles = !!(this.headerTitle || this.headerSubtitle);
281
- const hasBuiltinActions = this.aiEnabled || (this.resolvedConfig?.switchableTypes?.length ?? 0) > 0;
333
+ const menuEnabled = this.resolvedConfig?.menu?.enabled === true || this.menu?.enabled === true;
334
+ const hasBuiltinActions = this.aiEnabled || (this.resolvedConfig?.switchableTypes?.length ?? 0) > 0 || menuEnabled;
282
335
  return (hasTitles ||
283
336
  this.hasHeaderSlotContent ||
284
337
  this.hasHeaderActionsSlot ||
@@ -286,36 +339,102 @@ let NileChart = class NileChart extends NileElement {
286
339
  this.lightDomHasSlot('header-actions') ||
287
340
  hasBuiltinActions);
288
341
  }
289
- renderTypeSwitcher() {
290
- const types = this.resolvedConfig?.switchableTypes;
291
- if (!types || types.length === 0)
342
+ buildExportingOptions() {
343
+ return { exporting: { enabled: true, buttons: { contextButton: { enabled: false } } } };
344
+ }
345
+ /** Ensures exporting modules are loaded and the chart's exporting/fullscreen instances exist. */
346
+ async ensureExporting() {
347
+ await initNileChartExporting();
348
+ const chart = this._hcChart;
349
+ if (chart && !chart.exporting) {
350
+ const HC = await getHighcharts();
351
+ HC.fireEvent(chart, 'afterInit');
352
+ }
353
+ }
354
+ async viewFullscreen() {
355
+ this.menuOpen = false;
356
+ await this.ensureExporting();
357
+ if (this._hcChart?.fullscreen?.open) {
358
+ this._hcChart.fullscreen.open();
359
+ return;
360
+ }
361
+ const target = this.shadowRoot?.querySelector('.nile-chart-card') ?? this;
362
+ target.requestFullscreen?.();
363
+ }
364
+ async printChart() {
365
+ this.menuOpen = false;
366
+ await this.ensureExporting();
367
+ this._hcChart?.print?.();
368
+ }
369
+ async exportChart(type) {
370
+ this.menuOpen = false;
371
+ await this.ensureExporting();
372
+ const filename = (this.headerTitle || 'chart').replace(/[^a-z0-9_-]+/gi, '_');
373
+ this._hcChart?.exportChartLocal?.({ type, filename, local: true });
374
+ }
375
+ async downloadCsv() {
376
+ this.menuOpen = false;
377
+ await this.ensureExporting();
378
+ this._hcChart?.downloadCSV?.();
379
+ }
380
+ renderActionsMenu() {
381
+ if (!this.resolvedConfig)
292
382
  return nothing;
383
+ const menuCfg = { ...(this.resolvedConfig.menu ?? {}), ...(this.menu ?? {}) };
384
+ if (menuCfg.enabled !== true)
385
+ return nothing;
386
+ const types = this.resolvedConfig.switchableTypes;
387
+ const allItems = [
388
+ ...(this.resolvedConfig.menuItems ?? []),
389
+ ...(menuCfg.items ?? []),
390
+ ];
391
+ const hasChart = !!this._hcChart;
392
+ const hasAnyItems = !!(types?.length || allItems.length);
293
393
  return html `
294
- <div class="chart-menu-anchor">
394
+ <div class="nile-chart-menu-anchor">
295
395
  <button
296
- class="chart-menu-trigger"
396
+ type="button"
397
+ class="nile-chart-menu-trigger"
297
398
  aria-haspopup="true"
298
399
  aria-expanded=${this.menuOpen ? 'true' : 'false'}
299
- aria-label="Change chart type"
300
- @click=${this.toggleMenu}
400
+ aria-label="Chart actions"
401
+ @click=${(e) => this.toggleMenu(e)}
301
402
  >
302
403
  <nile-glyph name="options" size="16"></nile-glyph>
303
404
  </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}
405
+ ${this.menuOpen && hasAnyItems ? html `
406
+ <div class="nile-chart-menu-dropdown" role="menu">
407
+ ${types?.length ? html `
408
+ ${types.map(type => html `
409
+ <button type="button" class="nile-chart-menu-item ${type === this.activeType ? 'active' : ''}" role="menuitem" @click=${() => this.switchType(type)}>
410
+ ${chartTypeLabel(type)}
411
+ </button>
412
+ `)}
413
+ ${allItems.length ? html `<div class="nile-chart-menu-separator"></div>` : nothing}
414
+ ` : nothing}
415
+ ${allItems.map(item => {
416
+ if (item.type === 'custom') {
417
+ return html `
418
+ ${item.divider ? html `<div class="nile-chart-menu-separator"></div>` : nothing}
419
+ <button type="button" class="nile-chart-menu-item" role="menuitem"
420
+ @click=${() => { this.menuOpen = false; this.emit('nile-menu-change', { id: item.id }); }}>
421
+ ${item.label}
422
+ </button>
423
+ `;
424
+ }
425
+ if (!hasChart)
426
+ return nothing;
427
+ return html `
428
+ ${item.divider ? html `<div class="nile-chart-menu-separator"></div>` : nothing}
429
+ ${item.fullscreen ? html `<button type="button" class="nile-chart-menu-item" role="menuitem" @click=${() => this.viewFullscreen()}>${item.label ?? 'Fullscreen'}</button>` : nothing}
430
+ ${item.print ? html `<button type="button" class="nile-chart-menu-item" role="menuitem" @click=${() => this.printChart()}>${item.label ?? 'Print'}</button>` : nothing}
431
+ ${item.downloadPng ? html `<button type="button" class="nile-chart-menu-item" role="menuitem" @click=${() => this.exportChart('image/png')}>${item.label ?? 'Download PNG'}</button>` : nothing}
432
+ ${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}
433
+ ${item.downloadCsv ? html `<button type="button" class="nile-chart-menu-item" role="menuitem" @click=${() => this.downloadCsv()}>${item.label ?? 'Download CSV'}</button>` : nothing}
434
+ `;
435
+ })}
436
+ </div>
437
+ ` : nothing}
319
438
  </div>
320
439
  `;
321
440
  }
@@ -324,16 +443,28 @@ let NileChart = class NileChart extends NileElement {
324
443
  return nothing;
325
444
  return html `
326
445
  <button
327
- class="ai-trigger ${this.chatOpen ? 'active' : ''}"
446
+ class="nile-ai-trigger ${this.chatOpen ? 'active' : ''}"
328
447
  aria-label="Ask AI about this chart"
329
448
  aria-expanded=${this.chatOpen ? 'true' : 'false'}
330
449
  @click=${this.toggleChat}
331
450
  >
332
451
  <nile-glyph name="smart-code" size="16"></nile-glyph>
333
452
  </button>
334
- <slot name="ai-trigger-after"></slot>
453
+ <slot name="nile-ai-trigger-after"></slot>
335
454
  `;
336
455
  }
456
+ _showHeaderTip(text, x, y) {
457
+ if (!this._headerTipEl)
458
+ return;
459
+ this._headerTipEl.textContent = text;
460
+ this._headerTipEl.style.left = `${x}px`;
461
+ this._headerTipEl.style.top = `${y}px`;
462
+ this._headerTipEl.style.display = 'block';
463
+ }
464
+ _hideHeaderTip() {
465
+ if (this._headerTipEl)
466
+ this._headerTipEl.style.display = 'none';
467
+ }
337
468
  renderHeader() {
338
469
  if (!this.shouldShowHeader())
339
470
  return nothing;
@@ -342,20 +473,20 @@ let NileChart = class NileChart extends NileElement {
342
473
  const showDefaultTitles = !this.hasHeaderSlotContent && !!(title || subtitle);
343
474
  const headerCompact = !subtitle?.trim();
344
475
  return html `
345
- <div class="chart-header ${headerCompact ? 'chart-header--compact' : ''}">
346
- <div class="chart-header-titles">
476
+ <div class="nile-chart-header ${headerCompact ? 'nile-chart-header--compact' : ''}">
477
+ <div class="nile-chart-header-titles">
347
478
  <slot name="header" @slotchange=${this.onHeaderSlotChange}></slot>
348
479
  ${showDefaultTitles
349
480
  ? html `
350
- ${title ? html `<p class="chart-header-title">${title}</p>` : nothing}
351
- ${subtitle ? html `<p class="chart-header-subtitle">${subtitle}</p>` : nothing}
481
+ ${title ? html `<p class="nile-chart-header-title" @mouseenter=${this._onTitleEnter} @mouseleave=${() => this._hideHeaderTip()}>${title}</p>` : nothing}
482
+ ${subtitle ? html `<p class="nile-chart-header-subtitle" @mouseenter=${this._onSubtitleEnter} @mouseleave=${() => this._hideHeaderTip()}>${subtitle}</p>` : nothing}
352
483
  `
353
484
  : nothing}
354
485
  </div>
355
- <div class="chart-header-actions">
486
+ <div class="nile-chart-header-actions">
356
487
  <slot name="header-actions" @slotchange=${this.onHeaderActionsSlotChange}></slot>
357
488
  ${this.renderAiTrigger()}
358
- ${this.renderTypeSwitcher()}
489
+ ${this.renderActionsMenu()}
359
490
  </div>
360
491
  </div>
361
492
  `;
@@ -371,7 +502,7 @@ let NileChart = class NileChart extends NileElement {
371
502
  const summaryMessage = summary;
372
503
  const welcomeMessage = summary ? '' : (aiConfig?.welcomeMessage ?? '');
373
504
  return html `
374
- <div class="ai-panel-overlay" ?data-open=${this.chatOpen}>
505
+ <div class="nile-ai-panel-overlay" ?data-open=${this.chatOpen}>
375
506
  <nile-ai-panel
376
507
  .placeholder=${aiConfig?.placeholder ?? 'Ask about this chart...'}
377
508
  .welcomeMessage=${welcomeMessage}
@@ -385,7 +516,7 @@ let NileChart = class NileChart extends NileElement {
385
516
  const config = this.activeConfig;
386
517
  // Suppress inner Highcharts title/subtitle — the card header shows them
387
518
  const noTitle = { title: { text: undefined }, subtitle: { text: undefined } };
388
- const mergedOptions = { ...(config.options ?? {}), ...noTitle };
519
+ const mergedOptions = deepMerge(deepMerge(config.options ?? {}, noTitle), this.buildExportingOptions());
389
520
  switch (config.type) {
390
521
  case 'bar':
391
522
  return html `<nile-bar-chart
@@ -542,7 +673,6 @@ let NileChart = class NileChart extends NileElement {
542
673
  .height=${this.hasAttribute('fit') ? null : (config.height ?? '100%')}
543
674
  .innerSize=${config.innerSize ?? '50%'}
544
675
  .semiCircle=${config.semiCircle ?? false}
545
- .semiCircle=${config.semiCircle ?? false}
546
676
  .showDataLabels=${config.showDataLabels ?? true}
547
677
  .showLegend=${config.showLegend ?? true}
548
678
  .options=${mergedOptions}
@@ -1370,6 +1500,7 @@ let NileChart = class NileChart extends NileElement {
1370
1500
  ></nile-xrange-chart>`;
1371
1501
  case 'kpi': {
1372
1502
  const k = config;
1503
+ const kpiOptions = deepMerge(k.options ?? {}, this.buildExportingOptions());
1373
1504
  return html `<nile-kpi-chart
1374
1505
  embed-in-nile-chart
1375
1506
  .config=${{
@@ -1429,8 +1560,11 @@ let NileChart = class NileChart extends NileElement {
1429
1560
  contentGap: k.contentGap,
1430
1561
  tooltipEnabled: k.tooltipEnabled,
1431
1562
  loading: k.loading,
1432
- options: k.options,
1563
+ options: kpiOptions,
1433
1564
  height: k.height,
1565
+ valueFormat: k.valueFormat,
1566
+ precision: k.precision,
1567
+ unit: k.unit,
1434
1568
  },
1435
1569
  aq: {
1436
1570
  chartSubtitle: k.chartSubtitle,
@@ -1442,9 +1576,6 @@ let NileChart = class NileChart extends NileElement {
1442
1576
  const gridChrome = '--nile-data-grid-radius:0;' +
1443
1577
  '--nile-data-grid-border-color:transparent;' +
1444
1578
  '--nile-data-grid-shadow:none;';
1445
- const gridStyle = config.height
1446
- ? `${gridChrome}height:${config.height};`
1447
- : gridChrome;
1448
1579
  return html `<nile-data-grid
1449
1580
  class="nile-chart-grid"
1450
1581
  .data=${config.data}
@@ -1454,9 +1585,9 @@ let NileChart = class NileChart extends NileElement {
1454
1585
  .hoverable=${config.hoverable ?? false}
1455
1586
  .stickyHeader=${config.stickyHeader ?? false}
1456
1587
  .emptyMessage=${config.emptyMessage ?? 'No data'}
1457
- .loadingMessage=${config.loadingMessage ?? 'Loading\u2026'}
1588
+ .loadingMessage=${config.loadingMessage ?? 'Loading'}
1458
1589
  .noMatchMessage=${config.noMatchMessage ?? 'No matching rows'}
1459
- style=${gridStyle}
1590
+ style=${gridChrome}
1460
1591
  ></nile-data-grid>`;
1461
1592
  }
1462
1593
  default: {
@@ -1467,28 +1598,32 @@ let NileChart = class NileChart extends NileElement {
1467
1598
  }
1468
1599
  renderSkeleton() {
1469
1600
  return html `
1470
- <div class="chart-skeleton" aria-busy="true" aria-label="Loading chart">
1471
- <div class="chart-skeleton-body">
1601
+ <div class="nile-chart-skeleton" aria-busy="true" aria-label="Loading chart">
1602
+ <div class="nile-chart-skeleton-body">
1472
1603
  ${[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>
1604
+ <div class="nile-chart-skeleton-row">
1605
+ <div class="nile-chart-skeleton-ylabel"></div>
1606
+ <div class="nile-chart-skeleton-bar" style="--w: ${w}%"></div>
1476
1607
  </div>
1477
1608
  `)}
1478
1609
  </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>`)}
1610
+ <div class="nile-chart-skeleton-xaxis-row">
1611
+ ${[0, 1, 2, 3, 4].map(i => html `<div class="nile-chart-skeleton-xlabel" style="--d: ${i * 80}ms"></div>`)}
1481
1612
  </div>
1482
1613
  </div>
1483
1614
  `;
1484
1615
  }
1485
1616
  render() {
1486
1617
  const isLoading = this.loading || (this.activeConfig?.loading ?? false);
1618
+ const isGrid = this.activeConfig?.type === 'grid';
1619
+ const cardStyle = isGrid && this.activeConfig?.height
1620
+ ? `height:${this.activeConfig.height}`
1621
+ : '';
1487
1622
  return html `
1488
- <div class="chart-card">
1623
+ <div class="nile-chart-card ${isGrid ? 'nile-chart-card--grid' : ''}" style=${cardStyle}>
1489
1624
  ${this.renderHeader()}
1490
- <div class="chart-wrapper">
1491
- <div class="chart-inner ${this.activeConfig?.type === 'kpi' ? 'chart-inner--kpi' : ''}">
1625
+ <div class="nile-chart-wrapper">
1626
+ <div class="nile-chart-inner ${this.activeConfig?.type === 'kpi' ? 'nile-chart-inner--kpi' : ''}">
1492
1627
  ${isLoading
1493
1628
  ? this.renderSkeleton()
1494
1629
  : this.activeConfig
@@ -1515,6 +1650,9 @@ __decorate([
1515
1650
  __decorate([
1516
1651
  property({ type: String })
1517
1652
  ], NileChart.prototype, "summary", void 0);
1653
+ __decorate([
1654
+ property({ type: Object })
1655
+ ], NileChart.prototype, "menu", void 0);
1518
1656
  __decorate([
1519
1657
  state()
1520
1658
  ], NileChart.prototype, "activeType", void 0);
@@ -1527,6 +1665,9 @@ __decorate([
1527
1665
  __decorate([
1528
1666
  state()
1529
1667
  ], NileChart.prototype, "chatOpen", void 0);
1668
+ __decorate([
1669
+ state()
1670
+ ], NileChart.prototype, "_hcChart", void 0);
1530
1671
  __decorate([
1531
1672
  state()
1532
1673
  ], 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;