@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.
- package/dist/src/nile-chart/nile-chart-config.d.ts +2 -0
- package/dist/src/nile-chart/nile-chart-skeleton.d.ts +2 -0
- package/dist/src/nile-chart/nile-chart-skeleton.js +188 -0
- package/dist/src/nile-chart/nile-chart.css.js +399 -36
- package/dist/src/nile-chart/nile-chart.js +3 -16
- package/dist/src/nile-filter-chart/nile-filter-chart.css.js +11 -18
- package/dist/src/nile-filter-chart/nile-filter-chart.d.ts +5 -0
- package/dist/src/nile-filter-chart/nile-filter-chart.js +28 -0
- package/dist/src/nile-filter-chart/utils/prompt.js +120 -44
- package/dist/src/nile-filter-chart/utils/types.d.ts +4 -0
- package/dist/src/nile-kpi-chart/nile-kpi-chart.css.js +310 -17
- package/dist/src/nile-kpi-chart/nile-kpi-chart.d.ts +4 -2
- package/dist/src/nile-kpi-chart/nile-kpi-chart.js +145 -26
- package/package.json +1 -1
|
@@ -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
|
-
<
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
161
|
-
flex: 0
|
|
162
|
-
overflow:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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;
|