@aquera/nile-visualization 2.9.4 → 2.9.6

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.
@@ -111,6 +111,8 @@ export declare class NileChart extends NileElement {
111
111
  addAiResponse(text: string): void;
112
112
  private handleAiSend;
113
113
  private switchType;
114
+ /** Single-control filter chart where the in-body label should be promoted to the chart header. */
115
+ private get isPromotableFilter();
114
116
  private get headerTitle();
115
117
  private get headerSubtitle();
116
118
  private onHeaderSlotChange;
@@ -286,11 +286,32 @@ let NileChart = class NileChart extends NileElement {
286
286
  config: converted,
287
287
  });
288
288
  }
289
+ /** Single-control filter chart where the in-body label should be promoted to the chart header. */
290
+ get isPromotableFilter() {
291
+ const cfg = (this.activeConfig ?? this.resolvedConfig);
292
+ return cfg?.type === 'filter'
293
+ && Array.isArray(cfg?.controls)
294
+ && cfg.controls.length === 1;
295
+ }
289
296
  get headerTitle() {
290
- return this.activeConfig?.chartTitle ?? this.resolvedConfig?.chartTitle ?? '';
297
+ const explicit = this.activeConfig?.chartTitle ?? this.resolvedConfig?.chartTitle;
298
+ if (explicit)
299
+ return explicit;
300
+ if (this.isPromotableFilter) {
301
+ const cfg = (this.activeConfig ?? this.resolvedConfig);
302
+ return cfg.controls[0]?.label ?? '';
303
+ }
304
+ return '';
291
305
  }
292
306
  get headerSubtitle() {
293
- return this.activeConfig?.chartSubtitle ?? this.resolvedConfig?.chartSubtitle ?? '';
307
+ const explicit = this.activeConfig?.chartSubtitle ?? this.resolvedConfig?.chartSubtitle;
308
+ if (explicit)
309
+ return explicit;
310
+ if (this.isPromotableFilter) {
311
+ const cfg = (this.activeConfig ?? this.resolvedConfig);
312
+ return cfg.controls[0]?.description ?? '';
313
+ }
314
+ return '';
294
315
  }
295
316
  onHeaderSlotChange(e) {
296
317
  const slot = e.target;
@@ -1675,8 +1696,10 @@ let NileChart = class NileChart extends NileElement {
1675
1696
  ></nile-data-grid>`;
1676
1697
  }
1677
1698
  case 'filter': {
1699
+ const promote = this.isPromotableFilter;
1678
1700
  return html `<nile-filter-chart
1679
1701
  .config=${{ chart: config }}
1702
+ ?hide-control-headers=${promote}
1680
1703
  @nile-change="${(e) => this.emit('nile-change', e.detail)}"
1681
1704
  ></nile-filter-chart>`;
1682
1705
  }
@@ -90,7 +90,10 @@ export const styles = css `
90
90
  color: var(--nile-colors-neutral-700, var(--ng-colors-text-tertiary-600));
91
91
  text-transform: uppercase;
92
92
  letter-spacing: 0.05em;
93
+ line-height: 1.4;
94
+ padding-top: 2px;
93
95
  margin-bottom: var(--nile-spacing-4px, var(--ng-spacing-1));
96
+ overflow: visible;
94
97
  }
95
98
 
96
99
  .fc-control__desc {
@@ -487,9 +490,10 @@ export const styles = css `
487
490
  outline: none;
488
491
  background: transparent;
489
492
  border-radius: inherit;
490
- padding: var(--nile-spacing-2xl, var(--ng-spacing-5)) var(--nile-spacing-xl, var(--ng-spacing-4));
493
+ padding: var(--nile-spacing-md, var(--ng-spacing-2)) var(--nile-spacing-xl, var(--ng-spacing-4));
491
494
  font-family: var(--nile-font-family-serif, var(--ng-font-family-body));
492
495
  font-size: var(--nile-type-scale-4, var(--ng-font-size-text-md));
496
+ font-weight: 400;
493
497
  color: var(--nile-colors-dark-900, var(--ng-colors-text-primary-900));
494
498
  line-height: 1.4;
495
499
  box-sizing: border-box;
@@ -602,18 +606,14 @@ export const styles = css `
602
606
  color: transparent;
603
607
  }
604
608
 
605
- /* ── Custom suggestion dropdown ─── */
606
- /* Hidden by default; shown only when fc-prompt__inner has focus-within.*/
607
- /* Mousedown on a suggestion preventDefaults to keep input focus, so */
608
- /* the dropdown stays open across picks. Click outside → focus-within */
609
- /* fails → dropdown hides. */
610
- .fc-prompt__suggestions {
611
- display: none;
612
- position: absolute;
613
- top: calc(100% + 6px);
614
- left: 0;
615
- right: 0;
616
- z-index: 20;
609
+ .fc-prompt__dropdown {
610
+ flex: 1;
611
+ min-width: 0;
612
+ display: block;
613
+ }
614
+
615
+ .fc-prompt__dropdown::part(panel) {
616
+ display: flex;
617
617
  flex-direction: column;
618
618
  max-height: 280px;
619
619
  overflow-y: auto;
@@ -625,10 +625,6 @@ export const styles = css `
625
625
  padding: var(--nile-spacing-4px, var(--ng-spacing-1));
626
626
  }
627
627
 
628
- .fc-prompt__inner:focus-within .fc-prompt__suggestions {
629
- display: flex;
630
- }
631
-
632
628
  .fc-prompt__suggestion {
633
629
  display: flex;
634
630
  align-items: center;
@@ -5,6 +5,9 @@ export type { NileTagVariant, FilterOption, QueryLanguageConfig, QueryNode, Quer
5
5
  export declare class NileFilterChart extends NileElement implements FilterChartHost {
6
6
  static get styles(): CSSResultArray;
7
7
  config: FilterChartSeparatedPayload | null;
8
+ /** When set, the host (e.g. nile-chart) is already rendering the control's label/description
9
+ * in its own header — skip the in-body copies to avoid duplication. */
10
+ hideControlHeaders: boolean;
8
11
  selectedValues: Map<string, unknown>;
9
12
  private collapsedGroups;
10
13
  /** Currently displayed (animated) placeholder text per prompt-variant control id. */
@@ -15,6 +18,9 @@ export declare class NileFilterChart extends NileElement implements FilterChartH
15
18
  promptModes: Map<string, PromptMode>;
16
19
  /** Highlighted suggestion index per prompt control id (-1 = none highlighted). */
17
20
  promptActiveIndex: Map<string, number>;
21
+ /** Id of the prompt control whose input currently has focus (drives the
22
+ * nile-dropdown panel open state). null when no prompt is focused. */
23
+ promptFocusedId: string | null;
18
24
  /** Active typewriter timers per prompt control id (so we can stop them). */
19
25
  private _promptTimers;
20
26
  /** Compiled filtrex predicates per prompt control id (strict-mode successes). */
@@ -30,6 +36,7 @@ export declare class NileFilterChart extends NileElement implements FilterChartH
30
36
  connectedCallback(): void;
31
37
  disconnectedCallback(): void;
32
38
  updated(changed: Map<string, unknown>): void;
39
+ private _syncPortalDropdowns;
33
40
  setValue(id: string, value: unknown): void;
34
41
  /**
35
42
  * Update a prompt control's value. The value lands in `selectedValues`
@@ -52,6 +59,7 @@ export declare class NileFilterChart extends NileElement implements FilterChartH
52
59
  */
53
60
  submitPrompt(ctrl: NormalizedFilterControl): void;
54
61
  setPromptActiveIndex(id: string, idx: number): void;
62
+ setPromptFocused(id: string, focused: boolean): void;
55
63
  private _validateOrClear;
56
64
  private _clearPromptValidation;
57
65
  private _syncPromptAnimations;
@@ -21,6 +21,9 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
21
21
  constructor() {
22
22
  super(...arguments);
23
23
  this.config = null;
24
+ /** When set, the host (e.g. nile-chart) is already rendering the control's label/description
25
+ * in its own header — skip the in-body copies to avoid duplication. */
26
+ this.hideControlHeaders = false;
24
27
  this.selectedValues = new Map();
25
28
  this.collapsedGroups = new Set();
26
29
  /** Currently displayed (animated) placeholder text per prompt-variant control id. */
@@ -31,6 +34,9 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
31
34
  this.promptModes = new Map();
32
35
  /** Highlighted suggestion index per prompt control id (-1 = none highlighted). */
33
36
  this.promptActiveIndex = new Map();
37
+ /** Id of the prompt control whose input currently has focus (drives the
38
+ * nile-dropdown panel open state). null when no prompt is focused. */
39
+ this.promptFocusedId = null;
34
40
  /** Active typewriter timers per prompt control id (so we can stop them). */
35
41
  this._promptTimers = new Map();
36
42
  /** Compiled filtrex predicates per prompt control id (strict-mode successes). */
@@ -66,6 +72,20 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
66
72
  this._initValues();
67
73
  this._syncPromptAnimations();
68
74
  }
75
+ // Keep portaled suggestion panels in sync with the latest panel content.
76
+ // nile-dropdown clones the panel once at open-time and never re-clones,
77
+ // so without this the suggestions appear frozen at the value they had
78
+ // when the dropdown first opened.
79
+ this._syncPortalDropdowns();
80
+ }
81
+ _syncPortalDropdowns() {
82
+ const dropdowns = this.shadowRoot?.querySelectorAll('nile-dropdown.fc-prompt__dropdown');
83
+ dropdowns?.forEach((dd) => {
84
+ const d = dd;
85
+ if (d.open && d.portal && d.portalManager?.clonedPanel) {
86
+ d.portalManager.updatePortalPanel?.();
87
+ }
88
+ });
69
89
  }
70
90
  // ── FilterChartHost surface ─────────────────────────────────────────────────
71
91
  setValue(id, value) {
@@ -124,6 +144,14 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
124
144
  setPromptActiveIndex(id, idx) {
125
145
  this.promptActiveIndex = new Map(this.promptActiveIndex).set(id, idx);
126
146
  }
147
+ setPromptFocused(id, focused) {
148
+ if (focused) {
149
+ this.promptFocusedId = id;
150
+ }
151
+ else if (this.promptFocusedId === id) {
152
+ this.promptFocusedId = null;
153
+ }
154
+ }
127
155
  _validateOrClear(ctrl, value, opts = {}) {
128
156
  const isNql = this.promptModes.get(ctrl.id) === 'nql';
129
157
  if (value.trim() === '') {
@@ -468,10 +496,11 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
468
496
  break;
469
497
  default: body = html ``;
470
498
  }
499
+ const showHeader = !this.hideControlHeaders;
471
500
  return html `
472
501
  <div class="fc-control" part="filter-control">
473
- ${ctrl.label ? html `<div class="fc-control__label">${ctrl.label}</div>` : nothing}
474
- ${ctrl.description ? html `<div class="fc-control__desc">${ctrl.description}</div>` : nothing}
502
+ ${showHeader && ctrl.label ? html `<div class="fc-control__label">${ctrl.label}</div>` : nothing}
503
+ ${showHeader && ctrl.description ? html `<div class="fc-control__desc">${ctrl.description}</div>` : nothing}
475
504
  <div class="fc-control__body">${body}</div>
476
505
  </div>`;
477
506
  }
@@ -541,6 +570,9 @@ NileFilterChart._PROMPT_DEBOUNCE_MS = 500;
541
570
  __decorate([
542
571
  property({ attribute: false })
543
572
  ], NileFilterChart.prototype, "config", void 0);
573
+ __decorate([
574
+ property({ type: Boolean, attribute: 'hide-control-headers', reflect: true })
575
+ ], NileFilterChart.prototype, "hideControlHeaders", void 0);
544
576
  __decorate([
545
577
  state()
546
578
  ], NileFilterChart.prototype, "selectedValues", void 0);
@@ -559,6 +591,9 @@ __decorate([
559
591
  __decorate([
560
592
  state()
561
593
  ], NileFilterChart.prototype, "promptActiveIndex", void 0);
594
+ __decorate([
595
+ state()
596
+ ], NileFilterChart.prototype, "promptFocusedId", void 0);
562
597
  NileFilterChart = NileFilterChart_1 = __decorate([
563
598
  customElement('nile-filter-chart')
564
599
  ], NileFilterChart);
@@ -264,7 +264,42 @@ function renderModeToggle(host, ctrl, mode) {
264
264
  </nile-button-toggle-group>
265
265
  `;
266
266
  }
267
+ /**
268
+ * Layout-only global stylesheet for the portaled suggestion menu. The portal
269
+ * clones the panel into document.body, so component-scoped CSS can't reach
270
+ * it — these two rules use nile-menu-item's exposed `label` part to grow the
271
+ * label so the suffix (type pill) sticks to the row's right edge, and style
272
+ * the pill itself. Idempotent: bails if already injected.
273
+ */
274
+ const SUGGESTION_LAYOUT_STYLE_ID = 'fc-prompt-suggestion-layout';
275
+ function ensureSuggestionLayoutStyles() {
276
+ if (typeof document === 'undefined')
277
+ return;
278
+ if (document.getElementById(SUGGESTION_LAYOUT_STYLE_ID))
279
+ return;
280
+ const el = document.createElement('style');
281
+ el.id = SUGGESTION_LAYOUT_STYLE_ID;
282
+ el.textContent = `
283
+ .fc-prompt__menu nile-menu-item::part(label) {
284
+ flex: 1;
285
+ min-width: 0;
286
+ }
287
+ .fc-prompt__suggestion-tag {
288
+ flex-shrink: 0;
289
+ padding: 1px 8px;
290
+ border-radius: 999px;
291
+ background: var(--nile-colors-neutral-100, var(--ng-colors-bg-secondary));
292
+ color: var(--nile-colors-neutral-700, var(--ng-colors-text-quaternary-500));
293
+ font-size: 10px;
294
+ font-weight: 600;
295
+ text-transform: uppercase;
296
+ letter-spacing: 0.04em;
297
+ }
298
+ `;
299
+ document.head.appendChild(el);
300
+ }
267
301
  export function renderPrompt(host, ctrl) {
302
+ ensureSuggestionLayoutStyles();
268
303
  const value = String(host.selectedValues.get(ctrl.id) ?? '');
269
304
  const animated = host.promptPlaceholder.get(ctrl.id) ?? '';
270
305
  const error = host.promptErrors.get(ctrl.id);
@@ -608,60 +643,101 @@ export function renderPrompt(host, ctrl) {
608
643
  ctrl.noAiBorder ? 'fc-prompt--no-ai-border' : '',
609
644
  error ? 'fc-prompt--error' : '',
610
645
  ].filter(Boolean).join(' ');
646
+ const onFocus = () => host.setPromptFocused(ctrl.id, true);
647
+ // Don't close on blur if focus moved into the suggestion menu (or its
648
+ // portaled clone) — mousedown on a nile-menu-item momentarily shifts focus
649
+ // because of the menu's roving tab-index. We let nile-dropdown's own
650
+ // outside-click / Tab / Escape handlers close the panel.
651
+ const onBlur = (e) => {
652
+ const rel = e.relatedTarget;
653
+ if (rel) {
654
+ if (rel.closest('nile-menu-item'))
655
+ return;
656
+ if (rel.closest('nile-menu'))
657
+ return;
658
+ if (rel.closest('nile-dropdown'))
659
+ return;
660
+ if (rel.closest('.nile-dropdown-portal-append'))
661
+ return;
662
+ }
663
+ host.setPromptFocused(ctrl.id, false);
664
+ };
665
+ const isFocused = host.promptFocusedId === ctrl.id;
666
+ const dropdownOpen = isFiltrexMode && isFocused && suggestionData.length > 0;
611
667
  return html `
612
668
  <div class="fc-prompt-row">
613
669
  <div class="${classes} fc-prompt--row-input" part="filter-prompt" style="${inlineStyle}">
614
670
  <div class="fc-prompt__inner">
615
- <div class="fc-prompt__field${isFiltrexMode ? ' fc-prompt__field--highlight' : ''}" part="filter-prompt-field">
616
- ${isFiltrexMode ? html `
617
- <div class="fc-prompt__highlight" aria-hidden="true">${tokens.map((tok) => {
671
+ <nile-dropdown
672
+ class="fc-prompt__dropdown"
673
+ placement="bottom-start"
674
+ sync="width"
675
+ distance="6"
676
+ hoist
677
+ portal
678
+ stay-open-on-select
679
+ ?open=${dropdownOpen}
680
+ .noOpenOnClick=${true}
681
+ >
682
+ <div
683
+ slot="trigger"
684
+ class="fc-prompt__field${isFiltrexMode ? ' fc-prompt__field--highlight' : ''}"
685
+ part="filter-prompt-field"
686
+ >
687
+ ${isFiltrexMode ? html `
688
+ <div class="fc-prompt__highlight" aria-hidden="true">${tokens.map((tok) => {
618
689
  const color = TOKEN_COLOR[tok.type] ?? 'inherit';
619
690
  return html `<span style="color:${color};${tok.type === 'keyword' ? 'font-weight:600;' : ''}">${tok.text}</span>`;
620
691
  })}</div>
621
- ` : nothing}
622
- <input
623
- type="text"
624
- class="fc-prompt__input"
625
- id="fc-prompt-${ctrl.id}"
626
- name="${ctrl.id}"
627
- part="filter-prompt-input"
628
- autocomplete="off"
629
- spellcheck="false"
630
- aria-invalid="${error ? 'true' : 'false'}"
631
- placeholder="${animated || (ctrl.placeholders?.[0] ?? '')}"
632
- .value="${value}"
633
- @input="${onInput}"
634
- @keydown="${onKeyDown}"
635
- @scroll="${onScroll}"
636
- />
637
- </div>
638
- ${isFiltrexMode && suggestionData.length > 0 ? html `
639
- <div class="fc-prompt__suggestions" part="filter-prompt-suggestions">
640
- ${suggestionData.map((item, idx) => html `
641
- <button
642
- type="button"
643
- class="fc-prompt__suggestion${idx === activeIdx ? ' fc-prompt__suggestion--active' : ''}"
644
- @mouseenter="${() => {
645
- if (idx !== activeIdx)
646
- host.setPromptActiveIndex(ctrl.id, idx);
647
- }}"
648
- @mousedown="${(e) => {
649
- // Prevent focus loss on the input so we can update its
650
- // value imperatively after the merge.
651
- e.preventDefault();
652
- const inputEl = e.currentTarget
653
- .closest('.fc-prompt__inner')
654
- ?.querySelector('input.fc-prompt__input');
692
+ ` : nothing}
693
+ <input
694
+ type="text"
695
+ class="fc-prompt__input"
696
+ id="fc-prompt-${ctrl.id}"
697
+ name="${ctrl.id}"
698
+ part="filter-prompt-input"
699
+ autocomplete="off"
700
+ spellcheck="false"
701
+ aria-invalid="${error ? 'true' : 'false'}"
702
+ placeholder="${animated || (ctrl.placeholders?.[0] ?? '')}"
703
+ .value="${value}"
704
+ @input="${onInput}"
705
+ @keydown="${onKeyDown}"
706
+ @scroll="${onScroll}"
707
+ @focus="${onFocus}"
708
+ @blur="${onBlur}"
709
+ />
710
+ </div>
711
+ ${isFiltrexMode && suggestionData.length > 0 ? html `
712
+ <nile-menu
713
+ class="fc-prompt__menu"
714
+ @nile-select="${(e) => {
715
+ const idx = Number(e.detail?.value);
716
+ const item = suggestionData[idx];
717
+ if (!item)
718
+ return;
719
+ const inputId = `fc-prompt-${ctrl.id}`;
720
+ const root = e.currentTarget.getRootNode();
721
+ const inputEl = (root.querySelector(`#${CSS.escape(inputId)}`)
722
+ ?? document.getElementById(inputId));
655
723
  if (inputEl)
656
724
  pickItem(item, inputEl);
657
725
  }}"
658
- >
659
- <span class="fc-prompt__suggestion-label">${item.label}</span>
660
- ${item.type ? html `<span class="fc-prompt__suggestion-tag">${item.type}</span>` : nothing}
661
- </button>
662
- `)}
663
- </div>
664
- ` : nothing}
726
+ >
727
+ ${suggestionData.map((item, idx) => html `
728
+ <nile-menu-item
729
+ class="fc-prompt__suggestion"
730
+ part="filter-prompt-suggestion${idx === activeIdx ? ' filter-prompt-suggestion-active' : ''}"
731
+ value="${idx}"
732
+ ?active="${idx === activeIdx}"
733
+ >
734
+ ${item.label}
735
+ ${item.type ? html `<span slot="suffix" class="fc-prompt__suggestion-tag">${item.type}</span>` : nothing}
736
+ </nile-menu-item>
737
+ `)}
738
+ </nile-menu>
739
+ ` : nothing}
740
+ </nile-dropdown>
665
741
  ${showToggle ? renderModeToggle(host, ctrl, mode) : nothing}
666
742
  </div>
667
743
  ${error ? html `
@@ -321,6 +321,8 @@ export interface FilterChartHost {
321
321
  readonly promptModes: Map<string, PromptMode>;
322
322
  /** Highlighted suggestion index per prompt control id (-1 = none highlighted). */
323
323
  readonly promptActiveIndex: Map<string, number>;
324
+ /** Id of the focused prompt control (drives the suggestion dropdown's open state). */
325
+ readonly promptFocusedId: string | null;
324
326
  setValue(id: string, value: unknown): void;
325
327
  emit(name: string, detail?: unknown): void;
326
328
  /** Update a prompt's value and (when in NQL mode) re-validate it. */
@@ -331,4 +333,6 @@ export interface FilterChartHost {
331
333
  submitPrompt(ctrl: NormalizedFilterControl): void;
332
334
  /** Set the highlighted suggestion index. Use -1 to clear the highlight. */
333
335
  setPromptActiveIndex(id: string, idx: number): void;
336
+ /** Mark a prompt control's input as focused / unfocused. */
337
+ setPromptFocused(id: string, focused: boolean): void;
334
338
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aquera/nile-visualization",
3
- "version": "2.9.4",
3
+ "version": "2.9.6",
4
4
  "description": "A visualization Library for the Nile Design System",
5
5
  "license": "MIT",
6
6
  "author": "Aquera Inc",