@memberjunction/ng-filter-builder 0.0.1 → 2.123.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.
Files changed (30) hide show
  1. package/dist/lib/filter-builder/filter-builder.component.d.ts +155 -0
  2. package/dist/lib/filter-builder/filter-builder.component.d.ts.map +1 -0
  3. package/dist/lib/filter-builder/filter-builder.component.js +482 -0
  4. package/dist/lib/filter-builder/filter-builder.component.js.map +1 -0
  5. package/dist/lib/filter-builder.module.d.ts +39 -0
  6. package/dist/lib/filter-builder.module.d.ts.map +1 -0
  7. package/dist/lib/filter-builder.module.js +66 -0
  8. package/dist/lib/filter-builder.module.js.map +1 -0
  9. package/dist/lib/filter-group/filter-group.component.d.ts +102 -0
  10. package/dist/lib/filter-group/filter-group.component.d.ts.map +1 -0
  11. package/dist/lib/filter-group/filter-group.component.js +328 -0
  12. package/dist/lib/filter-group/filter-group.component.js.map +1 -0
  13. package/dist/lib/filter-rule/filter-rule.component.d.ts +165 -0
  14. package/dist/lib/filter-rule/filter-rule.component.d.ts.map +1 -0
  15. package/dist/lib/filter-rule/filter-rule.component.js +682 -0
  16. package/dist/lib/filter-rule/filter-rule.component.js.map +1 -0
  17. package/dist/lib/types/filter.types.d.ts +142 -0
  18. package/dist/lib/types/filter.types.d.ts.map +1 -0
  19. package/dist/lib/types/filter.types.js +82 -0
  20. package/dist/lib/types/filter.types.js.map +1 -0
  21. package/dist/lib/types/operators.d.ts +49 -0
  22. package/dist/lib/types/operators.d.ts.map +1 -0
  23. package/dist/lib/types/operators.js +99 -0
  24. package/dist/lib/types/operators.js.map +1 -0
  25. package/dist/public-api.d.ts +15 -0
  26. package/dist/public-api.d.ts.map +1 -0
  27. package/dist/public-api.js +19 -0
  28. package/dist/public-api.js.map +1 -0
  29. package/package.json +38 -6
  30. package/README.md +0 -45
@@ -0,0 +1,155 @@
1
+ import { EventEmitter, OnInit, OnChanges, SimpleChanges } from '@angular/core';
2
+ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
3
+ import { CompositeFilterDescriptor, FilterFieldInfo, FilterBuilderConfig } from '../types/filter.types';
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * FilterBuilderComponent - Main filter builder component
7
+ *
8
+ * Provides a complete UI for building complex filter expressions
9
+ * with AND/OR logic and nested groups. Outputs Kendo-compatible
10
+ * CompositeFilterDescriptor JSON format.
11
+ *
12
+ * @example
13
+ * ```html
14
+ * <mj-filter-builder
15
+ * [fields]="filterFields"
16
+ * [filter]="currentFilter"
17
+ * (filterChange)="onFilterChange($event)"
18
+ * (apply)="onApply($event)">
19
+ * </mj-filter-builder>
20
+ * ```
21
+ */
22
+ export declare class FilterBuilderComponent implements OnInit, OnChanges {
23
+ private sanitizer;
24
+ /**
25
+ * Available fields to filter on
26
+ */
27
+ fields: FilterFieldInfo[];
28
+ /**
29
+ * Current filter state (Kendo-compatible CompositeFilterDescriptor)
30
+ */
31
+ filter: CompositeFilterDescriptor | null;
32
+ /**
33
+ * Configuration options
34
+ */
35
+ config: Partial<FilterBuilderConfig>;
36
+ /**
37
+ * Whether the component is disabled
38
+ */
39
+ disabled: boolean;
40
+ /**
41
+ * Whether to show the natural language filter summary at the bottom
42
+ */
43
+ showSummary: boolean;
44
+ /**
45
+ * Whether the filter summary is expanded (visible)
46
+ */
47
+ isSummaryExpanded: boolean;
48
+ /**
49
+ * Emitted when the filter changes
50
+ */
51
+ filterChange: EventEmitter<CompositeFilterDescriptor>;
52
+ /**
53
+ * Emitted when the Apply button is clicked (if showApplyButton is true)
54
+ */
55
+ apply: EventEmitter<CompositeFilterDescriptor>;
56
+ /**
57
+ * Emitted when the Clear button is clicked
58
+ */
59
+ clear: EventEmitter<void>;
60
+ /**
61
+ * Internal filter state
62
+ */
63
+ internalFilter: CompositeFilterDescriptor;
64
+ /**
65
+ * Merged configuration
66
+ */
67
+ mergedConfig: FilterBuilderConfig;
68
+ /**
69
+ * Whether there are any active filters
70
+ */
71
+ hasActiveFilters: boolean;
72
+ constructor(sanitizer: DomSanitizer);
73
+ ngOnInit(): void;
74
+ ngOnChanges(changes: SimpleChanges): void;
75
+ /**
76
+ * Initialize the internal filter state
77
+ */
78
+ private initializeFilter;
79
+ /**
80
+ * Merge provided config with defaults
81
+ */
82
+ private mergeConfig;
83
+ /**
84
+ * Handle filter change from the filter group
85
+ */
86
+ onFilterChange(filter: CompositeFilterDescriptor): void;
87
+ /**
88
+ * Handle Apply button click
89
+ */
90
+ onApply(): void;
91
+ /**
92
+ * Handle Clear button click
93
+ */
94
+ onClear(): void;
95
+ /**
96
+ * Get the count of active filter rules
97
+ */
98
+ getFilterCount(): number;
99
+ /**
100
+ * Count filters recursively
101
+ */
102
+ private countFilters;
103
+ /**
104
+ * Update hasActiveFilters flag
105
+ */
106
+ private updateHasActiveFilters;
107
+ /**
108
+ * Deep clone a filter to prevent mutation
109
+ */
110
+ private deepCloneFilter;
111
+ /**
112
+ * Toggle the filter summary visibility
113
+ */
114
+ toggleSummary(): void;
115
+ /**
116
+ * Generate HTML-formatted summary of the filter expression with syntax highlighting
117
+ */
118
+ getFilterSummaryHtml(): SafeHtml;
119
+ /**
120
+ * Inline styles for syntax highlighting (needed because Angular view encapsulation
121
+ * doesn't apply component CSS to dynamically injected innerHTML)
122
+ */
123
+ private readonly styles;
124
+ /**
125
+ * Build HTML summary recursively with indentation and syntax highlighting
126
+ */
127
+ private buildFilterSummaryHtml;
128
+ /**
129
+ * Build HTML summary for a single rule with syntax highlighting
130
+ */
131
+ private buildRuleSummaryHtml;
132
+ /**
133
+ * Check if operator is a null-check operator (doesn't need a value)
134
+ */
135
+ private isNullCheckOperator;
136
+ /**
137
+ * Get human-readable label for an operator
138
+ */
139
+ private getOperatorLabel;
140
+ /**
141
+ * Format a value for HTML display with syntax highlighting
142
+ */
143
+ private formatValueHtml;
144
+ /**
145
+ * Check if a string looks like an ISO date
146
+ */
147
+ private isIsoDateString;
148
+ /**
149
+ * Escape HTML characters to prevent XSS
150
+ */
151
+ private escapeHtml;
152
+ static ɵfac: i0.ɵɵFactoryDeclaration<FilterBuilderComponent, never>;
153
+ static ɵcmp: i0.ɵɵComponentDeclaration<FilterBuilderComponent, "mj-filter-builder", never, { "fields": { "alias": "fields"; "required": false; }; "filter": { "alias": "filter"; "required": false; }; "config": { "alias": "config"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "showSummary": { "alias": "showSummary"; "required": false; }; }, { "filterChange": "filterChange"; "apply": "apply"; "clear": "clear"; }, never, never, false, never>;
154
+ }
155
+ //# sourceMappingURL=filter-builder.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-builder.component.d.ts","sourceRoot":"","sources":["../../../src/lib/filter-builder/filter-builder.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EACL,yBAAyB,EAEzB,eAAe,EACf,mBAAmB,EAGpB,MAAM,uBAAuB,CAAC;;AAa/B;;;;;;;;;;;;;;;;GAgBG;AACH,qBAKa,sBAAuB,YAAW,MAAM,EAAE,SAAS;IA6DlD,OAAO,CAAC,SAAS;IA5D7B;;OAEG;IACM,MAAM,EAAE,eAAe,EAAE,CAAM;IAExC;;OAEG;IACM,MAAM,EAAE,yBAAyB,GAAG,IAAI,CAAQ;IAEzD;;OAEG;IACM,MAAM,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAM;IAEnD;;OAEG;IACM,QAAQ,EAAE,OAAO,CAAS;IAEnC;;OAEG;IACM,WAAW,EAAE,OAAO,CAAS;IAEtC;;OAEG;IACI,iBAAiB,EAAE,OAAO,CAAS;IAE1C;;OAEG;IACO,YAAY,0CAAiD;IAEvE;;OAEG;IACO,KAAK,0CAAiD;IAEhE;;OAEG;IACO,KAAK,qBAA4B;IAE3C;;OAEG;IACI,cAAc,EAAE,yBAAyB,CAAuB;IAEvE;;OAEG;IACI,YAAY,EAAE,mBAAmB,CAAyB;IAEjE;;OAEG;IACI,gBAAgB,EAAE,OAAO,CAAS;gBAErB,SAAS,EAAE,YAAY;IAE3C,QAAQ,IAAI,IAAI;IAKhB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IASzC;;OAEG;IACH,OAAO,CAAC,gBAAgB;IASxB;;OAEG;IACH,OAAO,CAAC,WAAW;IAInB;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI;IASvD;;OAEG;IACH,OAAO,IAAI,IAAI;IAKf;;OAEG;IACH,OAAO,IAAI,IAAI;IAOf;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;OAEG;IACH,OAAO,CAAC,YAAY;IAgBpB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAI9B;;OAEG;IACH,OAAO,CAAC,eAAe;IAIvB;;OAEG;IACH,aAAa,IAAI,IAAI;IAIrB;;OAEG;IACH,oBAAoB,IAAI,QAAQ;IAQhC;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,MAAM,CAWrB;IAEF;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA8B9B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA0B5B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAI3B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAoBxB;;OAEG;IACH,OAAO,CAAC,eAAe;IAmDvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAKvB;;OAEG;IACH,OAAO,CAAC,UAAU;yCAtWP,sBAAsB;2CAAtB,sBAAsB;CA2WlC"}
@@ -0,0 +1,482 @@
1
+ import { Component, Input, Output, EventEmitter } from '@angular/core';
2
+ import { createEmptyFilter, isCompositeFilter } from '../types/filter.types';
3
+ import * as i0 from "@angular/core";
4
+ import * as i1 from "@angular/platform-browser";
5
+ import * as i2 from "../filter-group/filter-group.component";
6
+ function FilterBuilderComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
7
+ i0.ɵɵelementStart(0, "span", 5);
8
+ i0.ɵɵtext(1);
9
+ i0.ɵɵelementEnd();
10
+ } if (rf & 2) {
11
+ const ctx_r0 = i0.ɵɵnextContext();
12
+ i0.ɵɵadvance();
13
+ i0.ɵɵtextInterpolate2("", ctx_r0.getFilterCount(), " condition", ctx_r0.getFilterCount() !== 1 ? "s" : "", "");
14
+ } }
15
+ function FilterBuilderComponent_Conditional_6_Template(rf, ctx) { if (rf & 1) {
16
+ i0.ɵɵelementStart(0, "span", 6);
17
+ i0.ɵɵtext(1, "No filters applied");
18
+ i0.ɵɵelementEnd();
19
+ } }
20
+ function FilterBuilderComponent_Conditional_8_Template(rf, ctx) { if (rf & 1) {
21
+ const _r2 = i0.ɵɵgetCurrentView();
22
+ i0.ɵɵelementStart(0, "button", 14);
23
+ i0.ɵɵlistener("click", function FilterBuilderComponent_Conditional_8_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.onClear()); });
24
+ i0.ɵɵelement(1, "i", 15);
25
+ i0.ɵɵelementStart(2, "span");
26
+ i0.ɵɵtext(3, "Clear All");
27
+ i0.ɵɵelementEnd()();
28
+ } if (rf & 2) {
29
+ const ctx_r0 = i0.ɵɵnextContext();
30
+ i0.ɵɵproperty("disabled", ctx_r0.disabled);
31
+ } }
32
+ function FilterBuilderComponent_Conditional_9_Template(rf, ctx) { if (rf & 1) {
33
+ const _r3 = i0.ɵɵgetCurrentView();
34
+ i0.ɵɵelementStart(0, "button", 16);
35
+ i0.ɵɵlistener("click", function FilterBuilderComponent_Conditional_9_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.onApply()); });
36
+ i0.ɵɵelement(1, "i", 17);
37
+ i0.ɵɵelementStart(2, "span");
38
+ i0.ɵɵtext(3, "Apply");
39
+ i0.ɵɵelementEnd()();
40
+ } if (rf & 2) {
41
+ const ctx_r0 = i0.ɵɵnextContext();
42
+ i0.ɵɵproperty("disabled", ctx_r0.disabled);
43
+ } }
44
+ function FilterBuilderComponent_Conditional_12_Template(rf, ctx) { if (rf & 1) {
45
+ i0.ɵɵelementStart(0, "div", 12)(1, "p");
46
+ i0.ɵɵelement(2, "i", 18);
47
+ i0.ɵɵelementStart(3, "strong");
48
+ i0.ɵɵtext(4, "Tip:");
49
+ i0.ɵɵelementEnd();
50
+ i0.ɵɵtext(5, " Click \"Add Condition\" to create filter rules. Use \"Add Group\" for complex AND/OR combinations. ");
51
+ i0.ɵɵelementEnd()();
52
+ } }
53
+ function FilterBuilderComponent_Conditional_13_Conditional_8_Template(rf, ctx) { if (rf & 1) {
54
+ i0.ɵɵelementStart(0, "div", 25);
55
+ i0.ɵɵelement(1, "pre", 26);
56
+ i0.ɵɵelementEnd();
57
+ } if (rf & 2) {
58
+ const ctx_r0 = i0.ɵɵnextContext(2);
59
+ i0.ɵɵadvance();
60
+ i0.ɵɵproperty("innerHTML", ctx_r0.getFilterSummaryHtml(), i0.ɵɵsanitizeHtml);
61
+ } }
62
+ function FilterBuilderComponent_Conditional_13_Template(rf, ctx) { if (rf & 1) {
63
+ const _r4 = i0.ɵɵgetCurrentView();
64
+ i0.ɵɵelementStart(0, "div", 19)(1, "button", 20);
65
+ i0.ɵɵlistener("click", function FilterBuilderComponent_Conditional_13_Template_button_click_1_listener() { i0.ɵɵrestoreView(_r4); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.toggleSummary()); });
66
+ i0.ɵɵelement(2, "i", 21)(3, "i", 22);
67
+ i0.ɵɵelementStart(4, "span", 23);
68
+ i0.ɵɵtext(5, "View Filter Expression");
69
+ i0.ɵɵelementEnd();
70
+ i0.ɵɵelementStart(6, "span", 24);
71
+ i0.ɵɵtext(7);
72
+ i0.ɵɵelementEnd()();
73
+ i0.ɵɵtemplate(8, FilterBuilderComponent_Conditional_13_Conditional_8_Template, 2, 1, "div", 25);
74
+ i0.ɵɵelementEnd();
75
+ } if (rf & 2) {
76
+ const ctx_r0 = i0.ɵɵnextContext();
77
+ i0.ɵɵclassProp("expanded", ctx_r0.isSummaryExpanded);
78
+ i0.ɵɵadvance(2);
79
+ i0.ɵɵclassProp("fa-chevron-right", !ctx_r0.isSummaryExpanded)("fa-chevron-down", ctx_r0.isSummaryExpanded);
80
+ i0.ɵɵadvance(5);
81
+ i0.ɵɵtextInterpolate(ctx_r0.getFilterCount());
82
+ i0.ɵɵadvance();
83
+ i0.ɵɵconditional(ctx_r0.isSummaryExpanded ? 8 : -1);
84
+ } }
85
+ /**
86
+ * Default configuration for the filter builder
87
+ */
88
+ const DEFAULT_CONFIG = {
89
+ maxDepth: 3,
90
+ allowGroups: true,
91
+ showClearButton: true,
92
+ showApplyButton: false,
93
+ applyOnChange: true
94
+ };
95
+ /**
96
+ * FilterBuilderComponent - Main filter builder component
97
+ *
98
+ * Provides a complete UI for building complex filter expressions
99
+ * with AND/OR logic and nested groups. Outputs Kendo-compatible
100
+ * CompositeFilterDescriptor JSON format.
101
+ *
102
+ * @example
103
+ * ```html
104
+ * <mj-filter-builder
105
+ * [fields]="filterFields"
106
+ * [filter]="currentFilter"
107
+ * (filterChange)="onFilterChange($event)"
108
+ * (apply)="onApply($event)">
109
+ * </mj-filter-builder>
110
+ * ```
111
+ */
112
+ export class FilterBuilderComponent {
113
+ sanitizer;
114
+ /**
115
+ * Available fields to filter on
116
+ */
117
+ fields = [];
118
+ /**
119
+ * Current filter state (Kendo-compatible CompositeFilterDescriptor)
120
+ */
121
+ filter = null;
122
+ /**
123
+ * Configuration options
124
+ */
125
+ config = {};
126
+ /**
127
+ * Whether the component is disabled
128
+ */
129
+ disabled = false;
130
+ /**
131
+ * Whether to show the natural language filter summary at the bottom
132
+ */
133
+ showSummary = false;
134
+ /**
135
+ * Whether the filter summary is expanded (visible)
136
+ */
137
+ isSummaryExpanded = false;
138
+ /**
139
+ * Emitted when the filter changes
140
+ */
141
+ filterChange = new EventEmitter();
142
+ /**
143
+ * Emitted when the Apply button is clicked (if showApplyButton is true)
144
+ */
145
+ apply = new EventEmitter();
146
+ /**
147
+ * Emitted when the Clear button is clicked
148
+ */
149
+ clear = new EventEmitter();
150
+ /**
151
+ * Internal filter state
152
+ */
153
+ internalFilter = createEmptyFilter();
154
+ /**
155
+ * Merged configuration
156
+ */
157
+ mergedConfig = { ...DEFAULT_CONFIG };
158
+ /**
159
+ * Whether there are any active filters
160
+ */
161
+ hasActiveFilters = false;
162
+ constructor(sanitizer) {
163
+ this.sanitizer = sanitizer;
164
+ }
165
+ ngOnInit() {
166
+ this.initializeFilter();
167
+ this.mergeConfig();
168
+ }
169
+ ngOnChanges(changes) {
170
+ if (changes['filter']) {
171
+ this.initializeFilter();
172
+ }
173
+ if (changes['config']) {
174
+ this.mergeConfig();
175
+ }
176
+ }
177
+ /**
178
+ * Initialize the internal filter state
179
+ */
180
+ initializeFilter() {
181
+ if (this.filter && isCompositeFilter(this.filter)) {
182
+ this.internalFilter = this.deepCloneFilter(this.filter);
183
+ }
184
+ else {
185
+ this.internalFilter = createEmptyFilter();
186
+ }
187
+ this.updateHasActiveFilters();
188
+ }
189
+ /**
190
+ * Merge provided config with defaults
191
+ */
192
+ mergeConfig() {
193
+ this.mergedConfig = { ...DEFAULT_CONFIG, ...this.config };
194
+ }
195
+ /**
196
+ * Handle filter change from the filter group
197
+ */
198
+ onFilterChange(filter) {
199
+ this.internalFilter = filter;
200
+ this.updateHasActiveFilters();
201
+ if (this.mergedConfig.applyOnChange) {
202
+ this.filterChange.emit(filter);
203
+ }
204
+ }
205
+ /**
206
+ * Handle Apply button click
207
+ */
208
+ onApply() {
209
+ this.filterChange.emit(this.internalFilter);
210
+ this.apply.emit(this.internalFilter);
211
+ }
212
+ /**
213
+ * Handle Clear button click
214
+ */
215
+ onClear() {
216
+ this.internalFilter = createEmptyFilter();
217
+ this.updateHasActiveFilters();
218
+ this.filterChange.emit(this.internalFilter);
219
+ this.clear.emit();
220
+ }
221
+ /**
222
+ * Get the count of active filter rules
223
+ */
224
+ getFilterCount() {
225
+ return this.countFilters(this.internalFilter);
226
+ }
227
+ /**
228
+ * Count filters recursively
229
+ */
230
+ countFilters(filter) {
231
+ let count = 0;
232
+ for (const item of filter.filters || []) {
233
+ if (isCompositeFilter(item)) {
234
+ count += this.countFilters(item);
235
+ }
236
+ else {
237
+ // Only count if the filter has a valid field and value (or null-check operators)
238
+ const rule = item;
239
+ if (rule.field) {
240
+ count++;
241
+ }
242
+ }
243
+ }
244
+ return count;
245
+ }
246
+ /**
247
+ * Update hasActiveFilters flag
248
+ */
249
+ updateHasActiveFilters() {
250
+ this.hasActiveFilters = this.getFilterCount() > 0;
251
+ }
252
+ /**
253
+ * Deep clone a filter to prevent mutation
254
+ */
255
+ deepCloneFilter(filter) {
256
+ return JSON.parse(JSON.stringify(filter));
257
+ }
258
+ /**
259
+ * Toggle the filter summary visibility
260
+ */
261
+ toggleSummary() {
262
+ this.isSummaryExpanded = !this.isSummaryExpanded;
263
+ }
264
+ /**
265
+ * Generate HTML-formatted summary of the filter expression with syntax highlighting
266
+ */
267
+ getFilterSummaryHtml() {
268
+ if (!this.hasActiveFilters) {
269
+ return this.sanitizer.bypassSecurityTrustHtml('<span style="color: #9ca3af; font-style: italic;">No filters applied</span>');
270
+ }
271
+ const html = this.buildFilterSummaryHtml(this.internalFilter, 0);
272
+ return this.sanitizer.bypassSecurityTrustHtml(html);
273
+ }
274
+ /**
275
+ * Inline styles for syntax highlighting (needed because Angular view encapsulation
276
+ * doesn't apply component CSS to dynamically injected innerHTML)
277
+ */
278
+ styles = {
279
+ fieldName: 'color: #0369a1; font-weight: 600;',
280
+ operator: 'color: #6b7280; font-style: italic;',
281
+ valueString: 'color: #059669; font-weight: 500;',
282
+ valueNumber: 'color: #7c3aed; font-weight: 500;',
283
+ valueDate: 'color: #c2410c; font-weight: 500;',
284
+ valueTrue: 'color: #16a34a; font-weight: 600;',
285
+ valueFalse: 'color: #dc2626; font-weight: 600;',
286
+ logicAnd: 'display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; letter-spacing: 0.5px; background: #dbeafe; color: #1d4ed8;',
287
+ logicOr: 'display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; letter-spacing: 0.5px; background: #fef3c7; color: #b45309;',
288
+ groupBracket: 'color: #9333ea; font-weight: 700; font-size: 15px;'
289
+ };
290
+ /**
291
+ * Build HTML summary recursively with indentation and syntax highlighting
292
+ */
293
+ buildFilterSummaryHtml(filter, depth) {
294
+ const parts = [];
295
+ const indent = ' '.repeat(depth);
296
+ for (const item of filter.filters || []) {
297
+ if (isCompositeFilter(item)) {
298
+ const groupSummary = this.buildFilterSummaryHtml(item, depth + 1);
299
+ if (groupSummary) {
300
+ parts.push(`<span style="${this.styles.groupBracket}">(</span>\n${groupSummary}\n${indent}<span style="${this.styles.groupBracket}">)</span>`);
301
+ }
302
+ }
303
+ else {
304
+ const rule = item;
305
+ const ruleSummary = this.buildRuleSummaryHtml(rule);
306
+ if (ruleSummary) {
307
+ parts.push(ruleSummary);
308
+ }
309
+ }
310
+ }
311
+ if (parts.length === 0) {
312
+ return '';
313
+ }
314
+ const logicStyle = filter.logic === 'and' ? this.styles.logicAnd : this.styles.logicOr;
315
+ const logicLabel = filter.logic === 'and' ? 'AND' : 'OR';
316
+ const connector = `\n${indent}<span style="${logicStyle}">${logicLabel}</span>\n${indent}`;
317
+ return `${indent}${parts.join(connector)}`;
318
+ }
319
+ /**
320
+ * Build HTML summary for a single rule with syntax highlighting
321
+ */
322
+ buildRuleSummaryHtml(rule) {
323
+ if (!rule.field) {
324
+ return '';
325
+ }
326
+ // Get the field display name
327
+ const field = this.fields.find(f => f.name === rule.field);
328
+ const fieldName = field?.displayName || rule.field;
329
+ // Get the operator label
330
+ const operatorLabel = this.getOperatorLabel(rule.operator);
331
+ // Format the value
332
+ const formattedValue = this.formatValueHtml(rule.value, rule.operator);
333
+ // Build the summary based on operator type
334
+ const fieldHtml = `<span style="${this.styles.fieldName}">${this.escapeHtml(fieldName)}</span>`;
335
+ const operatorHtml = `<span style="${this.styles.operator}">${operatorLabel}</span>`;
336
+ if (this.isNullCheckOperator(rule.operator)) {
337
+ return `${fieldHtml} ${operatorHtml}`;
338
+ }
339
+ return `${fieldHtml} ${operatorHtml} ${formattedValue}`;
340
+ }
341
+ /**
342
+ * Check if operator is a null-check operator (doesn't need a value)
343
+ */
344
+ isNullCheckOperator(operator) {
345
+ return ['isnull', 'isnotnull', 'isempty', 'isnotempty'].includes(operator);
346
+ }
347
+ /**
348
+ * Get human-readable label for an operator
349
+ */
350
+ getOperatorLabel(operator) {
351
+ const labels = {
352
+ 'eq': 'equals',
353
+ 'neq': 'does not equal',
354
+ 'contains': 'contains',
355
+ 'doesnotcontain': 'does not contain',
356
+ 'startswith': 'starts with',
357
+ 'endswith': 'ends with',
358
+ 'isnull': 'is empty',
359
+ 'isnotnull': 'is not empty',
360
+ 'isempty': 'is empty',
361
+ 'isnotempty': 'is not empty',
362
+ 'gt': 'is greater than',
363
+ 'gte': 'is greater than or equal to',
364
+ 'lt': 'is less than',
365
+ 'lte': 'is less than or equal to'
366
+ };
367
+ return labels[operator] || operator;
368
+ }
369
+ /**
370
+ * Format a value for HTML display with syntax highlighting
371
+ */
372
+ formatValueHtml(value, operator) {
373
+ if (value === null || value === undefined) {
374
+ return '';
375
+ }
376
+ // Don't show value for null-check operators
377
+ if (this.isNullCheckOperator(operator)) {
378
+ return '';
379
+ }
380
+ // Handle ISO date strings
381
+ if (typeof value === 'string' && this.isIsoDateString(value)) {
382
+ const date = new Date(value);
383
+ const formatted = date.toLocaleDateString('en-US', {
384
+ year: 'numeric',
385
+ month: 'short',
386
+ day: 'numeric'
387
+ });
388
+ return `<span style="${this.styles.valueDate}">${formatted}</span>`;
389
+ }
390
+ // Handle strings
391
+ if (typeof value === 'string') {
392
+ return `<span style="${this.styles.valueString}">"${this.escapeHtml(value)}"</span>`;
393
+ }
394
+ // Handle booleans
395
+ if (typeof value === 'boolean') {
396
+ const boolStyle = value ? this.styles.valueTrue : this.styles.valueFalse;
397
+ return `<span style="${boolStyle}">${value ? 'Yes' : 'No'}</span>`;
398
+ }
399
+ // Handle numbers
400
+ if (typeof value === 'number') {
401
+ return `<span style="${this.styles.valueNumber}">${value}</span>`;
402
+ }
403
+ // Handle Date objects
404
+ if (value instanceof Date) {
405
+ const formatted = value.toLocaleDateString('en-US', {
406
+ year: 'numeric',
407
+ month: 'short',
408
+ day: 'numeric'
409
+ });
410
+ return `<span style="${this.styles.valueDate}">${formatted}</span>`;
411
+ }
412
+ // Default
413
+ return `<span style="font-weight: 500;">${this.escapeHtml(String(value))}</span>`;
414
+ }
415
+ /**
416
+ * Check if a string looks like an ISO date
417
+ */
418
+ isIsoDateString(value) {
419
+ // Match ISO 8601 date format
420
+ return /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?)?$/.test(value);
421
+ }
422
+ /**
423
+ * Escape HTML characters to prevent XSS
424
+ */
425
+ escapeHtml(text) {
426
+ const div = document.createElement('div');
427
+ div.textContent = text;
428
+ return div.innerHTML;
429
+ }
430
+ static ɵfac = function FilterBuilderComponent_Factory(t) { return new (t || FilterBuilderComponent)(i0.ɵɵdirectiveInject(i1.DomSanitizer)); };
431
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: FilterBuilderComponent, selectors: [["mj-filter-builder"]], inputs: { fields: "fields", filter: "filter", config: "config", disabled: "disabled", showSummary: "showSummary" }, outputs: { filterChange: "filterChange", apply: "apply", clear: "clear" }, features: [i0.ɵɵNgOnChangesFeature], decls: 14, vars: 13, consts: [[1, "filter-builder"], [1, "filter-header"], [1, "filter-summary"], [1, "filter-icon"], [1, "fa-solid", "fa-filter"], [1, "filter-count"], [1, "filter-count", "no-filters"], [1, "filter-actions"], ["type", "button", "title", "Clear all filters", 1, "action-btn", "clear-btn", 3, "disabled"], ["type", "button", "title", "Apply filters", 1, "action-btn", "apply-btn", 3, "disabled"], [1, "filter-content"], [3, "filterChange", "filter", "fields", "isRoot", "depth", "maxDepth", "disabled"], [1, "filter-tips"], [1, "filter-expression-summary", 3, "expanded"], ["type", "button", "title", "Clear all filters", 1, "action-btn", "clear-btn", 3, "click", "disabled"], [1, "fa-solid", "fa-times"], ["type", "button", "title", "Apply filters", 1, "action-btn", "apply-btn", 3, "click", "disabled"], [1, "fa-solid", "fa-check"], [1, "fa-solid", "fa-lightbulb"], [1, "filter-expression-summary"], ["type", "button", 1, "expression-toggle", 3, "click"], [1, "fa-solid"], [1, "fa-solid", "fa-code", "expression-icon"], [1, "expression-label"], [1, "expression-badge"], [1, "expression-content"], [1, "expression-text", 3, "innerHTML"]], template: function FilterBuilderComponent_Template(rf, ctx) { if (rf & 1) {
432
+ i0.ɵɵelementStart(0, "div", 0)(1, "div", 1)(2, "div", 2)(3, "span", 3);
433
+ i0.ɵɵelement(4, "i", 4);
434
+ i0.ɵɵelementEnd();
435
+ i0.ɵɵtemplate(5, FilterBuilderComponent_Conditional_5_Template, 2, 2, "span", 5)(6, FilterBuilderComponent_Conditional_6_Template, 2, 0, "span", 6);
436
+ i0.ɵɵelementEnd();
437
+ i0.ɵɵelementStart(7, "div", 7);
438
+ i0.ɵɵtemplate(8, FilterBuilderComponent_Conditional_8_Template, 4, 1, "button", 8)(9, FilterBuilderComponent_Conditional_9_Template, 4, 1, "button", 9);
439
+ i0.ɵɵelementEnd()();
440
+ i0.ɵɵelementStart(10, "div", 10)(11, "mj-filter-group", 11);
441
+ i0.ɵɵlistener("filterChange", function FilterBuilderComponent_Template_mj_filter_group_filterChange_11_listener($event) { return ctx.onFilterChange($event); });
442
+ i0.ɵɵelementEnd()();
443
+ i0.ɵɵtemplate(12, FilterBuilderComponent_Conditional_12_Template, 6, 0, "div", 12)(13, FilterBuilderComponent_Conditional_13_Template, 9, 8, "div", 13);
444
+ i0.ɵɵelementEnd();
445
+ } if (rf & 2) {
446
+ i0.ɵɵclassProp("disabled", ctx.disabled);
447
+ i0.ɵɵadvance(5);
448
+ i0.ɵɵconditional(ctx.hasActiveFilters ? 5 : 6);
449
+ i0.ɵɵadvance(3);
450
+ i0.ɵɵconditional(ctx.mergedConfig.showClearButton && ctx.hasActiveFilters ? 8 : -1);
451
+ i0.ɵɵadvance();
452
+ i0.ɵɵconditional(ctx.mergedConfig.showApplyButton ? 9 : -1);
453
+ i0.ɵɵadvance(2);
454
+ i0.ɵɵproperty("filter", ctx.internalFilter)("fields", ctx.fields)("isRoot", true)("depth", 0)("maxDepth", ctx.mergedConfig.maxDepth)("disabled", ctx.disabled);
455
+ i0.ɵɵadvance();
456
+ i0.ɵɵconditional(!ctx.hasActiveFilters ? 12 : -1);
457
+ i0.ɵɵadvance();
458
+ i0.ɵɵconditional(ctx.showSummary && ctx.hasActiveFilters ? 13 : -1);
459
+ } }, dependencies: [i2.FilterGroupComponent], styles: [".filter-builder[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.filter-builder.disabled[_ngcontent-%COMP%] {\n opacity: 0.6;\n pointer-events: none;\n}\n\n\n\n.filter-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-bottom: 12px;\n border-bottom: 1px solid #eee;\n}\n\n.filter-summary[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.filter-icon[_ngcontent-%COMP%] {\n color: #1976d2;\n font-size: 14px;\n}\n\n.filter-count[_ngcontent-%COMP%] {\n font-size: 13px;\n color: #333;\n font-weight: 500;\n}\n\n.filter-count.no-filters[_ngcontent-%COMP%] {\n color: #999;\n font-weight: 400;\n}\n\n\n\n.filter-actions[_ngcontent-%COMP%] {\n display: flex;\n gap: 8px;\n}\n\n.action-btn[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 6px 12px;\n border: none;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.action-btn[_ngcontent-%COMP%]:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.action-btn[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 11px;\n}\n\n.clear-btn[_ngcontent-%COMP%] {\n background: #f5f5f5;\n color: #666;\n}\n\n.clear-btn[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: #ffebee;\n color: #c62828;\n}\n\n.apply-btn[_ngcontent-%COMP%] {\n background: #1976d2;\n color: white;\n}\n\n.apply-btn[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: #1565c0;\n}\n\n\n\n.filter-content[_ngcontent-%COMP%] {\n min-height: 60px;\n}\n\n\n\n.filter-tips[_ngcontent-%COMP%] {\n padding: 12px;\n background: #f5f9ff;\n border-radius: 6px;\n border: 1px solid #e3edfa;\n}\n\n.filter-tips[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 13px;\n color: #666;\n display: flex;\n align-items: flex-start;\n gap: 8px;\n}\n\n.filter-tips[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: #ffc107;\n margin-top: 2px;\n}\n\n.filter-tips[_ngcontent-%COMP%] strong[_ngcontent-%COMP%] {\n color: #333;\n}\n\n\n\n.filter-expression-summary[_ngcontent-%COMP%] {\n background: #f8f9fa;\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n margin-top: 8px;\n overflow: hidden;\n transition: all 0.2s ease;\n}\n\n.filter-expression-summary.expanded[_ngcontent-%COMP%] {\n background: white;\n border-color: #d1d5db;\n}\n\n\n\n.expression-toggle[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 12px 16px;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 13px;\n font-weight: 500;\n color: #6b7280;\n text-align: left;\n transition: all 0.15s ease;\n}\n\n.expression-toggle[_ngcontent-%COMP%]:hover {\n background: rgba(0, 0, 0, 0.03);\n color: #374151;\n}\n\n.expression-toggle[_ngcontent-%COMP%] i[_ngcontent-%COMP%]:first-child {\n font-size: 10px;\n color: #9ca3af;\n width: 12px;\n transition: transform 0.2s ease;\n}\n\n.expression-icon[_ngcontent-%COMP%] {\n color: #6366f1;\n font-size: 13px;\n}\n\n.expression-label[_ngcontent-%COMP%] {\n flex: 1;\n}\n\n.expression-badge[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 20px;\n height: 20px;\n padding: 0 6px;\n background: #e0e7ff;\n color: #4f46e5;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n}\n\n\n\n.expression-content[_ngcontent-%COMP%] {\n padding: 0 16px 16px 16px;\n animation: _ngcontent-%COMP%_slideDown 0.2s ease;\n}\n\n@keyframes _ngcontent-%COMP%_slideDown {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.expression-text[_ngcontent-%COMP%] {\n font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;\n font-size: 13px;\n line-height: 1.8;\n color: #374151;\n padding: 16px;\n margin: 0;\n background: linear-gradient(135deg, #fafbfc 0%, #f3f4f6 100%);\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n overflow-x: auto;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n\n\n.expression-text[_ngcontent-%COMP%] .field-name[_ngcontent-%COMP%] {\n color: #0369a1;\n font-weight: 600;\n}\n\n.expression-text[_ngcontent-%COMP%] .operator[_ngcontent-%COMP%] {\n color: #6b7280;\n font-style: italic;\n}\n\n.expression-text[_ngcontent-%COMP%] .value[_ngcontent-%COMP%] {\n font-weight: 500;\n}\n\n.expression-text[_ngcontent-%COMP%] .value-string[_ngcontent-%COMP%] {\n color: #059669;\n}\n\n.expression-text[_ngcontent-%COMP%] .value-number[_ngcontent-%COMP%] {\n color: #7c3aed;\n}\n\n.expression-text[_ngcontent-%COMP%] .value-boolean[_ngcontent-%COMP%] {\n font-weight: 600;\n}\n\n.expression-text[_ngcontent-%COMP%] .value-true[_ngcontent-%COMP%] {\n color: #16a34a;\n}\n\n.expression-text[_ngcontent-%COMP%] .value-false[_ngcontent-%COMP%] {\n color: #dc2626;\n}\n\n.expression-text[_ngcontent-%COMP%] .value-date[_ngcontent-%COMP%] {\n color: #c2410c;\n font-weight: 500;\n}\n\n.expression-text[_ngcontent-%COMP%] .logic-keyword[_ngcontent-%COMP%] {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 4px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.5px;\n text-transform: uppercase;\n}\n\n.expression-text[_ngcontent-%COMP%] .logic-and[_ngcontent-%COMP%] {\n background: #dbeafe;\n color: #1d4ed8;\n}\n\n.expression-text[_ngcontent-%COMP%] .logic-or[_ngcontent-%COMP%] {\n background: #fef3c7;\n color: #b45309;\n}\n\n.expression-text[_ngcontent-%COMP%] .group-bracket[_ngcontent-%COMP%] {\n color: #9333ea;\n font-weight: 700;\n font-size: 15px;\n}\n\n.expression-text[_ngcontent-%COMP%] .no-filters[_ngcontent-%COMP%] {\n color: #9ca3af;\n font-style: italic;\n}\n\n\n\n@media (max-width: 600px) {\n .filter-header[_ngcontent-%COMP%] {\n flex-direction: column;\n align-items: stretch;\n gap: 12px;\n }\n\n .filter-summary[_ngcontent-%COMP%] {\n justify-content: center;\n }\n\n .filter-actions[_ngcontent-%COMP%] {\n justify-content: center;\n }\n\n .expression-toggle[_ngcontent-%COMP%] {\n padding: 10px 12px;\n }\n\n .expression-content[_ngcontent-%COMP%] {\n padding: 0 12px 12px 12px;\n }\n\n .expression-text[_ngcontent-%COMP%] {\n font-size: 12px;\n padding: 12px;\n }\n}"] });
460
+ }
461
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(FilterBuilderComponent, [{
462
+ type: Component,
463
+ args: [{ selector: 'mj-filter-builder', template: "<div class=\"filter-builder\" [class.disabled]=\"disabled\">\n <!-- Header with summary and actions -->\n <div class=\"filter-header\">\n <div class=\"filter-summary\">\n <span class=\"filter-icon\">\n <i class=\"fa-solid fa-filter\"></i>\n </span>\n @if (hasActiveFilters) {\n <span class=\"filter-count\">{{ getFilterCount() }} condition{{ getFilterCount() !== 1 ? 's' : '' }}</span>\n } @else {\n <span class=\"filter-count no-filters\">No filters applied</span>\n }\n </div>\n\n <div class=\"filter-actions\">\n @if (mergedConfig.showClearButton && hasActiveFilters) {\n <button\n type=\"button\"\n class=\"action-btn clear-btn\"\n (click)=\"onClear()\"\n [disabled]=\"disabled\"\n title=\"Clear all filters\">\n <i class=\"fa-solid fa-times\"></i>\n <span>Clear All</span>\n </button>\n }\n @if (mergedConfig.showApplyButton) {\n <button\n type=\"button\"\n class=\"action-btn apply-btn\"\n (click)=\"onApply()\"\n [disabled]=\"disabled\"\n title=\"Apply filters\">\n <i class=\"fa-solid fa-check\"></i>\n <span>Apply</span>\n </button>\n }\n </div>\n </div>\n\n <!-- Filter Builder Content -->\n <div class=\"filter-content\">\n <mj-filter-group\n [filter]=\"internalFilter\"\n [fields]=\"fields\"\n [isRoot]=\"true\"\n [depth]=\"0\"\n [maxDepth]=\"mergedConfig.maxDepth\"\n [disabled]=\"disabled\"\n (filterChange)=\"onFilterChange($event)\">\n </mj-filter-group>\n </div>\n\n <!-- Quick tips (shown when no filters) -->\n @if (!hasActiveFilters) {\n <div class=\"filter-tips\">\n <p>\n <i class=\"fa-solid fa-lightbulb\"></i>\n <strong>Tip:</strong> Click \"Add Condition\" to create filter rules.\n Use \"Add Group\" for complex AND/OR combinations.\n </p>\n </div>\n }\n\n <!-- Collapsible filter expression summary -->\n @if (showSummary && hasActiveFilters) {\n <div class=\"filter-expression-summary\" [class.expanded]=\"isSummaryExpanded\">\n <button type=\"button\" class=\"expression-toggle\" (click)=\"toggleSummary()\">\n <i class=\"fa-solid\" [class.fa-chevron-right]=\"!isSummaryExpanded\" [class.fa-chevron-down]=\"isSummaryExpanded\"></i>\n <i class=\"fa-solid fa-code expression-icon\"></i>\n <span class=\"expression-label\">View Filter Expression</span>\n <span class=\"expression-badge\">{{ getFilterCount() }}</span>\n </button>\n @if (isSummaryExpanded) {\n <div class=\"expression-content\">\n <pre class=\"expression-text\" [innerHTML]=\"getFilterSummaryHtml()\"></pre>\n </div>\n }\n </div>\n }\n</div>\n", styles: [".filter-builder {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.filter-builder.disabled {\n opacity: 0.6;\n pointer-events: none;\n}\n\n/* Header */\n.filter-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-bottom: 12px;\n border-bottom: 1px solid #eee;\n}\n\n.filter-summary {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.filter-icon {\n color: #1976d2;\n font-size: 14px;\n}\n\n.filter-count {\n font-size: 13px;\n color: #333;\n font-weight: 500;\n}\n\n.filter-count.no-filters {\n color: #999;\n font-weight: 400;\n}\n\n/* Actions */\n.filter-actions {\n display: flex;\n gap: 8px;\n}\n\n.action-btn {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 6px 12px;\n border: none;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.action-btn:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.action-btn i {\n font-size: 11px;\n}\n\n.clear-btn {\n background: #f5f5f5;\n color: #666;\n}\n\n.clear-btn:hover:not(:disabled) {\n background: #ffebee;\n color: #c62828;\n}\n\n.apply-btn {\n background: #1976d2;\n color: white;\n}\n\n.apply-btn:hover:not(:disabled) {\n background: #1565c0;\n}\n\n/* Content */\n.filter-content {\n min-height: 60px;\n}\n\n/* Tips */\n.filter-tips {\n padding: 12px;\n background: #f5f9ff;\n border-radius: 6px;\n border: 1px solid #e3edfa;\n}\n\n.filter-tips p {\n margin: 0;\n font-size: 13px;\n color: #666;\n display: flex;\n align-items: flex-start;\n gap: 8px;\n}\n\n.filter-tips i {\n color: #ffc107;\n margin-top: 2px;\n}\n\n.filter-tips strong {\n color: #333;\n}\n\n/* Collapsible Filter Expression Summary */\n.filter-expression-summary {\n background: #f8f9fa;\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n margin-top: 8px;\n overflow: hidden;\n transition: all 0.2s ease;\n}\n\n.filter-expression-summary.expanded {\n background: white;\n border-color: #d1d5db;\n}\n\n/* Toggle Button */\n.expression-toggle {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 12px 16px;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 13px;\n font-weight: 500;\n color: #6b7280;\n text-align: left;\n transition: all 0.15s ease;\n}\n\n.expression-toggle:hover {\n background: rgba(0, 0, 0, 0.03);\n color: #374151;\n}\n\n.expression-toggle i:first-child {\n font-size: 10px;\n color: #9ca3af;\n width: 12px;\n transition: transform 0.2s ease;\n}\n\n.expression-icon {\n color: #6366f1;\n font-size: 13px;\n}\n\n.expression-label {\n flex: 1;\n}\n\n.expression-badge {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 20px;\n height: 20px;\n padding: 0 6px;\n background: #e0e7ff;\n color: #4f46e5;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n}\n\n/* Expression Content */\n.expression-content {\n padding: 0 16px 16px 16px;\n animation: slideDown 0.2s ease;\n}\n\n@keyframes slideDown {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.expression-text {\n font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;\n font-size: 13px;\n line-height: 1.8;\n color: #374151;\n padding: 16px;\n margin: 0;\n background: linear-gradient(135deg, #fafbfc 0%, #f3f4f6 100%);\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n overflow-x: auto;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n/* Syntax Highlighting */\n.expression-text .field-name {\n color: #0369a1;\n font-weight: 600;\n}\n\n.expression-text .operator {\n color: #6b7280;\n font-style: italic;\n}\n\n.expression-text .value {\n font-weight: 500;\n}\n\n.expression-text .value-string {\n color: #059669;\n}\n\n.expression-text .value-number {\n color: #7c3aed;\n}\n\n.expression-text .value-boolean {\n font-weight: 600;\n}\n\n.expression-text .value-true {\n color: #16a34a;\n}\n\n.expression-text .value-false {\n color: #dc2626;\n}\n\n.expression-text .value-date {\n color: #c2410c;\n font-weight: 500;\n}\n\n.expression-text .logic-keyword {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 4px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.5px;\n text-transform: uppercase;\n}\n\n.expression-text .logic-and {\n background: #dbeafe;\n color: #1d4ed8;\n}\n\n.expression-text .logic-or {\n background: #fef3c7;\n color: #b45309;\n}\n\n.expression-text .group-bracket {\n color: #9333ea;\n font-weight: 700;\n font-size: 15px;\n}\n\n.expression-text .no-filters {\n color: #9ca3af;\n font-style: italic;\n}\n\n/* Responsive */\n@media (max-width: 600px) {\n .filter-header {\n flex-direction: column;\n align-items: stretch;\n gap: 12px;\n }\n\n .filter-summary {\n justify-content: center;\n }\n\n .filter-actions {\n justify-content: center;\n }\n\n .expression-toggle {\n padding: 10px 12px;\n }\n\n .expression-content {\n padding: 0 12px 12px 12px;\n }\n\n .expression-text {\n font-size: 12px;\n padding: 12px;\n }\n}\n"] }]
464
+ }], () => [{ type: i1.DomSanitizer }], { fields: [{
465
+ type: Input
466
+ }], filter: [{
467
+ type: Input
468
+ }], config: [{
469
+ type: Input
470
+ }], disabled: [{
471
+ type: Input
472
+ }], showSummary: [{
473
+ type: Input
474
+ }], filterChange: [{
475
+ type: Output
476
+ }], apply: [{
477
+ type: Output
478
+ }], clear: [{
479
+ type: Output
480
+ }] }); })();
481
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(FilterBuilderComponent, { className: "FilterBuilderComponent" }); })();
482
+ //# sourceMappingURL=filter-builder.component.js.map