@aquera/nile-visualization 2.9.1 → 2.9.3
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.css.js +6 -0
- package/dist/src/nile-filter-chart/nile-filter-chart.css.js +274 -4
- package/dist/src/nile-filter-chart/nile-filter-chart.d.ts +59 -206
- package/dist/src/nile-filter-chart/nile-filter-chart.js +330 -436
- package/dist/src/nile-filter-chart/utils/badge.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/badge.js +33 -0
- package/dist/src/nile-filter-chart/utils/comparison.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/comparison.js +24 -0
- package/dist/src/nile-filter-chart/utils/dropdown.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/dropdown.js +24 -0
- package/dist/src/nile-filter-chart/utils/preset.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/preset.js +16 -0
- package/dist/src/nile-filter-chart/utils/prompt.d.ts +12 -0
- package/dist/src/nile-filter-chart/utils/prompt.js +676 -0
- package/dist/src/nile-filter-chart/utils/radio.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/radio.js +13 -0
- package/dist/src/nile-filter-chart/utils/search.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/search.js +12 -0
- package/dist/src/nile-filter-chart/utils/segmented.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/segmented.js +15 -0
- package/dist/src/nile-filter-chart/utils/threshold.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/threshold.js +58 -0
- package/dist/src/nile-filter-chart/utils/toggle.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/toggle.js +19 -0
- package/dist/src/nile-filter-chart/utils/types.d.ts +334 -0
- package/dist/src/nile-filter-chart/utils/types.js +2 -0
- package/dist/src/nile-kpi-chart/nile-kpi-chart.css.js +62 -4
- package/package.json +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
export function renderRadio(host, ctrl) {
|
|
3
|
+
const current = host.selectedValues.get(ctrl.id) ?? '';
|
|
4
|
+
return html `
|
|
5
|
+
<nile-radio-group
|
|
6
|
+
.value="${current}"
|
|
7
|
+
@nile-change="${(e) => { e.stopPropagation(); host.setValue(ctrl.id, e.detail.value); }}"
|
|
8
|
+
>
|
|
9
|
+
${(ctrl.options ?? []).map(opt => html `
|
|
10
|
+
<nile-radio value="${opt.value}" ?checked="${current === opt.value}">${opt.label}</nile-radio>`)}
|
|
11
|
+
</nile-radio-group>`;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=radio.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
export function renderSearch(host, ctrl) {
|
|
3
|
+
return html `
|
|
4
|
+
<nile-input
|
|
5
|
+
type="search"
|
|
6
|
+
placeholder="${ctrl.placeholders?.[0] ?? `Search ${ctrl.label}…`}"
|
|
7
|
+
.value="${host.selectedValues.get(ctrl.id) ?? ''}"
|
|
8
|
+
clearable
|
|
9
|
+
@nile-input="${(e) => { e.stopPropagation(); host.setValue(ctrl.id, e.detail.value); }}"
|
|
10
|
+
></nile-input>`;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
export function renderSegmented(host, ctrl) {
|
|
3
|
+
return html `
|
|
4
|
+
<div class="fc-segmented-scroll">
|
|
5
|
+
<nile-button-toggle-group
|
|
6
|
+
.value="${host.selectedValues.get(ctrl.id) ?? ''}"
|
|
7
|
+
?multiple="${ctrl.selection === 'multi'}"
|
|
8
|
+
@nile-change="${(e) => { e.stopPropagation(); host.setValue(ctrl.id, e.detail.value); }}"
|
|
9
|
+
>
|
|
10
|
+
${(ctrl.options ?? []).map(opt => html `
|
|
11
|
+
<nile-button-toggle value="${opt.value}">${opt.label}</nile-button-toggle>`)}
|
|
12
|
+
</nile-button-toggle-group>
|
|
13
|
+
</div>`;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=segmented.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { html, nothing } from 'lit';
|
|
2
|
+
const OPERATORS = [
|
|
3
|
+
{ value: '>', label: '> (greater)' },
|
|
4
|
+
{ value: '>=', label: '>= (min)' },
|
|
5
|
+
{ value: '<', label: '< (less)' },
|
|
6
|
+
{ value: '<=', label: '<= (max)' },
|
|
7
|
+
{ value: '=', label: '= (equals)' },
|
|
8
|
+
{ value: '!=', label: '≠ (not)' },
|
|
9
|
+
];
|
|
10
|
+
export function renderThreshold(host, ctrl) {
|
|
11
|
+
const metric = host.selectedValues.get(ctrl.id) ?? '';
|
|
12
|
+
const op = host.selectedValues.get(`${ctrl.id}__op`) ?? '>';
|
|
13
|
+
const val = host.selectedValues.get(`${ctrl.id}__val`) ?? '';
|
|
14
|
+
const metricLabel = (ctrl.options ?? []).find(o => o.value === metric)?.label ?? metric;
|
|
15
|
+
const hasPreview = metric && val !== '';
|
|
16
|
+
const previewText = hasPreview ? `${metricLabel} ${op} ${val}` : 'Set metric, condition, and value above';
|
|
17
|
+
return html `
|
|
18
|
+
<div class="fc-threshold">
|
|
19
|
+
<div class="fc-threshold-metric-row">
|
|
20
|
+
<span class="fc-threshold-where">WHERE</span>
|
|
21
|
+
<div class="fc-threshold-field fc-threshold-field--metric">
|
|
22
|
+
<span class="fc-threshold-field-label">Metric</span>
|
|
23
|
+
<nile-select
|
|
24
|
+
.value="${metric}"
|
|
25
|
+
placeholder="Select metric…"
|
|
26
|
+
@nile-change="${(e) => { e.stopPropagation(); host.setValue(ctrl.id, e.detail.value); }}"
|
|
27
|
+
>
|
|
28
|
+
${(ctrl.options ?? []).map(o => html `<nile-option value="${o.value}">${o.label}</nile-option>`)}
|
|
29
|
+
</nile-select>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="fc-threshold-cond-row">
|
|
33
|
+
<div class="fc-threshold-field fc-threshold-field--op">
|
|
34
|
+
<span class="fc-threshold-field-label">Condition</span>
|
|
35
|
+
<nile-select
|
|
36
|
+
.value="${op}"
|
|
37
|
+
@nile-change="${(e) => { e.stopPropagation(); host.setValue(`${ctrl.id}__op`, e.detail.value); }}"
|
|
38
|
+
>
|
|
39
|
+
${OPERATORS.map(o => html `<nile-option value="${o.value}">${o.label}</nile-option>`)}
|
|
40
|
+
</nile-select>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="fc-threshold-field fc-threshold-field--val">
|
|
43
|
+
<span class="fc-threshold-field-label">Value</span>
|
|
44
|
+
<nile-input
|
|
45
|
+
type="number"
|
|
46
|
+
placeholder="e.g. 10000"
|
|
47
|
+
.value="${String(val)}"
|
|
48
|
+
@nile-input="${(e) => { e.stopPropagation(); host.setValue(`${ctrl.id}__val`, e.detail.value); }}"
|
|
49
|
+
></nile-input>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="fc-threshold-preview ${hasPreview ? '' : 'fc-threshold-preview--empty'}">
|
|
53
|
+
${hasPreview ? html `<strong>Filter:</strong>` : nothing}
|
|
54
|
+
${previewText}
|
|
55
|
+
</div>
|
|
56
|
+
</div>`;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=threshold.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
export function renderToggle(host, ctrl) {
|
|
3
|
+
const current = host.selectedValues.get(ctrl.id) ?? {};
|
|
4
|
+
return html `
|
|
5
|
+
<div class="fc-toggle-group">
|
|
6
|
+
${(ctrl.options ?? []).map(opt => html `
|
|
7
|
+
<nile-slide-toggle
|
|
8
|
+
label="${opt.label}"
|
|
9
|
+
?isChecked="${!!current[opt.value]}"
|
|
10
|
+
fullWidth
|
|
11
|
+
@nile-change="${(e) => {
|
|
12
|
+
e.stopPropagation();
|
|
13
|
+
const updated = { ...current, [opt.value]: e.detail.checked };
|
|
14
|
+
host.setValue(ctrl.id, updated);
|
|
15
|
+
}}"
|
|
16
|
+
></nile-slide-toggle>`)}
|
|
17
|
+
</div>`;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=toggle.js.map
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
export type NileTagVariant = 'primary' | 'success' | 'normal' | 'warning' | 'error' | 'info';
|
|
2
|
+
export interface FilterOption {
|
|
3
|
+
label: string;
|
|
4
|
+
value: string;
|
|
5
|
+
/** Optional color dot (CSS color string). */
|
|
6
|
+
color?: string;
|
|
7
|
+
/** Optional nile-tag semantic variant — uses ng tokens directly (preferred over color). */
|
|
8
|
+
ngVariant?: NileTagVariant;
|
|
9
|
+
/** Optional icon name for preset variant. */
|
|
10
|
+
icon?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Suggestion shown in the prompt's autocomplete dropdown. */
|
|
13
|
+
export interface SuggestionItem {
|
|
14
|
+
/**
|
|
15
|
+
* Stable identifier. Meaning depends on `type`:
|
|
16
|
+
* - `attribute` → the token inserted into the expression on pick (lets a
|
|
17
|
+
* friendly `label` like "Status" map to the actual field token `status`).
|
|
18
|
+
* Falls back to `label` if omitted.
|
|
19
|
+
* - `value` → the parent attribute's `id`. Scopes the value to that
|
|
20
|
+
* attribute (`status == |` only suggests values whose `id === 'status'`).
|
|
21
|
+
* Omit for cross-attribute literals (`true` / `false` / `null`).
|
|
22
|
+
* - other types → optional metadata, currently unused for routing.
|
|
23
|
+
*/
|
|
24
|
+
id?: string;
|
|
25
|
+
/** Primary text shown in the row. For values, also the literal inserted (auto-quoted). */
|
|
26
|
+
label: string;
|
|
27
|
+
/**
|
|
28
|
+
* Drives both the row's colour in the dropdown and where in the expression
|
|
29
|
+
* the item shows up (context routing):
|
|
30
|
+
* - `attribute` → field position (status, priority, …)
|
|
31
|
+
* - `operator` → after a known field (==, !=, <, …)
|
|
32
|
+
* - `value` → after `<attr> <op>` (scope per-attribute by setting `id` to the attribute's id)
|
|
33
|
+
* - `connector` → after a complete predicate (and, or, not, in)
|
|
34
|
+
* - `keyword` → trailing SQL-ish keywords (ORDER BY, ASC, DESC, LIMIT)
|
|
35
|
+
* - `function` → callable templates (abs(value), …)
|
|
36
|
+
* - `snippet` → ready-made full expressions
|
|
37
|
+
* - `clause` → DEPRECATED. Treated as `connector` at routing time.
|
|
38
|
+
*/
|
|
39
|
+
type?: 'attribute' | 'operator' | 'value' | 'connector' | 'keyword' | 'function' | 'snippet' | 'clause';
|
|
40
|
+
/** Legacy: text inserted on pick. New shape uses `label` for both. */
|
|
41
|
+
value?: string;
|
|
42
|
+
/** Legacy: muted secondary text. */
|
|
43
|
+
description?: string;
|
|
44
|
+
/** Legacy: category — superseded by `type`. */
|
|
45
|
+
group?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Visual config for one segment of the prompt's mode toggle. */
|
|
48
|
+
export interface PromptModeToggleOption {
|
|
49
|
+
/** Visible text inside the segment. Defaults: 'Expression' for basic, '' for nql. */
|
|
50
|
+
label?: string;
|
|
51
|
+
/** nile-glyph icon name (e.g. `'ng-ai_magic'`). Defaults: none for basic, `'ng-ai_magic'` for nql. */
|
|
52
|
+
icon?: string;
|
|
53
|
+
/** ARIA label for screen readers — required when the segment is icon-only. */
|
|
54
|
+
ariaLabel?: string;
|
|
55
|
+
}
|
|
56
|
+
/** Override the Basic / NQL toggle's two segments. */
|
|
57
|
+
export interface PromptModeToggleConfig {
|
|
58
|
+
basic?: PromptModeToggleOption;
|
|
59
|
+
nql?: PromptModeToggleOption;
|
|
60
|
+
}
|
|
61
|
+
export interface QueryLanguageConfig {
|
|
62
|
+
/** Enables the query layer at all. Default: false. */
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Customise the Basic / NQL mode toggle's segments. Defaults:
|
|
66
|
+
* - basic → label `'Basic'`
|
|
67
|
+
* - nql → label `'JQL'`
|
|
68
|
+
* Either segment can be overridden with a label, an icon, or both.
|
|
69
|
+
*/
|
|
70
|
+
toggle?: PromptModeToggleConfig;
|
|
71
|
+
/**
|
|
72
|
+
* - `'auto'` → no validation; the raw string flows through `nile-change`.
|
|
73
|
+
* - `'strict'` → every change is parsed with filtrex. On success the compiled
|
|
74
|
+
* predicate appears in `nile-change.detail.evaluators[id]`.
|
|
75
|
+
* On failure the message appears in `nile-change.detail.errors[id]`
|
|
76
|
+
* and the input is rendered in an error state.
|
|
77
|
+
* Default: `'auto'`.
|
|
78
|
+
*/
|
|
79
|
+
mode?: 'auto' | 'strict';
|
|
80
|
+
/** Parser engine. Currently only `'filtrex'` is supported. */
|
|
81
|
+
engine?: 'filtrex';
|
|
82
|
+
/**
|
|
83
|
+
* When validation runs in NQL mode.
|
|
84
|
+
* - `'change'` (default) → debounced parse on every keystroke.
|
|
85
|
+
* - `'submit'` → parse only when the user presses Enter.
|
|
86
|
+
*
|
|
87
|
+
* In `'submit'` mode `nile-change` still fires after the debounce with the
|
|
88
|
+
* raw text, but `errors` / `evaluators` only populate after Enter.
|
|
89
|
+
*/
|
|
90
|
+
validateOn?: 'change' | 'submit';
|
|
91
|
+
/** Optional hint text shown beneath the input describing the syntax. */
|
|
92
|
+
syntaxHint?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Extra functions registered with filtrex on top of its built-in set
|
|
95
|
+
* (`abs`, `ceil`, `floor`, `log`, `log2`, `log10`, `max`, `min`, `round`,
|
|
96
|
+
* `sqrt`, `exists`, `empty`) and our project additions (`contains`,
|
|
97
|
+
* `startsWith`, `endsWith`, `lower`, `upper`, `len`, `isEmpty`,
|
|
98
|
+
* `isNotEmpty`, `between`, `year`, `month`, `day`, `daysAgo`, `matches`,
|
|
99
|
+
* `coalesce`).
|
|
100
|
+
*
|
|
101
|
+
* Example: `{ daysAgo: (d) => (Date.now() - +new Date(d)) / 86400000 }`
|
|
102
|
+
* Used in expressions like `daysAgo(lastSeen) < 7`.
|
|
103
|
+
*/
|
|
104
|
+
extraFunctions?: Record<string, (...args: any[]) => unknown>;
|
|
105
|
+
/**
|
|
106
|
+
* Constants made available to expressions without polluting the data
|
|
107
|
+
* object. `'pi'` (single-quoted in the expression) escapes the constant
|
|
108
|
+
* to fall back to data access.
|
|
109
|
+
*
|
|
110
|
+
* Example: `{ pi: Math.PI, currentUser: 'Birat' }` →
|
|
111
|
+
* `assignee == currentUser` resolves `currentUser` to `"Birat"`.
|
|
112
|
+
*/
|
|
113
|
+
constants?: Record<string, unknown>;
|
|
114
|
+
/**
|
|
115
|
+
* Override comparison / arithmetic operators. Use this to localise
|
|
116
|
+
* comparisons (e.g. case-insensitive string equality) or to plug in
|
|
117
|
+
* a domain-specific arithmetic.
|
|
118
|
+
*
|
|
119
|
+
* Example: `{ '==': (a, b) => String(a).toLowerCase() === String(b).toLowerCase() }`
|
|
120
|
+
*/
|
|
121
|
+
operators?: Partial<{
|
|
122
|
+
'+': (a: unknown, b: unknown) => unknown;
|
|
123
|
+
'-': (a: unknown, b?: unknown) => unknown;
|
|
124
|
+
'*': (a: unknown, b: unknown) => unknown;
|
|
125
|
+
'/': (a: unknown, b: unknown) => unknown;
|
|
126
|
+
'%': (a: unknown, b: unknown) => unknown;
|
|
127
|
+
'^': (a: unknown, b: unknown) => unknown;
|
|
128
|
+
'==': (a: unknown, b: unknown) => boolean;
|
|
129
|
+
'!=': (a: unknown, b: unknown) => boolean;
|
|
130
|
+
'<': (a: unknown, b: unknown) => boolean;
|
|
131
|
+
'>=': (a: unknown, b: unknown) => boolean;
|
|
132
|
+
'<=': (a: unknown, b: unknown) => boolean;
|
|
133
|
+
'>': (a: unknown, b: unknown) => boolean;
|
|
134
|
+
'~=': (a: unknown, b: unknown) => boolean;
|
|
135
|
+
}>;
|
|
136
|
+
/**
|
|
137
|
+
* Optional value-getter for accessing row properties. Filtrex calls
|
|
138
|
+
* this for every identifier reference. Default: dotted property
|
|
139
|
+
* access (`row.foo.bar` works out of the box).
|
|
140
|
+
*
|
|
141
|
+
* Override when your data model uses a Map / class / proxy that
|
|
142
|
+
* standard property access can't read. The fourth argument is the
|
|
143
|
+
* lookup type (`'unescaped'` for bare identifiers, `'single-quoted'`
|
|
144
|
+
* for `'foo-bar'` style).
|
|
145
|
+
*/
|
|
146
|
+
customProp?: (name: string, get: (key: string) => unknown, obj: unknown, type?: 'unescaped' | 'single-quoted') => unknown;
|
|
147
|
+
/**
|
|
148
|
+
* Convenience flag — turn on filtrex's bundled `useDotAccessOperator`
|
|
149
|
+
* customProp. Lets users type `foo.bar` instead of `bar of foo`.
|
|
150
|
+
* Mutually exclusive with a custom `customProp`.
|
|
151
|
+
*/
|
|
152
|
+
dotAccess?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Convenience flag — turn on filtrex's bundled optional-chaining
|
|
155
|
+
* customProp. `a of b` (or `b.a` with `dotAccess`) returns `null`
|
|
156
|
+
* when the LHS is `null`/`undefined` instead of throwing.
|
|
157
|
+
* Mutually exclusive with a custom `customProp`.
|
|
158
|
+
*/
|
|
159
|
+
optionalChaining?: boolean;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Recursive ESTree-shaped node returned by the AST parser. The shape
|
|
163
|
+
* matches what `jsep` produces — a small, well-known JS-expression AST.
|
|
164
|
+
* Pure JSON: every field is a string, number, boolean, null, or another
|
|
165
|
+
* `QueryNode`, so `JSON.stringify` round-trips cleanly.
|
|
166
|
+
*/
|
|
167
|
+
export type QueryNode = {
|
|
168
|
+
type: 'Identifier';
|
|
169
|
+
name: string;
|
|
170
|
+
} | {
|
|
171
|
+
type: 'Literal';
|
|
172
|
+
value: string | number | boolean | null;
|
|
173
|
+
raw?: string;
|
|
174
|
+
} | {
|
|
175
|
+
type: 'BinaryExpression';
|
|
176
|
+
operator: string;
|
|
177
|
+
left: QueryNode;
|
|
178
|
+
right: QueryNode;
|
|
179
|
+
} | {
|
|
180
|
+
type: 'LogicalExpression';
|
|
181
|
+
operator: string;
|
|
182
|
+
left: QueryNode;
|
|
183
|
+
right: QueryNode;
|
|
184
|
+
} | {
|
|
185
|
+
type: 'UnaryExpression';
|
|
186
|
+
operator: string;
|
|
187
|
+
argument: QueryNode;
|
|
188
|
+
prefix: boolean;
|
|
189
|
+
} | {
|
|
190
|
+
type: 'CallExpression';
|
|
191
|
+
callee: QueryNode;
|
|
192
|
+
arguments: QueryNode[];
|
|
193
|
+
} | {
|
|
194
|
+
type: 'MemberExpression';
|
|
195
|
+
computed: boolean;
|
|
196
|
+
object: QueryNode;
|
|
197
|
+
property: QueryNode;
|
|
198
|
+
} | {
|
|
199
|
+
type: 'ArrayExpression';
|
|
200
|
+
elements: QueryNode[];
|
|
201
|
+
} | {
|
|
202
|
+
type: 'Compound';
|
|
203
|
+
body: QueryNode[];
|
|
204
|
+
};
|
|
205
|
+
export type QueryJson = {
|
|
206
|
+
/** The exact string the user typed, retained for display / serialization. */
|
|
207
|
+
source: string;
|
|
208
|
+
ast: QueryNode;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Detail emitted with `nile-change`.
|
|
212
|
+
*
|
|
213
|
+
* `filters[id]` shape depends on the variant + (for prompts) the runtime mode:
|
|
214
|
+
* - any non-prompt variant → the variant's natural value
|
|
215
|
+
* - prompt in `'basic'` mode → plain `string` (raw text)
|
|
216
|
+
* - prompt in `'nql'` mode,
|
|
217
|
+
* parse succeeded → `QueryJson` ({ source, ast }) — JSON-serializable
|
|
218
|
+
* - prompt in `'nql'` mode,
|
|
219
|
+
* pre-validation / parse fail → plain `string` (the typed text)
|
|
220
|
+
*
|
|
221
|
+
* `errors` and `evaluators` are populated for prompt controls in NQL mode.
|
|
222
|
+
*/
|
|
223
|
+
export type FilterChangeDetail = {
|
|
224
|
+
filters: Record<string, unknown>;
|
|
225
|
+
/** Map of control id → human-readable validation message. Present when at least one control failed validation. */
|
|
226
|
+
errors?: Record<string, string>;
|
|
227
|
+
/** Map of control id → compiled filtrex predicate. Present for prompt controls whose expression validated. */
|
|
228
|
+
evaluators?: Record<string, (row: unknown) => unknown>;
|
|
229
|
+
};
|
|
230
|
+
export interface FilterControl {
|
|
231
|
+
/**
|
|
232
|
+
* Unique key under which this control's value is emitted in
|
|
233
|
+
* `nile-change.detail.filters`. Optional — when omitted the component
|
|
234
|
+
* derives one from `label` (slugified) and appends a numeric suffix on
|
|
235
|
+
* collision, falling back to `ctrl_<index>` if `label` is also missing.
|
|
236
|
+
* Provide an explicit `id` when downstream code reads a stable key.
|
|
237
|
+
*/
|
|
238
|
+
id?: string;
|
|
239
|
+
label: string;
|
|
240
|
+
/** Optional subtitle shown below the label. */
|
|
241
|
+
description?: string;
|
|
242
|
+
selection: 'single' | 'multi';
|
|
243
|
+
variant: 'badge' | 'dropdown' | 'segmented' | 'radio' | 'toggle' | 'search' | 'comparison' | 'threshold' | 'preset' | 'prompt' | 'expression' | 'hybrid';
|
|
244
|
+
options?: FilterOption[];
|
|
245
|
+
value?: string | string[] | number[] | boolean;
|
|
246
|
+
/**
|
|
247
|
+
* Placeholder text. Always an array — `placeholders[0]` is the initial /
|
|
248
|
+
* static placeholder used by every variant (search, dropdown, prompt).
|
|
249
|
+
* For the `prompt` variant only, additional entries are cycled through
|
|
250
|
+
* with a typewriter effect.
|
|
251
|
+
*/
|
|
252
|
+
placeholders?: string[];
|
|
253
|
+
/** Typewriter speed in ms per character (default 60). */
|
|
254
|
+
typeSpeedMs?: number;
|
|
255
|
+
/** Pause before deleting a typed phrase (default 1400). */
|
|
256
|
+
pauseBeforeDeleteMs?: number;
|
|
257
|
+
/** Pause between phrases after deletion (default 400). */
|
|
258
|
+
pauseBetweenMs?: number;
|
|
259
|
+
queryLanguage?: QueryLanguageConfig;
|
|
260
|
+
gradientColors?: string[];
|
|
261
|
+
gradientDirection?: string;
|
|
262
|
+
gradientSpeedMs?: number;
|
|
263
|
+
/** Prompt variant only. When true, disables the animated AI gradient border. */
|
|
264
|
+
noAiBorder?: boolean;
|
|
265
|
+
/**
|
|
266
|
+
* Prompt variant only. Inline autocomplete suggestions shown below the
|
|
267
|
+
* input when the prompt is focused (Basic mode). Items are filtered
|
|
268
|
+
* client-side by case-insensitive substring match against the current
|
|
269
|
+
* input text.
|
|
270
|
+
*/
|
|
271
|
+
suggestions?: SuggestionItem[];
|
|
272
|
+
/**
|
|
273
|
+
* How a clicked suggestion is inserted into the input value.
|
|
274
|
+
* - `'replace'` (default) → suggestion's `value` replaces the entire input
|
|
275
|
+
* - `'append'` → appends the suggestion to the existing text (space-separated)
|
|
276
|
+
*/
|
|
277
|
+
suggestionInsert?: 'replace' | 'append';
|
|
278
|
+
valueB?: string;
|
|
279
|
+
operator?: string;
|
|
280
|
+
thresholdValue?: number | string;
|
|
281
|
+
}
|
|
282
|
+
export interface FilterGroup {
|
|
283
|
+
type: 'group';
|
|
284
|
+
label: string;
|
|
285
|
+
description?: string;
|
|
286
|
+
collapsible?: boolean;
|
|
287
|
+
controls: FilterControl[];
|
|
288
|
+
}
|
|
289
|
+
export type FilterEntry = FilterControl | FilterGroup;
|
|
290
|
+
export type FilterChartConfig = {
|
|
291
|
+
type: 'filter';
|
|
292
|
+
controls: FilterEntry[];
|
|
293
|
+
[key: string]: unknown;
|
|
294
|
+
};
|
|
295
|
+
export type FilterChartSeparatedPayload = {
|
|
296
|
+
chart: FilterChartConfig;
|
|
297
|
+
aq?: Record<string, unknown>;
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Surface that variant render functions need from the host element.
|
|
301
|
+
* The chart class implements this so render functions stay decoupled
|
|
302
|
+
* from the LitElement subclass.
|
|
303
|
+
*/
|
|
304
|
+
/** Runtime mode for a prompt control. `'basic'` = plain text, no validation. `'nql'` = strict filtrex parsing. */
|
|
305
|
+
export type PromptMode = 'basic' | 'nql';
|
|
306
|
+
/**
|
|
307
|
+
* A `FilterControl` after the chart has filled in a guaranteed `id` (either
|
|
308
|
+
* the author-supplied one, or one derived from `label`). Render helpers and
|
|
309
|
+
* host methods receive this — they always have a stable key to read/write
|
|
310
|
+
* per-control state.
|
|
311
|
+
*/
|
|
312
|
+
export type NormalizedFilterControl = FilterControl & {
|
|
313
|
+
id: string;
|
|
314
|
+
};
|
|
315
|
+
export interface FilterChartHost {
|
|
316
|
+
readonly selectedValues: Map<string, unknown>;
|
|
317
|
+
readonly promptPlaceholder: Map<string, string>;
|
|
318
|
+
/** Prompt validation messages, keyed by control id. */
|
|
319
|
+
readonly promptErrors: Map<string, string>;
|
|
320
|
+
/** Per-control prompt mode (only set for prompt controls with `queryLanguage.enabled`). */
|
|
321
|
+
readonly promptModes: Map<string, PromptMode>;
|
|
322
|
+
/** Highlighted suggestion index per prompt control id (-1 = none highlighted). */
|
|
323
|
+
readonly promptActiveIndex: Map<string, number>;
|
|
324
|
+
setValue(id: string, value: unknown): void;
|
|
325
|
+
emit(name: string, detail?: unknown): void;
|
|
326
|
+
/** Update a prompt's value and (when in NQL mode) re-validate it. */
|
|
327
|
+
handlePromptInput(ctrl: NormalizedFilterControl, value: string): void;
|
|
328
|
+
/** Switch a prompt control's mode at runtime; clears or re-runs validation accordingly. */
|
|
329
|
+
setPromptMode(ctrl: NormalizedFilterControl, mode: PromptMode): void;
|
|
330
|
+
/** Force-validate a prompt's current value (used by `validateOn: 'submit'` on Enter). */
|
|
331
|
+
submitPrompt(ctrl: NormalizedFilterControl): void;
|
|
332
|
+
/** Set the highlighted suggestion index. Use -1 to clear the highlight. */
|
|
333
|
+
setPromptActiveIndex(id: string, idx: number): void;
|
|
334
|
+
}
|
|
@@ -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(
|
|
50
|
+
--nile-kpi-value-font-size: clamp(1rem, 5cqi + 0.25rem, 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));
|
|
@@ -111,13 +111,17 @@ export const styles = css `
|
|
|
111
111
|
flex: 1 1 auto;
|
|
112
112
|
display: flex;
|
|
113
113
|
flex-direction: column;
|
|
114
|
-
gap: var(--nile-kpi-content-gap);
|
|
114
|
+
/* gap: var(--nile-kpi-content-gap); */
|
|
115
115
|
padding: var(--nile-kpi-padding-v) var(--nile-kpi-padding-h);
|
|
116
116
|
min-width: 0;
|
|
117
|
+
width: 100%;
|
|
118
|
+
max-width: 100%;
|
|
119
|
+
box-sizing: border-box;
|
|
117
120
|
overflow: hidden;
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
.kpi-label {
|
|
124
|
+
display: block;
|
|
121
125
|
margin: 0;
|
|
122
126
|
font-family: var(--nile-font-family-serif, var(--ng-font-family-body));
|
|
123
127
|
font-size: var(--nile-kpi-label-font-size);
|
|
@@ -129,6 +133,8 @@ export const styles = css `
|
|
|
129
133
|
text-overflow: ellipsis;
|
|
130
134
|
min-width: 0;
|
|
131
135
|
width: 100%;
|
|
136
|
+
max-width: 100%;
|
|
137
|
+
box-sizing: border-box;
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
.kpi-value-row {
|
|
@@ -149,7 +155,10 @@ export const styles = css `
|
|
|
149
155
|
line-height: 1.2;
|
|
150
156
|
cursor: default;
|
|
151
157
|
white-space: nowrap;
|
|
152
|
-
|
|
158
|
+
min-width: 0;
|
|
159
|
+
flex: 0 1 auto;
|
|
160
|
+
overflow: hidden;
|
|
161
|
+
text-overflow: ellipsis;
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
.kpi-prefix,
|
|
@@ -204,16 +213,18 @@ export const styles = css `
|
|
|
204
213
|
}
|
|
205
214
|
|
|
206
215
|
.kpi-description {
|
|
216
|
+
display: block;
|
|
207
217
|
margin: 0;
|
|
208
218
|
font-family: var(--nile-font-family-serif, var(--ng-font-family-body));
|
|
209
219
|
font-size: var(--nile-kpi-description-font-size);
|
|
210
220
|
color: var(--nile-kpi-description-color);
|
|
211
221
|
line-height: 1.5;
|
|
212
|
-
white-space: nowrap;
|
|
213
222
|
overflow: hidden;
|
|
214
223
|
text-overflow: ellipsis;
|
|
215
224
|
min-width: 0;
|
|
216
225
|
width: 100%;
|
|
226
|
+
max-width: 100%;
|
|
227
|
+
box-sizing: border-box;
|
|
217
228
|
}
|
|
218
229
|
|
|
219
230
|
.kpi-sparkline {
|
|
@@ -223,6 +234,53 @@ export const styles = css `
|
|
|
223
234
|
margin-top: var(--nile-spacing-xs, var(--ng-spacing-xs));
|
|
224
235
|
}
|
|
225
236
|
|
|
237
|
+
/* ── Container queries: scale down for narrow cards ── */
|
|
238
|
+
|
|
239
|
+
/* Medium-small: ~280px and below — tighten padding, shrink prefix/suffix. */
|
|
240
|
+
@container (max-width: 280px) {
|
|
241
|
+
:host {
|
|
242
|
+
--nile-kpi-padding-v: var(--nile-spacing-lg, var(--ng-spacing-lg));
|
|
243
|
+
--nile-kpi-padding-h: var(--nile-spacing-xl, var(--ng-spacing-xl));
|
|
244
|
+
--nile-kpi-prefix-suffix-font-size: var(--nile-type-scale-4, var(--ng-font-size-text-md));
|
|
245
|
+
--nile-kpi-label-font-size: var(--nile-type-scale-2, var(--ng-font-size-text-xs));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.kpi-value-row {
|
|
249
|
+
gap: var(--nile-spacing-xs, var(--ng-spacing-xs));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.kpi-trend {
|
|
253
|
+
padding: 0;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* Small: ~220px and below — tighten further; description/sparkline stay visible. */
|
|
258
|
+
@container (max-width: 220px) {
|
|
259
|
+
:host {
|
|
260
|
+
--nile-kpi-padding-v: var(--nile-spacing-md, var(--ng-spacing-md));
|
|
261
|
+
--nile-kpi-padding-h: var(--nile-spacing-lg, var(--ng-spacing-lg));
|
|
262
|
+
--nile-kpi-prefix-suffix-font-size: var(--nile-type-scale-3, var(--ng-font-size-text-sm));
|
|
263
|
+
--nile-kpi-description-font-size: var(--nile-type-scale-1, var(--ng-font-size-text-xs));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.kpi-sparkline {
|
|
267
|
+
flex-basis: 32px;
|
|
268
|
+
min-height: 18px;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* Very small: under ~180px — stack the trend below the value so neither clips. */
|
|
273
|
+
@container (max-width: 180px) {
|
|
274
|
+
.kpi-value-row {
|
|
275
|
+
flex-wrap: wrap;
|
|
276
|
+
row-gap: var(--nile-spacing-xs, var(--ng-spacing-xs));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.kpi-trend {
|
|
280
|
+
flex-basis: 100%;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
226
284
|
/* ── Gauge variant ── */
|
|
227
285
|
|
|
228
286
|
.kpi--gauge {
|