@aquera/nile-visualization 2.6.0 → 2.7.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.
@@ -17,6 +17,99 @@ export interface TreeNode {
17
17
  expanded?: boolean;
18
18
  children?: TreeNode[];
19
19
  }
20
+ export interface QueryLanguageConfig {
21
+ /** Enables the query layer at all. Default: false. */
22
+ enabled: boolean;
23
+ /**
24
+ * - `'auto'` → submit emits the raw typed string.
25
+ * - `'strict'` → submit parses the input with filtrex and emits a
26
+ * pure-JSON payload (`{ source }`) plus a sibling
27
+ * `evaluate` function on the event detail. On
28
+ * parse failure, `nile-prompt-parse-error` fires and
29
+ * `nile-prompt-submit` does NOT fire.
30
+ * Default: `'auto'`.
31
+ */
32
+ mode?: 'auto' | 'strict';
33
+ /** Parser engine. Currently only `'filtrex'` is supported. */
34
+ engine?: 'filtrex';
35
+ /** Optional hint text shown beneath the input describing the syntax. */
36
+ syntaxHint?: string;
37
+ /**
38
+ * Extra functions registered with filtrex on top of the built-in set.
39
+ * Each function receives the resolved argument values from the
40
+ * expression and returns a value usable in further comparisons.
41
+ *
42
+ * Example: `{ daysAgo: (d) => (Date.now() - +new Date(d)) / 86400000 }`
43
+ * Used in expressions like `daysAgo(lastSeen) < 7`.
44
+ */
45
+ extraFunctions?: Record<string, (...args: any[]) => unknown>;
46
+ /**
47
+ * Optional value-getter for accessing row properties. Filtrex calls
48
+ * this for every identifier reference. Default: dotted property
49
+ * access (`row.foo.bar` works out of the box).
50
+ *
51
+ * Override when your data model uses a Map / class / proxy that
52
+ * standard property access can't read.
53
+ */
54
+ customProp?: (name: string, get: (key: string) => unknown, obj: unknown) => unknown;
55
+ }
56
+ /**
57
+ * Recursive ESTree-shaped node returned by the AST parser. The shape
58
+ * matches what `jsep` produces — a small, well-known JS-expression AST.
59
+ * Pure JSON: every field is a string, number, boolean, null, or another
60
+ * `QueryNode`, so `JSON.stringify` round-trips cleanly.
61
+ */
62
+ export type QueryNode = {
63
+ type: 'Identifier';
64
+ name: string;
65
+ } | {
66
+ type: 'Literal';
67
+ value: string | number | boolean | null;
68
+ raw?: string;
69
+ } | {
70
+ type: 'BinaryExpression';
71
+ operator: string;
72
+ left: QueryNode;
73
+ right: QueryNode;
74
+ } | {
75
+ type: 'LogicalExpression';
76
+ operator: string;
77
+ left: QueryNode;
78
+ right: QueryNode;
79
+ } | {
80
+ type: 'UnaryExpression';
81
+ operator: string;
82
+ argument: QueryNode;
83
+ prefix: boolean;
84
+ } | {
85
+ type: 'CallExpression';
86
+ callee: QueryNode;
87
+ arguments: QueryNode[];
88
+ } | {
89
+ type: 'MemberExpression';
90
+ computed: boolean;
91
+ object: QueryNode;
92
+ property: QueryNode;
93
+ } | {
94
+ type: 'ArrayExpression';
95
+ elements: QueryNode[];
96
+ } | {
97
+ type: 'Compound';
98
+ body: QueryNode[];
99
+ };
100
+ export type QueryJson = {
101
+ /** The exact string the user typed, retained for display / serialization. */
102
+ source: string;
103
+ ast: QueryNode;
104
+ };
105
+ export type QuerySubmitDetail = {
106
+ id: string;
107
+ /** JSON-serializable payload — string in auto mode, `QueryJson` in strict. */
108
+ value: string | QueryJson;
109
+ /** Compiled filtrex predicate. Only present in strict mode on success.
110
+ * Use as `data.filter(e.detail.evaluate)`. */
111
+ evaluate?: (row: unknown) => unknown;
112
+ };
20
113
  export interface FilterControl {
21
114
  id: string;
22
115
  label: string;
@@ -50,28 +143,26 @@ export interface FilterControl {
50
143
  * Prompt variant only. Inline callback invoked when the user presses
51
144
  * Enter inside the prompt input. Called in addition to the
52
145
  * `nile-prompt-submit` event.
53
- */
54
- onSubmit?: (value: string, id: string) => void;
55
- /**
56
- * Prompt variant only. Colors used for the animated border gradient.
57
- * Pass any number of CSS color strings (hex / rgb / hsl / named); they
58
- * are evenly distributed across the gradient by default. To control
59
- * stops explicitly, include positions in the strings themselves
60
- * (e.g. `'#2563eb 0%'`, `'#06b6d4 50%'`).
61
146
  *
62
- * Example: `['#ec4899', '#f97316', '#facc15', '#ec4899']`
63
- * Default: an indigocyan violet indigo blue palette.
147
+ * - auto mode (default) → `value` is the raw `string` the user typed.
148
+ * - strict mode (success)`value` is a `QueryJson` (pure-JSON
149
+ * `{ source }`); the compiled predicate is
150
+ * passed as the third argument so callers
151
+ * that want to filter inline can. JSON
152
+ * consumers can ignore that argument.
153
+ * - strict mode (parse fail) → routes through `onParseError` instead;
154
+ * `onSubmit` is NOT called.
64
155
  */
65
- gradientColors?: string[];
156
+ onSubmit?: (value: string | QueryJson, id: string, evaluate?: (row: unknown) => unknown) => void;
66
157
  /**
67
- * Prompt variant only. Linear-gradient direction (e.g. `'90deg'`,
68
- * `'to right'`, `'45deg'`). Default: `'90deg'`.
158
+ * Prompt variant only. Inline callback invoked when `queryLanguage.mode`
159
+ * is `'strict'` and the user-typed expression FAILS to parse on submit.
160
+ * Mirrors the `nile-prompt-parse-error` event.
69
161
  */
162
+ onParseError?: (input: string, error: Error, id: string) => void;
163
+ queryLanguage?: QueryLanguageConfig;
164
+ gradientColors?: string[];
70
165
  gradientDirection?: string;
71
- /**
72
- * Prompt variant only. Animation duration in milliseconds for one full
73
- * gradient sweep. Default: 4500.
74
- */
75
166
  gradientSpeedMs?: number;
76
167
  valueB?: string;
77
168
  operator?: string;
@@ -124,6 +215,12 @@ export declare class NileFilterChart extends NileElement {
124
215
  private _renderComparison;
125
216
  private _renderThreshold;
126
217
  private _renderPreset;
218
+ private static readonly _builtInFiltrexFns;
219
+ private static _filtrexLoading;
220
+ private static _getCompileExpression;
221
+ private static _jsepLoading;
222
+ private static _getJsep;
223
+ private _handleSubmit;
127
224
  private _renderPrompt;
128
225
  private _renderControl;
129
226
  private _renderGroup;
@@ -1,9 +1,10 @@
1
+ var NileFilterChart_1;
1
2
  import { __decorate } from "tslib";
2
3
  import { html, nothing } from 'lit';
3
4
  import { customElement, property, state } from 'lit/decorators.js';
4
5
  import { styles } from './nile-filter-chart.css.js';
5
6
  import NileElement from '../internal/nile-element.js';
6
- let NileFilterChart = class NileFilterChart extends NileElement {
7
+ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElement {
7
8
  constructor() {
8
9
  super(...arguments);
9
10
  this.config = null;
@@ -425,21 +426,80 @@ let NileFilterChart = class NileFilterChart extends NileElement {
425
426
  </button>`)}
426
427
  </div>`;
427
428
  }
428
- // ── Prompt ───────────────────────────────────────────────────────────────────
429
- //
430
- // Renders a free-text input wrapped in a div whose border is an animated
431
- // multi-stop blue gradient (CSS @keyframes — purely visual, no JS in the
432
- // animation loop). The placeholder is driven by a typewriter that cycles
433
- // through `ctrl.placeholders`. Each keystroke fires `nile-change` with the
434
- // current string; pressing Enter additionally fires `nile-prompt-submit`
435
- // for AI-style "ask" UIs.
429
+ static async _getCompileExpression() {
430
+ if (!NileFilterChart_1._filtrexLoading) {
431
+ NileFilterChart_1._filtrexLoading = import('filtrex').then(m => {
432
+ const fn = m.compileExpression
433
+ ?? m.default?.compileExpression;
434
+ if (typeof fn !== 'function') {
435
+ throw new Error('filtrex: compileExpression export not found');
436
+ }
437
+ return fn;
438
+ });
439
+ }
440
+ return NileFilterChart_1._filtrexLoading;
441
+ }
442
+ static async _getJsep() {
443
+ if (!NileFilterChart_1._jsepLoading) {
444
+ NileFilterChart_1._jsepLoading = import('jsep').then(m => {
445
+ const candidate = m.default ?? m;
446
+ if (typeof candidate !== 'function') {
447
+ throw new Error('jsep: function export not found');
448
+ }
449
+ const fn = candidate;
450
+ try {
451
+ fn.addBinaryOp?.('and', 2);
452
+ fn.addBinaryOp?.('or', 1);
453
+ fn.addBinaryOp?.('in', 6);
454
+ fn.addUnaryOp?.('not');
455
+ }
456
+ catch {
457
+ }
458
+ return fn;
459
+ });
460
+ }
461
+ return NileFilterChart_1._jsepLoading;
462
+ }
463
+ async _handleSubmit(value, ctrl) {
464
+ const ql = ctrl.queryLanguage;
465
+ if (!ql?.enabled || (ql.mode ?? 'auto') === 'auto') {
466
+ this.emit('nile-prompt-submit', { id: ctrl.id, value });
467
+ if (typeof ctrl.onSubmit === 'function')
468
+ ctrl.onSubmit(value, ctrl.id);
469
+ return;
470
+ }
471
+ try {
472
+ const jsep = await NileFilterChart_1._getJsep();
473
+ const ast = jsep(value);
474
+ const compileExpression = await NileFilterChart_1._getCompileExpression();
475
+ const extraFunctions = {
476
+ ...NileFilterChart_1._builtInFiltrexFns,
477
+ ...(ql.extraFunctions ?? {}),
478
+ };
479
+ const evaluate = compileExpression(value, {
480
+ extraFunctions,
481
+ ...(ql.customProp ? { customProp: ql.customProp } : {}),
482
+ });
483
+ const json = { source: value, ast };
484
+ this.emit('nile-prompt-submit', { id: ctrl.id, value: json, evaluate });
485
+ if (typeof ctrl.onSubmit === 'function')
486
+ ctrl.onSubmit(json, ctrl.id, evaluate);
487
+ }
488
+ catch (err) {
489
+ const error = err instanceof Error ? err : new Error(String(err));
490
+ this.emit('nile-prompt-parse-error', {
491
+ id: ctrl.id,
492
+ input: value,
493
+ error: { message: error.message },
494
+ });
495
+ if (typeof ctrl.onParseError === 'function') {
496
+ ctrl.onParseError(value, error, ctrl.id);
497
+ }
498
+ }
499
+ }
436
500
  _renderPrompt(ctrl) {
437
501
  const value = String(this.selectedValues.get(ctrl.id) ?? '');
438
502
  const animated = this._promptPlaceholder.get(ctrl.id) ?? '';
439
- // Build the inline style overrides as CSS custom properties — these
440
- // are picked up by the .fc-prompt rule via var(--fc-prompt-gradient,
441
- // <default>) / var(--fc-prompt-gradient-speed, 4.5s). Only emit a
442
- // declaration when the consumer actually supplied an override.
443
503
  const styleParts = [];
444
504
  if (ctrl.gradientColors && ctrl.gradientColors.length > 0) {
445
505
  const dir = ctrl.gradientDirection ?? '90deg';
@@ -484,10 +544,7 @@ let NileFilterChart = class NileFilterChart extends NileElement {
484
544
  @keydown="${(e) => {
485
545
  if (e.key === 'Enter') {
486
546
  const t = e.target;
487
- this.emit('nile-prompt-submit', { id: ctrl.id, value: t.value });
488
- if (typeof ctrl.onSubmit === 'function') {
489
- ctrl.onSubmit(t.value, ctrl.id);
490
- }
547
+ this._handleSubmit(t.value, ctrl);
491
548
  }
492
549
  }}"
493
550
  />
@@ -577,6 +634,25 @@ let NileFilterChart = class NileFilterChart extends NileElement {
577
634
  </div>`;
578
635
  }
579
636
  };
637
+ NileFilterChart._builtInFiltrexFns = {
638
+ contains: (s, sub) => String(s ?? '').includes(String(sub ?? '')),
639
+ startsWith: (s, p) => String(s ?? '').startsWith(String(p ?? '')),
640
+ endsWith: (s, p) => String(s ?? '').endsWith(String(p ?? '')),
641
+ lower: (s) => String(s ?? '').toLowerCase(),
642
+ upper: (s) => String(s ?? '').toUpperCase(),
643
+ len: (s) => (s == null ? 0 : Array.isArray(s) ? s.length : String(s).length),
644
+ isEmpty: (s) => s == null || s === '' || (Array.isArray(s) && s.length === 0),
645
+ isNotEmpty: (s) => !(s == null || s === '' || (Array.isArray(s) && s.length === 0)),
646
+ between: (n, lo, hi) => Number(n) >= Number(lo) && Number(n) <= Number(hi),
647
+ year: (d) => new Date(d).getFullYear(),
648
+ month: (d) => new Date(d).getMonth() + 1,
649
+ day: (d) => new Date(d).getDate(),
650
+ daysAgo: (d) => (Date.now() - +new Date(d)) / 86400000,
651
+ matches: (s, re) => new RegExp(String(re ?? '')).test(String(s ?? '')),
652
+ coalesce: (...xs) => xs.find(x => x != null && x !== '') ?? null,
653
+ };
654
+ NileFilterChart._filtrexLoading = null;
655
+ NileFilterChart._jsepLoading = null;
580
656
  __decorate([
581
657
  property({ attribute: false })
582
658
  ], NileFilterChart.prototype, "config", void 0);
@@ -589,7 +665,7 @@ __decorate([
589
665
  __decorate([
590
666
  state()
591
667
  ], NileFilterChart.prototype, "_promptPlaceholder", void 0);
592
- NileFilterChart = __decorate([
668
+ NileFilterChart = NileFilterChart_1 = __decorate([
593
669
  customElement('nile-filter-chart')
594
670
  ], NileFilterChart);
595
671
  export { NileFilterChart };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aquera/nile-visualization",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "A visualization Library for the Nile Design System",
5
5
  "license": "MIT",
6
6
  "author": "Aquera Inc",
@@ -57,7 +57,9 @@
57
57
  "clean": "rimraf dist/*"
58
58
  },
59
59
  "dependencies": {
60
- "lit": "^3.0.0"
60
+ "lit": "^3.0.0",
61
+ "filtrex": "^3.0.0",
62
+ "jsep": "^1.3.9"
61
63
  },
62
64
  "peerDependencies": {
63
65
  "highcharts": ">=10.0.0",