angular-data-mapper 1.0.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 (64) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/LICENSE +190 -0
  3. package/PUBLISHING.md +75 -0
  4. package/README.md +214 -0
  5. package/angular.json +121 -0
  6. package/package.json +67 -0
  7. package/projects/demo-app/public/favicon.ico +0 -0
  8. package/projects/demo-app/src/app/app.config.ts +12 -0
  9. package/projects/demo-app/src/app/app.html +36 -0
  10. package/projects/demo-app/src/app/app.routes.ts +62 -0
  11. package/projects/demo-app/src/app/app.scss +65 -0
  12. package/projects/demo-app/src/app/app.ts +11 -0
  13. package/projects/demo-app/src/app/layout/app-layout.component.ts +294 -0
  14. package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.html +87 -0
  15. package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.scss +202 -0
  16. package/projects/demo-app/src/app/pages/mapper-page/mapper-page.component.ts +192 -0
  17. package/projects/demo-app/src/app/pages/mappings-page/add-mapping-dialog.component.ts +163 -0
  18. package/projects/demo-app/src/app/pages/mappings-page/mappings-page.component.ts +306 -0
  19. package/projects/demo-app/src/app/pages/schema-creator-page/schema-creator-page.component.ts +88 -0
  20. package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.html +108 -0
  21. package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.scss +317 -0
  22. package/projects/demo-app/src/app/pages/schema-editor-page/schema-editor-page.component.ts +129 -0
  23. package/projects/demo-app/src/app/services/app-state.service.ts +233 -0
  24. package/projects/demo-app/src/app/services/sample-data.service.ts +228 -0
  25. package/projects/demo-app/src/index.html +15 -0
  26. package/projects/demo-app/src/main.ts +6 -0
  27. package/projects/demo-app/src/styles.scss +54 -0
  28. package/projects/demo-app/tsconfig.app.json +13 -0
  29. package/projects/ngx-data-mapper/ng-package.json +7 -0
  30. package/projects/ngx-data-mapper/package.json +40 -0
  31. package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.html +183 -0
  32. package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.scss +352 -0
  33. package/projects/ngx-data-mapper/src/lib/components/array-filter-modal/array-filter-modal.component.ts +277 -0
  34. package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.html +174 -0
  35. package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.scss +357 -0
  36. package/projects/ngx-data-mapper/src/lib/components/array-selector-modal/array-selector-modal.component.ts +258 -0
  37. package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.html +139 -0
  38. package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.scss +213 -0
  39. package/projects/ngx-data-mapper/src/lib/components/condition-builder/condition-builder.component.ts +261 -0
  40. package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.html +199 -0
  41. package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.scss +321 -0
  42. package/projects/ngx-data-mapper/src/lib/components/data-mapper/data-mapper.component.ts +618 -0
  43. package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.html +67 -0
  44. package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.scss +97 -0
  45. package/projects/ngx-data-mapper/src/lib/components/default-value-popover/default-value-popover.component.ts +105 -0
  46. package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.html +552 -0
  47. package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.scss +824 -0
  48. package/projects/ngx-data-mapper/src/lib/components/schema-editor/schema-editor.component.ts +730 -0
  49. package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.html +82 -0
  50. package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.scss +352 -0
  51. package/projects/ngx-data-mapper/src/lib/components/schema-tree/schema-tree.component.ts +225 -0
  52. package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.html +346 -0
  53. package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.scss +511 -0
  54. package/projects/ngx-data-mapper/src/lib/components/transformation-popover/transformation-popover.component.ts +368 -0
  55. package/projects/ngx-data-mapper/src/lib/models/json-schema.model.ts +164 -0
  56. package/projects/ngx-data-mapper/src/lib/models/schema.model.ts +173 -0
  57. package/projects/ngx-data-mapper/src/lib/services/mapping.service.ts +615 -0
  58. package/projects/ngx-data-mapper/src/lib/services/schema-parser.service.ts +270 -0
  59. package/projects/ngx-data-mapper/src/lib/services/svg-connector.service.ts +135 -0
  60. package/projects/ngx-data-mapper/src/lib/services/transformation.service.ts +453 -0
  61. package/projects/ngx-data-mapper/src/public-api.ts +22 -0
  62. package/projects/ngx-data-mapper/tsconfig.lib.json +13 -0
  63. package/projects/ngx-data-mapper/tsconfig.lib.prod.json +9 -0
  64. package/tsconfig.json +28 -0
@@ -0,0 +1,258 @@
1
+ import { Component, Input, Output, EventEmitter, computed, signal, OnInit } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { MatButtonModule } from '@angular/material/button';
5
+ import { MatIconModule } from '@angular/material/icon';
6
+ import { MatSelectModule } from '@angular/material/select';
7
+ import { MatInputModule } from '@angular/material/input';
8
+ import { MatFormFieldModule } from '@angular/material/form-field';
9
+ import { MatRadioModule } from '@angular/material/radio';
10
+ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
11
+ import { MatTooltipModule } from '@angular/material/tooltip';
12
+ import { MatDividerModule } from '@angular/material/divider';
13
+ import {
14
+ ArrayToObjectMapping,
15
+ ArraySelectorConfig,
16
+ ArraySelectionMode,
17
+ FilterCondition,
18
+ FilterGroup,
19
+ FilterItem,
20
+ FilterOperator,
21
+ SchemaField,
22
+ } from '../../models/schema.model';
23
+
24
+ interface OperatorOption {
25
+ value: FilterOperator;
26
+ label: string;
27
+ needsValue: boolean;
28
+ }
29
+
30
+ @Component({
31
+ selector: 'array-selector-modal',
32
+ standalone: true,
33
+ imports: [
34
+ CommonModule,
35
+ FormsModule,
36
+ MatButtonModule,
37
+ MatIconModule,
38
+ MatSelectModule,
39
+ MatInputModule,
40
+ MatFormFieldModule,
41
+ MatRadioModule,
42
+ MatSlideToggleModule,
43
+ MatTooltipModule,
44
+ MatDividerModule,
45
+ ],
46
+ templateUrl: './array-selector-modal.component.html',
47
+ styleUrl: './array-selector-modal.component.scss',
48
+ })
49
+ export class ArraySelectorModalComponent implements OnInit {
50
+ @Input({ required: true }) mapping!: ArrayToObjectMapping;
51
+ @Output() save = new EventEmitter<ArraySelectorConfig>();
52
+ @Output() close = new EventEmitter<void>();
53
+
54
+ selectionMode = signal<ArraySelectionMode>('first');
55
+ conditionGroup = signal<FilterGroup>(this.createEmptyGroup());
56
+
57
+ // Available fields from the source array's children
58
+ availableFields = computed(() => {
59
+ const fields: { path: string; name: string; type: string }[] = [];
60
+ this.collectFields(this.mapping.sourceArray.children || [], '', fields);
61
+ return fields;
62
+ });
63
+
64
+ // Operators by type
65
+ stringOperators: OperatorOption[] = [
66
+ { value: 'equals', label: 'equals', needsValue: true },
67
+ { value: 'notEquals', label: 'not equals', needsValue: true },
68
+ { value: 'contains', label: 'contains', needsValue: true },
69
+ { value: 'notContains', label: 'does not contain', needsValue: true },
70
+ { value: 'startsWith', label: 'starts with', needsValue: true },
71
+ { value: 'endsWith', label: 'ends with', needsValue: true },
72
+ { value: 'isEmpty', label: 'is empty', needsValue: false },
73
+ { value: 'isNotEmpty', label: 'is not empty', needsValue: false },
74
+ ];
75
+
76
+ numberOperators: OperatorOption[] = [
77
+ { value: 'equals', label: 'equals', needsValue: true },
78
+ { value: 'notEquals', label: 'not equals', needsValue: true },
79
+ { value: 'greaterThan', label: 'greater than', needsValue: true },
80
+ { value: 'lessThan', label: 'less than', needsValue: true },
81
+ { value: 'greaterThanOrEqual', label: 'greater or equal', needsValue: true },
82
+ { value: 'lessThanOrEqual', label: 'less or equal', needsValue: true },
83
+ ];
84
+
85
+ booleanOperators: OperatorOption[] = [
86
+ { value: 'isTrue', label: 'is true', needsValue: false },
87
+ { value: 'isFalse', label: 'is false', needsValue: false },
88
+ ];
89
+
90
+ ngOnInit(): void {
91
+ // Initialize from existing selector config
92
+ if (this.mapping.selector) {
93
+ this.selectionMode.set(this.mapping.selector.mode);
94
+ if (this.mapping.selector.condition) {
95
+ this.conditionGroup.set(this.cloneGroup(this.mapping.selector.condition));
96
+ }
97
+ }
98
+ }
99
+
100
+ private createEmptyGroup(): FilterGroup {
101
+ return {
102
+ id: this.generateId(),
103
+ type: 'group',
104
+ logic: 'and',
105
+ children: [],
106
+ };
107
+ }
108
+
109
+ private generateId(): string {
110
+ return `filter-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
111
+ }
112
+
113
+ private cloneGroup(group: FilterGroup): FilterGroup {
114
+ return {
115
+ ...group,
116
+ children: group.children.map((child) =>
117
+ child.type === 'group' ? this.cloneGroup(child) : { ...child }
118
+ ),
119
+ };
120
+ }
121
+
122
+ private collectFields(
123
+ fields: SchemaField[],
124
+ prefix: string,
125
+ result: { path: string; name: string; type: string }[]
126
+ ): void {
127
+ for (const field of fields) {
128
+ const path = prefix ? `${prefix}.${field.name}` : field.name;
129
+ if (field.type !== 'object' && field.type !== 'array') {
130
+ result.push({ path, name: field.name, type: field.type });
131
+ }
132
+ if (field.children) {
133
+ this.collectFields(field.children, path, result);
134
+ }
135
+ }
136
+ }
137
+
138
+ getOperatorsForField(fieldPath: string): OperatorOption[] {
139
+ const field = this.availableFields().find((f) => f.path === fieldPath);
140
+ if (!field) return this.stringOperators;
141
+
142
+ switch (field.type) {
143
+ case 'number':
144
+ return this.numberOperators;
145
+ case 'boolean':
146
+ return this.booleanOperators;
147
+ default:
148
+ return this.stringOperators;
149
+ }
150
+ }
151
+
152
+ operatorNeedsValue(operator: FilterOperator): boolean {
153
+ const allOperators = [...this.stringOperators, ...this.numberOperators, ...this.booleanOperators];
154
+ const op = allOperators.find((o) => o.value === operator);
155
+ return op?.needsValue ?? true;
156
+ }
157
+
158
+ isCondition(item: FilterItem): item is FilterCondition {
159
+ return item.type === 'condition';
160
+ }
161
+
162
+ isGroup(item: FilterItem): item is FilterGroup {
163
+ return item.type === 'group';
164
+ }
165
+
166
+ addCondition(group: FilterGroup): void {
167
+ const fields = this.availableFields();
168
+ const firstField = fields[0];
169
+ const newCondition: FilterCondition = {
170
+ id: this.generateId(),
171
+ type: 'condition',
172
+ field: firstField?.path || '',
173
+ fieldName: firstField?.name || '',
174
+ operator: 'equals',
175
+ value: '',
176
+ valueType: (firstField?.type as 'string' | 'number' | 'boolean') || 'string',
177
+ };
178
+ group.children = [...group.children, newCondition];
179
+ this.triggerUpdate();
180
+ }
181
+
182
+ addGroup(parentGroup: FilterGroup): void {
183
+ const newGroup: FilterGroup = {
184
+ id: this.generateId(),
185
+ type: 'group',
186
+ logic: parentGroup.logic === 'and' ? 'or' : 'and',
187
+ children: [],
188
+ };
189
+ parentGroup.children = [...parentGroup.children, newGroup];
190
+ this.triggerUpdate();
191
+ }
192
+
193
+ removeItem(parentGroup: FilterGroup, itemId: string): void {
194
+ parentGroup.children = parentGroup.children.filter((c) => c.id !== itemId);
195
+ this.triggerUpdate();
196
+ }
197
+
198
+ onFieldChange(condition: FilterCondition, fieldPath: string): void {
199
+ const field = this.availableFields().find((f) => f.path === fieldPath);
200
+ if (field) {
201
+ condition.field = fieldPath;
202
+ condition.fieldName = field.name;
203
+ condition.valueType = field.type as 'string' | 'number' | 'boolean';
204
+ const operators = this.getOperatorsForField(fieldPath);
205
+ condition.operator = operators[0].value;
206
+ condition.value = '';
207
+ }
208
+ this.triggerUpdate();
209
+ }
210
+
211
+ onOperatorChange(condition: FilterCondition, operator: FilterOperator): void {
212
+ condition.operator = operator;
213
+ if (!this.operatorNeedsValue(operator)) {
214
+ condition.value = '';
215
+ }
216
+ this.triggerUpdate();
217
+ }
218
+
219
+ onValueChange(condition: FilterCondition, value: string | boolean): void {
220
+ if (condition.valueType === 'number') {
221
+ condition.value = parseFloat(value as string) || 0;
222
+ } else {
223
+ condition.value = value;
224
+ }
225
+ this.triggerUpdate();
226
+ }
227
+
228
+ onLogicChange(group: FilterGroup, logic: 'and' | 'or'): void {
229
+ group.logic = logic;
230
+ this.triggerUpdate();
231
+ }
232
+
233
+ private triggerUpdate(): void {
234
+ this.conditionGroup.set(this.cloneGroup(this.conditionGroup()));
235
+ }
236
+
237
+ onSave(): void {
238
+ const config: ArraySelectorConfig = {
239
+ mode: this.selectionMode(),
240
+ };
241
+
242
+ if (this.selectionMode() === 'condition') {
243
+ config.condition = this.conditionGroup();
244
+ }
245
+
246
+ this.save.emit(config);
247
+ }
248
+
249
+ onClose(): void {
250
+ this.close.emit();
251
+ }
252
+
253
+ onBackdropClick(event: MouseEvent): void {
254
+ if ((event.target as HTMLElement).classList.contains('modal-backdrop')) {
255
+ this.onClose();
256
+ }
257
+ }
258
+ }
@@ -0,0 +1,139 @@
1
+ <div class="condition-builder" [class.compact]="compact">
2
+ <ng-container *ngTemplateOutlet="groupTemplate; context: { group: rootGroup, isRoot: true }"></ng-container>
3
+ </div>
4
+
5
+ <!-- Recursive group template -->
6
+ <ng-template #groupTemplate let-group="group" let-isRoot="isRoot" let-parentGroup="parentGroup">
7
+ <div class="filter-group" [class.root-group]="isRoot" [class.nested-group]="!isRoot">
8
+ <!-- Group header with logic toggle (only show if multiple children or nested) -->
9
+ @if (group.children.length > 1 || !isRoot) {
10
+ <div class="group-header">
11
+ <div class="logic-toggle">
12
+ <button
13
+ type="button"
14
+ class="logic-btn"
15
+ [class.active]="group.logic === 'and'"
16
+ (click)="onLogicChange(group, 'and')"
17
+ >
18
+ AND
19
+ </button>
20
+ <button
21
+ type="button"
22
+ class="logic-btn"
23
+ [class.active]="group.logic === 'or'"
24
+ (click)="onLogicChange(group, 'or')"
25
+ >
26
+ OR
27
+ </button>
28
+ </div>
29
+ @if (!isRoot) {
30
+ <button mat-icon-button class="remove-group-btn" matTooltip="Remove group" (click)="removeItem(parentGroup, group.id)">
31
+ <mat-icon>close</mat-icon>
32
+ </button>
33
+ }
34
+ </div>
35
+ }
36
+
37
+ <!-- Group children -->
38
+ <div class="group-children">
39
+ @for (item of group.children; track item.id; let i = $index) {
40
+ <!-- Logic connector between items -->
41
+ @if (i > 0) {
42
+ <div class="logic-connector">
43
+ <span class="logic-badge" [class.and]="group.logic === 'and'" [class.or]="group.logic === 'or'">
44
+ {{ group.logic | uppercase }}
45
+ </span>
46
+ </div>
47
+ }
48
+
49
+ @if (isCondition(item)) {
50
+ <!-- Condition row -->
51
+ <div class="condition-row">
52
+ <!-- Field selector (only if multiple fields) -->
53
+ @if (showFieldSelector) {
54
+ <mat-form-field appearance="outline" class="field-select">
55
+ <mat-select [value]="item.field" (selectionChange)="onFieldChange(item, $event.value)">
56
+ @for (field of availableFields; track field.path) {
57
+ <mat-option [value]="field.path">{{ field.name }}</mat-option>
58
+ }
59
+ </mat-select>
60
+ </mat-form-field>
61
+ }
62
+
63
+ <!-- Operator selector -->
64
+ <mat-form-field appearance="outline" class="operator-select">
65
+ <mat-select [value]="item.operator" (selectionChange)="onOperatorChange(item, $event.value)">
66
+ @for (op of getOperatorsForField(item.field); track op.value) {
67
+ <mat-option [value]="op.value">{{ op.label }}</mat-option>
68
+ }
69
+ </mat-select>
70
+ </mat-form-field>
71
+
72
+ <!-- Value input (only if operator needs value) -->
73
+ @if (operatorNeedsValue(item.operator)) {
74
+ @if (item.valueType === 'boolean') {
75
+ <mat-slide-toggle
76
+ [checked]="item.value === true"
77
+ (change)="onValueChange(item, $event.checked)"
78
+ class="bool-toggle"
79
+ >
80
+ </mat-slide-toggle>
81
+ } @else if (item.valueType === 'number') {
82
+ <mat-form-field appearance="outline" class="value-input">
83
+ <input
84
+ matInput
85
+ type="number"
86
+ [value]="item.value"
87
+ (input)="onValueChange(item, $any($event.target).value)"
88
+ placeholder="value"
89
+ />
90
+ </mat-form-field>
91
+ } @else {
92
+ <mat-form-field appearance="outline" class="value-input">
93
+ <input
94
+ matInput
95
+ type="text"
96
+ [value]="item.value"
97
+ (input)="onValueChange(item, $any($event.target).value)"
98
+ placeholder="value"
99
+ />
100
+ </mat-form-field>
101
+ }
102
+ }
103
+
104
+ <!-- Remove condition button -->
105
+ <button mat-icon-button class="remove-btn" matTooltip="Remove" (click)="removeItem(group, item.id)">
106
+ <mat-icon>close</mat-icon>
107
+ </button>
108
+ </div>
109
+ } @else if (isGroup(item)) {
110
+ <!-- Nested group -->
111
+ <ng-container *ngTemplateOutlet="groupTemplate; context: { group: item, isRoot: false, parentGroup: group }"></ng-container>
112
+ }
113
+ }
114
+
115
+ <!-- Empty state -->
116
+ @if (group.children.length === 0) {
117
+ <div class="empty-group">
118
+ <span>No conditions defined</span>
119
+ </div>
120
+ }
121
+ </div>
122
+
123
+ <!-- Group actions -->
124
+ <div class="group-actions">
125
+ <button type="button" class="add-btn" (click)="addCondition(group)">
126
+ <mat-icon>add</mat-icon>
127
+ @if (!compact) {
128
+ <span>Add condition</span>
129
+ }
130
+ </button>
131
+ @if (!compact && group.children.length > 0) {
132
+ <button type="button" class="add-btn add-group-btn" (click)="addGroup(group)">
133
+ <mat-icon>folder_open</mat-icon>
134
+ <span>Add group</span>
135
+ </button>
136
+ }
137
+ </div>
138
+ </div>
139
+ </ng-template>
@@ -0,0 +1,213 @@
1
+ .condition-builder {
2
+ &.compact {
3
+ .filter-group {
4
+ padding: 8px;
5
+ }
6
+
7
+ .condition-row {
8
+ gap: 6px;
9
+ }
10
+
11
+ .operator-select {
12
+ min-width: 100px;
13
+ }
14
+
15
+ .value-input {
16
+ min-width: 80px;
17
+ }
18
+
19
+ .add-btn {
20
+ padding: 4px 8px;
21
+ font-size: 12px;
22
+ }
23
+ }
24
+ }
25
+
26
+ .filter-group {
27
+ background: #f8fafc;
28
+ border: 1px solid #e2e8f0;
29
+ border-radius: 8px;
30
+ padding: 12px;
31
+
32
+ &.nested-group {
33
+ margin-top: 8px;
34
+ background: #f1f5f9;
35
+ border-color: #cbd5e1;
36
+ }
37
+ }
38
+
39
+ .group-header {
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: space-between;
43
+ margin-bottom: 8px;
44
+ }
45
+
46
+ .logic-toggle {
47
+ display: flex;
48
+ gap: 4px;
49
+ }
50
+
51
+ .logic-btn {
52
+ padding: 4px 12px;
53
+ border: 1px solid #cbd5e1;
54
+ background: white;
55
+ border-radius: 4px;
56
+ font-size: 11px;
57
+ font-weight: 600;
58
+ color: #64748b;
59
+ cursor: pointer;
60
+ transition: all 0.15s ease;
61
+
62
+ &:hover {
63
+ border-color: #94a3b8;
64
+ }
65
+
66
+ &.active {
67
+ background: #6366f1;
68
+ border-color: #6366f1;
69
+ color: white;
70
+ }
71
+ }
72
+
73
+ .group-children {
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: 8px;
77
+ }
78
+
79
+ .logic-connector {
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ padding: 4px 0;
84
+ }
85
+
86
+ .logic-badge {
87
+ font-size: 10px;
88
+ font-weight: 700;
89
+ padding: 2px 8px;
90
+ border-radius: 4px;
91
+
92
+ &.and {
93
+ background: #dbeafe;
94
+ color: #1d4ed8;
95
+ }
96
+
97
+ &.or {
98
+ background: #fef3c7;
99
+ color: #b45309;
100
+ }
101
+ }
102
+
103
+ .condition-row {
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 8px;
107
+ flex-wrap: wrap;
108
+ }
109
+
110
+ .field-select {
111
+ min-width: 120px;
112
+ }
113
+
114
+ .operator-select {
115
+ min-width: 130px;
116
+ }
117
+
118
+ .value-input {
119
+ min-width: 100px;
120
+ flex: 1;
121
+ }
122
+
123
+ .bool-toggle {
124
+ margin: 0 8px;
125
+ }
126
+
127
+ .remove-btn,
128
+ .remove-group-btn {
129
+ width: 28px;
130
+ height: 28px;
131
+ line-height: 28px;
132
+ flex-shrink: 0;
133
+
134
+ mat-icon {
135
+ font-size: 18px;
136
+ width: 18px;
137
+ height: 18px;
138
+ color: #94a3b8;
139
+ }
140
+
141
+ &:hover mat-icon {
142
+ color: #ef4444;
143
+ }
144
+ }
145
+
146
+ .empty-group {
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: center;
150
+ padding: 12px;
151
+ color: #94a3b8;
152
+ font-size: 13px;
153
+ font-style: italic;
154
+ }
155
+
156
+ .group-actions {
157
+ display: flex;
158
+ gap: 8px;
159
+ margin-top: 8px;
160
+ padding-top: 8px;
161
+ border-top: 1px dashed #e2e8f0;
162
+ }
163
+
164
+ .add-btn {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 4px;
168
+ padding: 6px 12px;
169
+ background: white;
170
+ border: 1px dashed #cbd5e1;
171
+ border-radius: 6px;
172
+ font-size: 13px;
173
+ color: #64748b;
174
+ cursor: pointer;
175
+ transition: all 0.15s ease;
176
+
177
+ mat-icon {
178
+ font-size: 16px;
179
+ width: 16px;
180
+ height: 16px;
181
+ }
182
+
183
+ &:hover {
184
+ border-color: #6366f1;
185
+ color: #6366f1;
186
+ background: #f5f3ff;
187
+ }
188
+
189
+ &.add-group-btn {
190
+ border-style: solid;
191
+ }
192
+ }
193
+
194
+ // Material form field overrides
195
+ ::ng-deep .condition-builder {
196
+ .mat-mdc-form-field {
197
+ font-size: 13px;
198
+ }
199
+
200
+ .mat-mdc-form-field-subscript-wrapper {
201
+ display: none;
202
+ }
203
+
204
+ .mdc-text-field--outlined {
205
+ --mdc-outlined-text-field-container-shape: 6px;
206
+ }
207
+
208
+ .mat-mdc-form-field-infix {
209
+ padding-top: 8px !important;
210
+ padding-bottom: 8px !important;
211
+ min-height: 36px;
212
+ }
213
+ }