@aquera/nile-visualization 2.9.8 → 2.9.10

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.
@@ -1,4 +1,9 @@
1
1
  import { html, nothing } from 'lit';
2
+ import { live } from 'lit/directives/live.js';
3
+ import { ref } from 'lit/directives/ref.js';
4
+ // Per-element ResizeObserver bookkeeping. WeakMap so observers are GC'd
5
+ // alongside their elements when the prompt is unmounted.
6
+ const _fieldObservers = new WeakMap();
2
7
  const PUNCT_TOKENS = new Set(['(', ')', '[', ']', ',']);
3
8
  /**
4
9
  * Splits the input into the tokens analyzePromptCtx reasons about:
@@ -227,21 +232,28 @@ export async function validatePromptExpression(value, ctrl) {
227
232
  return { ast, evaluate };
228
233
  }
229
234
  /**
230
- * Renders one segment of the Basic / NQL toggle. Defaults match the Jira-style
231
- * layout: Basic shows "Basic" text; NQL shows "JQL" text. Either segment can
232
- * be overridden with a label, an icon, or both.
235
+ * Renders one segment of the Prompt / Expression toggle. Defaults: Prompt
236
+ * shows "Prompt"; Expression shows "Expression". Either segment can be
237
+ * overridden with a label, an icon, or both.
233
238
  */
234
239
  function renderModeToggleSegment(segValue, cfg) {
235
- const defaults = segValue === 'basic'
236
- ? { label: 'Basic', icon: undefined, aria: 'Basic mode' }
237
- : { label: 'JQL', icon: undefined, aria: 'JQL / expression mode' };
240
+ const defaults = segValue === 'prompt'
241
+ ? { label: 'Prompt', icon: undefined, aria: 'Prompt mode', glyph: 'ng-sparkles' }
242
+ : { label: 'Expression', icon: undefined, aria: 'Expression mode', glyph: 'ng-code' };
238
243
  const label = cfg?.label ?? defaults.label;
239
244
  const icon = cfg?.icon ?? defaults.icon;
240
245
  const aria = cfg?.ariaLabel ?? label ?? defaults.aria;
241
246
  return html `
242
247
  <nile-button-toggle value="${segValue}" aria-label="${aria}">
243
248
  ${icon ? html `<nile-icon class="fc-prompt__mode-icon" name="${icon}" size="14"></nile-icon>` : nothing}
244
- ${label ? html `<span>${label}</span>` : nothing}
249
+ ${label ? html `<span class="fc-prompt__mode-label">${label}</span>` : nothing}
250
+ <nile-glyph
251
+ class="fc-prompt__mode-glyph"
252
+ name="${defaults.glyph}"
253
+ size="14"
254
+ method="stroke"
255
+ aria-hidden="true"
256
+ ></nile-glyph>
245
257
  </nile-button-toggle>
246
258
  `;
247
259
  }
@@ -255,23 +267,16 @@ function renderModeToggle(host, ctrl, mode) {
255
267
  @nile-change="${(e) => {
256
268
  e.stopPropagation();
257
269
  const next = e.detail?.value;
258
- if (next === 'basic' || next === 'nql')
270
+ if (next === 'prompt' || next === 'expression')
259
271
  host.setPromptMode(ctrl, next);
260
272
  }}"
261
273
  >
262
- ${renderModeToggleSegment('nql', toggleCfg?.nql)}
263
- ${renderModeToggleSegment('basic', toggleCfg?.basic)}
274
+ ${renderModeToggleSegment('expression', toggleCfg?.expression)}
275
+ ${renderModeToggleSegment('prompt', toggleCfg?.prompt)}
264
276
  </nile-button-toggle-group>
265
277
  `;
266
278
  }
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';
279
+ const SUGGESTION_LAYOUT_STYLE_ID = 'fc-prompt-suggestion-layout-v4';
275
280
  function ensureSuggestionLayoutStyles() {
276
281
  if (typeof document === 'undefined')
277
282
  return;
@@ -283,6 +288,41 @@ function ensureSuggestionLayoutStyles() {
283
288
  .fc-prompt__menu nile-menu-item::part(label) {
284
289
  flex: 1;
285
290
  min-width: 0;
291
+ overflow: hidden;
292
+ }
293
+
294
+ .fc-prompt__suggestion-label {
295
+ display: block;
296
+ max-width: 100%;
297
+ overflow: hidden;
298
+ text-overflow: ellipsis;
299
+ white-space: nowrap;
300
+ }
301
+
302
+ .fc-prompt__suggestion-tooltip {
303
+ display: block;
304
+ flex: 1;
305
+ min-width: 0;
306
+ overflow: hidden;
307
+ }
308
+
309
+
310
+
311
+ [data-tippy-root] {
312
+ max-width: var(--fc-prompt-tooltip-max-width, 320px);
313
+ }
314
+ [data-tippy-root] > .tippy-box,
315
+ .tippy-box[data-theme~='lite'] {
316
+ max-width: var(--fc-prompt-tooltip-max-width, 320px);
317
+ white-space: normal;
318
+ overflow-wrap: anywhere;
319
+ word-break: break-word;
320
+ }
321
+ [data-tippy-root] > .tippy-box > .tippy-content {
322
+ white-space: normal;
323
+ overflow-wrap: anywhere;
324
+ word-break: break-word;
325
+ max-width: 100%;
286
326
  }
287
327
  .fc-prompt__suggestion-tag {
288
328
  flex-shrink: 0;
@@ -304,16 +344,26 @@ export function renderPrompt(host, ctrl) {
304
344
  const animated = host.promptPlaceholder.get(ctrl.id) ?? '';
305
345
  const error = host.promptErrors.get(ctrl.id);
306
346
  const mode = host.promptModes.get(ctrl.id);
347
+ // Pick the placeholder for the active mode. Hybrid/Expression variants
348
+ // can expose mode-specific lists; everything else falls back to the shared
349
+ // `placeholders[0]`. The animated typewriter (when active) overrides all.
350
+ const supportsModePlaceholders = ctrl.variant === 'hybrid' || ctrl.variant === 'expression';
351
+ const modePlaceholder = supportsModePlaceholders && mode === 'expression' && ctrl.expressionPlaceholders?.length
352
+ ? ctrl.expressionPlaceholders[0]
353
+ : supportsModePlaceholders && mode === 'prompt' && ctrl.promptPlaceholders?.length
354
+ ? ctrl.promptPlaceholders[0]
355
+ : (ctrl.placeholders?.[0] ?? '');
356
+ const placeholder = animated || modePlaceholder;
307
357
  // Toggle shows only when the consumer explicitly opted into a hybrid surface —
308
358
  // the standalone `expression` variant is filtrex-only by design and never
309
359
  // renders a Basic / JQL switch even though queryLanguage is enabled.
310
360
  const showToggle = !!ctrl.queryLanguage?.enabled && !!mode && ctrl.variant !== 'expression';
311
361
  // Suggestions + inline syntax highlighting are filtrex-mode-only:
312
362
  // - `expression` variant → always on
313
- // - `hybrid` variant → only when the toggle is on Expression (`nql`)
363
+ // - `hybrid` variant → only when the toggle is on Expression
314
364
  // - `prompt` variant → never (plain free text)
315
365
  const isFiltrexMode = ctrl.variant === 'expression'
316
- || (ctrl.variant === 'hybrid' && mode === 'nql');
366
+ || (ctrl.variant === 'hybrid' && mode === 'expression');
317
367
  const styleParts = [];
318
368
  if (ctrl.gradientColors && ctrl.gradientColors.length > 0) {
319
369
  const dir = ctrl.gradientDirection ?? '90deg';
@@ -376,7 +426,7 @@ export function renderPrompt(host, ctrl) {
376
426
  default:
377
427
  return userSuggestions.filter(s => s.type === 'attribute'
378
428
  || s.type === 'function'
379
- || s.type === 'snippet'
429
+ || s.type === 'suggestion'
380
430
  || !s.type);
381
431
  }
382
432
  })();
@@ -511,10 +561,19 @@ export function renderPrompt(host, ctrl) {
511
561
  host.setPromptActiveIndex(ctrl.id, -1);
512
562
  host.handlePromptInput(ctrl, next);
513
563
  requestAnimationFrame(() => {
514
- if (inputEl.isConnected && atEnd()) {
515
- // Over-scroll past max so the browser pins to the absolute right edge.
516
- inputEl.scrollLeft = inputEl.scrollWidth + 100;
517
- }
564
+ if (!inputEl.isConnected || !atEnd())
565
+ return;
566
+ // Over-scroll past max so the browser pins to the absolute righ t edge.
567
+ inputEl.scrollLeft = inputEl.scrollWidth + 100;
568
+ // Explicitly re-sync the overlay AFTER Lit has re-rendered its tokens
569
+ // for the new value. Relying on @scroll for this fails: that event
570
+ // fires when the browser scrolls the input on keystroke (BEFORE the
571
+ // re-render), at which point the overlay's old, shorter content
572
+ // clamps the synced scrollLeft to its stale max — leaving the typed
573
+ // glyph offscreen and the caret visually one char ahead of the text.
574
+ const highlightEl = inputEl.parentElement?.querySelector('.fc-prompt__highlight');
575
+ if (highlightEl)
576
+ highlightEl.scrollLeft = inputEl.scrollLeft;
518
577
  });
519
578
  };
520
579
  const pickItem = (item, inputEl) => {
@@ -621,17 +680,19 @@ export function renderPrompt(host, ctrl) {
621
680
  }
622
681
  return;
623
682
  }
683
+ if (e.key === 'Tab' && !e.shiftKey && len > 0) {
684
+ const idx = current >= 0 ? current : 0;
685
+ e.preventDefault();
686
+ e.stopPropagation();
687
+ pickItem(suggestionData[idx], inputEl);
688
+ return;
689
+ }
624
690
  if (e.key === 'Escape' && current >= 0) {
625
691
  e.preventDefault();
626
692
  e.stopPropagation();
627
693
  host.setPromptActiveIndex(ctrl.id, -1);
628
694
  }
629
695
  };
630
- // The native <input> scrolls horizontally to keep the caret visible, but
631
- // the syntax-highlight overlay (`.fc-prompt__highlight`) is `overflow:
632
- // hidden` and would otherwise clip at the right edge — leaving the typed
633
- // text invisible past the field width. Mirror the input's scrollLeft onto
634
- // the overlay so both layers stay aligned.
635
696
  const onScroll = (e) => {
636
697
  const inputEl = e.currentTarget;
637
698
  const highlightEl = inputEl.parentElement?.querySelector('.fc-prompt__highlight');
@@ -640,7 +701,7 @@ export function renderPrompt(host, ctrl) {
640
701
  };
641
702
  const classes = [
642
703
  'fc-prompt',
643
- ctrl.noAiBorder ? 'fc-prompt--no-ai-border' : '',
704
+ ctrl.aiBorder ? 'fc-prompt--ai-border' : '',
644
705
  error ? 'fc-prompt--error' : '',
645
706
  ].filter(Boolean).join(' ');
646
707
  const onFocus = () => host.setPromptFocused(ctrl.id, true);
@@ -664,6 +725,27 @@ export function renderPrompt(host, ctrl) {
664
725
  };
665
726
  const isFocused = host.promptFocusedId === ctrl.id;
666
727
  const dropdownOpen = isFiltrexMode && isFocused && suggestionData.length > 0;
728
+ // Clears the prompt value, fires the existing `nile-change` flow (via
729
+ // submitPrompt → _emitChange) and additionally emits a dedicated
730
+ // `nile-clear` event so consumers can distinguish a user-initiated clear
731
+ // from a normal value change. `source` lets them tell the two surfaces
732
+ // apart ('icon' = in-input cross, 'button' = sibling Clear filter button).
733
+ const clearValue = (e, source) => {
734
+ host.handlePromptInput(ctrl, '');
735
+ host.submitPrompt(ctrl);
736
+ host.setPromptActiveIndex(ctrl.id, -1);
737
+ host.emit('nile-clear', { id: ctrl.id, source });
738
+ const target = e.currentTarget;
739
+ const root = (target?.getRootNode() ?? document);
740
+ const inputId = `fc-prompt-${ctrl.id}`;
741
+ requestAnimationFrame(() => {
742
+ const inp = root.querySelector(`#${CSS.escape(inputId)}`);
743
+ if (!inp)
744
+ return;
745
+ inp.value = '';
746
+ inp.focus({ preventScroll: true });
747
+ });
748
+ };
667
749
  return html `
668
750
  <div class="fc-prompt-row">
669
751
  <div class="${classes} fc-prompt--row-input" part="filter-prompt" style="${inlineStyle}">
@@ -683,30 +765,64 @@ export function renderPrompt(host, ctrl) {
683
765
  slot="trigger"
684
766
  class="fc-prompt__field${isFiltrexMode ? ' fc-prompt__field--highlight' : ''}"
685
767
  part="filter-prompt-field"
768
+ ${ref((el) => {
769
+ if (!el)
770
+ return;
771
+ if (_fieldObservers.has(el))
772
+ return;
773
+ const obs = new ResizeObserver(([entry]) => {
774
+ if (entry)
775
+ host.setPromptFieldWidth(ctrl.id, entry.contentRect.width);
776
+ });
777
+ obs.observe(el);
778
+ _fieldObservers.set(el, obs);
779
+ host.setPromptFieldWidth(ctrl.id, el.getBoundingClientRect().width);
780
+ })}
686
781
  >
687
- ${isFiltrexMode ? html `
688
- <div class="fc-prompt__highlight" aria-hidden="true">${tokens.map((tok) => {
782
+ <div class="fc-prompt__field-input">
783
+ ${isFiltrexMode ? html `
784
+ <div class="fc-prompt__highlight" aria-hidden="true">${tokens.map((tok) => {
689
785
  const color = TOKEN_COLOR[tok.type] ?? 'inherit';
690
- return html `<span style="color:${color};${tok.type === 'keyword' ? 'font-weight:600;' : ''}">${tok.text}</span>`;
786
+ return html `<span style="color:${color};">${tok.text}</span>`;
691
787
  })}</div>
788
+ ` : nothing}
789
+ <input
790
+ type="text"
791
+ class="fc-prompt__input"
792
+ id="fc-prompt-${ctrl.id}"
793
+ name="${ctrl.id}"
794
+ part="filter-prompt-input"
795
+ autocomplete="off"
796
+ spellcheck="false"
797
+ aria-invalid="${error ? 'true' : 'false'}"
798
+ placeholder="${placeholder}"
799
+ .value="${live(value)}"
800
+ @input="${onInput}"
801
+ @keydown="${onKeyDown}"
802
+ @scroll="${onScroll}"
803
+ @focus="${onFocus}"
804
+ @blur="${onBlur}"
805
+ />
806
+ </div>
807
+ ${value && ctrl.showClearIcon !== false ? html `
808
+ <nile-button
809
+ variant="ghost"
810
+ class="fc-prompt__clear-icon"
811
+ aria-label="Clear input"
812
+ title="Clear"
813
+ @mousedown="${(e) => e.preventDefault()}"
814
+ @click="${(e) => clearValue(e, 'icon')}"
815
+ >
816
+ <nile-glyph
817
+ slot="prefix"
818
+ name="close"
819
+ size="14"
820
+ method="fill"
821
+ color="var(--nile-colors-neutral-700, var(--ng-colors-text-quaternary-500, #6b7280))"
822
+ ></nile-glyph>
823
+ </nile-button>
692
824
  ` : 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
- />
825
+ ${showToggle ? renderModeToggle(host, ctrl, mode) : nothing}
710
826
  </div>
711
827
  ${isFiltrexMode && suggestionData.length > 0 ? html `
712
828
  <nile-menu
@@ -717,28 +833,45 @@ export function renderPrompt(host, ctrl) {
717
833
  if (!item)
718
834
  return;
719
835
  const inputId = `fc-prompt-${ctrl.id}`;
720
- const root = e.currentTarget.getRootNode();
721
- const inputEl = (root.querySelector(`#${CSS.escape(inputId)}`)
722
- ?? document.getElementById(inputId));
836
+ // The dropdown is portaled to <body>, so the bubbled
837
+ // event's root is `document` — not the chart's shadow
838
+ // root that owns the input. Look the input up via the
839
+ // host's shadow root directly.
840
+ const inputEl = host.shadowRoot
841
+ ?.querySelector(`#${CSS.escape(inputId)}`);
723
842
  if (inputEl)
724
843
  pickItem(item, inputEl);
725
844
  }}"
726
845
  >
727
- ${suggestionData.map((item, idx) => html `
846
+ ${suggestionData.map((item, idx) => {
847
+ const fieldWidth = host.promptFieldWidth.get(ctrl.id) ?? 320;
848
+ const charBudget = Math.max(8, Math.floor((fieldWidth - 80) / 7));
849
+ const isTruncated = item.label.length > charBudget;
850
+ const wantsTooltip = ctrl.suggestionTooltip && isTruncated;
851
+ const showTypeBadge = fieldWidth >= 480;
852
+ return html `
728
853
  <nile-menu-item
729
854
  class="fc-prompt__suggestion"
730
- part="filter-prompt-suggestion${idx === activeIdx ? ' filter-prompt-suggestion-active' : ''}"
731
855
  value="${idx}"
732
856
  ?active="${idx === activeIdx}"
733
857
  >
734
- ${item.label}
735
- ${item.type ? html `<span slot="suffix" class="fc-prompt__suggestion-tag">${item.type}</span>` : nothing}
736
- </nile-menu-item>
737
- `)}
858
+ ${wantsTooltip
859
+ ? html `<nile-lite-tooltip
860
+ hoist
861
+ placement="top"
862
+ content="${item.label}"
863
+ class="fc-prompt__suggestion-tooltip"
864
+ .maxWidth=${fieldWidth}
865
+ >
866
+ <span class="fc-prompt__suggestion-label">${item.label}</span>
867
+ </nile-lite-tooltip>`
868
+ : html `<span class="fc-prompt__suggestion-label">${item.label}</span>`}
869
+ ${item.type && showTypeBadge ? html `<span slot="suffix" class="fc-prompt__suggestion-tag">${item.type}</span>` : nothing}
870
+ </nile-menu-item>`;
871
+ })}
738
872
  </nile-menu>
739
873
  ` : nothing}
740
874
  </nile-dropdown>
741
- ${showToggle ? renderModeToggle(host, ctrl, mode) : nothing}
742
875
  </div>
743
876
  ${error ? html `
744
877
  <div class="fc-prompt__error" role="alert">
@@ -747,6 +880,23 @@ export function renderPrompt(host, ctrl) {
747
880
  </div>
748
881
  ` : nothing}
749
882
  </div>
883
+ ${ctrl.showClearButton === true ? html `
884
+ <nile-button
885
+ variant="secondary"
886
+ class="fc-prompt__clear-filter"
887
+ ?disabled="${!value}"
888
+ @click="${(e) => clearValue(e, 'button')}"
889
+ >
890
+ <nile-glyph
891
+ slot="prefix"
892
+ name="close"
893
+ size="14"
894
+ method="fill"
895
+ color="currentColor"
896
+ ></nile-glyph>
897
+ Clear
898
+ </nile-button>
899
+ ` : nothing}
750
900
  </div>`;
751
901
  }
752
902
  //# sourceMappingURL=prompt.js.map
@@ -30,13 +30,13 @@ export interface SuggestionItem {
30
30
  * - `attribute` → field position (status, priority, …)
31
31
  * - `operator` → after a known field (==, !=, <, …)
32
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.
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
+ * - `suggestion` → ready-made full expressions
37
+ * - `clause` → DEPRECATED. Treated as `connector` at routing time.
38
38
  */
39
- type?: 'attribute' | 'operator' | 'value' | 'connector' | 'keyword' | 'function' | 'snippet' | 'clause';
39
+ type?: 'attribute' | 'operator' | 'value' | 'connector' | 'keyword' | 'function' | 'suggestion' | 'clause';
40
40
  /** Legacy: text inserted on pick. New shape uses `label` for both. */
41
41
  value?: string;
42
42
  /** Legacy: muted secondary text. */
@@ -46,25 +46,25 @@ export interface SuggestionItem {
46
46
  }
47
47
  /** Visual config for one segment of the prompt's mode toggle. */
48
48
  export interface PromptModeToggleOption {
49
- /** Visible text inside the segment. Defaults: 'Expression' for basic, '' for nql. */
49
+ /** Visible text inside the segment. Defaults: `'Prompt'` for the prompt segment, `'Expression'` for the expression segment. */
50
50
  label?: string;
51
- /** nile-glyph icon name (e.g. `'ng-ai_magic'`). Defaults: none for basic, `'ng-ai_magic'` for nql. */
51
+ /** nile-glyph icon name (e.g. `'ng-ai_magic'`). Defaults: none. */
52
52
  icon?: string;
53
53
  /** ARIA label for screen readers — required when the segment is icon-only. */
54
54
  ariaLabel?: string;
55
55
  }
56
- /** Override the Basic / NQL toggle's two segments. */
56
+ /** Override the Prompt / Expression toggle's two segments. */
57
57
  export interface PromptModeToggleConfig {
58
- basic?: PromptModeToggleOption;
59
- nql?: PromptModeToggleOption;
58
+ prompt?: PromptModeToggleOption;
59
+ expression?: PromptModeToggleOption;
60
60
  }
61
61
  export interface QueryLanguageConfig {
62
62
  /** Enables the query layer at all. Default: false. */
63
63
  enabled: boolean;
64
64
  /**
65
- * Customise the Basic / NQL mode toggle's segments. Defaults:
66
- * - basic → label `'Basic'`
67
- * - nql → label `'JQL'`
65
+ * Customise the Prompt / Expression mode toggle's segments. Defaults:
66
+ * - prompt → label `'Prompt'`
67
+ * - expression → label `'Expression'`
68
68
  * Either segment can be overridden with a label, an icon, or both.
69
69
  */
70
70
  toggle?: PromptModeToggleConfig;
@@ -211,14 +211,14 @@ export type QueryJson = {
211
211
  * Detail emitted with `nile-change`.
212
212
  *
213
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)
214
+ * - any non-prompt variant → the variant's natural value
215
+ * - prompt in `'prompt'` mode → plain `string` (raw text)
216
+ * - prompt in `'expression'` mode,
217
+ * parse succeeded → `QueryJson` ({ source, ast }) — JSON-serializable
218
+ * - prompt in `'expression'` mode,
219
+ * pre-validation / parse fail → plain `string` (the typed text)
220
220
  *
221
- * `errors` and `evaluators` are populated for prompt controls in NQL mode.
221
+ * `errors` and `evaluators` are populated for prompt controls in Expression mode.
222
222
  */
223
223
  export type FilterChangeDetail = {
224
224
  filters: Record<string, unknown>;
@@ -246,10 +246,26 @@ export interface FilterControl {
246
246
  /**
247
247
  * Placeholder text. Always an array — `placeholders[0]` is the initial /
248
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.
249
+ * For the `prompt`, `hybrid`, and `expression` variants, additional
250
+ * entries are cycled through with a typewriter effect.
251
+ *
252
+ * For `hybrid`, you can supply mode-specific overrides via
253
+ * `promptPlaceholders` and `expressionPlaceholders`; when present they
254
+ * take precedence over `placeholders` for that mode.
251
255
  */
252
256
  placeholders?: string[];
257
+ /**
258
+ * Hybrid variant only. Placeholder phrases used while the toggle is on
259
+ * the Prompt side. Cycled with the typewriter effect when multiple
260
+ * entries are provided. Falls back to `placeholders` when unset.
261
+ */
262
+ promptPlaceholders?: string[];
263
+ /**
264
+ * Hybrid variant only. Placeholder phrases used while the toggle is on
265
+ * the Expression side. Cycled with the typewriter effect when multiple
266
+ * entries are provided. Falls back to `placeholders` when unset.
267
+ */
268
+ expressionPlaceholders?: string[];
253
269
  /** Typewriter speed in ms per character (default 60). */
254
270
  typeSpeedMs?: number;
255
271
  /** Pause before deleting a typed phrase (default 1400). */
@@ -260,8 +276,13 @@ export interface FilterControl {
260
276
  gradientColors?: string[];
261
277
  gradientDirection?: string;
262
278
  gradientSpeedMs?: number;
263
- /** Prompt variant only. When true, disables the animated AI gradient border. */
264
- noAiBorder?: boolean;
279
+ aiBorder?: boolean;
280
+ /**
281
+ * Prompt variant only. Shows the inline clear (×) icon inside the input
282
+ * when the value is non-empty. Defaults to `false` (hidden). Set `true` to
283
+ * opt in.
284
+ */
285
+ showClearIcon?: boolean;
265
286
  /**
266
287
  * Prompt variant only. Inline autocomplete suggestions shown below the
267
288
  * input when the prompt is focused (Basic mode). Items are filtered
@@ -275,6 +296,8 @@ export interface FilterControl {
275
296
  * - `'append'` → appends the suggestion to the existing text (space-separated)
276
297
  */
277
298
  suggestionInsert?: 'replace' | 'append';
299
+ suggestionTooltip?: boolean;
300
+ showClearButton?: boolean;
278
301
  valueB?: string;
279
302
  operator?: string;
280
303
  thresholdValue?: number | string;
@@ -290,6 +313,12 @@ export type FilterEntry = FilterControl | FilterGroup;
290
313
  export type FilterChartConfig = {
291
314
  type: 'filter';
292
315
  controls: FilterEntry[];
316
+ /**
317
+ * Visual appearance of the filter chart.
318
+ * - `'default'` (omitted) → standard 16px / 20px padding around each control.
319
+ * - `'minimal'` → collapses control padding to 6px for compact layouts.
320
+ */
321
+ appearance?: 'default' | 'minimal';
293
322
  [key: string]: unknown;
294
323
  };
295
324
  export type FilterChartSeparatedPayload = {
@@ -301,8 +330,8 @@ export type FilterChartSeparatedPayload = {
301
330
  * The chart class implements this so render functions stay decoupled
302
331
  * from the LitElement subclass.
303
332
  */
304
- /** Runtime mode for a prompt control. `'basic'` = plain text, no validation. `'nql'` = strict filtrex parsing. */
305
- export type PromptMode = 'basic' | 'nql';
333
+ /** Runtime mode for a prompt control. `'prompt'` = plain text, no validation. `'expression'` = strict filtrex parsing. */
334
+ export type PromptMode = 'prompt' | 'expression';
306
335
  /**
307
336
  * A `FilterControl` after the chart has filled in a guaranteed `id` (either
308
337
  * the author-supplied one, or one derived from `label`). Render helpers and
@@ -335,4 +364,8 @@ export interface FilterChartHost {
335
364
  setPromptActiveIndex(id: string, idx: number): void;
336
365
  /** Mark a prompt control's input as focused / unfocused. */
337
366
  setPromptFocused(id: string, focused: boolean): void;
367
+ /** Pixel width of the bordered prompt field, keyed by control id. */
368
+ readonly promptFieldWidth: Map<string, number>;
369
+ /** Update the tracked width for a prompt control (called from a ResizeObserver). */
370
+ setPromptFieldWidth(id: string, width: number): void;
338
371
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aquera/nile-visualization",
3
- "version": "2.9.8",
3
+ "version": "2.9.10",
4
4
  "description": "A visualization Library for the Nile Design System",
5
5
  "license": "MIT",
6
6
  "author": "Aquera Inc",