@aquera/nile-visualization 2.1.0 → 2.3.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.
@@ -92,6 +92,8 @@ export { NileSpiderwebChart } from './nile-spiderweb-chart/index.js';
92
92
  export type { SpiderwebChartSeriesData } from './nile-spiderweb-chart/index.js';
93
93
  export { NileKpiChart } from './nile-kpi-chart/index.js';
94
94
  export type { ChartKpiSeparatedPayload, KpiConfig, NileKpiConfigInputType, TrendDirection, KpiVariant, } from './nile-kpi-chart/index.js';
95
+ export { NileFilterChart } from './nile-filter-chart/index.js';
96
+ export type { FilterChartSeparatedPayload, FilterControl, FilterOption, FilterChartConfig, } from './nile-filter-chart/nile-filter-chart.js';
95
97
  export { NileMapChart } from './nile-map-chart/index.js';
96
98
  export type { MapChartDataPoint } from './nile-map-chart/index.js';
97
99
  export { NileAiSender } from './nile-ai-sender/index.js';
package/dist/src/index.js CHANGED
@@ -49,6 +49,7 @@ export { NileHeatmapChart } from './nile-heatmap-chart/index.js';
49
49
  export { NileFlameChart } from './nile-flame-chart/index.js';
50
50
  export { NileSpiderwebChart } from './nile-spiderweb-chart/index.js';
51
51
  export { NileKpiChart } from './nile-kpi-chart/index.js';
52
+ export { NileFilterChart } from './nile-filter-chart/index.js';
52
53
  export { NileMapChart } from './nile-map-chart/index.js';
53
54
  export { NileAiSender } from './nile-ai-sender/index.js';
54
55
  export { NileAiPanel } from './nile-ai-panel/index.js';
@@ -0,0 +1,6 @@
1
+ import type { FilterChartConfig, FilterControl, FilterOption } from '../../nile-filter-chart/nile-filter-chart.js';
2
+ /** `chart` slice for `type: 'filter'` — use with `<nile-chart>` or `{ chart, aq }` on `<nile-filter-chart>`. */
3
+ export type ChartFilterConfigType = FilterChartConfig;
4
+ /** Filter fields without the `type` discriminator. */
5
+ export type ChartFilterPropsType = Omit<FilterChartConfig, 'type'>;
6
+ export type { FilterControl, FilterOption };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=chart-filter-config.type.js.map
@@ -13,6 +13,7 @@ import type { ChartHeatmapConfigType } from './chart-heatmap-config.type.js';
13
13
  import type { ChartFlameConfigType } from './chart-flame-config.type.js';
14
14
  import type { ChartSpiderwebConfigType } from './chart-spiderweb-config.type.js';
15
15
  import type { ChartKpiConfigType } from './chart-kpi-config.type.js';
16
+ import type { ChartFilterConfigType } from './chart-filter-config.type.js';
16
17
  import type { ChartInvertedAreaConfigType } from './chart-area-config.type.js';
17
18
  import type { ChartColumnPyramidConfigType } from './chart-column-pyramid-config.type.js';
18
19
  import type { ChartLollipopConfigType } from './chart-lollipop-config.type.js';
@@ -33,4 +34,4 @@ import type { ChartXrangeConfigType } from './chart-xrange-config.type.js';
33
34
  * Chart configs for primitive `<nile-*-chart>` elements (`el.config = { chart, aq }`).
34
35
  * Discriminated on `chart.type`.
35
36
  */
36
- export type PrimitiveChartConfigType = ChartInvertedAreaConfigType | ChartColumnPyramidConfigType | ChartLollipopConfigType | ChartAreaSplineConfigType | ChartAreaNegativeConfigType | ChartAreaRangeConfigType | ChartColumnRangeConfigType | ChartColumnDrilldownConfigType | ChartRadialBarConfigType | ChartVariablePieConfigType | ChartDumbbellLowerConfigType | ChartDumbbellUpperConfigType | ChartEulerConfigType | ChartPolygonConfigType | ChartVectorConfigType | ChartXrangeConfigType | ChartClusterConfigType | ChartStackedConfigType | ChartHistogramConfigType | ChartBellcurveConfigType | ChartBoxplotConfigType | ChartTimelineConfigType | ChartDumbbellConfigType | ChartFanConfigType | ChartFunnelConfigType | ChartOrganizationConfigType | ChartLineColumnConfigType | ChartHeatmapConfigType | ChartFlameConfigType | ChartSpiderwebConfigType | ChartKpiConfigType;
37
+ export type PrimitiveChartConfigType = ChartInvertedAreaConfigType | ChartColumnPyramidConfigType | ChartLollipopConfigType | ChartAreaSplineConfigType | ChartAreaNegativeConfigType | ChartAreaRangeConfigType | ChartColumnRangeConfigType | ChartColumnDrilldownConfigType | ChartRadialBarConfigType | ChartVariablePieConfigType | ChartDumbbellLowerConfigType | ChartDumbbellUpperConfigType | ChartEulerConfigType | ChartPolygonConfigType | ChartVectorConfigType | ChartXrangeConfigType | ChartClusterConfigType | ChartStackedConfigType | ChartHistogramConfigType | ChartBellcurveConfigType | ChartBoxplotConfigType | ChartTimelineConfigType | ChartDumbbellConfigType | ChartFanConfigType | ChartFunnelConfigType | ChartOrganizationConfigType | ChartLineColumnConfigType | ChartHeatmapConfigType | ChartFlameConfigType | ChartSpiderwebConfigType | ChartKpiConfigType | ChartFilterConfigType;
@@ -536,5 +536,11 @@ export interface NileKpiChartConfig extends NileChartConfigBase {
536
536
  numberSystem?: KpiNumberSystem;
537
537
  tooltipEnabled?: boolean;
538
538
  }
539
+ export type { FilterOption, FilterControl } from '../nile-filter-chart/nile-filter-chart.js';
540
+ import type { FilterControl } from '../nile-filter-chart/nile-filter-chart.js';
541
+ export interface NileFilterChartConfig extends NileChartConfigBase {
542
+ type: 'filter';
543
+ controls: FilterControl[];
544
+ }
539
545
  /** Discriminated union of every named chart config — use for typed `<nile-chart>` configs. */
540
- export type NileChartConfig = NileBarChartConfig | NilePieChartConfig | NileLineChartConfig | NileAreaChartConfig | NileColumnChartConfig | NileDonutChartConfig | NileScatterChartConfig | NileBubbleChartConfig | NileSplineChartConfig | NileRadarChartConfig | NileGaugeChartConfig | NileWaterfallChartConfig | NileMapChartConfig | NileGridChartConfig | NileTrendlineChartConfig | NileAnomalyChartConfig | NileHistogramChartConfig | NileBellcurveChartConfig | NileBoxplotChartConfig | NileStackedChartConfig | NileClusterChartConfig | NileColumnPyramidChartConfig | NileColumnRangeChartConfig | NileColumnDrilldownChartConfig | NileLollipopChartConfig | NileInvertedAreaChartConfig | NileAreaSplineChartConfig | NileAreaNegativeChartConfig | NileAreaRangeChartConfig | NileDumbbellChartConfig | NileDumbbellLowerChartConfig | NileDumbbellUpperChartConfig | NileRadialBarChartConfig | NileSpiderwebChartConfig | NileTimelineChartConfig | NileFanChartConfig | NileFunnelChartConfig | NileOrganizationChartConfig | NileHeatmapChartConfig | NileFlameChartConfig | NileVariablePieChartConfig | NileEulerChartConfig | NilePolygonChartConfig | NileVectorChartConfig | NileXrangeChartConfig | NileLineColumnChartConfig | NileKpiChartConfig;
546
+ export type NileChartConfig = NileBarChartConfig | NilePieChartConfig | NileLineChartConfig | NileAreaChartConfig | NileColumnChartConfig | NileDonutChartConfig | NileScatterChartConfig | NileBubbleChartConfig | NileSplineChartConfig | NileRadarChartConfig | NileGaugeChartConfig | NileWaterfallChartConfig | NileMapChartConfig | NileGridChartConfig | NileTrendlineChartConfig | NileAnomalyChartConfig | NileHistogramChartConfig | NileBellcurveChartConfig | NileBoxplotChartConfig | NileStackedChartConfig | NileClusterChartConfig | NileColumnPyramidChartConfig | NileColumnRangeChartConfig | NileColumnDrilldownChartConfig | NileLollipopChartConfig | NileInvertedAreaChartConfig | NileAreaSplineChartConfig | NileAreaNegativeChartConfig | NileAreaRangeChartConfig | NileDumbbellChartConfig | NileDumbbellLowerChartConfig | NileDumbbellUpperChartConfig | NileRadialBarChartConfig | NileSpiderwebChartConfig | NileTimelineChartConfig | NileFanChartConfig | NileFunnelChartConfig | NileOrganizationChartConfig | NileHeatmapChartConfig | NileFlameChartConfig | NileVariablePieChartConfig | NileEulerChartConfig | NilePolygonChartConfig | NileVectorChartConfig | NileXrangeChartConfig | NileLineColumnChartConfig | NileKpiChartConfig | NileFilterChartConfig;
@@ -172,30 +172,28 @@ export const styles = css `
172
172
  contain: layout style;
173
173
  }
174
174
 
175
- /* Grid: height applies to the whole card — header + grid together */
175
+ /* Grid layout:
176
+ - card uses clip-path (not overflow:hidden) so position:sticky on the header is not broken
177
+ - height goes on nile-data-grid itself so .scroll-container inside shadow DOM scrolls,
178
+ making position:sticky on <th> work correctly
179
+ - AI panel is rendered outside the wrapper and anchored via position:relative on the card */
176
180
  .nile-chart-card--grid {
177
- display: flex;
178
- flex-direction: column;
181
+ position: relative;
182
+ clip-path: inset(0);
179
183
  }
180
184
 
181
- .nile-chart-card--grid .nile-chart-wrapper {
182
- flex: 1;
183
- min-height: 0;
184
- display: flex;
185
- flex-direction: column;
185
+ .nile-chart-card--grid .nile-chart-header {
186
+ position: sticky;
187
+ top: 0;
188
+ z-index: 2;
189
+ background: var(--nile-colors-white-base, var(--ng-colors-bg-primary));
186
190
  }
187
191
 
188
192
  .nile-chart-card--grid .nile-chart-inner {
189
- flex: 1;
190
- min-height: 0;
193
+ overflow: visible;
191
194
  contain: none;
192
195
  }
193
196
 
194
- .nile-chart-card--grid nile-data-grid {
195
- display: block;
196
- height: 100%;
197
- }
198
-
199
197
  /* ── Default slot (custom chart body only — not named slots) ── */
200
198
  slot:not([name])::slotted(*) {
201
199
  display: block;
@@ -419,6 +417,10 @@ export const styles = css `
419
417
  contain: none;
420
418
  }
421
419
 
420
+ .nile-chart-inner--filter {
421
+ contain: none;
422
+ }
423
+
422
424
  .nile-ai-trigger.active {
423
425
  background: var(--ng-componentcolors-utility-brand-100, #DBE8FF);
424
426
  }
@@ -48,6 +48,7 @@ import '../nile-polygon-chart/index.js';
48
48
  import '../nile-vector-chart/index.js';
49
49
  import '../nile-xrange-chart/index.js';
50
50
  import '../nile-kpi-chart/index.js';
51
+ import '../nile-filter-chart/index.js';
51
52
  import '@aquera/nile-data-grid';
52
53
  import '../nile-ai-panel/index.js';
53
54
  export declare class NileChart extends NileElement {
@@ -59,6 +59,7 @@ import '../nile-polygon-chart/index.js';
59
59
  import '../nile-vector-chart/index.js';
60
60
  import '../nile-xrange-chart/index.js';
61
61
  import '../nile-kpi-chart/index.js';
62
+ import '../nile-filter-chart/index.js';
62
63
  import '@aquera/nile-data-grid';
63
64
  import '../nile-ai-panel/index.js';
64
65
  const CORE_CHART_LABELS = {
@@ -1657,7 +1658,8 @@ let NileChart = class NileChart extends NileElement {
1657
1658
  case 'grid': {
1658
1659
  const gridChrome = '--nile-data-grid-radius:0;' +
1659
1660
  '--nile-data-grid-border-color:transparent;' +
1660
- '--nile-data-grid-shadow:none;';
1661
+ '--nile-data-grid-shadow:none;' +
1662
+ (config.height ? `height:${config.height};` : '');
1661
1663
  return html `<nile-data-grid
1662
1664
  class="nile-chart-grid"
1663
1665
  .data=${config.data}
@@ -1671,6 +1673,12 @@ let NileChart = class NileChart extends NileElement {
1671
1673
  .noMatchMessage=${config.noMatchMessage ?? 'No matching rows'}
1672
1674
  style=${gridChrome}
1673
1675
  ></nile-data-grid>`;
1676
+ }
1677
+ case 'filter': {
1678
+ return html `<nile-filter-chart
1679
+ .config=${{ chart: config }}
1680
+ @nile-change="${(e) => this.emit('nile-change', e.detail)}"
1681
+ ></nile-filter-chart>`;
1674
1682
  }
1675
1683
  default: {
1676
1684
  const _exhaustive = config;
@@ -1698,22 +1706,20 @@ let NileChart = class NileChart extends NileElement {
1698
1706
  render() {
1699
1707
  const isLoading = this.loading || (this.activeConfig?.loading ?? false);
1700
1708
  const isGrid = this.activeConfig?.type === 'grid';
1701
- const cardStyle = isGrid && this.activeConfig?.height
1702
- ? `height:${this.activeConfig.height}`
1703
- : '';
1704
1709
  return html `
1705
- <div class="nile-chart-card ${isGrid ? 'nile-chart-card--grid' : ''}" style=${cardStyle}>
1710
+ <div class="nile-chart-card ${isGrid ? 'nile-chart-card--grid' : ''}">
1706
1711
  ${this.renderHeader()}
1707
1712
  <div class="nile-chart-wrapper">
1708
- <div class="nile-chart-inner ${this.activeConfig?.type === 'kpi' ? 'nile-chart-inner--kpi' : ''}">
1713
+ <div class="nile-chart-inner ${this.activeConfig?.type === 'kpi' ? 'nile-chart-inner--kpi' : ''} ${this.activeConfig?.type === 'filter' ? 'nile-chart-inner--filter' : ''}">
1709
1714
  ${isLoading
1710
1715
  ? this.renderSkeleton()
1711
1716
  : this.activeConfig
1712
1717
  ? this.renderChartContent()
1713
1718
  : html `<slot></slot>`}
1714
- ${this.renderAiPanel()}
1719
+ ${isGrid ? nothing : this.renderAiPanel()}
1715
1720
  </div>
1716
1721
  </div>
1722
+ ${isGrid ? this.renderAiPanel() : nothing}
1717
1723
  <slot name="footer"></slot>
1718
1724
  </div>
1719
1725
  `;
@@ -0,0 +1,2 @@
1
+ export { NileFilterChart } from './nile-filter-chart.js';
2
+ export type { FilterChartSeparatedPayload } from './nile-filter-chart.js';
@@ -0,0 +1,2 @@
1
+ export { NileFilterChart } from './nile-filter-chart.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ export declare const styles: import("lit").CSSResult;
@@ -0,0 +1,361 @@
1
+ import { css } from 'lit';
2
+ export const styles = css `
3
+ :host { display: block; width: 100%; }
4
+
5
+ .fc-root {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: var(--nile-spacing-none, var(--ng-spacing-0));
9
+ }
10
+
11
+ /* ── Group ─── */
12
+ .fc-group {
13
+ border: 1px solid var(--nile-colors-neutral-400, var(--ng-colors-border-secondary));
14
+ border-radius: var(--nile-radius-md, var(--ng-radius-md));
15
+ overflow: hidden;
16
+ }
17
+
18
+ .fc-group__header {
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: space-between;
22
+ padding: var(--nile-spacing-lg, var(--ng-spacing-3)) var(--nile-spacing-2xl, var(--ng-spacing-5));
23
+ background: var(--nile-colors-neutral-100, var(--ng-colors-bg-secondary));
24
+ border-bottom: 1px solid var(--nile-colors-neutral-400, var(--ng-colors-border-secondary));
25
+ gap: var(--nile-spacing-8px, var(--ng-spacing-2));
26
+ }
27
+
28
+ .fc-group__header--collapsible {
29
+ cursor: pointer;
30
+ user-select: none;
31
+ }
32
+
33
+ .fc-group__header--collapsible:hover {
34
+ background: var(--nile-colors-dark-200, var(--ng-colors-bg-secondary-hover));
35
+ }
36
+
37
+ .fc-group__header-text {
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: var(--nile-spacing-2px, var(--ng-spacing-0-5));
41
+ }
42
+
43
+ .fc-group__label {
44
+ font-size: var(--nile-font-size-sm, 13px);
45
+ font-weight: var(--nile-font-weight-bold, var(--ng-font-weight-700));
46
+ color: var(--nile-colors-dark-900, var(--ng-colors-text-primary-900));
47
+ letter-spacing: 0.02em;
48
+ }
49
+
50
+ .fc-group__desc {
51
+ font-size: var(--nile-type-scale-2, var(--ng-font-size-text-xs));
52
+ color: var(--nile-colors-neutral-700, var(--ng-colors-text-quaternary-500));
53
+ line-height: 1.4;
54
+ }
55
+
56
+ .fc-group__chevron {
57
+ font-size: var(--nile-type-scale-5, var(--ng-font-size-text-lg));
58
+ font-weight: var(--ng-font-weight-300, 300);
59
+ color: var(--nile-colors-neutral-700, var(--ng-colors-text-quaternary-500));
60
+ transition: transform 0.2s ease;
61
+ transform: rotate(0deg);
62
+ flex-shrink: 0;
63
+ }
64
+
65
+ .fc-group__chevron--open {
66
+ transform: rotate(90deg);
67
+ }
68
+
69
+ .fc-group__body {
70
+ display: flex;
71
+ flex-direction: column;
72
+ }
73
+
74
+ .fc-group__body .fc-control {
75
+ border-bottom: 1px solid var(--nile-colors-neutral-400, var(--ng-colors-border-secondary));
76
+ }
77
+
78
+ .fc-group__body .fc-control:last-child {
79
+ border-bottom: none;
80
+ }
81
+
82
+ /* ── Control card ─── */
83
+ .fc-control {
84
+ padding: var(--nile-spacing-xl, var(--ng-spacing-4)) var(--nile-spacing-2xl, var(--ng-spacing-5));
85
+ }
86
+
87
+ .fc-control__label {
88
+ font-size: var(--nile-font-size-sm, 13px);
89
+ font-weight: var(--nile-font-weight-semi-bold, var(--ng-font-weight-600));
90
+ color: var(--nile-colors-neutral-700, var(--ng-colors-text-tertiary-600));
91
+ text-transform: uppercase;
92
+ letter-spacing: 0.05em;
93
+ margin-bottom: var(--nile-spacing-4px, var(--ng-spacing-1));
94
+ }
95
+
96
+ .fc-control__desc {
97
+ font-size: var(--nile-type-scale-2, var(--ng-font-size-text-xs));
98
+ color: var(--nile-colors-neutral-700, var(--ng-colors-text-quaternary-500));
99
+ margin-bottom: var(--nile-spacing-lg, var(--ng-spacing-3));
100
+ line-height: 1.4;
101
+ }
102
+
103
+ .fc-control__body {
104
+ display: flex;
105
+ flex-direction: column;
106
+ gap: var(--nile-spacing-8px, var(--ng-spacing-2));
107
+ }
108
+
109
+ /* ── Badge ─── */
110
+ .fc-badge-group {
111
+ display: flex;
112
+ flex-wrap: wrap;
113
+ gap: var(--nile-spacing-6px, var(--ng-spacing-1-5));
114
+ }
115
+
116
+ .fc-badge {
117
+ display: inline-flex;
118
+ align-items: center;
119
+ gap: var(--nile-spacing-6px, var(--ng-spacing-1-5));
120
+ padding: var(--nile-spacing-4px, var(--ng-spacing-1)) var(--nile-spacing-14px, var(--ng-spacing-3-5));
121
+ border-radius: var(--nile-radius-full, var(--ng-radius-full));
122
+ font-size: var(--nile-font-size-sm, 13px);
123
+ font-weight: var(--nile-font-weight-medium, var(--ng-font-weight-500));
124
+ cursor: pointer;
125
+ border: 1.5px solid var(--nile-colors-neutral-500, var(--ng-colors-border-primary));
126
+ background: var(--nile-colors-white-base, var(--ng-color-base-white));
127
+ color: var(--nile-colors-dark-500, var(--ng-colors-text-secondary-700));
128
+ transition: background 0.12s, border-color 0.12s, color 0.12s;
129
+ white-space: nowrap;
130
+ line-height: 1.4;
131
+ }
132
+
133
+ .fc-badge:hover {
134
+ border-color: var(--nile-colors-primary-400, var(--ng-color-bluedark-400));
135
+ background: var(--nile-colors-primary-100, var(--ng-colors-bg-brand-primary));
136
+ }
137
+
138
+ .fc-badge--selected {
139
+ border-color: var(--nile-colors-primary-600, var(--ng-colors-bg-brand-solid));
140
+ background: var(--nile-colors-primary-600, var(--ng-colors-bg-brand-solid));
141
+ color: var(--nile-colors-white-base, var(--ng-color-base-white));
142
+ }
143
+
144
+ .fc-badge--selected .fc-dot { border-color: var(--nile-colors-white-500, var(--ng-color-graydarkmodealpha-500)); }
145
+
146
+ .fc-dot {
147
+ width: var(--nile-spacing-8px, var(--ng-spacing-2));
148
+ height: var(--nile-spacing-8px, var(--ng-spacing-2));
149
+ border-radius: var(--nile-radius-full, var(--ng-radius-full));
150
+ flex-shrink: 0;
151
+ border: 1.5px solid var(--nile-colors-transparent, var(--ng-color-base-transparent));
152
+ }
153
+
154
+ /* ── Slider ─── */
155
+ .fc-slider-wrap { display: flex; flex-direction: column; gap: var(--nile-spacing-10px, var(--ng-spacing-2-5)); }
156
+
157
+ .fc-slider-label {
158
+ display: flex;
159
+ justify-content: space-between;
160
+ font-size: var(--nile-font-size-sm, 13px);
161
+ color: var(--nile-colors-neutral-700, var(--ng-colors-text-tertiary-600));
162
+ }
163
+
164
+ .fc-slider-range strong { color: var(--nile-colors-primary-600, var(--ng-color-bluedark-600)); }
165
+
166
+ nile-tag { cursor: pointer; }
167
+
168
+ /* Expand slider to fill the card width */
169
+ nile-slider { display: block; width: 100%; }
170
+
171
+ nile-slider::part(base) {
172
+ height: auto;
173
+ padding: var(--nile-spacing-4px, var(--ng-spacing-1)) var(--nile-spacing-none, var(--ng-spacing-0));
174
+ }
175
+
176
+ nile-slider::part(range-container) {
177
+ flex: 1;
178
+ width: 100%;
179
+ min-width: 0;
180
+ }
181
+
182
+ /* Make the track fill the container and be easier to see */
183
+ nile-slider::part(range) {
184
+ width: 100%;
185
+ height: var(--nile-height-6px, var(--ng-spacing-1-5));
186
+ }
187
+
188
+ /* The filled portion between the two handles */
189
+ nile-slider::part(range-completed) {
190
+ background-color: var(--nile-colors-primary-600, var(--ng-color-bluedark-600));
191
+ height: 100%;
192
+ }
193
+
194
+ /* ── Tree ─── */
195
+ /* nile-tree sets font-size:0 on :host for em-based indentation maths.
196
+ Without --nile-type-scale-4 defined the label inherits 0 and becomes invisible.
197
+ We also enable indent guide lines and fix the indent step size. */
198
+ nile-tree {
199
+ --nile-type-scale-4: var(--nile-font-size-sm, 13px);
200
+ --indent-size: 0.875rem;
201
+ --indent-guide-width: 1px;
202
+ --indent-guide-style: solid;
203
+ --indent-guide-color: var(--nile-colors-neutral-400, var(--ng-colors-border-secondary));
204
+ }
205
+
206
+ /* ── Segmented ─── */
207
+ .fc-segmented-scroll {
208
+ overflow-x: auto;
209
+ overflow-y: hidden;
210
+ -webkit-overflow-scrolling: touch;
211
+ scrollbar-width: none;
212
+ }
213
+
214
+ .fc-segmented-scroll::-webkit-scrollbar { display: none; }
215
+
216
+ .fc-segmented-scroll nile-button-toggle-group {
217
+ display: inline-flex;
218
+ white-space: nowrap;
219
+ min-width: max-content;
220
+ }
221
+
222
+ /* ── Toggle ─── */
223
+ .fc-toggle-group {
224
+ display: flex;
225
+ flex-direction: column;
226
+ gap: var(--nile-spacing-10px, var(--ng-spacing-2-5));
227
+ }
228
+
229
+
230
+ /* ── Comparison ─── */
231
+ .fc-comparison {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: var(--nile-spacing-10px, var(--ng-spacing-2-5));
235
+ }
236
+
237
+ .fc-comparison nile-select { flex: 1; }
238
+
239
+ .fc-vs {
240
+ flex-shrink: 0;
241
+ font-size: 11px;
242
+ font-weight: var(--nile-font-weight-bold, var(--ng-font-weight-700));
243
+ color: var(--nile-colors-white-base, var(--ng-color-base-white));
244
+ background: var(--nile-colors-neutral-700, var(--ng-color-graylightmode-700));
245
+ border-radius: var(--nile-radius-sm, var(--ng-radius-xs));
246
+ padding: 3px var(--nile-spacing-6px, var(--ng-spacing-1-5));
247
+ letter-spacing: 0.05em;
248
+ }
249
+
250
+ /* ── Threshold ─── */
251
+ .fc-threshold {
252
+ display: flex;
253
+ flex-direction: column;
254
+ gap: var(--nile-spacing-lg, var(--ng-spacing-3));
255
+ }
256
+
257
+ .fc-threshold-metric-row {
258
+ display: flex;
259
+ align-items: flex-end;
260
+ gap: var(--nile-spacing-10px, var(--ng-spacing-2-5));
261
+ }
262
+
263
+ .fc-threshold-cond-row {
264
+ display: flex;
265
+ align-items: flex-end;
266
+ gap: var(--nile-spacing-lg, var(--ng-spacing-3));
267
+ padding-left: var(--nile-spacing-4px, var(--ng-spacing-1));
268
+ }
269
+
270
+ .fc-threshold-field {
271
+ display: flex;
272
+ flex-direction: column;
273
+ gap: var(--nile-spacing-6px, var(--ng-spacing-1-5));
274
+ }
275
+
276
+ .fc-threshold-field-label {
277
+ font-size: 11px;
278
+ font-weight: var(--nile-font-weight-semi-bold, var(--ng-font-weight-600));
279
+ text-transform: uppercase;
280
+ letter-spacing: 0.06em;
281
+ color: var(--nile-colors-neutral-700, var(--ng-colors-text-quaternary-500));
282
+ }
283
+
284
+ .fc-threshold-field--metric { flex: 1; }
285
+ .fc-threshold-field--op { flex: 0 0 150px; }
286
+ .fc-threshold-field--val { flex: 1; }
287
+
288
+ .fc-threshold-where {
289
+ flex-shrink: 0;
290
+ align-self: flex-end;
291
+ margin-bottom: var(--nile-spacing-6px, var(--ng-spacing-1-5));
292
+ font-size: 11px;
293
+ font-weight: var(--nile-font-weight-bold, var(--ng-font-weight-700));
294
+ letter-spacing: 0.06em;
295
+ color: var(--nile-colors-white-base, var(--ng-color-base-white));
296
+ background: var(--nile-colors-dark-500, var(--ng-color-graylightmode-600));
297
+ border-radius: var(--nile-radius-sm, var(--ng-radius-xs));
298
+ padding: var(--nile-spacing-4px, var(--ng-spacing-1)) var(--nile-spacing-10px, var(--ng-spacing-2-5));
299
+ }
300
+
301
+ .fc-threshold-preview {
302
+ display: flex;
303
+ align-items: center;
304
+ gap: var(--nile-spacing-6px, var(--ng-spacing-1-5));
305
+ padding: var(--nile-spacing-8px, var(--ng-spacing-2)) var(--nile-spacing-lg, var(--ng-spacing-3));
306
+ border-radius: var(--nile-radius-radius-lg, var(--ng-radius-sm));
307
+ background: var(--nile-colors-primary-100, var(--ng-colors-bg-brand-primary));
308
+ border: 1px solid var(--nile-colors-primary-400, var(--ng-color-bluedark-200));
309
+ font-size: var(--nile-font-size-sm, 13px);
310
+ color: var(--nile-colors-primary-700, var(--ng-colors-text-brand-secondary-700));
311
+ font-family: monospace;
312
+ }
313
+
314
+ .fc-threshold-preview--empty {
315
+ background: var(--nile-colors-neutral-100, var(--ng-colors-bg-secondary));
316
+ border-color: var(--nile-colors-neutral-400, var(--ng-colors-border-secondary));
317
+ color: var(--nile-colors-text-placeholder, var(--ng-colors-text-placeholder));
318
+ font-family: inherit;
319
+ font-style: italic;
320
+ }
321
+
322
+ /* ── Preset ─── */
323
+ .fc-preset-list {
324
+ display: flex;
325
+ flex-direction: column;
326
+ gap: var(--nile-spacing-6px, var(--ng-spacing-1-5));
327
+ }
328
+
329
+ .fc-preset-item {
330
+ display: flex;
331
+ align-items: center;
332
+ gap: var(--nile-spacing-10px, var(--ng-spacing-2-5));
333
+ padding: var(--nile-spacing-10px, var(--ng-spacing-2-5)) var(--nile-spacing-14px, var(--ng-spacing-3-5));
334
+ border-radius: var(--nile-radius-md, var(--ng-radius-md));
335
+ border: 1.5px solid var(--nile-colors-neutral-400, var(--ng-colors-border-secondary));
336
+ background: var(--nile-colors-white-base, var(--ng-color-base-white));
337
+ font-size: var(--nile-font-size-sm, 13px);
338
+ font-weight: var(--nile-font-weight-medium, var(--ng-font-weight-500));
339
+ color: var(--nile-colors-dark-500, var(--ng-colors-text-secondary-700));
340
+ cursor: pointer;
341
+ text-align: left;
342
+ transition: background 0.12s, border-color 0.12s;
343
+ }
344
+
345
+ .fc-preset-item:hover {
346
+ background: var(--nile-colors-neutral-100, var(--ng-colors-bg-secondary));
347
+ border-color: var(--nile-colors-neutral-500, var(--ng-colors-border-primary));
348
+ }
349
+
350
+ .fc-preset-item--selected {
351
+ border-color: var(--nile-colors-primary-600, var(--ng-colors-border-brand));
352
+ background: var(--nile-colors-primary-100, var(--ng-colors-bg-brand-primary));
353
+ color: var(--nile-colors-primary-700, var(--ng-colors-text-brand-secondary-700));
354
+ }
355
+
356
+ .fc-preset-icon {
357
+ font-size: var(--nile-type-scale-4, var(--ng-font-size-text-md));
358
+ line-height: 1;
359
+ }
360
+ `;
361
+ //# sourceMappingURL=nile-filter-chart.css.js.map
@@ -0,0 +1,96 @@
1
+ import { CSSResultArray, TemplateResult } from 'lit';
2
+ import NileElement from '../internal/nile-element.js';
3
+ import '@aquera/nile-elements/nile-select';
4
+ import '@aquera/nile-elements/nile-option';
5
+ import '@aquera/nile-elements/nile-input';
6
+ import '@aquera/nile-elements/nile-slide-toggle';
7
+ import '@aquera/nile-elements/nile-radio-group';
8
+ import '@aquera/nile-elements/nile-radio';
9
+ import '@aquera/nile-elements/nile-tag';
10
+ import '@aquera/nile-elements/nile-button-toggle-group';
11
+ import '@aquera/nile-elements/nile-button-toggle';
12
+ export type NileTagVariant = 'primary' | 'success' | 'normal' | 'warning' | 'error' | 'info';
13
+ export interface FilterOption {
14
+ label: string;
15
+ value: string;
16
+ /** Optional color dot (CSS color string). */
17
+ color?: string;
18
+ /** Optional nile-tag semantic variant — uses ng tokens directly (preferred over color). */
19
+ ngVariant?: NileTagVariant;
20
+ /** Optional icon name for preset variant. */
21
+ icon?: string;
22
+ }
23
+ export interface TreeNode {
24
+ label: string;
25
+ value: string;
26
+ expanded?: boolean;
27
+ children?: TreeNode[];
28
+ }
29
+ export interface FilterControl {
30
+ id: string;
31
+ label: string;
32
+ /** Optional subtitle shown below the label. */
33
+ description?: string;
34
+ selection: 'single' | 'multi';
35
+ variant: 'badge' | 'dropdown' | 'segmented' | 'radio' | 'toggle' | 'slider' | 'search' | 'comparison' | 'threshold' | 'tree' | 'preset';
36
+ options?: FilterOption[];
37
+ value?: string | string[] | number[] | boolean;
38
+ min?: number;
39
+ max?: number;
40
+ step?: number;
41
+ prefix?: string;
42
+ suffix?: string;
43
+ placeholder?: string;
44
+ valueB?: string;
45
+ operator?: string;
46
+ thresholdValue?: number | string;
47
+ treeData?: TreeNode[];
48
+ }
49
+ export interface FilterGroup {
50
+ type: 'group';
51
+ label: string;
52
+ description?: string;
53
+ collapsible?: boolean;
54
+ controls: FilterControl[];
55
+ }
56
+ export type FilterEntry = FilterControl | FilterGroup;
57
+ export type FilterChartConfig = {
58
+ type: 'filter';
59
+ controls: FilterEntry[];
60
+ [key: string]: unknown;
61
+ };
62
+ export type FilterChartSeparatedPayload = {
63
+ chart: FilterChartConfig;
64
+ aq?: Record<string, unknown>;
65
+ };
66
+ export declare class NileFilterChart extends NileElement {
67
+ static get styles(): CSSResultArray;
68
+ config: FilterChartSeparatedPayload | null;
69
+ private selectedValues;
70
+ private collapsedGroups;
71
+ connectedCallback(): void;
72
+ disconnectedCallback(): void;
73
+ updated(changed: Map<string, unknown>): void;
74
+ private _flatControls;
75
+ private _initValues;
76
+ private _set;
77
+ private _emitChange;
78
+ private _renderBadge;
79
+ private _renderDropdown;
80
+ private _renderSegmented;
81
+ private _renderRadio;
82
+ private _renderToggle;
83
+ private _renderSearch;
84
+ private _renderComparison;
85
+ private _renderThreshold;
86
+ private _renderPreset;
87
+ private _renderControl;
88
+ private _renderGroup;
89
+ render(): TemplateResult;
90
+ }
91
+ export default NileFilterChart;
92
+ declare global {
93
+ interface HTMLElementTagNameMap {
94
+ 'nile-filter-chart': NileFilterChart;
95
+ }
96
+ }
@@ -0,0 +1,453 @@
1
+ import { __decorate } from "tslib";
2
+ import { html, nothing } from 'lit';
3
+ import { customElement, property, state } from 'lit/decorators.js';
4
+ import { styles } from './nile-filter-chart.css.js';
5
+ import NileElement from '../internal/nile-element.js';
6
+ import '@aquera/nile-elements/nile-select';
7
+ import '@aquera/nile-elements/nile-option';
8
+ import '@aquera/nile-elements/nile-input';
9
+ import '@aquera/nile-elements/nile-slide-toggle';
10
+ // import '@aquera/nile-elements/nile-slider';
11
+ import '@aquera/nile-elements/nile-radio-group';
12
+ import '@aquera/nile-elements/nile-radio';
13
+ import '@aquera/nile-elements/nile-tag';
14
+ import '@aquera/nile-elements/nile-button-toggle-group';
15
+ import '@aquera/nile-elements/nile-button-toggle';
16
+ let NileFilterChart = class NileFilterChart extends NileElement {
17
+ constructor() {
18
+ super(...arguments);
19
+ this.config = null;
20
+ this.selectedValues = new Map();
21
+ this.collapsedGroups = new Set();
22
+ }
23
+ static get styles() {
24
+ return [styles];
25
+ }
26
+ connectedCallback() {
27
+ super.connectedCallback();
28
+ this._initValues();
29
+ this.emit('nile-init');
30
+ }
31
+ disconnectedCallback() {
32
+ super.disconnectedCallback();
33
+ this.emit('nile-destroy');
34
+ }
35
+ updated(changed) {
36
+ super.updated(changed);
37
+ if (changed.has('config')) {
38
+ this._initValues();
39
+ }
40
+ }
41
+ _flatControls() {
42
+ const entries = this.config?.chart?.controls ?? [];
43
+ const out = [];
44
+ for (const entry of entries) {
45
+ if (entry.type === 'group') {
46
+ out.push(...entry.controls);
47
+ }
48
+ else {
49
+ out.push(entry);
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+ _initValues() {
55
+ const controls = this._flatControls();
56
+ const map = new Map();
57
+ for (const ctrl of controls) {
58
+ if (ctrl.value !== undefined) {
59
+ map.set(ctrl.id, ctrl.value);
60
+ if (ctrl.variant === 'comparison') {
61
+ map.set(`${ctrl.id}__b`, ctrl.valueB ?? '');
62
+ }
63
+ if (ctrl.variant === 'threshold') {
64
+ map.set(`${ctrl.id}__op`, ctrl.operator ?? '>');
65
+ map.set(`${ctrl.id}__val`, ctrl.thresholdValue ?? '');
66
+ }
67
+ }
68
+ else {
69
+ if (ctrl.variant === 'slider') {
70
+ map.set(ctrl.id, [ctrl.min ?? 0, ctrl.max ?? 100]);
71
+ }
72
+ else if (ctrl.variant === 'toggle') {
73
+ const defaults = {};
74
+ (ctrl.options ?? []).forEach(o => { defaults[o.value] = false; });
75
+ map.set(ctrl.id, defaults);
76
+ }
77
+ else if (ctrl.selection === 'multi') {
78
+ map.set(ctrl.id, []);
79
+ }
80
+ else {
81
+ map.set(ctrl.id, '');
82
+ }
83
+ if (ctrl.variant === 'comparison')
84
+ map.set(`${ctrl.id}__b`, '');
85
+ if (ctrl.variant === 'threshold') {
86
+ map.set(`${ctrl.id}__op`, '>');
87
+ map.set(`${ctrl.id}__val`, '');
88
+ }
89
+ }
90
+ }
91
+ this.selectedValues = map;
92
+ }
93
+ _set(id, value) {
94
+ this.selectedValues = new Map(this.selectedValues).set(id, value);
95
+ this._emitChange();
96
+ }
97
+ _emitChange() {
98
+ const filters = {};
99
+ this.selectedValues.forEach((val, id) => { filters[id] = val; });
100
+ this.emit('nile-change', { filters });
101
+ }
102
+ // ── Badge ────────────────────────────────────────────────────────────────────
103
+ _renderBadge(ctrl) {
104
+ const current = this.selectedValues.get(ctrl.id);
105
+ return html `
106
+ <div class="fc-badge-group" role="group" aria-label="${ctrl.label}">
107
+ ${(ctrl.options ?? []).map(opt => {
108
+ const sel = ctrl.selection === 'multi'
109
+ ? (Array.isArray(current) && current.includes(opt.value))
110
+ : current === opt.value;
111
+ return html `
112
+ <nile-tag
113
+ pill
114
+ size="medium"
115
+ variant="${sel ? (opt.ngVariant ?? 'primary') : 'normal'}"
116
+ @click="${() => {
117
+ if (ctrl.selection === 'single') {
118
+ this._set(ctrl.id, sel ? '' : opt.value);
119
+ }
120
+ else {
121
+ const arr = Array.isArray(current) ? [...current] : [];
122
+ const idx = arr.indexOf(opt.value);
123
+ if (idx === -1)
124
+ arr.push(opt.value);
125
+ else
126
+ arr.splice(idx, 1);
127
+ this._set(ctrl.id, arr);
128
+ }
129
+ }}"
130
+ >${opt.label}</nile-tag>`;
131
+ })}
132
+ </div>`;
133
+ }
134
+ // ── Dropdown ─────────────────────────────────────────────────────────────────
135
+ _renderDropdown(ctrl) {
136
+ const raw = this.selectedValues.get(ctrl.id);
137
+ const current = Array.isArray(raw) ? raw : (raw ? [raw] : []);
138
+ const isMulti = ctrl.selection === 'multi';
139
+ return html `
140
+ <nile-select
141
+ .value="${isMulti ? current : (current[0] ?? '')}"
142
+ ?multiple="${isMulti}"
143
+ searchEnabled
144
+ placeholder="${ctrl.placeholder ?? `Select ${ctrl.label || 'option'}…`}"
145
+ @nile-change="${(e) => {
146
+ e.stopPropagation();
147
+ this._set(ctrl.id, e.detail.value);
148
+ }}"
149
+ >
150
+ ${(ctrl.options ?? []).map(opt => html `
151
+ <nile-option
152
+ value="${opt.value}"
153
+ ?selected="${isMulti ? current.includes(opt.value) : current[0] === opt.value}"
154
+ >${opt.label}</nile-option>`)}
155
+ </nile-select>`;
156
+ }
157
+ // ── Segmented ────────────────────────────────────────────────────────────────
158
+ _renderSegmented(ctrl) {
159
+ return html `
160
+ <div class="fc-segmented-scroll">
161
+ <nile-button-toggle-group
162
+ .value="${this.selectedValues.get(ctrl.id) ?? ''}"
163
+ ?multiple="${ctrl.selection === 'multi'}"
164
+ @nile-change="${(e) => { e.stopPropagation(); this._set(ctrl.id, e.detail.value); }}"
165
+ >
166
+ ${(ctrl.options ?? []).map(opt => html `
167
+ <nile-button-toggle value="${opt.value}">${opt.label}</nile-button-toggle>`)}
168
+ </nile-button-toggle-group>
169
+ </div>`;
170
+ }
171
+ // ── Radio ────────────────────────────────────────────────────────────────────
172
+ _renderRadio(ctrl) {
173
+ const current = this.selectedValues.get(ctrl.id) ?? '';
174
+ return html `
175
+ <nile-radio-group
176
+ .value="${current}"
177
+ @nile-change="${(e) => { e.stopPropagation(); this._set(ctrl.id, e.detail.value); }}"
178
+ >
179
+ ${(ctrl.options ?? []).map(opt => html `
180
+ <nile-radio value="${opt.value}" ?checked="${current === opt.value}">${opt.label}</nile-radio>`)}
181
+ </nile-radio-group>`;
182
+ }
183
+ // ── Toggle ───────────────────────────────────────────────────────────────────
184
+ _renderToggle(ctrl) {
185
+ const current = this.selectedValues.get(ctrl.id) ?? {};
186
+ return html `
187
+ <div class="fc-toggle-group">
188
+ ${(ctrl.options ?? []).map(opt => html `
189
+ <nile-slide-toggle
190
+ label="${opt.label}"
191
+ ?isChecked="${!!current[opt.value]}"
192
+ fullWidth
193
+ @nile-change="${(e) => {
194
+ e.stopPropagation();
195
+ const updated = { ...current, [opt.value]: e.detail.checked };
196
+ this._set(ctrl.id, updated);
197
+ }}"
198
+ ></nile-slide-toggle>`)}
199
+ </div>`;
200
+ }
201
+ // ── Slider ───────────────────────────────────────────────────────────────────
202
+ // private _renderSlider(ctrl: FilterControl): TemplateResult {
203
+ // const min = ctrl.min ?? 0;
204
+ // const max = ctrl.max ?? 100;
205
+ // const val: number[] = this.selectedValues.get(ctrl.id) ?? [min, max];
206
+ // const fmt = (n: number) => `${ctrl.prefix ?? ''}${n.toLocaleString()}${ctrl.suffix ?? ''}`;
207
+ //
208
+ // return html`
209
+ // <div class="fc-slider-wrap">
210
+ // <div class="fc-slider-label">
211
+ // <span class="fc-slider-range">${fmt(val[0])} — <strong>${fmt(val[1])}</strong></span>
212
+ // </div>
213
+ // <nile-slider
214
+ // .minValue="${min}"
215
+ // .maxValue="${max}"
216
+ // ?rangeSlider="${true}"
217
+ // .rangeOneValue="${val[0]}"
218
+ // .rangeTwoValue="${val[1]}"
219
+ // @nile-button-first-change-end="${(e: CustomEvent) => {
220
+ // e.stopPropagation();
221
+ // this._set(ctrl.id, [e.detail.value, val[1]]);
222
+ // }}"
223
+ // @nile-button-last-change-end="${(e: CustomEvent) => {
224
+ // e.stopPropagation();
225
+ // this._set(ctrl.id, [val[0], e.detail.value]);
226
+ // }}"
227
+ // >
228
+ // <span slot="prefix">${fmt(min)}</span>
229
+ // <span slot="suffix">${fmt(max)}</span>
230
+ // </nile-slider>
231
+ // </div>`;
232
+ // }
233
+ // ── Search ───────────────────────────────────────────────────────────────────
234
+ _renderSearch(ctrl) {
235
+ return html `
236
+ <nile-input
237
+ type="search"
238
+ placeholder="${ctrl.placeholder ?? `Search ${ctrl.label}…`}"
239
+ .value="${this.selectedValues.get(ctrl.id) ?? ''}"
240
+ clearable
241
+ @nile-input="${(e) => { e.stopPropagation(); this._set(ctrl.id, e.detail.value); }}"
242
+ ></nile-input>`;
243
+ }
244
+ // ── Comparison ───────────────────────────────────────────────────────────────
245
+ _renderComparison(ctrl) {
246
+ const valA = this.selectedValues.get(ctrl.id) ?? '';
247
+ const valB = this.selectedValues.get(`${ctrl.id}__b`) ?? '';
248
+ return html `
249
+ <div class="fc-comparison">
250
+ <nile-select
251
+ .value="${valA}"
252
+ placeholder="Period A"
253
+ @nile-change="${(e) => { e.stopPropagation(); this._set(ctrl.id, e.detail.value); }}"
254
+ >
255
+ ${(ctrl.options ?? []).map(o => html `<nile-option value="${o.value}">${o.label}</nile-option>`)}
256
+ </nile-select>
257
+ <span class="fc-vs">VS</span>
258
+ <nile-select
259
+ .value="${valB}"
260
+ placeholder="Period B"
261
+ @nile-change="${(e) => { e.stopPropagation(); this._set(`${ctrl.id}__b`, e.detail.value); }}"
262
+ >
263
+ ${(ctrl.options ?? []).map(o => html `<nile-option value="${o.value}">${o.label}</nile-option>`)}
264
+ </nile-select>
265
+ </div>`;
266
+ }
267
+ // ── Threshold ────────────────────────────────────────────────────────────────
268
+ _renderThreshold(ctrl) {
269
+ const metric = this.selectedValues.get(ctrl.id) ?? '';
270
+ const op = this.selectedValues.get(`${ctrl.id}__op`) ?? '>';
271
+ const val = this.selectedValues.get(`${ctrl.id}__val`) ?? '';
272
+ const operators = [
273
+ { value: '>', label: '> (greater)' },
274
+ { value: '>=', label: '>= (min)' },
275
+ { value: '<', label: '< (less)' },
276
+ { value: '<=', label: '<= (max)' },
277
+ { value: '=', label: '= (equals)' },
278
+ { value: '!=', label: '≠ (not)' },
279
+ ];
280
+ const metricLabel = (ctrl.options ?? []).find(o => o.value === metric)?.label ?? metric;
281
+ const hasPreview = metric && val !== '';
282
+ const previewText = hasPreview ? `${metricLabel} ${op} ${val}` : 'Set metric, condition, and value above';
283
+ return html `
284
+ <div class="fc-threshold">
285
+ <div class="fc-threshold-metric-row">
286
+ <span class="fc-threshold-where">WHERE</span>
287
+ <div class="fc-threshold-field fc-threshold-field--metric">
288
+ <span class="fc-threshold-field-label">Metric</span>
289
+ <nile-select
290
+ .value="${metric}"
291
+ placeholder="Select metric…"
292
+ @nile-change="${(e) => { e.stopPropagation(); this._set(ctrl.id, e.detail.value); }}"
293
+ >
294
+ ${(ctrl.options ?? []).map(o => html `<nile-option value="${o.value}">${o.label}</nile-option>`)}
295
+ </nile-select>
296
+ </div>
297
+ </div>
298
+ <div class="fc-threshold-cond-row">
299
+ <div class="fc-threshold-field fc-threshold-field--op">
300
+ <span class="fc-threshold-field-label">Condition</span>
301
+ <nile-select
302
+ .value="${op}"
303
+ @nile-change="${(e) => { e.stopPropagation(); this._set(`${ctrl.id}__op`, e.detail.value); }}"
304
+ >
305
+ ${operators.map(o => html `<nile-option value="${o.value}">${o.label}</nile-option>`)}
306
+ </nile-select>
307
+ </div>
308
+ <div class="fc-threshold-field fc-threshold-field--val">
309
+ <span class="fc-threshold-field-label">Value</span>
310
+ <nile-input
311
+ type="number"
312
+ placeholder="e.g. 10000"
313
+ .value="${String(val)}"
314
+ @nile-input="${(e) => { e.stopPropagation(); this._set(`${ctrl.id}__val`, e.detail.value); }}"
315
+ ></nile-input>
316
+ </div>
317
+ </div>
318
+ <div class="fc-threshold-preview ${hasPreview ? '' : 'fc-threshold-preview--empty'}">
319
+ ${hasPreview ? html `<strong>Filter:</strong>` : nothing}
320
+ ${previewText}
321
+ </div>
322
+ </div>`;
323
+ }
324
+ // ── Tree ─────────────────────────────────────────────────────────────────────
325
+ // private _renderTree(ctrl: FilterControl): TemplateResult {
326
+ // const renderNodes = (nodes: TreeNode[]): TemplateResult[] =>
327
+ // nodes.map(node => html`
328
+ // <nile-tree-item value="${node.value}" ?expanded="${node.expanded ?? false}">
329
+ // <span>${node.label}</span>
330
+ // ${node.children ? renderNodes(node.children) : nothing}
331
+ // </nile-tree-item>`);
332
+ //
333
+ // return html`
334
+ // <nile-tree
335
+ // selection="${ctrl.selection === 'multi' ? 'multiple' : 'single'}"
336
+ // @nile-selection-change="${(e: CustomEvent) => {
337
+ // e.stopPropagation();
338
+ // this._set(ctrl.id, e.detail?.detail?.selection ?? []);
339
+ // }}"
340
+ // >
341
+ // ${renderNodes(ctrl.treeData ?? [])}
342
+ // </nile-tree>`;
343
+ // }
344
+ // ── Preset ───────────────────────────────────────────────────────────────────
345
+ _renderPreset(ctrl) {
346
+ const current = this.selectedValues.get(ctrl.id);
347
+ return html `
348
+ <div class="fc-preset-list">
349
+ ${(ctrl.options ?? []).map(opt => html `
350
+ <button
351
+ class="fc-preset-item ${current === opt.value ? 'fc-preset-item--selected' : ''}"
352
+ @click="${() => this._set(ctrl.id, opt.value)}"
353
+ >
354
+ ${opt.icon ? html `<span class="fc-preset-icon">${opt.icon}</span>` : nothing}
355
+ ${opt.label}
356
+ </button>`)}
357
+ </div>`;
358
+ }
359
+ // ── Card wrapper ─────────────────────────────────────────────────────────────
360
+ _renderControl(ctrl) {
361
+ let body;
362
+ switch (ctrl.variant) {
363
+ case 'badge':
364
+ body = this._renderBadge(ctrl);
365
+ break;
366
+ case 'dropdown':
367
+ body = this._renderDropdown(ctrl);
368
+ break;
369
+ case 'segmented':
370
+ body = this._renderSegmented(ctrl);
371
+ break;
372
+ case 'radio':
373
+ body = this._renderRadio(ctrl);
374
+ break;
375
+ case 'toggle':
376
+ body = this._renderToggle(ctrl);
377
+ break;
378
+ // case 'slider': body = this._renderSlider(ctrl); break;
379
+ case 'search':
380
+ body = this._renderSearch(ctrl);
381
+ break;
382
+ case 'comparison':
383
+ body = this._renderComparison(ctrl);
384
+ break;
385
+ case 'threshold':
386
+ body = this._renderThreshold(ctrl);
387
+ break;
388
+ // case 'tree': body = this._renderTree(ctrl); break;
389
+ case 'preset':
390
+ body = this._renderPreset(ctrl);
391
+ break;
392
+ default: body = html ``;
393
+ }
394
+ return html `
395
+ <div class="fc-control" part="filter-control">
396
+ ${ctrl.label ? html `<div class="fc-control__label">${ctrl.label}</div>` : nothing}
397
+ ${ctrl.description ? html `<div class="fc-control__desc">${ctrl.description}</div>` : nothing}
398
+ <div class="fc-control__body">${body}</div>
399
+ </div>`;
400
+ }
401
+ _renderGroup(group) {
402
+ const collapsed = this.collapsedGroups.has(group.label);
403
+ const toggle = () => {
404
+ const next = new Set(this.collapsedGroups);
405
+ if (collapsed)
406
+ next.delete(group.label);
407
+ else
408
+ next.add(group.label);
409
+ this.collapsedGroups = next;
410
+ };
411
+ return html `
412
+ <div class="fc-group" part="filter-group">
413
+ <div class="fc-group__header ${group.collapsible ? 'fc-group__header--collapsible' : ''}"
414
+ @click="${group.collapsible ? toggle : nothing}">
415
+ <div class="fc-group__header-text">
416
+ <span class="fc-group__label">${group.label}</span>
417
+ ${group.description ? html `<span class="fc-group__desc">${group.description}</span>` : nothing}
418
+ </div>
419
+ ${group.collapsible ? html `
420
+ <span class="fc-group__chevron ${collapsed ? '' : 'fc-group__chevron--open'}">&#8250;</span>
421
+ ` : nothing}
422
+ </div>
423
+ ${collapsed ? nothing : html `
424
+ <div class="fc-group__body">
425
+ ${group.controls.map(ctrl => this._renderControl(ctrl))}
426
+ </div>`}
427
+ </div>`;
428
+ }
429
+ render() {
430
+ const entries = this.config?.chart?.controls ?? [];
431
+ return html `
432
+ <div class="fc-root" part="filter-root">
433
+ ${entries.map(entry => entry.type === 'group'
434
+ ? this._renderGroup(entry)
435
+ : this._renderControl(entry))}
436
+ </div>`;
437
+ }
438
+ };
439
+ __decorate([
440
+ property({ attribute: false })
441
+ ], NileFilterChart.prototype, "config", void 0);
442
+ __decorate([
443
+ state()
444
+ ], NileFilterChart.prototype, "selectedValues", void 0);
445
+ __decorate([
446
+ state()
447
+ ], NileFilterChart.prototype, "collapsedGroups", void 0);
448
+ NileFilterChart = __decorate([
449
+ customElement('nile-filter-chart')
450
+ ], NileFilterChart);
451
+ export { NileFilterChart };
452
+ export default NileFilterChart;
453
+ //# sourceMappingURL=nile-filter-chart.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aquera/nile-visualization",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "A visualization Library for the Nile Design System",
5
5
  "license": "MIT",
6
6
  "author": "Aquera Inc",