@rkosafo/cai.components 0.0.75 → 0.0.78

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 (127) hide show
  1. package/README.md +8 -8
  2. package/dist/baseEditor/index.svelte +32 -32
  3. package/dist/builders/filters/FilterBuilder.svelte +641 -641
  4. package/dist/forms/FormCheckbox/FormCheckbox.svelte +53 -53
  5. package/dist/forms/FormClEditor/ClEdito.svelte +68 -68
  6. package/dist/forms/FormDatepicker/FormDatepicker.svelte +159 -159
  7. package/dist/forms/FormFileUpload/FormFileUplad.svelte +134 -134
  8. package/dist/forms/FormInput/FormInput.svelte +87 -87
  9. package/dist/forms/FormRadio/FormRadio.svelte +53 -53
  10. package/dist/forms/FormSelect/FormSelect.svelte +88 -88
  11. package/dist/forms/FormTextarea/FormTextarea.svelte +78 -78
  12. package/dist/forms/button-toggle/ButtonToggle.svelte +119 -0
  13. package/dist/forms/button-toggle/ButtonToggle.svelte.d.ts +139 -0
  14. package/dist/forms/button-toggle/ButtonToggleGroup.svelte +0 -0
  15. package/dist/forms/button-toggle/ButtonToggleGroup.svelte.d.ts +26 -0
  16. package/dist/forms/button-toggle/CheckIcon.svelte +28 -0
  17. package/dist/forms/button-toggle/CheckIcon.svelte.d.ts +4 -0
  18. package/dist/forms/button-toggle/index.d.ts +4 -0
  19. package/dist/forms/button-toggle/index.js +4 -0
  20. package/dist/forms/button-toggle/theme.d.ts +347 -0
  21. package/dist/forms/button-toggle/theme.js +129 -0
  22. package/dist/forms/checkbox/Checkbox.svelte +82 -82
  23. package/dist/forms/checkbox/CheckboxButton.svelte +92 -92
  24. package/dist/forms/datepicker/Datepicker.svelte +707 -707
  25. package/dist/forms/form/Form.svelte +69 -69
  26. package/dist/forms/input/Input.svelte +363 -363
  27. package/dist/forms/label/Label.svelte +38 -38
  28. package/dist/forms/radio/Radio.svelte +48 -48
  29. package/dist/forms/radio/RadioButton.svelte +22 -22
  30. package/dist/forms/select/Select.svelte +56 -56
  31. package/dist/forms/textarea/Textarea.svelte +165 -165
  32. package/dist/forms/toggle/Toggle.svelte +70 -0
  33. package/dist/forms/toggle/Toggle.svelte.d.ts +3 -0
  34. package/dist/forms/toggle/index.d.ts +2 -0
  35. package/dist/forms/toggle/index.js +2 -0
  36. package/dist/forms/toggle/theme.d.ts +280 -0
  37. package/dist/forms/toggle/theme.js +97 -0
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.js +3 -0
  40. package/dist/layout/Chat/CategorySelector.svelte +52 -52
  41. package/dist/layout/Chat/ChatEntry.svelte +246 -246
  42. package/dist/layout/Chat/ChatEntrySkeleton.svelte +81 -81
  43. package/dist/layout/Chat/ChatHeader.svelte +172 -172
  44. package/dist/layout/Chat/ChatInput.svelte +207 -207
  45. package/dist/layout/Chat/DraggableWindow.svelte +230 -230
  46. package/dist/layout/Chat/PreviewPage.svelte +182 -182
  47. package/dist/layout/Chat/RichText.svelte +216 -216
  48. package/dist/layout/ComponentCanvas/Canvas.svelte +40 -40
  49. package/dist/layout/ComponentCanvas/ComponentRenderer.svelte +85 -85
  50. package/dist/layout/TF/Content/Content.svelte +21 -21
  51. package/dist/layout/TF/Header/Header.svelte +166 -166
  52. package/dist/layout/TF/Sidebar/Sidebar.svelte +148 -148
  53. package/dist/layout/TF/Wrapper/Wrapper.svelte +17 -17
  54. package/dist/layout/mailing/MailPaginator.svelte +36 -36
  55. package/dist/layout/mailing/MailSidebar.svelte +39 -39
  56. package/dist/layout/mailing/MailToolBar.svelte +174 -174
  57. package/dist/layout/mailing/MailingContent.svelte +10 -10
  58. package/dist/layout/mailing/MailingHeader.svelte +55 -55
  59. package/dist/layout/mailing/MailingMessageCard.svelte +112 -112
  60. package/dist/layout/mailing/MailingMessageViewer.svelte +87 -87
  61. package/dist/layout/mailing/MailingModule.svelte +448 -448
  62. package/dist/styles/docs.css +615 -615
  63. package/dist/styles/tf-layout.css +185 -185
  64. package/dist/themes/ThemeProvider.svelte +20 -20
  65. package/dist/themes/themes.d.ts +3 -0
  66. package/dist/themes/themes.js +3 -0
  67. package/dist/types/index.d.ts +57 -1
  68. package/dist/typography/heading/Heading.svelte +35 -35
  69. package/dist/ui/accordion/Accordion.svelte +49 -49
  70. package/dist/ui/accordion/AccordionItem.svelte +173 -173
  71. package/dist/ui/alert/Alert.svelte +83 -83
  72. package/dist/ui/alertDialog/AlertDialog.svelte +40 -40
  73. package/dist/ui/avatar/Avatar.svelte +77 -77
  74. package/dist/ui/box/Box.svelte +28 -28
  75. package/dist/ui/breadcrumb/Breadcrumb.svelte +39 -39
  76. package/dist/ui/buttons/ActionButton.svelte +234 -234
  77. package/dist/ui/buttons/Button.svelte +102 -102
  78. package/dist/ui/buttons/GradientButton.svelte +59 -59
  79. package/dist/ui/datatable/Datatable.svelte +525 -525
  80. package/dist/ui/drawer/Drawer.svelte +300 -300
  81. package/dist/ui/dropdown/Dropdown.svelte +36 -36
  82. package/dist/ui/dropdown/DropdownDivider.svelte +11 -11
  83. package/dist/ui/dropdown/DropdownGroup.svelte +14 -14
  84. package/dist/ui/dropdown/DropdownHeader.svelte +14 -14
  85. package/dist/ui/dropdown/DropdownItem.svelte +52 -52
  86. package/dist/ui/footer/Footer.svelte +15 -15
  87. package/dist/ui/footer/FooterBrand.svelte +37 -37
  88. package/dist/ui/footer/FooterCopyright.svelte +45 -45
  89. package/dist/ui/footer/FooterIcon.svelte +22 -22
  90. package/dist/ui/footer/FooterLink.svelte +33 -33
  91. package/dist/ui/footer/FooterLinkGroup.svelte +13 -13
  92. package/dist/ui/icons/IconifyIcon.svelte +7 -7
  93. package/dist/ui/indicator/Indicator.svelte +42 -42
  94. package/dist/ui/modal/Modal.svelte +265 -265
  95. package/dist/ui/modal/theme.d.ts +26 -26
  96. package/dist/ui/modal/theme.js +25 -25
  97. package/dist/ui/notificationList/NotificationList.svelte +123 -123
  98. package/dist/ui/pageLoader/PageLoader.svelte +14 -14
  99. package/dist/ui/paginate/Paginate.svelte +96 -96
  100. package/dist/ui/speedDial/SpeedDial.svelte +77 -0
  101. package/dist/ui/speedDial/SpeedDial.svelte.d.ts +21 -0
  102. package/dist/ui/speedDial/SpeedDialButton.svelte +75 -0
  103. package/dist/ui/speedDial/SpeedDialButton.svelte.d.ts +20 -0
  104. package/dist/ui/speedDial/SpeedDialTrigger.svelte +79 -0
  105. package/dist/ui/speedDial/SpeedDialTrigger.svelte.d.ts +18 -0
  106. package/dist/ui/speedDial/index.d.ts +4 -0
  107. package/dist/ui/speedDial/index.js +4 -0
  108. package/dist/ui/speedDial/theme.d.ts +75 -0
  109. package/dist/ui/speedDial/theme.js +35 -0
  110. package/dist/ui/tab/Tab.svelte +67 -67
  111. package/dist/ui/table/Table.svelte +396 -396
  112. package/dist/ui/tableLoader/TableLoader.svelte +24 -24
  113. package/dist/ui/toast/Toast.svelte +337 -337
  114. package/dist/ui/toast/Toast.svelte.d.ts +10 -10
  115. package/dist/ui/toast/index.d.ts +1 -2
  116. package/dist/ui/toast/index.js +3 -1
  117. package/dist/ui/toolbar/Toolbar.svelte +59 -59
  118. package/dist/ui/toolbar/ToolbarButton.svelte +56 -56
  119. package/dist/ui/toolbar/ToolbarGroup.svelte +43 -43
  120. package/dist/ui/tooltip/Tooltip.svelte +51 -51
  121. package/dist/utils/Popper.svelte +257 -257
  122. package/dist/utils/closeButton/CloseButton.svelte +88 -88
  123. package/dist/utils/index.d.ts +3 -2
  124. package/dist/utils/index.js +13 -3
  125. package/dist/utils/singleSelection.svelte.js +48 -48
  126. package/dist/youtube/index.svelte +12 -12
  127. package/package.json +2 -1
@@ -1,641 +1,641 @@
1
- <script lang="ts" module>
2
- function getDefaultValue(type: FilterBuilderColumnConfig['type']): any {
3
- switch (type) {
4
- case 'string':
5
- return '';
6
- case 'number':
7
- return null;
8
- case 'date':
9
- return '';
10
- case 'boolean':
11
- return true;
12
- default:
13
- return '';
14
- }
15
- }
16
-
17
- function isFilterValid(filter: FilterBuilderItem): boolean {
18
- return !!(filter.column && filter.operator && filter.value !== null && filter.value !== '');
19
- }
20
-
21
- function getUniqueFilters(...filterArrays: FilterBuilderItem[][]): FilterBuilderItem[] {
22
- const combinedFilters = filterArrays.flat();
23
- const uniqueFilters: FilterBuilderItem[] = [];
24
- const seen = new Set<string>();
25
-
26
- combinedFilters.forEach((filter) => {
27
- // Create a unique identifier based on all filter properties
28
- const identifier = `${filter.column}-${filter.operator}-${String(filter.value).toLowerCase()}-${filter.logicalOperator || 'AND'}`;
29
-
30
- if (!seen.has(identifier)) {
31
- seen.add(identifier);
32
- uniqueFilters.push(filter);
33
- }
34
- });
35
-
36
- return uniqueFilters;
37
- }
38
-
39
- // Alternative version with configurable uniqueness criteria
40
- function getUniqueFiltersAdvanced(
41
- filterArrays: FilterBuilderItem[][],
42
- uniquenessKey: (filter: FilterBuilderItem) => string = defaultUniquenessKey
43
- ): FilterBuilderItem[] {
44
- const combinedFilters = filterArrays.flat();
45
- const uniqueFilters: FilterBuilderItem[] = [];
46
- const seen = new Set<string>();
47
-
48
- combinedFilters.forEach((filter) => {
49
- const key = uniquenessKey(filter);
50
-
51
- if (!seen.has(key)) {
52
- seen.add(key);
53
- uniqueFilters.push(filter);
54
- }
55
- });
56
-
57
- return uniqueFilters;
58
- }
59
-
60
- // Default uniqueness key function
61
- function defaultUniquenessKey(filter: FilterBuilderItem): string {
62
- return `${filter.column}-${filter.operator}-${String(filter.value).toLowerCase()}-${filter.logicalOperator || 'AND'}`;
63
- }
64
- </script>
65
-
66
- <script lang="ts">
67
- import { browser } from '$app/environment';
68
-
69
- import {
70
- Button,
71
- Datepicker,
72
- Input,
73
- Label,
74
- Select,
75
- type FilterBuilderColumnConfig,
76
- type FilterBuilderItem,
77
- type FilterBuilderProps
78
- } from '../../index.js';
79
- import { nanoid } from 'nanoid';
80
- import { onMount } from 'svelte';
81
- import { fade } from 'svelte/transition';
82
-
83
- let { columns = [], filters = $bindable([]), onRunQuery }: FilterBuilderProps = $props();
84
-
85
- // Operator mappings for each type
86
- const operatorsByType = {
87
- string: [
88
- { value: 'contains', label: 'contains', sqlOp: 'ILIKE' },
89
- { value: '=', label: 'equals', sqlOp: '=' },
90
- { value: '!=', label: 'does not equal', sqlOp: '!=' },
91
- { value: 'starts_with', label: 'starts with', sqlOp: 'ILIKE' }
92
- ],
93
- number: [
94
- { value: '=', label: 'equals', sqlOp: '=' },
95
- { value: '>', label: 'is greater than', sqlOp: '>' },
96
- { value: '<', label: 'is less than', sqlOp: '<' },
97
- { value: '>=', label: 'is at least', sqlOp: '>=' },
98
- { value: '<=', label: 'is at most', sqlOp: '<=' },
99
- { value: '!=', label: 'does not equal', sqlOp: '!=' }
100
- ],
101
- date: [
102
- { value: '=', label: 'is on', sqlOp: '=' },
103
- { value: '>', label: 'is after', sqlOp: '>' },
104
- { value: '<', label: 'is before', sqlOp: '<' },
105
- { value: '>=', label: 'is on or after', sqlOp: '>=' },
106
- { value: '<=', label: 'is on or before', sqlOp: '<=' },
107
- { value: '!=', label: 'is not on', sqlOp: '!=' }
108
- ],
109
- boolean: [
110
- { value: '=', label: 'is', sqlOp: '=' },
111
- { value: '!=', label: 'is not', sqlOp: '!=' }
112
- ]
113
- };
114
-
115
- let activeTab = $state('SQL');
116
- let completedFilters = $state<FilterBuilderItem[]>([]);
117
-
118
- function addFilter() {
119
- const newFilter: FilterBuilderItem = {
120
- id: nanoid(),
121
- column: columns[0]?.name || '',
122
- operator: operatorsByType[columns[0]?.type || 'string'][0].value,
123
- value: getDefaultValue(columns[0]?.type || 'string'),
124
- logicalOperator: completedFilters.length > 0 ? 'AND' : undefined
125
- // label: getColumnLabel(columns[0]?.name || '')
126
- };
127
-
128
- filters = [...filters, newFilter];
129
- }
130
-
131
- function clearAllFilters() {
132
- filters = [];
133
- completedFilters = [];
134
- }
135
-
136
- function getColumnType(columnName: string): FilterBuilderColumnConfig['type'] {
137
- return columns.find((col) => col.name === columnName)?.type || 'string';
138
- }
139
-
140
- function getColumnLabel(columnName: string): string {
141
- return columns.find((col) => col.name === columnName)?.label || columnName;
142
- }
143
-
144
- function updateFilter(filterId: string, field: keyof FilterBuilderItem, value: any) {
145
- const filterIndex = filters.findIndex((f) => f.id === filterId);
146
- if (filterIndex === -1) return;
147
-
148
- const updatedFilter = { ...filters[filterIndex], [field]: value };
149
-
150
- // Reset operator and value when column changes
151
- if (field === 'column') {
152
- const columnType = getColumnType(value);
153
- updatedFilter.operator = operatorsByType[columnType][0].value;
154
- updatedFilter.value = getDefaultValue(columnType);
155
- }
156
-
157
- filters[filterIndex] = updatedFilter;
158
- filters = [...filters];
159
- }
160
-
161
- function completeFilter(filterId: string) {
162
- const filter = filters.find((f) => f.id === filterId);
163
- if (filter && isFilterValid(filter) && !completedFilters.some((f) => f.id === filterId)) {
164
- completedFilters = [...completedFilters, filter];
165
- }
166
-
167
- // console.log('Completed Filters:', completedFilters);
168
-
169
- removeFilter(filterId);
170
- }
171
-
172
- function removeFilter(filterId: string) {
173
- const filterIndex = filters.findIndex((f) => f.id === filterId);
174
- if (filterIndex === -1) return;
175
-
176
- // If removing the first filter and there's a second one, remove the logical operator from the second
177
- if (filterIndex === 0 && filters.length > 1) {
178
- filters[1].logicalOperator = undefined;
179
- }
180
-
181
- filters = filters.filter((f) => f.id !== filterId);
182
- }
183
-
184
- function removeCompleteFilter(filterId: string) {
185
- completedFilters = completedFilters.filter((f) => f.id !== filterId);
186
- }
187
-
188
- function toggleLogicalOperator(filterId: string) {
189
- const filter = filters.find((f) => f.id === filterId);
190
- if (filter && filter.logicalOperator) {
191
- filter.logicalOperator = filter.logicalOperator === 'AND' ? 'OR' : 'AND';
192
- filters = [...filters];
193
- }
194
- }
195
-
196
- function generateSQLQuery(): string {
197
- const uniqueFilters = getUniqueFilters(filters, completedFilters);
198
- const validFilterList = uniqueFilters.filter(isFilterValid);
199
-
200
- if (validFilterList.length === 0) return '';
201
-
202
- const conditions = validFilterList.map((filter, index) => {
203
- let value = filter.value;
204
- const columnType = getColumnType(filter.column);
205
- const operatorConfig = operatorsByType[columnType].find((op) => op.value === filter.operator);
206
- const sqlOperator = operatorConfig?.sqlOp || filter.operator;
207
-
208
- // Handle string values based on operator type
209
- if (typeof value === 'string') {
210
- if (filter.operator === 'contains') {
211
- value = `'%${value}%'`;
212
- } else if (filter.operator === 'starts_with') {
213
- value = `'${value}%'`;
214
- } else {
215
- value = `'${value}'`;
216
- }
217
- }
218
-
219
- const condition = `${filter.column} ${sqlOperator} ${value}`;
220
-
221
- if (index === 0) {
222
- return condition;
223
- }
224
-
225
- return `${filter.logicalOperator} ${condition}`;
226
- });
227
-
228
- return `WHERE ${conditions.join(' ')}`;
229
- }
230
-
231
- function generateJSONQuery(): any[] {
232
- const uniqueFilters = getUniqueFilters(filters, completedFilters);
233
- return uniqueFilters.filter(isFilterValid).map((filter) => ({
234
- column: filter.column,
235
- operator: filter.operator,
236
- value: filter.value,
237
- ...(filter.logicalOperator && { logicalOperator: filter.logicalOperator })
238
- }));
239
- }
240
-
241
- function getLogicalOperatorCounts() {
242
- const counts = { OR: 0, AND: 0 };
243
- const uniqueFilters = getUniqueFilters(filters, completedFilters);
244
-
245
- uniqueFilters.forEach((filter) => {
246
- if (filter.logicalOperator) {
247
- counts[filter.logicalOperator]++;
248
- }
249
- });
250
- return counts;
251
- }
252
-
253
- function getColumnTypeCounts() {
254
- const uniqueFilters = getUniqueFilters(filters, completedFilters);
255
-
256
- const usedTypes = new Set(
257
- uniqueFilters.filter(isFilterValid).map((filter) => getColumnType(filter.column))
258
- );
259
- return usedTypes.size;
260
- }
261
-
262
- function loadValidFilters() {
263
- const uniqueFilters = getUniqueFilters(filters, completedFilters);
264
- const validFilterList = uniqueFilters.filter(isFilterValid);
265
-
266
- return validFilterList;
267
- }
268
-
269
- function buildDecisionTree() {
270
- if (completedFilters.length === 0) return null;
271
-
272
- const tree = {
273
- type: 'root',
274
- children: []
275
- };
276
-
277
- let currentNode = tree;
278
-
279
- completedFilters.forEach((filter, index) => {
280
- const filterNode = {
281
- type: 'condition',
282
- filter: filter,
283
- children: []
284
- };
285
-
286
- if (index === 0) {
287
- currentNode.children.push(filterNode);
288
- } else {
289
- const operatorNode = {
290
- type: 'operator',
291
- operator: filter.logicalOperator,
292
- children: [filterNode]
293
- };
294
- currentNode.children.push(operatorNode);
295
- }
296
- });
297
-
298
- return tree;
299
- }
300
-
301
- function renderDecisionNode(node, level = 0) {
302
- if (!node) return '';
303
-
304
- const indent = level * 40;
305
-
306
- if (node.type === 'condition') {
307
- return `
308
-
309
- <div class="relative flex items-center">
310
- <div class="relative">
311
- <div class="flex items-center bg-blue-100 text-blue-800 px-3 py-2 rounded-lg text-sm gap-0.5 w-fit h-fit">
312
- ${node.filter.column}<div class="w-2 h-2 bg-red-500 rounded-full"></div>${node.filter.operator}<div class="w-2 h-2 bg-red-500 rounded-full"></div>${node.filter.value}
313
- </div>
314
- <div class="absolute right-1 top-1">
315
- <button onclick="removeCompleteFilter('${node.filter.id}')" class="p-1 text-gray-400 hover:text-red-500">
316
- <iconify-icon class="absolute -top-2 -right-2 text-gray-400 hover:text-red-500" icon="ic:round-close"></iconify-icon>
317
- </button>
318
- </div>
319
- </div>
320
- <div class="w-5 h-0.5 bg-gray-300"></div>
321
- </div>
322
- `;
323
- } else if (node.type === 'operator') {
324
- const operatorColor =
325
- node.operator === 'AND' ? 'bg-green-100 text-green-800' : 'bg-orange-100 text-orange-800';
326
- return `
327
- <div class="flex flex-wrap items-center h-fit">
328
- <div class="flex items-center justify-center ${operatorColor} px-2 py-1 rounded text-xs font-semibold mb-2 w-fit">
329
- ${node.operator}
330
- </div>
331
- <div class="w-5 h-0.5 bg-gray-300"></div>
332
- ${node.children.map((child) => renderDecisionNode(child, level + 1)).join('')}
333
- </div>
334
- `;
335
- } else {
336
- return node.children.map((child) => renderDecisionNode(child, level)).join('');
337
- }
338
- }
339
-
340
- // Generate SQL WHERE clause
341
- let sqlQuery = $derived(generateSQLQuery());
342
- let jsonQuery = $derived(generateJSONQuery());
343
- let validFilters = $derived(loadValidFilters());
344
- let logicalOperatorCounts = $derived(getLogicalOperatorCounts());
345
- let columnTypeCounts = $derived(getColumnTypeCounts());
346
- let decisionTree = $derived(buildDecisionTree());
347
-
348
- onMount(() => {
349
- if (browser) {
350
- (window as any).removeCompleteFilter = removeCompleteFilter;
351
- }
352
- });
353
- </script>
354
-
355
- <div class="flex h-screen bg-gray-50">
356
- <!-- Left Panel - Filter Builder -->
357
- <div class="flex w-3/5 flex-col border-r border-gray-200 bg-white">
358
- <!-- Header -->
359
- <div class="border-b border-gray-200 p-6">
360
- <h2 class="mb-2 text-lg font-semibold text-gray-900">Build Filters</h2>
361
- <p class="text-sm text-gray-600">Add conditions to filter your data</p>
362
- </div>
363
-
364
- <!-- Filter Controls -->
365
- <div class="flex-1 overflow-y-auto p-6">
366
- <div class="mb-6 flex items-center justify-between">
367
- <Button onclick={addFilter}>
368
- {#snippet leadingIcon()}
369
- <iconify-icon class="me-1.5" style="font-size: 20px;" icon="ei:plus"></iconify-icon>
370
- {/snippet}
371
- Add Filter
372
- </Button>
373
-
374
- <Button color="alternative" onclick={clearAllFilters}>
375
- {#snippet leadingIcon()}
376
- <iconify-icon class="me-1.5" icon="mi:delete"></iconify-icon>
377
- {/snippet}
378
- Clear All
379
- </Button>
380
- </div>
381
-
382
- <!-- Filters List -->
383
- <div class="space-y-4">
384
- {#each filters as filter, index}
385
- <div class="relative rounded-lg bg-gray-50 p-4">
386
- <!-- Logical Operator (for filters after the first) -->
387
- {#if completedFilters.length > 0}
388
- <div class="mt-4 mb-2 flex justify-center">
389
- <div class="flex overflow-hidden rounded-md border border-gray-300 bg-white">
390
- <button
391
- onclick={() => toggleLogicalOperator(filter.id)}
392
- class="px-4 py-1 text-sm font-medium transition-colors {filter?.logicalOperator ===
393
- 'AND'
394
- ? 'bg-blue-600 text-white'
395
- : 'text-gray-600 hover:bg-gray-50'}"
396
- >
397
- AND
398
- </button>
399
- <button
400
- onclick={() => toggleLogicalOperator(filter.id)}
401
- class="px-4 py-1 text-sm font-medium transition-colors {filter?.logicalOperator ===
402
- 'OR'
403
- ? 'bg-orange-500 text-white'
404
- : 'text-gray-600 hover:bg-gray-50'}"
405
- >
406
- OR
407
- </button>
408
- </div>
409
- </div>
410
- {/if}
411
-
412
- <!-- Column, Operator, Value Row -->
413
- <div class="mb-3 grid grid-cols-3 gap-3">
414
- <div>
415
- <Label class="mb-2 block text-xs font-medium text-gray-700">Column</Label>
416
-
417
- <Select
418
- clearable={false}
419
- minHeight={40}
420
- options={columns}
421
- onChange={(e) => {
422
- updateFilter(filter.id, 'column', e.name);
423
- }}
424
- />
425
- </div>
426
-
427
- <div>
428
- <Label class="mb-2 block text-xs font-medium text-gray-700">Operator</Label>
429
-
430
- <Select
431
- clearable={false}
432
- minHeight={40}
433
- onChange={(e) => {
434
- updateFilter(filter.id, 'operator', e.value);
435
- }}
436
- options={operatorsByType[getColumnType(filter.column)]}
437
- />
438
- </div>
439
-
440
- <div>
441
- <Label class="mb-2 block text-xs font-medium text-gray-700">Value</Label>
442
-
443
- {#if getColumnType(filter.column) === 'string'}
444
- <Input
445
- type="text"
446
- oninput={(e) => {
447
- updateFilter(filter.id, 'value', e.target.value);
448
- }}
449
- />
450
- {:else if getColumnType(filter.column) === 'number'}
451
- <Input
452
- type="number"
453
- oninput={(e) =>
454
- updateFilter(filter.id, 'value', parseFloat(e.target.value) || 0)}
455
- />
456
- {:else if getColumnType(filter.column) === 'date'}
457
- <Datepicker
458
- onselect={(e) => {
459
- updateFilter(filter.id, 'value', e?.toLocaleString());
460
- }}
461
- />
462
- {:else if getColumnType(filter.column) === 'boolean'}
463
- <Select
464
- clearable={false}
465
- minHeight={40}
466
- onChange={(e) => updateFilter(filter.id, 'value', e.value)}
467
- options={[
468
- { value: true, label: 'True' },
469
- { value: false, label: 'False' }
470
- ]}
471
- />
472
- {/if}
473
- </div>
474
- </div>
475
-
476
- <!-- Filter Validation -->
477
- {#if isFilterValid(filter)}
478
- <div transition:fade class="flex justify-between">
479
- <div class="flex items-center text-xs text-green-600">
480
- <svg class="mr-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
481
- <path
482
- fill-rule="evenodd"
483
- d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
484
- clip-rule="evenodd"
485
- ></path>
486
- </svg>
487
- Filter is valid
488
- </div>
489
- <Button color="green" size="xs" onclick={() => completeFilter(filter.id)}
490
- >Done</Button
491
- >
492
- </div>
493
- {/if}
494
-
495
- <!-- Remove Button -->
496
- <button
497
- aria-label="removeFilter"
498
- onclick={() => removeFilter(filter.id)}
499
- class="absolute top-2 right-2 p-1 text-gray-400 transition-colors hover:text-red-500"
500
- >
501
- <iconify-icon icon="ic:round-close"></iconify-icon>
502
- </button>
503
- </div>
504
- {/each}
505
- </div>
506
- {#if decisionTree}
507
- <div class="space-y-4">
508
- <div class="rounded-lg bg-green-50 p-4">
509
- <div class="rounded-lg border bg-white p-4">
510
- {#if decisionTree}
511
- <div class="flex flex-wrap space-y-3">
512
- {@html renderDecisionNode(decisionTree)}
513
- </div>
514
- {:else}
515
- <div class="flex h-32 items-center justify-center text-gray-500">
516
- <p class="text-sm">No decision tree to display</p>
517
- </div>
518
- {/if}
519
- </div>
520
- </div>
521
- </div>
522
- {/if}
523
- </div>
524
- </div>
525
-
526
- <!-- Right Panel - Generated Query -->
527
- <div class="flex w-2/5 flex-col bg-white">
528
- <!-- Header -->
529
- <div class="flex justify-between border-b border-gray-200 p-6">
530
- <div class="flex flex-col">
531
- <div class="mb-2 flex items-center justify-between">
532
- <div class="flex items-center gap-2">
533
- <h2 class="text-lg font-semibold text-gray-900">Generated Query</h2>
534
- <div class="flex items-center text-sm text-gray-600">
535
- <span
536
- class="mr-2 inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800"
537
- >
538
- {validFilters.length} active filters
539
- </span>
540
- </div>
541
- </div>
542
- </div>
543
- <p class="text-sm text-gray-600">Live preview of your filter conditions</p>
544
- </div>
545
- <div class="flex items-center">
546
- <Button
547
- color="blue"
548
- onclick={() => onRunQuery({ jsonBuild: jsonQuery, sqlBuild: sqlQuery })}>Run Query</Button
549
- >
550
- </div>
551
- </div>
552
-
553
- <!-- Tabs -->
554
- <div class="border-b border-gray-200">
555
- <div class="flex">
556
- <button
557
- onclick={() => (activeTab = 'SQL')}
558
- class="border-b-2 px-4 py-2 text-sm font-medium transition-colors {activeTab === 'SQL'
559
- ? 'border-blue-500 text-blue-600'
560
- : 'border-transparent text-gray-600 hover:text-gray-800'}"
561
- >
562
- SQL
563
- </button>
564
- <button
565
- onclick={() => (activeTab = 'JSON')}
566
- class="border-b-2 px-4 py-2 text-sm font-medium transition-colors {activeTab === 'JSON'
567
- ? 'border-blue-500 text-blue-600'
568
- : 'border-transparent text-gray-600 hover:text-gray-800'}"
569
- >
570
- JSON
571
- </button>
572
- </div>
573
- </div>
574
-
575
- <!-- Query Content -->
576
- <div class="flex-1 overflow-y-auto p-6">
577
- {#if validFilters.length === 0}
578
- <div class="flex h-32 items-center justify-center text-gray-500">
579
- <div class="text-center">
580
- <svg
581
- class="mx-auto mb-2 h-8 w-8 opacity-50"
582
- fill="none"
583
- stroke="currentColor"
584
- viewBox="0 0 24 24"
585
- >
586
- <path
587
- stroke-linecap="round"
588
- stroke-linejoin="round"
589
- stroke-width="2"
590
- d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
591
- ></path>
592
- </svg>
593
- <p class="text-sm">No active filters</p>
594
- </div>
595
- </div>
596
- {:else if activeTab === 'SQL'}
597
- <div class="space-y-4">
598
- <div class="rounded-lg bg-gray-50 p-4">
599
- <h3 class="mb-2 text-sm font-medium text-gray-900">SQL WHERE Clause</h3>
600
- <div class="rounded border bg-white p-3 font-mono text-sm">
601
- <span class="text-blue-600">WHERE</span>
602
- {sqlQuery.replace('WHERE ', '')}
603
- </div>
604
- </div>
605
- </div>
606
- {:else}
607
- <div class="space-y-4">
608
- <div class="rounded-lg bg-gray-50 p-4">
609
- <h3 class="mb-2 text-sm font-medium text-gray-900">Filter Objects</h3>
610
- <pre
611
- class="overflow-x-auto rounded border bg-white p-3 text-xs text-gray-800">{JSON.stringify(
612
- jsonQuery,
613
- null,
614
- 2
615
- )}</pre>
616
- </div>
617
- </div>
618
- {/if}
619
-
620
- <!-- Statistics -->
621
- <div class="mt-6 grid grid-cols-2 gap-4">
622
- <div class="rounded-lg bg-blue-50 p-4">
623
- <h4 class="mb-2 text-sm font-medium text-blue-900">Logical Operators</h4>
624
- <div class="space-y-1">
625
- <div class="text-xl font-bold text-blue-600">{logicalOperatorCounts.OR}</div>
626
- <div class="text-xs text-blue-800">{logicalOperatorCounts.OR} OR</div>
627
- <div class="text-xs text-blue-800">{logicalOperatorCounts.AND} AND</div>
628
- </div>
629
- </div>
630
-
631
- <div class="rounded-lg bg-green-50 p-4">
632
- <h4 class="mb-2 text-sm font-medium text-green-900">Column Types</h4>
633
- <div class="space-y-1">
634
- <div class="text-xl font-bold text-green-600">{columnTypeCounts}</div>
635
- <div class="text-xs text-green-800">types used</div>
636
- </div>
637
- </div>
638
- </div>
639
- </div>
640
- </div>
641
- </div>
1
+ <script lang="ts" module>
2
+ function getDefaultValue(type: FilterBuilderColumnConfig['type']): any {
3
+ switch (type) {
4
+ case 'string':
5
+ return '';
6
+ case 'number':
7
+ return null;
8
+ case 'date':
9
+ return '';
10
+ case 'boolean':
11
+ return true;
12
+ default:
13
+ return '';
14
+ }
15
+ }
16
+
17
+ function isFilterValid(filter: FilterBuilderItem): boolean {
18
+ return !!(filter.column && filter.operator && filter.value !== null && filter.value !== '');
19
+ }
20
+
21
+ function getUniqueFilters(...filterArrays: FilterBuilderItem[][]): FilterBuilderItem[] {
22
+ const combinedFilters = filterArrays.flat();
23
+ const uniqueFilters: FilterBuilderItem[] = [];
24
+ const seen = new Set<string>();
25
+
26
+ combinedFilters.forEach((filter) => {
27
+ // Create a unique identifier based on all filter properties
28
+ const identifier = `${filter.column}-${filter.operator}-${String(filter.value).toLowerCase()}-${filter.logicalOperator || 'AND'}`;
29
+
30
+ if (!seen.has(identifier)) {
31
+ seen.add(identifier);
32
+ uniqueFilters.push(filter);
33
+ }
34
+ });
35
+
36
+ return uniqueFilters;
37
+ }
38
+
39
+ // Alternative version with configurable uniqueness criteria
40
+ function getUniqueFiltersAdvanced(
41
+ filterArrays: FilterBuilderItem[][],
42
+ uniquenessKey: (filter: FilterBuilderItem) => string = defaultUniquenessKey
43
+ ): FilterBuilderItem[] {
44
+ const combinedFilters = filterArrays.flat();
45
+ const uniqueFilters: FilterBuilderItem[] = [];
46
+ const seen = new Set<string>();
47
+
48
+ combinedFilters.forEach((filter) => {
49
+ const key = uniquenessKey(filter);
50
+
51
+ if (!seen.has(key)) {
52
+ seen.add(key);
53
+ uniqueFilters.push(filter);
54
+ }
55
+ });
56
+
57
+ return uniqueFilters;
58
+ }
59
+
60
+ // Default uniqueness key function
61
+ function defaultUniquenessKey(filter: FilterBuilderItem): string {
62
+ return `${filter.column}-${filter.operator}-${String(filter.value).toLowerCase()}-${filter.logicalOperator || 'AND'}`;
63
+ }
64
+ </script>
65
+
66
+ <script lang="ts">
67
+ import { browser } from '$app/environment';
68
+
69
+ import {
70
+ Button,
71
+ Datepicker,
72
+ Input,
73
+ Label,
74
+ Select,
75
+ type FilterBuilderColumnConfig,
76
+ type FilterBuilderItem,
77
+ type FilterBuilderProps
78
+ } from '../../index.js';
79
+ import { nanoid } from 'nanoid';
80
+ import { onMount } from 'svelte';
81
+ import { fade } from 'svelte/transition';
82
+
83
+ let { columns = [], filters = $bindable([]), onRunQuery }: FilterBuilderProps = $props();
84
+
85
+ // Operator mappings for each type
86
+ const operatorsByType = {
87
+ string: [
88
+ { value: 'contains', label: 'contains', sqlOp: 'ILIKE' },
89
+ { value: '=', label: 'equals', sqlOp: '=' },
90
+ { value: '!=', label: 'does not equal', sqlOp: '!=' },
91
+ { value: 'starts_with', label: 'starts with', sqlOp: 'ILIKE' }
92
+ ],
93
+ number: [
94
+ { value: '=', label: 'equals', sqlOp: '=' },
95
+ { value: '>', label: 'is greater than', sqlOp: '>' },
96
+ { value: '<', label: 'is less than', sqlOp: '<' },
97
+ { value: '>=', label: 'is at least', sqlOp: '>=' },
98
+ { value: '<=', label: 'is at most', sqlOp: '<=' },
99
+ { value: '!=', label: 'does not equal', sqlOp: '!=' }
100
+ ],
101
+ date: [
102
+ { value: '=', label: 'is on', sqlOp: '=' },
103
+ { value: '>', label: 'is after', sqlOp: '>' },
104
+ { value: '<', label: 'is before', sqlOp: '<' },
105
+ { value: '>=', label: 'is on or after', sqlOp: '>=' },
106
+ { value: '<=', label: 'is on or before', sqlOp: '<=' },
107
+ { value: '!=', label: 'is not on', sqlOp: '!=' }
108
+ ],
109
+ boolean: [
110
+ { value: '=', label: 'is', sqlOp: '=' },
111
+ { value: '!=', label: 'is not', sqlOp: '!=' }
112
+ ]
113
+ };
114
+
115
+ let activeTab = $state('SQL');
116
+ let completedFilters = $state<FilterBuilderItem[]>([]);
117
+
118
+ function addFilter() {
119
+ const newFilter: FilterBuilderItem = {
120
+ id: nanoid(),
121
+ column: columns[0]?.name || '',
122
+ operator: operatorsByType[columns[0]?.type || 'string'][0].value,
123
+ value: getDefaultValue(columns[0]?.type || 'string'),
124
+ logicalOperator: completedFilters.length > 0 ? 'AND' : undefined
125
+ // label: getColumnLabel(columns[0]?.name || '')
126
+ };
127
+
128
+ filters = [...filters, newFilter];
129
+ }
130
+
131
+ function clearAllFilters() {
132
+ filters = [];
133
+ completedFilters = [];
134
+ }
135
+
136
+ function getColumnType(columnName: string): FilterBuilderColumnConfig['type'] {
137
+ return columns.find((col) => col.name === columnName)?.type || 'string';
138
+ }
139
+
140
+ function getColumnLabel(columnName: string): string {
141
+ return columns.find((col) => col.name === columnName)?.label || columnName;
142
+ }
143
+
144
+ function updateFilter(filterId: string, field: keyof FilterBuilderItem, value: any) {
145
+ const filterIndex = filters.findIndex((f) => f.id === filterId);
146
+ if (filterIndex === -1) return;
147
+
148
+ const updatedFilter = { ...filters[filterIndex], [field]: value };
149
+
150
+ // Reset operator and value when column changes
151
+ if (field === 'column') {
152
+ const columnType = getColumnType(value);
153
+ updatedFilter.operator = operatorsByType[columnType][0].value;
154
+ updatedFilter.value = getDefaultValue(columnType);
155
+ }
156
+
157
+ filters[filterIndex] = updatedFilter;
158
+ filters = [...filters];
159
+ }
160
+
161
+ function completeFilter(filterId: string) {
162
+ const filter = filters.find((f) => f.id === filterId);
163
+ if (filter && isFilterValid(filter) && !completedFilters.some((f) => f.id === filterId)) {
164
+ completedFilters = [...completedFilters, filter];
165
+ }
166
+
167
+ // console.log('Completed Filters:', completedFilters);
168
+
169
+ removeFilter(filterId);
170
+ }
171
+
172
+ function removeFilter(filterId: string) {
173
+ const filterIndex = filters.findIndex((f) => f.id === filterId);
174
+ if (filterIndex === -1) return;
175
+
176
+ // If removing the first filter and there's a second one, remove the logical operator from the second
177
+ if (filterIndex === 0 && filters.length > 1) {
178
+ filters[1].logicalOperator = undefined;
179
+ }
180
+
181
+ filters = filters.filter((f) => f.id !== filterId);
182
+ }
183
+
184
+ function removeCompleteFilter(filterId: string) {
185
+ completedFilters = completedFilters.filter((f) => f.id !== filterId);
186
+ }
187
+
188
+ function toggleLogicalOperator(filterId: string) {
189
+ const filter = filters.find((f) => f.id === filterId);
190
+ if (filter && filter.logicalOperator) {
191
+ filter.logicalOperator = filter.logicalOperator === 'AND' ? 'OR' : 'AND';
192
+ filters = [...filters];
193
+ }
194
+ }
195
+
196
+ function generateSQLQuery(): string {
197
+ const uniqueFilters = getUniqueFilters(filters, completedFilters);
198
+ const validFilterList = uniqueFilters.filter(isFilterValid);
199
+
200
+ if (validFilterList.length === 0) return '';
201
+
202
+ const conditions = validFilterList.map((filter, index) => {
203
+ let value = filter.value;
204
+ const columnType = getColumnType(filter.column);
205
+ const operatorConfig = operatorsByType[columnType].find((op) => op.value === filter.operator);
206
+ const sqlOperator = operatorConfig?.sqlOp || filter.operator;
207
+
208
+ // Handle string values based on operator type
209
+ if (typeof value === 'string') {
210
+ if (filter.operator === 'contains') {
211
+ value = `'%${value}%'`;
212
+ } else if (filter.operator === 'starts_with') {
213
+ value = `'${value}%'`;
214
+ } else {
215
+ value = `'${value}'`;
216
+ }
217
+ }
218
+
219
+ const condition = `${filter.column} ${sqlOperator} ${value}`;
220
+
221
+ if (index === 0) {
222
+ return condition;
223
+ }
224
+
225
+ return `${filter.logicalOperator} ${condition}`;
226
+ });
227
+
228
+ return `WHERE ${conditions.join(' ')}`;
229
+ }
230
+
231
+ function generateJSONQuery(): any[] {
232
+ const uniqueFilters = getUniqueFilters(filters, completedFilters);
233
+ return uniqueFilters.filter(isFilterValid).map((filter) => ({
234
+ column: filter.column,
235
+ operator: filter.operator,
236
+ value: filter.value,
237
+ ...(filter.logicalOperator && { logicalOperator: filter.logicalOperator })
238
+ }));
239
+ }
240
+
241
+ function getLogicalOperatorCounts() {
242
+ const counts = { OR: 0, AND: 0 };
243
+ const uniqueFilters = getUniqueFilters(filters, completedFilters);
244
+
245
+ uniqueFilters.forEach((filter) => {
246
+ if (filter.logicalOperator) {
247
+ counts[filter.logicalOperator]++;
248
+ }
249
+ });
250
+ return counts;
251
+ }
252
+
253
+ function getColumnTypeCounts() {
254
+ const uniqueFilters = getUniqueFilters(filters, completedFilters);
255
+
256
+ const usedTypes = new Set(
257
+ uniqueFilters.filter(isFilterValid).map((filter) => getColumnType(filter.column))
258
+ );
259
+ return usedTypes.size;
260
+ }
261
+
262
+ function loadValidFilters() {
263
+ const uniqueFilters = getUniqueFilters(filters, completedFilters);
264
+ const validFilterList = uniqueFilters.filter(isFilterValid);
265
+
266
+ return validFilterList;
267
+ }
268
+
269
+ function buildDecisionTree() {
270
+ if (completedFilters.length === 0) return null;
271
+
272
+ const tree = {
273
+ type: 'root',
274
+ children: []
275
+ };
276
+
277
+ let currentNode = tree;
278
+
279
+ completedFilters.forEach((filter, index) => {
280
+ const filterNode = {
281
+ type: 'condition',
282
+ filter: filter,
283
+ children: []
284
+ };
285
+
286
+ if (index === 0) {
287
+ currentNode.children.push(filterNode);
288
+ } else {
289
+ const operatorNode = {
290
+ type: 'operator',
291
+ operator: filter.logicalOperator,
292
+ children: [filterNode]
293
+ };
294
+ currentNode.children.push(operatorNode);
295
+ }
296
+ });
297
+
298
+ return tree;
299
+ }
300
+
301
+ function renderDecisionNode(node, level = 0) {
302
+ if (!node) return '';
303
+
304
+ const indent = level * 40;
305
+
306
+ if (node.type === 'condition') {
307
+ return `
308
+
309
+ <div class="relative flex items-center">
310
+ <div class="relative">
311
+ <div class="flex items-center bg-blue-100 text-blue-800 px-3 py-2 rounded-lg text-sm gap-0.5 w-fit h-fit">
312
+ ${node.filter.column}<div class="w-2 h-2 bg-red-500 rounded-full"></div>${node.filter.operator}<div class="w-2 h-2 bg-red-500 rounded-full"></div>${node.filter.value}
313
+ </div>
314
+ <div class="absolute right-1 top-1">
315
+ <button onclick="removeCompleteFilter('${node.filter.id}')" class="p-1 text-gray-400 hover:text-red-500">
316
+ <iconify-icon class="absolute -top-2 -right-2 text-gray-400 hover:text-red-500" icon="ic:round-close"></iconify-icon>
317
+ </button>
318
+ </div>
319
+ </div>
320
+ <div class="w-5 h-0.5 bg-gray-300"></div>
321
+ </div>
322
+ `;
323
+ } else if (node.type === 'operator') {
324
+ const operatorColor =
325
+ node.operator === 'AND' ? 'bg-green-100 text-green-800' : 'bg-orange-100 text-orange-800';
326
+ return `
327
+ <div class="flex flex-wrap items-center h-fit">
328
+ <div class="flex items-center justify-center ${operatorColor} px-2 py-1 rounded text-xs font-semibold mb-2 w-fit">
329
+ ${node.operator}
330
+ </div>
331
+ <div class="w-5 h-0.5 bg-gray-300"></div>
332
+ ${node.children.map((child) => renderDecisionNode(child, level + 1)).join('')}
333
+ </div>
334
+ `;
335
+ } else {
336
+ return node.children.map((child) => renderDecisionNode(child, level)).join('');
337
+ }
338
+ }
339
+
340
+ // Generate SQL WHERE clause
341
+ let sqlQuery = $derived(generateSQLQuery());
342
+ let jsonQuery = $derived(generateJSONQuery());
343
+ let validFilters = $derived(loadValidFilters());
344
+ let logicalOperatorCounts = $derived(getLogicalOperatorCounts());
345
+ let columnTypeCounts = $derived(getColumnTypeCounts());
346
+ let decisionTree = $derived(buildDecisionTree());
347
+
348
+ onMount(() => {
349
+ if (browser) {
350
+ (window as any).removeCompleteFilter = removeCompleteFilter;
351
+ }
352
+ });
353
+ </script>
354
+
355
+ <div class="flex h-screen bg-gray-50">
356
+ <!-- Left Panel - Filter Builder -->
357
+ <div class="flex w-3/5 flex-col border-r border-gray-200 bg-white">
358
+ <!-- Header -->
359
+ <div class="border-b border-gray-200 p-6">
360
+ <h2 class="mb-2 text-lg font-semibold text-gray-900">Build Filters</h2>
361
+ <p class="text-sm text-gray-600">Add conditions to filter your data</p>
362
+ </div>
363
+
364
+ <!-- Filter Controls -->
365
+ <div class="flex-1 overflow-y-auto p-6">
366
+ <div class="mb-6 flex items-center justify-between">
367
+ <Button onclick={addFilter}>
368
+ {#snippet leadingIcon()}
369
+ <iconify-icon class="me-1.5" style="font-size: 20px;" icon="ei:plus"></iconify-icon>
370
+ {/snippet}
371
+ Add Filter
372
+ </Button>
373
+
374
+ <Button color="alternative" onclick={clearAllFilters}>
375
+ {#snippet leadingIcon()}
376
+ <iconify-icon class="me-1.5" icon="mi:delete"></iconify-icon>
377
+ {/snippet}
378
+ Clear All
379
+ </Button>
380
+ </div>
381
+
382
+ <!-- Filters List -->
383
+ <div class="space-y-4">
384
+ {#each filters as filter, index}
385
+ <div class="relative rounded-lg bg-gray-50 p-4">
386
+ <!-- Logical Operator (for filters after the first) -->
387
+ {#if completedFilters.length > 0}
388
+ <div class="mt-4 mb-2 flex justify-center">
389
+ <div class="flex overflow-hidden rounded-md border border-gray-300 bg-white">
390
+ <button
391
+ onclick={() => toggleLogicalOperator(filter.id)}
392
+ class="px-4 py-1 text-sm font-medium transition-colors {filter?.logicalOperator ===
393
+ 'AND'
394
+ ? 'bg-blue-600 text-white'
395
+ : 'text-gray-600 hover:bg-gray-50'}"
396
+ >
397
+ AND
398
+ </button>
399
+ <button
400
+ onclick={() => toggleLogicalOperator(filter.id)}
401
+ class="px-4 py-1 text-sm font-medium transition-colors {filter?.logicalOperator ===
402
+ 'OR'
403
+ ? 'bg-orange-500 text-white'
404
+ : 'text-gray-600 hover:bg-gray-50'}"
405
+ >
406
+ OR
407
+ </button>
408
+ </div>
409
+ </div>
410
+ {/if}
411
+
412
+ <!-- Column, Operator, Value Row -->
413
+ <div class="mb-3 grid grid-cols-3 gap-3">
414
+ <div>
415
+ <Label class="mb-2 block text-xs font-medium text-gray-700">Column</Label>
416
+
417
+ <Select
418
+ clearable={false}
419
+ minHeight={40}
420
+ options={columns}
421
+ onChange={(e) => {
422
+ updateFilter(filter.id, 'column', e.name);
423
+ }}
424
+ />
425
+ </div>
426
+
427
+ <div>
428
+ <Label class="mb-2 block text-xs font-medium text-gray-700">Operator</Label>
429
+
430
+ <Select
431
+ clearable={false}
432
+ minHeight={40}
433
+ onChange={(e) => {
434
+ updateFilter(filter.id, 'operator', e.value);
435
+ }}
436
+ options={operatorsByType[getColumnType(filter.column)]}
437
+ />
438
+ </div>
439
+
440
+ <div>
441
+ <Label class="mb-2 block text-xs font-medium text-gray-700">Value</Label>
442
+
443
+ {#if getColumnType(filter.column) === 'string'}
444
+ <Input
445
+ type="text"
446
+ oninput={(e) => {
447
+ updateFilter(filter.id, 'value', e.target.value);
448
+ }}
449
+ />
450
+ {:else if getColumnType(filter.column) === 'number'}
451
+ <Input
452
+ type="number"
453
+ oninput={(e) =>
454
+ updateFilter(filter.id, 'value', parseFloat(e.target.value) || 0)}
455
+ />
456
+ {:else if getColumnType(filter.column) === 'date'}
457
+ <Datepicker
458
+ onselect={(e) => {
459
+ updateFilter(filter.id, 'value', e?.toLocaleString());
460
+ }}
461
+ />
462
+ {:else if getColumnType(filter.column) === 'boolean'}
463
+ <Select
464
+ clearable={false}
465
+ minHeight={40}
466
+ onChange={(e) => updateFilter(filter.id, 'value', e.value)}
467
+ options={[
468
+ { value: true, label: 'True' },
469
+ { value: false, label: 'False' }
470
+ ]}
471
+ />
472
+ {/if}
473
+ </div>
474
+ </div>
475
+
476
+ <!-- Filter Validation -->
477
+ {#if isFilterValid(filter)}
478
+ <div transition:fade class="flex justify-between">
479
+ <div class="flex items-center text-xs text-green-600">
480
+ <svg class="mr-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
481
+ <path
482
+ fill-rule="evenodd"
483
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
484
+ clip-rule="evenodd"
485
+ ></path>
486
+ </svg>
487
+ Filter is valid
488
+ </div>
489
+ <Button color="green" size="xs" onclick={() => completeFilter(filter.id)}
490
+ >Done</Button
491
+ >
492
+ </div>
493
+ {/if}
494
+
495
+ <!-- Remove Button -->
496
+ <button
497
+ aria-label="removeFilter"
498
+ onclick={() => removeFilter(filter.id)}
499
+ class="absolute top-2 right-2 p-1 text-gray-400 transition-colors hover:text-red-500"
500
+ >
501
+ <iconify-icon icon="ic:round-close"></iconify-icon>
502
+ </button>
503
+ </div>
504
+ {/each}
505
+ </div>
506
+ {#if decisionTree}
507
+ <div class="space-y-4">
508
+ <div class="rounded-lg bg-green-50 p-4">
509
+ <div class="rounded-lg border bg-white p-4">
510
+ {#if decisionTree}
511
+ <div class="flex flex-wrap space-y-3">
512
+ {@html renderDecisionNode(decisionTree)}
513
+ </div>
514
+ {:else}
515
+ <div class="flex h-32 items-center justify-center text-gray-500">
516
+ <p class="text-sm">No decision tree to display</p>
517
+ </div>
518
+ {/if}
519
+ </div>
520
+ </div>
521
+ </div>
522
+ {/if}
523
+ </div>
524
+ </div>
525
+
526
+ <!-- Right Panel - Generated Query -->
527
+ <div class="flex w-2/5 flex-col bg-white">
528
+ <!-- Header -->
529
+ <div class="flex justify-between border-b border-gray-200 p-6">
530
+ <div class="flex flex-col">
531
+ <div class="mb-2 flex items-center justify-between">
532
+ <div class="flex items-center gap-2">
533
+ <h2 class="text-lg font-semibold text-gray-900">Generated Query</h2>
534
+ <div class="flex items-center text-sm text-gray-600">
535
+ <span
536
+ class="mr-2 inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800"
537
+ >
538
+ {validFilters.length} active filters
539
+ </span>
540
+ </div>
541
+ </div>
542
+ </div>
543
+ <p class="text-sm text-gray-600">Live preview of your filter conditions</p>
544
+ </div>
545
+ <div class="flex items-center">
546
+ <Button
547
+ color="blue"
548
+ onclick={() => onRunQuery({ jsonBuild: jsonQuery, sqlBuild: sqlQuery })}>Run Query</Button
549
+ >
550
+ </div>
551
+ </div>
552
+
553
+ <!-- Tabs -->
554
+ <div class="border-b border-gray-200">
555
+ <div class="flex">
556
+ <button
557
+ onclick={() => (activeTab = 'SQL')}
558
+ class="border-b-2 px-4 py-2 text-sm font-medium transition-colors {activeTab === 'SQL'
559
+ ? 'border-blue-500 text-blue-600'
560
+ : 'border-transparent text-gray-600 hover:text-gray-800'}"
561
+ >
562
+ SQL
563
+ </button>
564
+ <button
565
+ onclick={() => (activeTab = 'JSON')}
566
+ class="border-b-2 px-4 py-2 text-sm font-medium transition-colors {activeTab === 'JSON'
567
+ ? 'border-blue-500 text-blue-600'
568
+ : 'border-transparent text-gray-600 hover:text-gray-800'}"
569
+ >
570
+ JSON
571
+ </button>
572
+ </div>
573
+ </div>
574
+
575
+ <!-- Query Content -->
576
+ <div class="flex-1 overflow-y-auto p-6">
577
+ {#if validFilters.length === 0}
578
+ <div class="flex h-32 items-center justify-center text-gray-500">
579
+ <div class="text-center">
580
+ <svg
581
+ class="mx-auto mb-2 h-8 w-8 opacity-50"
582
+ fill="none"
583
+ stroke="currentColor"
584
+ viewBox="0 0 24 24"
585
+ >
586
+ <path
587
+ stroke-linecap="round"
588
+ stroke-linejoin="round"
589
+ stroke-width="2"
590
+ d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
591
+ ></path>
592
+ </svg>
593
+ <p class="text-sm">No active filters</p>
594
+ </div>
595
+ </div>
596
+ {:else if activeTab === 'SQL'}
597
+ <div class="space-y-4">
598
+ <div class="rounded-lg bg-gray-50 p-4">
599
+ <h3 class="mb-2 text-sm font-medium text-gray-900">SQL WHERE Clause</h3>
600
+ <div class="rounded border bg-white p-3 font-mono text-sm">
601
+ <span class="text-blue-600">WHERE</span>
602
+ {sqlQuery.replace('WHERE ', '')}
603
+ </div>
604
+ </div>
605
+ </div>
606
+ {:else}
607
+ <div class="space-y-4">
608
+ <div class="rounded-lg bg-gray-50 p-4">
609
+ <h3 class="mb-2 text-sm font-medium text-gray-900">Filter Objects</h3>
610
+ <pre
611
+ class="overflow-x-auto rounded border bg-white p-3 text-xs text-gray-800">{JSON.stringify(
612
+ jsonQuery,
613
+ null,
614
+ 2
615
+ )}</pre>
616
+ </div>
617
+ </div>
618
+ {/if}
619
+
620
+ <!-- Statistics -->
621
+ <div class="mt-6 grid grid-cols-2 gap-4">
622
+ <div class="rounded-lg bg-blue-50 p-4">
623
+ <h4 class="mb-2 text-sm font-medium text-blue-900">Logical Operators</h4>
624
+ <div class="space-y-1">
625
+ <div class="text-xl font-bold text-blue-600">{logicalOperatorCounts.OR}</div>
626
+ <div class="text-xs text-blue-800">{logicalOperatorCounts.OR} OR</div>
627
+ <div class="text-xs text-blue-800">{logicalOperatorCounts.AND} AND</div>
628
+ </div>
629
+ </div>
630
+
631
+ <div class="rounded-lg bg-green-50 p-4">
632
+ <h4 class="mb-2 text-sm font-medium text-green-900">Column Types</h4>
633
+ <div class="space-y-1">
634
+ <div class="text-xl font-bold text-green-600">{columnTypeCounts}</div>
635
+ <div class="text-xs text-green-800">types used</div>
636
+ </div>
637
+ </div>
638
+ </div>
639
+ </div>
640
+ </div>
641
+ </div>