@aquera/nile-visualization 2.9.5 → 2.9.7

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.
@@ -264,7 +264,42 @@ function renderModeToggle(host, ctrl, mode) {
264
264
  </nile-button-toggle-group>
265
265
  `;
266
266
  }
267
+ /**
268
+ * Layout-only global stylesheet for the portaled suggestion menu. The portal
269
+ * clones the panel into document.body, so component-scoped CSS can't reach
270
+ * it — these two rules use nile-menu-item's exposed `label` part to grow the
271
+ * label so the suffix (type pill) sticks to the row's right edge, and style
272
+ * the pill itself. Idempotent: bails if already injected.
273
+ */
274
+ const SUGGESTION_LAYOUT_STYLE_ID = 'fc-prompt-suggestion-layout';
275
+ function ensureSuggestionLayoutStyles() {
276
+ if (typeof document === 'undefined')
277
+ return;
278
+ if (document.getElementById(SUGGESTION_LAYOUT_STYLE_ID))
279
+ return;
280
+ const el = document.createElement('style');
281
+ el.id = SUGGESTION_LAYOUT_STYLE_ID;
282
+ el.textContent = `
283
+ .fc-prompt__menu nile-menu-item::part(label) {
284
+ flex: 1;
285
+ min-width: 0;
286
+ }
287
+ .fc-prompt__suggestion-tag {
288
+ flex-shrink: 0;
289
+ padding: 1px 8px;
290
+ border-radius: 999px;
291
+ background: var(--nile-colors-neutral-100, var(--ng-colors-bg-secondary));
292
+ color: var(--nile-colors-neutral-700, var(--ng-colors-text-quaternary-500));
293
+ font-size: 10px;
294
+ font-weight: 600;
295
+ text-transform: uppercase;
296
+ letter-spacing: 0.04em;
297
+ }
298
+ `;
299
+ document.head.appendChild(el);
300
+ }
267
301
  export function renderPrompt(host, ctrl) {
302
+ ensureSuggestionLayoutStyles();
268
303
  const value = String(host.selectedValues.get(ctrl.id) ?? '');
269
304
  const animated = host.promptPlaceholder.get(ctrl.id) ?? '';
270
305
  const error = host.promptErrors.get(ctrl.id);
@@ -608,60 +643,101 @@ export function renderPrompt(host, ctrl) {
608
643
  ctrl.noAiBorder ? 'fc-prompt--no-ai-border' : '',
609
644
  error ? 'fc-prompt--error' : '',
610
645
  ].filter(Boolean).join(' ');
646
+ const onFocus = () => host.setPromptFocused(ctrl.id, true);
647
+ // Don't close on blur if focus moved into the suggestion menu (or its
648
+ // portaled clone) — mousedown on a nile-menu-item momentarily shifts focus
649
+ // because of the menu's roving tab-index. We let nile-dropdown's own
650
+ // outside-click / Tab / Escape handlers close the panel.
651
+ const onBlur = (e) => {
652
+ const rel = e.relatedTarget;
653
+ if (rel) {
654
+ if (rel.closest('nile-menu-item'))
655
+ return;
656
+ if (rel.closest('nile-menu'))
657
+ return;
658
+ if (rel.closest('nile-dropdown'))
659
+ return;
660
+ if (rel.closest('.nile-dropdown-portal-append'))
661
+ return;
662
+ }
663
+ host.setPromptFocused(ctrl.id, false);
664
+ };
665
+ const isFocused = host.promptFocusedId === ctrl.id;
666
+ const dropdownOpen = isFiltrexMode && isFocused && suggestionData.length > 0;
611
667
  return html `
612
668
  <div class="fc-prompt-row">
613
669
  <div class="${classes} fc-prompt--row-input" part="filter-prompt" style="${inlineStyle}">
614
670
  <div class="fc-prompt__inner">
615
- <div class="fc-prompt__field${isFiltrexMode ? ' fc-prompt__field--highlight' : ''}" part="filter-prompt-field">
616
- ${isFiltrexMode ? html `
617
- <div class="fc-prompt__highlight" aria-hidden="true">${tokens.map((tok) => {
671
+ <nile-dropdown
672
+ class="fc-prompt__dropdown"
673
+ placement="bottom-start"
674
+ sync="width"
675
+ distance="6"
676
+ hoist
677
+ portal
678
+ stay-open-on-select
679
+ ?open=${dropdownOpen}
680
+ .noOpenOnClick=${true}
681
+ >
682
+ <div
683
+ slot="trigger"
684
+ class="fc-prompt__field${isFiltrexMode ? ' fc-prompt__field--highlight' : ''}"
685
+ part="filter-prompt-field"
686
+ >
687
+ ${isFiltrexMode ? html `
688
+ <div class="fc-prompt__highlight" aria-hidden="true">${tokens.map((tok) => {
618
689
  const color = TOKEN_COLOR[tok.type] ?? 'inherit';
619
690
  return html `<span style="color:${color};${tok.type === 'keyword' ? 'font-weight:600;' : ''}">${tok.text}</span>`;
620
691
  })}</div>
621
- ` : nothing}
622
- <input
623
- type="text"
624
- class="fc-prompt__input"
625
- id="fc-prompt-${ctrl.id}"
626
- name="${ctrl.id}"
627
- part="filter-prompt-input"
628
- autocomplete="off"
629
- spellcheck="false"
630
- aria-invalid="${error ? 'true' : 'false'}"
631
- placeholder="${animated || (ctrl.placeholders?.[0] ?? '')}"
632
- .value="${value}"
633
- @input="${onInput}"
634
- @keydown="${onKeyDown}"
635
- @scroll="${onScroll}"
636
- />
637
- </div>
638
- ${isFiltrexMode && suggestionData.length > 0 ? html `
639
- <div class="fc-prompt__suggestions" part="filter-prompt-suggestions">
640
- ${suggestionData.map((item, idx) => html `
641
- <button
642
- type="button"
643
- class="fc-prompt__suggestion${idx === activeIdx ? ' fc-prompt__suggestion--active' : ''}"
644
- @mouseenter="${() => {
645
- if (idx !== activeIdx)
646
- host.setPromptActiveIndex(ctrl.id, idx);
647
- }}"
648
- @mousedown="${(e) => {
649
- // Prevent focus loss on the input so we can update its
650
- // value imperatively after the merge.
651
- e.preventDefault();
652
- const inputEl = e.currentTarget
653
- .closest('.fc-prompt__inner')
654
- ?.querySelector('input.fc-prompt__input');
692
+ ` : nothing}
693
+ <input
694
+ type="text"
695
+ class="fc-prompt__input"
696
+ id="fc-prompt-${ctrl.id}"
697
+ name="${ctrl.id}"
698
+ part="filter-prompt-input"
699
+ autocomplete="off"
700
+ spellcheck="false"
701
+ aria-invalid="${error ? 'true' : 'false'}"
702
+ placeholder="${animated || (ctrl.placeholders?.[0] ?? '')}"
703
+ .value="${value}"
704
+ @input="${onInput}"
705
+ @keydown="${onKeyDown}"
706
+ @scroll="${onScroll}"
707
+ @focus="${onFocus}"
708
+ @blur="${onBlur}"
709
+ />
710
+ </div>
711
+ ${isFiltrexMode && suggestionData.length > 0 ? html `
712
+ <nile-menu
713
+ class="fc-prompt__menu"
714
+ @nile-select="${(e) => {
715
+ const idx = Number(e.detail?.value);
716
+ const item = suggestionData[idx];
717
+ if (!item)
718
+ return;
719
+ const inputId = `fc-prompt-${ctrl.id}`;
720
+ const root = e.currentTarget.getRootNode();
721
+ const inputEl = (root.querySelector(`#${CSS.escape(inputId)}`)
722
+ ?? document.getElementById(inputId));
655
723
  if (inputEl)
656
724
  pickItem(item, inputEl);
657
725
  }}"
658
- >
659
- <span class="fc-prompt__suggestion-label">${item.label}</span>
660
- ${item.type ? html `<span class="fc-prompt__suggestion-tag">${item.type}</span>` : nothing}
661
- </button>
662
- `)}
663
- </div>
664
- ` : nothing}
726
+ >
727
+ ${suggestionData.map((item, idx) => html `
728
+ <nile-menu-item
729
+ class="fc-prompt__suggestion"
730
+ part="filter-prompt-suggestion${idx === activeIdx ? ' filter-prompt-suggestion-active' : ''}"
731
+ value="${idx}"
732
+ ?active="${idx === activeIdx}"
733
+ >
734
+ ${item.label}
735
+ ${item.type ? html `<span slot="suffix" class="fc-prompt__suggestion-tag">${item.type}</span>` : nothing}
736
+ </nile-menu-item>
737
+ `)}
738
+ </nile-menu>
739
+ ` : nothing}
740
+ </nile-dropdown>
665
741
  ${showToggle ? renderModeToggle(host, ctrl, mode) : nothing}
666
742
  </div>
667
743
  ${error ? html `
@@ -321,6 +321,8 @@ export interface FilterChartHost {
321
321
  readonly promptModes: Map<string, PromptMode>;
322
322
  /** Highlighted suggestion index per prompt control id (-1 = none highlighted). */
323
323
  readonly promptActiveIndex: Map<string, number>;
324
+ /** Id of the focused prompt control (drives the suggestion dropdown's open state). */
325
+ readonly promptFocusedId: string | null;
324
326
  setValue(id: string, value: unknown): void;
325
327
  emit(name: string, detail?: unknown): void;
326
328
  /** Update a prompt's value and (when in NQL mode) re-validate it. */
@@ -331,4 +333,6 @@ export interface FilterChartHost {
331
333
  submitPrompt(ctrl: NormalizedFilterControl): void;
332
334
  /** Set the highlighted suggestion index. Use -1 to clear the highlight. */
333
335
  setPromptActiveIndex(id: string, idx: number): void;
336
+ /** Mark a prompt control's input as focused / unfocused. */
337
+ setPromptFocused(id: string, focused: boolean): void;
334
338
  }
@@ -47,14 +47,14 @@ 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(1rem, 5cqi + 0.25rem, 36px);
50
+ --nile-kpi-value-font-size: clamp(20px, 9cqmin, 56px);
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));
54
54
  --nile-kpi-trend-up-color: var(--nile-colors-success-700, var(--ng-color-success-700));
55
55
  --nile-kpi-trend-down-color: var(--nile-colors-error-700, var(--ng-color-error-700));
56
56
  --nile-kpi-trend-neutral-color: var(--nile-colors-neutral-700, var(--ng-colors-text-secondary-700));
57
- --nile-kpi-description-font-size: var(--nile-type-scale-2, var(--ng-font-size-text-xs));
57
+ --nile-kpi-description-font-size: clamp(0.75rem, 1.2cqmin, 1rem);
58
58
  --nile-kpi-description-color: var(--nile-colors-neutral-700, var(--ng-colors-text-secondary-700));
59
59
  display: flex;
60
60
  flex-direction: column;
@@ -63,7 +63,8 @@ export const styles = css `
63
63
  position: relative;
64
64
  box-sizing: border-box;
65
65
  overflow: hidden;
66
- container-type: inline-size;
66
+ container-type: size;
67
+ container-name: kpi-card;
67
68
  }
68
69
 
69
70
  :host([hidden]) {
@@ -120,6 +121,118 @@ export const styles = css `
120
121
  overflow: hidden;
121
122
  }
122
123
 
124
+ /* ── Adaptive layout: classes set by render() based on what content exists.
125
+ .kpi--no-chart — no sparkline and no gauge
126
+ .kpi--no-desc — no description
127
+ Use both together for "value-only" centering. ── */
128
+ .kpi.kpi--no-chart {
129
+ display: flex;
130
+ flex-direction: column;
131
+ align-items: center;
132
+ }
133
+
134
+ /* When there is no chart, center value-row vertically in the leftover space
135
+ below the label, and push the description toward the bottom. With a chart
136
+ present, items stack from the top naturally (no auto margins). */
137
+ .kpi--no-chart .kpi-value-row {
138
+ margin-top: auto;
139
+ }
140
+
141
+ .kpi--no-chart .kpi-description {
142
+ margin-bottom: auto;
143
+ }
144
+
145
+ /* Reset any grid-only props inherited from the side-by-side rule. */
146
+ .kpi.kpi--no-chart .kpi-label,
147
+ .kpi.kpi--no-chart .kpi-value-row,
148
+ .kpi.kpi--no-chart .kpi-description {
149
+ grid-column: auto;
150
+ grid-row: auto;
151
+ }
152
+
153
+ .kpi--no-chart .kpi-label {
154
+ align-self: flex-start;
155
+ width: 100%;
156
+ }
157
+
158
+ .kpi--no-chart .kpi-value-row {
159
+ overflow: hidden;
160
+ flex-wrap: wrap;
161
+ row-gap: var(--nile-spacing-xs, var(--ng-spacing-xs));
162
+ justify-content: center;
163
+ align-self: stretch;
164
+ width: 100%;
165
+ max-width: 100%;
166
+ }
167
+
168
+ /* Center value-row vertically when there's nothing below it (no desc, no chart). */
169
+ .kpi--no-chart.kpi--no-desc .kpi-value-row {
170
+ margin-top: auto;
171
+ margin-bottom: auto;
172
+ }
173
+
174
+ /* Grow the value when there's room — no chart AND (no description OR no trend). */
175
+ .kpi--no-chart.kpi--no-desc,
176
+ .kpi--no-chart.kpi--no-trend {
177
+ --nile-kpi-value-font-size: clamp(20px, 9cqmin, 56px);
178
+ }
179
+
180
+ /* When trend is absent, the value sits alone in the row — make sure it
181
+ centers horizontally and the row sizes to content. */
182
+ .kpi--no-chart.kpi--no-trend .kpi-value-row {
183
+ justify-content: center;
184
+ }
185
+
186
+ .kpi--no-chart.kpi--no-trend .kpi-value {
187
+ text-align: center;
188
+ }
189
+
190
+ .kpi--no-chart .kpi-trend {
191
+ flex: 0 1 auto;
192
+ min-width: 0;
193
+ max-width: 100%;
194
+ overflow: hidden;
195
+ text-overflow: ellipsis;
196
+ white-space: nowrap;
197
+ }
198
+
199
+ /* Let the trend text size to its own content when there's no chart. The
200
+ default flex 1 1 0 (basis 0) gives the trend nearly zero natural width,
201
+ so the parent row gives it almost no space and the text clips. */
202
+ .kpi--no-chart .kpi-trend > span:last-child {
203
+ flex: 0 0 auto;
204
+ }
205
+
206
+ .kpi--no-chart:not(.kpi--no-desc) .kpi-description {
207
+ margin-bottom: auto;
208
+ align-self: center;
209
+ text-align: center;
210
+ }
211
+
212
+
213
+ /* When the card is narrow, the centered no-chart value can overflow both
214
+ edges (cut off on the left). Switch to left-aligned at narrow widths so
215
+ the value's left edge stays visible. */
216
+ @container (max-width: 160px) {
217
+ .kpi--no-chart {
218
+ align-items: stretch;
219
+ }
220
+
221
+ .kpi--no-chart .kpi-value-row {
222
+ justify-content: flex-start;
223
+ align-self: stretch;
224
+ width: 100%;
225
+ }
226
+
227
+ .kpi--no-chart.kpi--no-trend .kpi-value-row {
228
+ justify-content: flex-start;
229
+ }
230
+
231
+ .kpi--no-chart.kpi--no-trend .kpi-value {
232
+ text-align: left;
233
+ }
234
+ }
235
+
123
236
  .kpi-label {
124
237
  display: block;
125
238
  margin: 0;
@@ -141,11 +254,24 @@ export const styles = css `
141
254
  .kpi-value-row {
142
255
  display: flex;
143
256
  align-items: center;
257
+ justify-content: flex-start;
144
258
  gap: var(--nile-spacing-md, var(--ng-spacing-md));
145
259
  flex-wrap: nowrap;
260
+ width: 100%;
261
+ max-width: 100%;
146
262
  min-width: 0;
147
263
  overflow: hidden;
148
264
  flex-shrink: 0;
265
+ box-sizing: border-box;
266
+ }
267
+
268
+ /* When a chart is rendered, force value-row to the left of its column,
269
+ overriding any centering inherited from the .kpi--no-chart rules. */
270
+ .kpi:not(.kpi--no-chart) .kpi-value-row {
271
+ justify-content: flex-start;
272
+ align-self: stretch;
273
+ margin-left: 0;
274
+ margin-right: 0;
149
275
  }
150
276
 
151
277
  .kpi-value {
@@ -157,10 +283,9 @@ export const styles = css `
157
283
  line-height: 1.2;
158
284
  cursor: default;
159
285
  white-space: nowrap;
160
- min-width: 0;
161
- flex: 0 1 auto;
162
- overflow: hidden;
163
- text-overflow: ellipsis;
286
+ flex: 0 0 auto;
287
+ flex-shrink: 0;
288
+ overflow: visible;
164
289
  }
165
290
 
166
291
  .kpi-prefix,
@@ -172,7 +297,7 @@ export const styles = css `
172
297
  }
173
298
 
174
299
  .kpi-trend {
175
- display: inline-flex;
300
+ display: flex;
176
301
  align-items: center;
177
302
  gap: var(--nile-spacing-xs, var(--ng-spacing-xs));
178
303
  padding: var(--nile-spacing-xs, var(--ng-spacing-xs)) var(--nile-spacing-md, var(--ng-spacing-md));
@@ -181,10 +306,36 @@ export const styles = css `
181
306
  font-size: var(--nile-type-scale-2, var(--ng-font-size-text-xs));
182
307
  font-weight: var(--nile-font-weight-medium, var(--ng-font-weight-medium));
183
308
  line-height: 1;
184
- flex-shrink: 1;
309
+ flex: 0 1 auto;
310
+ min-width: 0;
311
+ max-width: 100%;
312
+ overflow: hidden;
313
+ }
314
+
315
+ .kpi-trend-text {
185
316
  min-width: 0;
317
+ min-height: 0;
318
+ max-width: 100%;
186
319
  overflow: hidden;
320
+ text-overflow: ellipsis;
187
321
  white-space: nowrap;
322
+ box-sizing: border-box;
323
+ }
324
+
325
+ .kpi-trend > span:last-child {
326
+ flex: 1 1 0;
327
+ overflow: hidden;
328
+ text-overflow: ellipsis;
329
+ white-space: nowrap;
330
+ min-width: 0;
331
+ }
332
+
333
+ /* ── When no chart is rendered, the trend has room — don't truncate. ── */
334
+ .kpi--no-chart .kpi-trend,
335
+ .kpi--no-chart .kpi-trend > span:last-child {
336
+ overflow: hidden;
337
+ text-overflow: ellipsis;
338
+ flex-shrink: 0;
188
339
  }
189
340
 
190
341
  .kpi-trend--up {
@@ -215,7 +366,9 @@ export const styles = css `
215
366
  }
216
367
 
217
368
  .kpi-description {
218
- display: block;
369
+ display: -webkit-box;
370
+ -webkit-box-orient: vertical;
371
+ -webkit-line-clamp: 3;
219
372
  margin: 0;
220
373
  font-family: var(--nile-font-family-serif, var(--ng-font-family-body));
221
374
  font-size: var(--nile-kpi-description-font-size);
@@ -223,13 +376,19 @@ export const styles = css `
223
376
  line-height: 1.5;
224
377
  overflow: hidden;
225
378
  text-overflow: ellipsis;
379
+ white-space: normal;
380
+ word-break: break-word;
226
381
  min-width: 0;
227
382
  width: 100%;
228
383
  max-width: 100%;
229
384
  box-sizing: border-box;
230
- flex-shrink: 1;
385
+ flex-shrink: 0;
231
386
  }
232
387
 
388
+ /* Description stays visible whenever there's room — sparkline gives up its
389
+ space first because it has flex-shrink and flex-basis: 0. */
390
+
391
+
233
392
  .kpi-sparkline {
234
393
  width: 100%;
235
394
  flex: 1 1 0;
@@ -241,7 +400,7 @@ export const styles = css `
241
400
  /* ── Container queries: scale down for narrow cards ── */
242
401
 
243
402
  /* Medium-small: ~280px and below — tighten padding, shrink prefix/suffix. */
244
- @container (max-width: 280px) {
403
+ @container (max-width: 10px) {
245
404
  :host {
246
405
  --nile-kpi-padding-v: var(--nile-spacing-lg, var(--ng-spacing-lg));
247
406
  --nile-kpi-padding-h: var(--nile-spacing-xl, var(--ng-spacing-xl));
@@ -268,7 +427,9 @@ export const styles = css `
268
427
  }
269
428
 
270
429
  .kpi-sparkline {
271
- min-height: 0;
430
+
431
+ min-height: 18px;
432
+
272
433
  }
273
434
  }
274
435
 
@@ -284,6 +445,136 @@ export const styles = css `
284
445
  }
285
446
  }
286
447
 
448
+
449
+
450
+ /* ── Named-container hide thresholds ──────────────────────────────────────
451
+ Use kpi-card (this host) so descendants hide when the host can't fit them.
452
+ Tune each height to match where the section first starts clipping. ── */
453
+
454
+ /* Hide sparkline (and gauge) when the card can't fit it cleanly. */
455
+ @container kpi-card (max-height: 10px) {
456
+ .kpi-sparkline,
457
+ .kpi-gauge-container {
458
+ display: none;
459
+ }
460
+ }
461
+
462
+ /* Hide description when the card is even shorter. */
463
+ @container kpi-card (max-height: 110px) {
464
+ .kpi-description {
465
+ display: none;
466
+ }
467
+ }
468
+
469
+ /* Hide trend at very small sizes — keeps only label + value. */
470
+ @container kpi-card (max-height: 20px) {
471
+ .kpi-trend {
472
+ display: none;
473
+ }
474
+ }
475
+
476
+ /* When the host is short, hide the sparkline. Centering of the value-row is
477
+ restricted to .kpi--no-chart only — with a chart present, items stack from
478
+ the top of the card naturally. */
479
+ @container (max-height: 80px) {
480
+ .kpi-sparkline {
481
+ display: none;
482
+ }
483
+
484
+ .kpi.kpi--no-chart {
485
+ align-items: center;
486
+ }
487
+
488
+ .kpi--no-chart .kpi-label {
489
+ align-self: flex-start;
490
+ width: 100%;
491
+ }
492
+
493
+ .kpi--no-chart .kpi-value-row {
494
+ overflow: visible;
495
+ flex-wrap: wrap;
496
+ row-gap: var(--nile-spacing-xs, var(--ng-spacing-xs));
497
+ justify-content: center;
498
+ align-self: center;
499
+ width: auto;
500
+ max-width: 100%;
501
+ margin-top: auto;
502
+ margin-bottom: auto;
503
+ }
504
+ }
505
+
506
+ /* SIDE_HEIGHT × SIDE_WIDTH — wide + short, chart on the right of text.
507
+ Only when a chart is present; otherwise text would be squeezed into the
508
+ left half while the right half stays empty. */
509
+ @container (max-height: 108px) and (min-width: 390px) {
510
+ .kpi:not(.kpi--no-chart) {
511
+ display: grid;
512
+ grid-template-columns: minmax(0, 1fr) minmax(1px, 1fr);
513
+
514
+ grid-template-rows: auto auto auto 1fr;
515
+ column-gap: var(--nile-spacing-2xl, var(--ng-spacing-2xl));
516
+ align-items: start;
517
+ height: 100%;
518
+ }
519
+
520
+ .kpi:not(.kpi--no-chart) .kpi-label {
521
+ grid-column: 1;
522
+ grid-row: 1;
523
+ }
524
+
525
+ .kpi:not(.kpi--no-chart) .kpi-value-row {
526
+ grid-column: 1;
527
+ grid-row: 2;
528
+ overflow: hidden;
529
+ min-width: 0;
530
+ width: 100%;
531
+ max-width: 100%;
532
+ justify-content: flex-start;
533
+ justify-self: stretch;
534
+ align-self: start;
535
+ }
536
+
537
+ .kpi:not(.kpi--no-chart) .kpi-description {
538
+ grid-column: 1;
539
+ grid-row: 3;
540
+ }
541
+
542
+ .kpi:not(.kpi--no-chart) .kpi-trend {
543
+ flex: 0 1 auto;
544
+ min-width: 0;
545
+ max-width: 100%;
546
+ overflow: hidden;
547
+ text-overflow: ellipsis;
548
+ white-space: nowrap;
549
+ }
550
+
551
+ .kpi:not(.kpi--no-chart) .kpi-trend > span:last-child {
552
+ overflow: hidden;
553
+ text-overflow: ellipsis;
554
+ min-width: 0;
555
+ }
556
+
557
+ .kpi-sparkline {
558
+ display: block;
559
+ grid-column: 2;
560
+ grid-row: 1 / -1;
561
+ align-self: stretch;
562
+ min-height: 0;
563
+ height: 100%;
564
+ margin-top: 0;
565
+ }
566
+
567
+ .kpi-gauge-container {
568
+ display: block;
569
+ grid-column: 2;
570
+ grid-row: 1 / -1;
571
+ align-self: stretch;
572
+ min-height: 0;
573
+ height: 100%;
574
+ margin-top: 0;
575
+ }
576
+ }
577
+
287
578
  /* ── Gauge variant ── */
288
579
 
289
580
  .kpi--gauge {
@@ -293,11 +584,13 @@ export const styles = css `
293
584
 
294
585
  .kpi-gauge-container {
295
586
  width: 100%;
296
- max-width: 160px;
297
- aspect-ratio: 1;
298
- flex: 0 1 160px;
299
- min-width: 72px;
587
+ flex: 1 1 0;
588
+ min-width: 0;
589
+ min-height: 0;
590
+ max-width: 100%;
591
+ max-height: 100%;
300
592
  margin: 0 auto;
593
+ overflow: hidden;
301
594
  }
302
595
 
303
596
  .kpi--gauge .kpi-value-row {
@@ -3,8 +3,8 @@ 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' | 'accent';
7
- export type SparklineType = 'area' | 'line';
6
+ export type KpiVariant = 'default' | 'card' | 'sparkline' | 'gauge' | 'accent';
7
+ export type SparklineType = 'area' | 'line' | 'column' | 'bar' | 'spline' | 'areaspline' | 'pie' | 'scatter';
8
8
  export type KpiValueFormat = 'auto' | 'K' | 'M' | 'B' | 'T' | 'none';
9
9
  export type KpiNumberSystem = 'indian' | 'international';
10
10
  /** `chart` slice for `<nile-kpi-chart>.config` (discriminated by `type: 'kpi'`). */
@@ -265,6 +265,7 @@ export declare class NileKpiChart extends NileElement {
265
265
  protected firstUpdated(): void;
266
266
  protected updated(changedProperties: PropertyValues): void;
267
267
  private syncSparklineChartSize;
268
+ private syncGaugeChartSize;
268
269
  private setupResizeObserver;
269
270
  private _onSparklineMouseMove;
270
271
  private _onSparklineMouseLeave;
@@ -279,6 +280,7 @@ export declare class NileKpiChart extends NileElement {
279
280
  private _onDescEnter;
280
281
  private _onLabelEnter;
281
282
  private _onGaugeEnter;
283
+ private _onTrendEnter;
282
284
  private _onTipLeave;
283
285
  private renderTrend;
284
286
  render(): TemplateResult;