@svadmin/ui 0.72.0 → 0.73.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 (252) hide show
  1. package/README.md +40 -25
  2. package/THIRD_PARTY_NOTICES.md +52 -0
  3. package/dist/app.css +7694 -5117
  4. package/dist/app.theme.css +7645 -5119
  5. package/dist/class-properties.generated.d.ts +1 -0
  6. package/dist/class-properties.generated.js +2 -0
  7. package/dist/classnames.d.ts +8 -0
  8. package/dist/classnames.js +26 -0
  9. package/dist/components/AutoForm.svelte +20 -6
  10. package/dist/components/AutoForm.svelte.d.ts +2 -0
  11. package/dist/components/AutoTable.svelte +249 -9
  12. package/dist/components/AutoTable.svelte.d.ts +5 -0
  13. package/dist/components/BoundRecordDetailDrawer.svelte +8 -2
  14. package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +2 -0
  15. package/dist/components/ComboboxField.svelte +31 -9
  16. package/dist/components/ComboboxField.svelte.d.ts +3 -2
  17. package/dist/components/CreatePage.svelte +4 -0
  18. package/dist/components/CreatePage.svelte.d.ts +2 -0
  19. package/dist/components/DateRangeInput.svelte +75 -0
  20. package/dist/components/DateRangeInput.svelte.d.ts +25 -0
  21. package/dist/components/DateTimeInput.svelte +65 -0
  22. package/dist/components/DateTimeInput.svelte.d.ts +19 -0
  23. package/dist/components/DevTools.svelte +2 -2
  24. package/dist/components/EditPage.svelte +4 -0
  25. package/dist/components/EditPage.svelte.d.ts +2 -0
  26. package/dist/components/FieldRenderer.svelte +90 -18
  27. package/dist/components/FileUpload.svelte +250 -0
  28. package/dist/components/FileUpload.svelte.d.ts +32 -0
  29. package/dist/components/FilterBuilder.svelte +236 -279
  30. package/dist/components/FilterBuilder.svelte.d.ts +3 -0
  31. package/dist/components/ImportWizard.svelte +181 -94
  32. package/dist/components/JsonSchemaForm.svelte +255 -99
  33. package/dist/components/JsonSchemaForm.svelte.d.ts +12 -1
  34. package/dist/components/Layout.svelte +1 -2
  35. package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +1 -1
  36. package/dist/components/ListPage.svelte +4 -0
  37. package/dist/components/ListPage.svelte.d.ts +2 -0
  38. package/dist/components/LoginPage.svelte +1 -1
  39. package/dist/components/MoneyInput.svelte +25 -0
  40. package/dist/components/MoneyInput.svelte.d.ts +20 -0
  41. package/dist/components/NumberInput.svelte +25 -0
  42. package/dist/components/NumberInput.svelte.d.ts +19 -0
  43. package/dist/components/NumericInput.svelte +110 -0
  44. package/dist/components/NumericInput.svelte.d.ts +23 -0
  45. package/dist/components/PercentInput.svelte +25 -0
  46. package/dist/components/PercentInput.svelte.d.ts +20 -0
  47. package/dist/components/QuickEditDrawer.svelte +4 -1
  48. package/dist/components/QuickEditDrawer.svelte.d.ts +2 -0
  49. package/dist/components/RecordDetailDrawer.svelte +4 -2
  50. package/dist/components/RecordDetailDrawer.svelte.d.ts +2 -0
  51. package/dist/components/ResourceOperationsPage.svelte +25 -19
  52. package/dist/components/ResourceOperationsPage.svelte.d.ts +5 -1
  53. package/dist/components/ShowInferencer.svelte +4 -2
  54. package/dist/components/ShowInferencer.svelte.d.ts +2 -0
  55. package/dist/components/ShowPage.svelte +7 -3
  56. package/dist/components/ShowPage.svelte.d.ts +2 -0
  57. package/dist/components/Sidebar.svelte +5 -5
  58. package/dist/components/SpreadsheetView.svelte +81 -286
  59. package/dist/components/StepsForm.svelte +5 -5
  60. package/dist/components/SvarAutoTable.svelte +39 -0
  61. package/dist/components/SvarAutoTable.svelte.d.ts +13 -0
  62. package/dist/components/SvarDataGrid.svelte +362 -0
  63. package/dist/components/SvarDataGrid.svelte.d.ts +57 -0
  64. package/dist/components/SvarGridCell.svelte +10 -0
  65. package/dist/components/SvarGridCell.svelte.d.ts +7 -0
  66. package/dist/components/SvarResourceRead.svelte +50 -0
  67. package/dist/components/SvarResourceRead.svelte.d.ts +23 -0
  68. package/dist/components/SvarResourceTable.svelte +63 -0
  69. package/dist/components/SvarResourceTable.svelte.d.ts +42 -0
  70. package/dist/components/SvarResourceTableView.svelte +364 -0
  71. package/dist/components/SvarResourceTableView.svelte.d.ts +4 -0
  72. package/dist/components/TaskQueueDrawer.svelte +72 -16
  73. package/dist/components/TreeSelect.svelte +208 -28
  74. package/dist/components/TreeSelect.svelte.d.ts +3 -1
  75. package/dist/components/auto-table-grid.d.ts +26 -0
  76. package/dist/components/auto-table-grid.js +1 -0
  77. package/dist/components/content/ContentPageHeader.svelte +12 -9
  78. package/dist/components/content/ContentPageShell.svelte +6 -9
  79. package/dist/components/content/FileBrowser.svelte +35 -0
  80. package/dist/components/content/FileBrowser.svelte.d.ts +14 -0
  81. package/dist/components/content/MetricBlock.svelte +10 -13
  82. package/dist/components/content/NetworkTable.svelte +3 -3
  83. package/dist/components/content/NetworkTable.svelte.d.ts +1 -0
  84. package/dist/components/content/PageToolbar.svelte +6 -10
  85. package/dist/components/content/SectionHeader.svelte +8 -13
  86. package/dist/components/content/SettingsFieldRow.svelte +9 -23
  87. package/dist/components/content/SettingsGroup.svelte +11 -27
  88. package/dist/components/content/StatusBadge.svelte +3 -8
  89. package/dist/components/content/WorkspaceLayout.svelte +8 -12
  90. package/dist/components/enterprise/filter-tree.d.ts +48 -0
  91. package/dist/components/enterprise/filter-tree.js +224 -0
  92. package/dist/components/enterprise/json-schema-form.d.ts +39 -0
  93. package/dist/components/enterprise/json-schema-form.js +195 -0
  94. package/dist/components/enterprise/recursive-schema-form.d.ts +30 -0
  95. package/dist/components/enterprise/recursive-schema-form.js +196 -0
  96. package/dist/components/enterprise/spreadsheet-formula.d.ts +12 -0
  97. package/dist/components/enterprise/spreadsheet-formula.js +245 -0
  98. package/dist/components/fieldComponentMap.js +5 -0
  99. package/dist/components/fields/DateField.svelte +4 -3
  100. package/dist/components/fields/date-field-value.d.ts +2 -0
  101. package/dist/components/fields/date-field-value.js +13 -0
  102. package/dist/components/json-schema-form-state.d.ts +37 -0
  103. package/dist/components/json-schema-form-state.js +168 -0
  104. package/dist/components/record-detail.svelte.d.ts +3 -0
  105. package/dist/components/record-detail.svelte.js +10 -4
  106. package/dist/components/svar-grid-cells.d.ts +14 -0
  107. package/dist/components/svar-grid-cells.js +2 -0
  108. package/dist/components/svar-grid-contract.d.ts +80 -0
  109. package/dist/components/svar-grid-contract.js +159 -0
  110. package/dist/components/svar-grid-interactions.d.ts +54 -0
  111. package/dist/components/svar-grid-interactions.js +174 -0
  112. package/dist/components/svar-grid-loading.d.ts +40 -0
  113. package/dist/components/svar-grid-loading.js +180 -0
  114. package/dist/components/svar-grid-operations.d.ts +37 -0
  115. package/dist/components/svar-grid-operations.js +186 -0
  116. package/dist/components/svar-grid-panes.d.ts +34 -0
  117. package/dist/components/svar-grid-panes.js +66 -0
  118. package/dist/components/svar-grid-resource-loading.d.ts +18 -0
  119. package/dist/components/svar-grid-resource-loading.js +68 -0
  120. package/dist/components/svar-grid-resource-session.svelte.d.ts +29 -0
  121. package/dist/components/svar-grid-resource-session.svelte.js +61 -0
  122. package/dist/components/ui/badge/badge-variants.js +3 -1
  123. package/dist/components/ui/button/button-variants.js +5 -2
  124. package/dist/components/ui/input/input.svelte +1 -4
  125. package/dist/components/ui/textarea/textarea.svelte +2 -1
  126. package/dist/components.css +3079 -0
  127. package/dist/design-contract.d.ts +13 -0
  128. package/dist/design-contract.js +11 -0
  129. package/dist/design-system.d.ts +15 -0
  130. package/dist/design-system.js +7 -0
  131. package/dist/index.d.ts +14 -0
  132. package/dist/index.js +9 -0
  133. package/dist/page-chrome.css +12 -49
  134. package/dist/recipes.d.ts +4 -0
  135. package/dist/recipes.js +4 -0
  136. package/dist/rendering/index.d.ts +87 -0
  137. package/dist/rendering/index.js +117 -0
  138. package/dist/styled-system/css/conditions.js +36 -0
  139. package/dist/styled-system/css/css.d.ts +22 -0
  140. package/dist/styled-system/css/css.js +45 -0
  141. package/dist/styled-system/css/cva.d.ts +6 -0
  142. package/dist/styled-system/css/cva.js +87 -0
  143. package/dist/styled-system/css/cx.d.ts +5 -0
  144. package/dist/styled-system/css/cx.js +15 -0
  145. package/dist/styled-system/css/index.d.ts +5 -0
  146. package/dist/styled-system/css/index.js +4 -0
  147. package/dist/styled-system/css/sva.d.ts +4 -0
  148. package/dist/styled-system/css/sva.js +46 -0
  149. package/dist/styled-system/helpers.js +316 -0
  150. package/dist/styled-system/patterns/aspect-ratio.d.ts +20 -0
  151. package/dist/styled-system/patterns/aspect-ratio.js +38 -0
  152. package/dist/styled-system/patterns/bleed.d.ts +21 -0
  153. package/dist/styled-system/patterns/bleed.js +24 -0
  154. package/dist/styled-system/patterns/box.d.ts +20 -0
  155. package/dist/styled-system/patterns/box.js +15 -0
  156. package/dist/styled-system/patterns/center.d.ts +20 -0
  157. package/dist/styled-system/patterns/center.js +21 -0
  158. package/dist/styled-system/patterns/circle.d.ts +20 -0
  159. package/dist/styled-system/patterns/circle.js +25 -0
  160. package/dist/styled-system/patterns/container.d.ts +20 -0
  161. package/dist/styled-system/patterns/container.js +21 -0
  162. package/dist/styled-system/patterns/cq.d.ts +21 -0
  163. package/dist/styled-system/patterns/cq.js +21 -0
  164. package/dist/styled-system/patterns/divider.d.ts +22 -0
  165. package/dist/styled-system/patterns/divider.js +25 -0
  166. package/dist/styled-system/patterns/flex.d.ts +26 -0
  167. package/dist/styled-system/patterns/flex.js +26 -0
  168. package/dist/styled-system/patterns/float.d.ts +23 -0
  169. package/dist/styled-system/patterns/float.js +52 -0
  170. package/dist/styled-system/patterns/grid-item.d.ts +25 -0
  171. package/dist/styled-system/patterns/grid-item.js +25 -0
  172. package/dist/styled-system/patterns/grid.d.ts +24 -0
  173. package/dist/styled-system/patterns/grid.js +27 -0
  174. package/dist/styled-system/patterns/hstack.d.ts +21 -0
  175. package/dist/styled-system/patterns/hstack.js +24 -0
  176. package/dist/styled-system/patterns/index.d.ts +21 -0
  177. package/dist/styled-system/patterns/index.js +20 -0
  178. package/dist/styled-system/patterns/link-overlay.d.ts +20 -0
  179. package/dist/styled-system/patterns/link-overlay.js +24 -0
  180. package/dist/styled-system/patterns/spacer.d.ts +20 -0
  181. package/dist/styled-system/patterns/spacer.js +25 -0
  182. package/dist/styled-system/patterns/square.d.ts +20 -0
  183. package/dist/styled-system/patterns/square.js +24 -0
  184. package/dist/styled-system/patterns/stack.d.ts +23 -0
  185. package/dist/styled-system/patterns/stack.js +24 -0
  186. package/dist/styled-system/patterns/visually-hidden.d.ts +20 -0
  187. package/dist/styled-system/patterns/visually-hidden.js +18 -0
  188. package/dist/styled-system/patterns/vstack.d.ts +21 -0
  189. package/dist/styled-system/patterns/vstack.js +24 -0
  190. package/dist/styled-system/patterns/wrap.d.ts +24 -0
  191. package/dist/styled-system/patterns/wrap.js +25 -0
  192. package/dist/styled-system/recipes/content-header.d.ts +31 -0
  193. package/dist/styled-system/recipes/content-header.js +65 -0
  194. package/dist/styled-system/recipes/content-page.d.ts +34 -0
  195. package/dist/styled-system/recipes/content-page.js +43 -0
  196. package/dist/styled-system/recipes/create-recipe.js +82 -0
  197. package/dist/styled-system/recipes/index.d.ts +17 -0
  198. package/dist/styled-system/recipes/index.js +16 -0
  199. package/dist/styled-system/recipes/metric-block.d.ts +34 -0
  200. package/dist/styled-system/recipes/metric-block.js +76 -0
  201. package/dist/styled-system/recipes/product-list.d.ts +31 -0
  202. package/dist/styled-system/recipes/product-list.js +73 -0
  203. package/dist/styled-system/recipes/product-section.d.ts +31 -0
  204. package/dist/styled-system/recipes/product-section.js +49 -0
  205. package/dist/styled-system/recipes/product-settings-row.d.ts +34 -0
  206. package/dist/styled-system/recipes/product-settings-row.js +58 -0
  207. package/dist/styled-system/recipes/product-settings.d.ts +31 -0
  208. package/dist/styled-system/recipes/product-settings.js +57 -0
  209. package/dist/styled-system/recipes/product-status.d.ts +34 -0
  210. package/dist/styled-system/recipes/product-status.js +45 -0
  211. package/dist/styled-system/recipes/product-toolbar.d.ts +31 -0
  212. package/dist/styled-system/recipes/product-toolbar.js +41 -0
  213. package/dist/styled-system/recipes/product-workspace.d.ts +34 -0
  214. package/dist/styled-system/recipes/product-workspace.js +58 -0
  215. package/dist/styled-system/recipes/surface-metric.d.ts +38 -0
  216. package/dist/styled-system/recipes/surface-metric.js +63 -0
  217. package/dist/styled-system/recipes/surface-table.d.ts +34 -0
  218. package/dist/styled-system/recipes/surface-table.js +62 -0
  219. package/dist/styled-system/recipes/ui-badge.d.ts +31 -0
  220. package/dist/styled-system/recipes/ui-badge.js +38 -0
  221. package/dist/styled-system/recipes/ui-button.d.ts +32 -0
  222. package/dist/styled-system/recipes/ui-button.js +43 -0
  223. package/dist/styled-system/recipes/ui-input.d.ts +31 -0
  224. package/dist/styled-system/recipes/ui-input.js +49 -0
  225. package/dist/styled-system/recipes/ui-textarea.d.ts +31 -0
  226. package/dist/styled-system/recipes/ui-textarea.js +24 -0
  227. package/dist/styled-system/styles.css +1257 -0
  228. package/dist/styled-system/tokens/index.d.ts +9 -0
  229. package/dist/styled-system/tokens/index.js +268 -0
  230. package/dist/styled-system/tokens/tokens.d.ts +36 -0
  231. package/dist/styled-system/types/composition.d.ts +227 -0
  232. package/dist/styled-system/types/conditions.d.ts +236 -0
  233. package/dist/styled-system/types/csstype.d.ts +22570 -0
  234. package/dist/styled-system/types/index.d.ts +6 -0
  235. package/dist/styled-system/types/parts.d.ts +8 -0
  236. package/dist/styled-system/types/pattern.d.ts +78 -0
  237. package/dist/styled-system/types/prop-type.d.ts +223 -0
  238. package/dist/styled-system/types/recipe.d.ts +181 -0
  239. package/dist/styled-system/types/selectors.d.ts +59 -0
  240. package/dist/styled-system/types/static-css.d.ts +56 -0
  241. package/dist/styled-system/types/style-props.d.ts +8088 -0
  242. package/dist/styled-system/types/system-types.d.ts +151 -0
  243. package/dist/styles/aliases.css +49 -0
  244. package/dist/styles/compatibility.epilogue.css +384 -0
  245. package/dist/styles/compatibility.prelude.css +5513 -0
  246. package/dist/styles/conditional-states.css +28 -0
  247. package/dist/styles/conditional-utilities.css +22 -0
  248. package/dist/styles/recipes.css +1262 -0
  249. package/dist/utils.d.ts +1 -1
  250. package/dist/utils.js +1 -1
  251. package/package.json +31 -13
  252. package/styles-compatibility.json +12 -0
@@ -1,321 +1,278 @@
1
1
  <script lang="ts">
2
+ import { untrack } from 'svelte';
2
3
  import { Plus, Trash2, RotateCcw, Filter as FilterIcon, Check } from '@lucide/svelte';
3
4
  import { cn } from '../utils.js';
4
- import { numericInputValue } from '../numeric-input.js';
5
5
  import { Button } from './ui/button/index.js';
6
6
  import { Input } from './ui/input/index.js';
7
7
  import { Select } from './ui/select/index.js';
8
8
  import { useTranslation } from '@svadmin/core/i18n';
9
- import type { CrudOperator, FieldDefinition, FieldFilter, Filter, LogicalFilter } from '@svadmin/core';
10
-
11
- export interface FilterRuleItem {
12
- id: string;
13
- field: string;
14
- operator: CrudOperator;
15
- value: unknown;
16
- }
17
-
18
- const i18n = useTranslation();
9
+ import type { CrudOperator, FieldDefinition, Filter } from '@svadmin/core';
10
+ import { readFilterTree, compileFilterTree, filterOperators, parseFilterInput, isCollectionOperator, isNullOperator,
11
+ FILTER_EDITOR_LIMITS, type FilterGroupNode, type FilterNode, type FilterRuleNode, type FilterEditorIssue, type FilterIssueCode } from './enterprise/filter-tree.js';
19
12
 
13
+ export interface FilterRuleItem { id: string; field: string; operator: CrudOperator; value: unknown }
20
14
  interface Props {
21
15
  fields?: FieldDefinition[];
22
16
  filters?: Filter[];
23
17
  logicalOperator?: 'and' | 'or';
18
+ disabled?: boolean;
24
19
  class?: string;
25
20
  onApply?: (filters: Filter[]) => void;
26
21
  onReset?: () => void;
22
+ onInvalid?: (issues: FilterEditorIssue[]) => void;
27
23
  }
24
+ let { fields = [], filters = $bindable([]), logicalOperator = $bindable('and'), disabled = false,
25
+ class: className, onApply, onReset, onInvalid }: Props = $props();
26
+ const uid = $props.id();
27
+ const i18n = useTranslation();
28
+ const chinese = $derived(i18n.locale.startsWith('zh'));
29
+ const availableFields = $derived(fields.filter((field) => field.filterable !== false));
30
+ let root = $state<FilterGroupNode>({ kind: 'group', id: 'root', operator: 'and', children: [], wrapped: false });
31
+ let loadIssues = $state<FilterEditorIssue[]>([]);
32
+ let attempted = $state(false);
33
+ let nextId = 0;
34
+ let lastInputFilters: Filter[] | undefined;
35
+ let preserved = $state.raw<ReadonlyMap<string, Filter>>(new Map());
36
+ const compilation = $derived(compileFilterTree({ ...root, operator: logicalOperator }, fields, preserved));
37
+ const issues = $derived(loadIssues.length ? loadIssues : attempted && !compilation.ok ? compilation.issues : []);
38
+ function countNodes(node: FilterNode): number { return 1 + (node.kind === 'group' ? node.children.reduce((sum, child) => sum + countNodes(child), 0) : 0); }
39
+ const nodeCount = $derived(countNodes(root));
40
+ const full = $derived(nodeCount >= FILTER_EDITOR_LIMITS.nodes);
28
41
 
29
- let {
30
- fields = [],
31
- filters = $bindable([]),
32
- logicalOperator = $bindable('and'),
33
- class: className,
34
- onApply,
35
- onReset,
36
- }: Props = $props();
37
-
38
- const operatorOptions: { value: CrudOperator; label: string }[] = [
39
- { value: 'eq', label: '等于 (eq)' },
40
- { value: 'ne', label: '不等于 (ne)' },
41
- { value: 'contains', label: '包含 (contains)' },
42
- { value: 'ncontains', label: '不包含 (ncontains)' },
43
- { value: 'gt', label: '大于 (>)' },
44
- { value: 'gte', label: '大于等于 (>=)' },
45
- { value: 'lt', label: '小于 (<)' },
46
- { value: 'lte', label: '小于等于 (<=)' },
47
- { value: 'null', label: '为空 (null)' },
48
- { value: 'nnull', label: '非空 (not null)' },
49
- ];
50
-
51
- let rules = $state<FilterRuleItem[]>([]);
52
-
53
- // Initialize rules from filters prop
42
+ function snapshotNode(node: FilterNode): Filter {
43
+ return node.kind === 'group' ? { operator: node.operator, value: node.children.map(snapshotNode) }
44
+ : { field: node.field, operator: node.operator, value: Array.isArray(node.value) ? [...node.value] : node.value };
45
+ }
46
+ function cannotEdit(node: FilterRuleNode): boolean {
47
+ return isCollectionOperator(node.operator) || node.value === ''
48
+ || !compileFilterTree({ kind: 'group', id: 'probe', operator: 'and', children: [{ ...node, readonly: false }] }, fields).ok;
49
+ }
50
+ function remember(node: FilterNode, saved: Map<string, Filter>): void {
51
+ saved.set(node.id, snapshotNode(node));
52
+ if (node.kind === 'group') node.children.forEach(child => remember(child, saved));
53
+ else node.readonly = cannotEdit(node);
54
+ }
55
+ function refreshReadonly(node: FilterNode): void {
56
+ if (node.kind === 'group') { node.children.forEach(refreshReadonly); return; }
57
+ const original = preserved.get(node.id);
58
+ node.readonly = original !== undefined && JSON.stringify(original) === JSON.stringify(snapshotNode(node)) && cannotEdit(node);
59
+ }
54
60
  $effect(() => {
55
- if (filters && filters.length > 0) {
56
- const parsedRules: FilterRuleItem[] = [];
57
- let nextLogical: 'and' | 'or' = 'and';
58
-
59
- for (const [i, item] of filters.entries()) {
60
- if ('field' in item) {
61
- parsedRules.push({
62
- id: `rule-${i}-${Date.now()}`,
63
- field: item.field,
64
- operator: item.operator,
65
- value: item.value,
66
- });
67
- } else if (item.operator === 'and' || item.operator === 'or') {
68
- nextLogical = item.operator;
69
- for (const [j, sub] of item.value.entries()) {
70
- if ('field' in sub) {
71
- parsedRules.push({
72
- id: `rule-${i}-${j}-${Date.now()}`,
73
- field: sub.field,
74
- operator: sub.operator,
75
- value: sub.value,
76
- });
77
- }
78
- }
79
- }
61
+ const metadata = availableFields;
62
+ if (filters !== lastInputFilters) {
63
+ lastInputFilters = filters;
64
+ const parsed = readFilterTree(filters);
65
+ if (parsed.ok) {
66
+ const saved = new Map<string, Filter>();
67
+ untrack(() => remember(parsed.root, saved));
68
+ root = parsed.root;
69
+ preserved = saved;
70
+ loadIssues = [];
71
+ if (filters.length) logicalOperator = parsed.root.operator;
72
+ } else {
73
+ preserved = new Map();
74
+ loadIssues = parsed.issues;
75
+ root = { kind: 'group', id: 'root', operator: 'and', children: [], wrapped: false };
80
76
  }
81
- rules = parsedRules;
82
- logicalOperator = nextLogical;
77
+ attempted = false;
83
78
  } else {
84
- rules = [];
79
+ untrack(() => { void metadata; refreshReadonly(root); });
85
80
  }
86
81
  });
87
-
88
- const availableFields = $derived(
89
- fields.filter((f) => f.filterable !== false)
90
- );
91
-
92
- export function addRule() {
93
- const firstField = availableFields[0]?.key ?? 'id';
94
- const firstFieldDef = availableFields.find((f) => f.key === firstField);
95
- const defaultOp: CrudOperator = firstFieldDef?.type === 'number' ? 'eq' : 'contains';
96
-
97
- rules = [
98
- ...rules,
99
- {
100
- id: `rule-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
101
- field: firstField,
102
- operator: defaultOp,
103
- value: '',
104
- },
105
- ];
82
+ function depthOf(target: FilterGroupNode, node: FilterGroupNode = root, depth = root.wrapped || logicalOperator === 'or' ? 1 : 0): number {
83
+ if (node === target) return depth;
84
+ for (const child of node.children) if (child.kind === 'group') {
85
+ const found = depthOf(target, child, depth + 1);
86
+ if (found >= 0) return found;
87
+ }
88
+ return -1;
106
89
  }
107
90
 
108
- export function removeRule(index: number) {
109
- rules = rules.filter((_, i) => i !== index);
91
+ function message(code: FilterIssueCode): string {
92
+ const labels: Record<FilterIssueCode, [string, string]> = {
93
+ 'invalid-filter': ['条件结构不受支持,原条件未被修改', 'Unsupported filter structure; the original query is unchanged'],
94
+ limit: ['条件数量或深度超过限制', 'Filter count or depth exceeds the limit'],
95
+ 'unknown-field': ['字段不可用于筛选', 'Field is unavailable for filtering'], operator: ['操作符不适用于此字段', 'Operator is not supported for this field'],
96
+ value: ['请输入有效的类型化值;集合与范围使用 JSON 数组', 'Enter a valid typed value; sets and ranges use JSON arrays'],
97
+ 'empty-group': ['条件组不能为空', 'A filter group cannot be empty'],
98
+ };
99
+ return labels[code][chinese ? 0 : 1];
110
100
  }
111
-
112
- export function reset() {
113
- rules = [];
101
+ function appendRule(group: FilterGroupNode): void {
102
+ if (disabled || loadIssues.length || full || depthOf(group) < 0 || depthOf(group) >= FILTER_EDITOR_LIMITS.depth) return;
103
+ const first = availableFields[0];
104
+ if (!first) return;
105
+ group.children = [...group.children, { kind: 'rule', id: `draft-${++nextId}`, field: first.key,
106
+ operator: filterOperators(first).includes('contains') ? 'contains' : 'eq', value: undefined }];
107
+ }
108
+ function appendGroup(group: FilterGroupNode): void {
109
+ if (disabled || loadIssues.length || full || depthOf(group) < 0 || depthOf(group) >= FILTER_EDITOR_LIMITS.depth) return;
110
+ group.children = [...group.children, { kind: 'group', id: `draft-${++nextId}`, operator: 'and', children: [] }];
111
+ }
112
+ function removeNode(group: FilterGroupNode, id: string): void {
113
+ if (disabled || loadIssues.length) return;
114
+ group.children = group.children.filter((node) => node.id !== id);
115
+ }
116
+ function changeField(rule: FilterRuleNode, key: string): void {
117
+ if (disabled || rule.readonly) return;
118
+ rule.field = key;
119
+ rule.operator = filterOperators(fields.find((field) => field.key === key)).includes('contains') ? 'contains' : 'eq';
120
+ rule.value = undefined;
121
+ }
122
+ function changeOperator(rule: FilterRuleNode, next: CrudOperator): void {
123
+ if (disabled || rule.readonly) return;
124
+ if (isNullOperator(next)) rule.value = null;
125
+ else if (isNullOperator(rule.operator) || isCollectionOperator(rule.operator) !== isCollectionOperator(next)) rule.value = undefined;
126
+ rule.operator = next;
127
+ }
128
+ function changeValue(rule: FilterRuleNode, raw: string): void {
129
+ if (disabled || rule.readonly) return;
130
+ try { rule.value = parseFilterInput(raw, fields.find((field) => field.key === rule.field), rule.operator); }
131
+ catch { rule.value = raw; }
132
+ }
133
+ function valueText(value: unknown): string { return Array.isArray(value) ? JSON.stringify(value) : value === undefined || value === null ? '' : String(value); }
134
+ export function addRule(): void { appendRule(root); }
135
+ export function removeRule(index: number): void { const node = root.children[index]; if (node) removeNode(root, node.id); }
136
+ export function reset(): void {
137
+ if (disabled) return;
138
+ root = { kind: 'group', id: 'root', operator: 'and', children: [], wrapped: false };
139
+ loadIssues = [];
140
+ preserved = new Map();
141
+ logicalOperator = 'and';
114
142
  filters = [];
143
+ attempted = false;
115
144
  onReset?.();
116
145
  }
117
-
118
- export function apply() {
119
- const validFieldFilters: FieldFilter[] = rules
120
- .filter((r) => r.field && (r.operator === 'null' || r.operator === 'nnull' || (r.value !== '' && r.value !== undefined)))
121
- .map((r) => ({
122
- field: r.field,
123
- operator: r.operator,
124
- value: r.operator === 'null' || r.operator === 'nnull' ? null : r.value,
125
- }));
126
-
127
- let compiledFilters: Filter[] = [];
128
- if (validFieldFilters.length === 1 || logicalOperator === 'and') {
129
- compiledFilters = validFieldFilters;
130
- } else if (validFieldFilters.length > 1 && logicalOperator === 'or') {
131
- const logical: LogicalFilter = {
132
- operator: 'or',
133
- value: validFieldFilters,
134
- };
135
- compiledFilters = [logical];
136
- }
137
-
138
- filters = compiledFilters;
139
- onApply?.(compiledFilters);
146
+ export function apply(): void {
147
+ if (disabled) return;
148
+ attempted = true;
149
+ if (loadIssues.length) { onInvalid?.(loadIssues); return; }
150
+ if (!compilation.ok) { onInvalid?.(compilation.issues); return; }
151
+ // 失败时绝不输出“剩余的有效条件”,避免改变查询的逻辑含义。
152
+ filters = compilation.filters;
153
+ lastInputFilters = filters;
154
+ onApply?.(compilation.filters);
140
155
  }
141
156
  </script>
142
157
 
143
- <div class={cn('svadmin-u-6da6a3c3f741 svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 svadmin-u-8e63407b5ceb svadmin-u-cef5b893cf23 svadmin-u-6ed543e2fbbb', className)} data-testid="filter-builder">
144
- <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-77a2a20e90d4 svadmin-u-65fdbade2025 svadmin-u-05faf5c801ff svadmin-u-7fcf9124b5df">
145
- <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
146
- <FilterIcon class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-bfa603190748" />
147
- <span class="svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">
148
- {i18n.t('common.filterBuilder', undefined) ?? '高级筛选'}
149
- </span>
150
- {#if rules.length > 1}
151
- <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-b00f43c30c2b svadmin-u-de8350a3bbad svadmin-u-359090c2d529">
152
- <button
153
- type="button"
154
- class={cn(
155
- 'svadmin-u-07389a777c1f svadmin-u-d5eab218aa34 svadmin-u-465609a240a8 svadmin-u-359090c2d529 svadmin-u-ceb69a6b0e5f',
156
- logicalOperator === 'and' ? 'svadmin-u-e6f9e383a762 svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359 svadmin-u-cef5b893cf23' : 'svadmin-u-bfa603190748 svadmin-u-ea7b2e9e070e'
157
- )}
158
- onclick={() => (logicalOperator = 'and')}
159
- >
160
- AND (且)
161
- </button>
162
- <button
163
- type="button"
164
- class={cn(
165
- 'svadmin-u-07389a777c1f svadmin-u-d5eab218aa34 svadmin-u-465609a240a8 svadmin-u-359090c2d529 svadmin-u-ceb69a6b0e5f',
166
- logicalOperator === 'or' ? 'svadmin-u-e6f9e383a762 svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359 svadmin-u-cef5b893cf23' : 'svadmin-u-bfa603190748 svadmin-u-ea7b2e9e070e'
167
- )}
168
- onclick={() => (logicalOperator = 'or')}
169
- >
170
- OR (或)
171
- </button>
172
- </div>
173
- {/if}
174
- </div>
175
-
176
- <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-58284b4ea568">
177
- <Button
178
- variant="ghost"
179
- size="sm"
180
- class="svadmin-u-ed8a5df7b2fb svadmin-u-44ee8ba0a421 svadmin-u-359090c2d529"
181
- data-testid="filter-builder-reset"
182
- onclick={reset}
183
- >
184
- <RotateCcw class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
185
- {i18n.t('common.reset', undefined) ?? '重置'}
186
- </Button>
187
- <Button
188
- variant="default"
189
- size="sm"
190
- class="svadmin-u-ed8a5df7b2fb svadmin-u-44ee8ba0a421 svadmin-u-359090c2d529"
191
- data-testid="filter-builder-apply"
192
- onclick={apply}
193
- >
194
- <Check class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
195
- {i18n.t('common.confirm', undefined) ?? '应用'}
158
+ {#snippet groupEditor(group: FilterGroupNode, depth: number, top: boolean)}
159
+ <fieldset class="filter-group" data-testid={top ? 'filter-builder-root' : 'filter-builder-group'} data-filter-group={group.id} disabled={disabled}>
160
+ <legend>{top ? (chinese ? '筛选条件' : 'Filters') : (chinese ? '条件组' : 'Filter group')}</legend>
161
+ <div class="group-actions">
162
+ <label for={`${uid}-${group.id}-logic`}>{chinese ? '逻辑' : 'Logic'}</label>
163
+ <Select id={`${uid}-${group.id}-logic`} aria-label={chinese ? '组合逻辑' : 'Group logic'}
164
+ value={top ? logicalOperator : group.operator}
165
+ onchange={(event: Event) => {
166
+ const target = event.currentTarget;
167
+ if (disabled || !(target instanceof HTMLSelectElement)) return;
168
+ const next = target.value === 'or' ? 'or' : 'and';
169
+ if (top) logicalOperator = next; else group.operator = next;
170
+ }}>
171
+ <option value="and">AND</option><option value="or">OR</option>
172
+ </Select>
173
+ <Button type="button" size="sm" variant="outline" disabled={full || depth >= FILTER_EDITOR_LIMITS.depth || !availableFields.length}
174
+ data-testid={top ? 'filter-builder-add-rule' : 'filter-group-add-rule'} onclick={() => appendRule(group)}>
175
+ <Plus size={14} aria-hidden="true" />{i18n.t('common.addRule')}
196
176
  </Button>
177
+ <Button type="button" size="sm" variant="ghost" disabled={full || depth >= FILTER_EDITOR_LIMITS.depth - 1}
178
+ data-testid="filter-builder-add-group" onclick={() => appendGroup(group)}>{chinese ? '添加条件组' : 'Add group'}</Button>
197
179
  </div>
198
- </div>
199
-
200
- <div class="svadmin-u-6f7e013d6499">
201
- {#each rules as rule, index (rule.id)}
202
- {@const fieldDef = availableFields.find((f) => f.key === rule.field)}
203
- <div class="svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-3fd0f778c8d9 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 svadmin-u-421ac2be5045 svadmin-u-967d113a1451 svadmin-u-7660b450905a svadmin-u-ca6bcd4b6f3f svadmin-u-6ee2d41e2d2d">
204
- <!-- Field select -->
205
- <div class="svadmin-u-df403bbae8fc svadmin-u-012fbd121f37">
206
- <Select
207
- class="svadmin-u-ed8a5df7b2fb svadmin-u-359090c2d529"
208
- value={rule.field}
209
- onchange={(e: Event) => {
210
- rule.field = (e.currentTarget as HTMLSelectElement).value;
211
- }}
212
- >
213
- {#each availableFields as f (f.key)}
214
- <option value={f.key}>{f.label || f.key}</option>
215
- {/each}
216
- </Select>
180
+ {#each group.children as node (node.id)}
181
+ {#if node.kind === 'group'}
182
+ <div class="nested-group">
183
+ {@render groupEditor(node, depth + 1, false)}
184
+ <Button type="button" size="sm" variant="ghost" aria-label={chinese ? '删除条件组' : 'Remove group'} onclick={() => removeNode(group, node.id)}><Trash2 size={14} aria-hidden="true" /></Button>
217
185
  </div>
218
-
219
- <!-- Operator select -->
220
- <div class="svadmin-u-df403bbae8fc svadmin-u-012fbd121f37">
221
- <Select
222
- class="svadmin-u-ed8a5df7b2fb svadmin-u-359090c2d529"
223
- value={rule.operator}
224
- onchange={(e: Event) => {
225
- rule.operator = (e.currentTarget as HTMLSelectElement).value as CrudOperator;
226
- }}
227
- >
228
- {#each operatorOptions as op (op.value)}
229
- <option value={op.value}>{op.label}</option>
230
- {/each}
231
- </Select>
232
- </div>
233
-
234
- <!-- Value input -->
235
- <div class="svadmin-u-36e579c0b41c svadmin-u-a9ef791a0777">
236
- {#if rule.operator === 'null' || rule.operator === 'nnull'}
237
- <div class="svadmin-u-ed8a5df7b2fb svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-0e17f2bd9074 svadmin-u-359090c2d529 svadmin-u-bfa603190748 italic svadmin-u-2859c861d7de svadmin-u-421ac2be5045">
238
- 无需填值
239
- </div>
240
- {:else if fieldDef?.type === 'select' && fieldDef.options}
241
- <Select
242
- class="svadmin-u-ed8a5df7b2fb svadmin-u-359090c2d529"
243
- value={String(rule.value ?? '')}
244
- onchange={(e: Event) => {
245
- rule.value = (e.currentTarget as HTMLSelectElement).value;
246
- }}
247
- >
248
- <option value="">{i18n.t('field.selectPlaceholder', undefined) ?? '请选择'}</option>
249
- {#each fieldDef.options as opt (opt.value)}
250
- <option value={String(opt.value)}>{opt.label}</option>
251
- {/each}
186
+ {:else}
187
+ {@const field = availableFields.find((item) => item.key === node.field)}
188
+ {@const allowed = filterOperators(field)}
189
+ {@const collection = isCollectionOperator(node.operator)}
190
+ {@const invalid = issues.some((issue) => issue.path === node.id)}
191
+ <div class="filter-rule" data-testid="filter-builder-rule" data-filter-rule={node.id}>
192
+ {#if node.readonly}
193
+ <output aria-label={i18n.t('filter.readonly')}>{node.field} {node.operator} {JSON.stringify(node.value)}</output>
194
+ {:else}
195
+ <div>
196
+ <label for={`${uid}-${node.id}-field`}>{chinese ? '字段' : 'Field'}</label>
197
+ <Select id={`${uid}-${node.id}-field`} value={node.field} onchange={(event: Event) => { if (event.currentTarget instanceof HTMLSelectElement) changeField(node, event.currentTarget.value); }}>
198
+ {#if !field}<option value={node.field} disabled>{node.field}</option>{/if}
199
+ {#each availableFields as option (option.key)}<option value={option.key}>{option.label || option.key}</option>{/each}
252
200
  </Select>
253
- {:else if fieldDef?.type === 'boolean'}
254
- <Select
255
- class="svadmin-u-ed8a5df7b2fb svadmin-u-359090c2d529"
256
- value={String(rule.value ?? '')}
257
- onchange={(e: Event) => {
258
- const val = (e.currentTarget as HTMLSelectElement).value;
259
- rule.value = val === 'true' ? true : val === 'false' ? false : '';
260
- }}
261
- >
262
- <option value="">{i18n.t('field.selectPlaceholder', undefined) ?? '请选择'}</option>
263
- <option value="true">{i18n.t('common.yes', undefined) ?? '是 (true)'}</option>
264
- <option value="false">{i18n.t('common.no', undefined) ?? '否 (false)'}</option>
201
+ </div>
202
+ <div>
203
+ <label for={`${uid}-${node.id}-operator`}>{chinese ? '操作符' : 'Operator'}</label>
204
+ <Select id={`${uid}-${node.id}-operator`} value={node.operator} onchange={(event: Event) => { if (event.currentTarget instanceof HTMLSelectElement) changeOperator(node, event.currentTarget.value as CrudOperator); }}>
205
+ {#if !allowed.includes(node.operator)}<option value={node.operator} disabled>{node.operator}</option>{/if}
206
+ {#each allowed as operator (operator)}<option value={operator}>{operator}</option>{/each}
265
207
  </Select>
266
- {:else if fieldDef?.type === 'number'}
267
- <Input
268
- type="number"
269
- class="svadmin-u-ed8a5df7b2fb svadmin-u-359090c2d529"
270
- placeholder="输入筛选值..."
271
- value={numericInputValue(rule.value)}
272
- oninput={(e) => {
273
- const input = e.currentTarget;
274
- if (!(input instanceof HTMLInputElement)) return;
275
- rule.value = input.value === '' ? null : numericInputValue(input.valueAsNumber);
276
- }}
277
- />
278
- {:else}
279
- <Input
280
- type="text"
281
- class="svadmin-u-ed8a5df7b2fb svadmin-u-359090c2d529"
282
- placeholder="输入筛选值..."
283
- value={String(rule.value ?? '')}
284
- oninput={(e) => {
285
- const input = e.currentTarget;
286
- if (input instanceof HTMLInputElement) rule.value = input.value;
287
- }}
288
- />
208
+ </div>
209
+ <div>
210
+ <label for={`${uid}-${node.id}-value`}>{collection ? (chinese ? '值(JSON 数组)' : 'Value (JSON array)') : (chinese ? '值' : 'Value')}</label>
211
+ {#if isNullOperator(node.operator)}
212
+ <Input id={`${uid}-${node.id}-value`} value={chinese ? '无需填值' : 'No value required'} disabled />
213
+ {:else if !collection && field?.options?.length}
214
+ <Select id={`${uid}-${node.id}-value`} aria-invalid={invalid}
215
+ value={field.options.findIndex((option) => option.value === node.value) < 0 ? '' : String(field.options.findIndex((option) => option.value === node.value))}
216
+ onchange={(event: Event) => {
217
+ if (disabled || node.readonly || !(event.currentTarget instanceof HTMLSelectElement)) return;
218
+ const raw = event.currentTarget.value;
219
+ if (raw === '') { node.value = undefined; return; }
220
+ if (!/^(0|[1-9][0-9]*)$/u.test(raw)) return;
221
+ const option = field.options?.[Number(raw)];
222
+ if (option && !option.disabled) node.value = option.value;
223
+ }}>
224
+ <option value="">{chinese ? '请选择' : 'Choose'}</option>
225
+ {#each field.options as option, index (index)}<option value={String(index)} disabled={option.disabled}>{option.label}</option>{/each}
226
+ </Select>
227
+ {:else if !collection && field?.type === 'boolean'}
228
+ <Select id={`${uid}-${node.id}-value`} aria-invalid={invalid} value={valueText(node.value)}
229
+ onchange={(event: Event) => { if (event.currentTarget instanceof HTMLSelectElement) changeValue(node, event.currentTarget.value); }}>
230
+ <option value="">{chinese ? '请选择' : 'Choose'}</option><option value="true">true</option><option value="false">false</option>
231
+ </Select>
232
+ {:else if !collection && field?.type === 'number'}
233
+ <Input id={`${uid}-${node.id}-value`} type="number" step="any" aria-invalid={invalid} value={typeof node.value === 'number' ? node.value : undefined}
234
+ oninput={(event) => { if (event.currentTarget instanceof HTMLInputElement) changeValue(node, event.currentTarget.value); }} />
235
+ {:else}
236
+ <Input id={`${uid}-${node.id}-value`} type="text" aria-invalid={invalid} value={valueText(node.value)} placeholder={collection ? '[1, 2]' : ''}
237
+ oninput={(event) => { if (event.currentTarget instanceof HTMLInputElement) changeValue(node, event.currentTarget.value); }} />
238
+ {/if}
239
+ </div>
289
240
  {/if}
241
+ <Button type="button" size="sm" variant="ghost" aria-label={chinese ? '删除条件' : 'Remove rule'} onclick={() => removeNode(group, node.id)}><Trash2 size={14} aria-hidden="true" /></Button>
290
242
  </div>
291
-
292
- <!-- Remove button -->
293
- <Button
294
- type="button"
295
- variant="ghost"
296
- size="icon-sm"
297
- class="svadmin-u-012fbd121f37 svadmin-u-bfa603190748 svadmin-u-51e95020d6f2"
298
- onclick={() => removeRule(index)}
299
- >
300
- <Trash2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
301
- </Button>
302
- </div>
243
+ {/if}
303
244
  {:else}
304
- <div class="svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-a29b7a649c77 svadmin-u-c9ed8c5f79ae svadmin-u-cb11fec3bb46 svadmin-u-ca6bf63030aa svadmin-u-359090c2d529 svadmin-u-bfa603190748">
305
- 暂无筛选条件,点击下方按钮添加规则
306
- </div>
245
+ <p class="empty">{chinese ? '暂无筛选条件,请添加规则。' : 'No filters yet. Add a rule.'}</p>
307
246
  {/each}
308
- </div>
247
+ </fieldset>
248
+ {/snippet}
309
249
 
310
- <Button
311
- type="button"
312
- variant="outline"
313
- size="sm"
314
- data-testid="filter-builder-add-rule"
315
- class="svadmin-u-6da6a3c3f741 svadmin-u-58284b4ea568 svadmin-u-a29b7a649c77 svadmin-u-359090c2d529 svadmin-u-ed8a5df7b2fb"
316
- onclick={addRule}
317
- >
318
- <Plus class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
319
- {i18n.t('common.addRule', undefined) ?? '添加筛选条件'}
320
- </Button>
250
+ <div class={cn('svadmin-filter-builder', className)} data-testid="filter-builder">
251
+ <div class="builder-header">
252
+ <span><FilterIcon size={16} aria-hidden="true" /> {i18n.t('common.filterBuilder')}</span>
253
+ <div class="group-actions">
254
+ <Button type="button" variant="ghost" size="sm" disabled={disabled} data-testid="filter-builder-reset" onclick={reset}><RotateCcw size={14} aria-hidden="true" />{i18n.t('common.reset')}</Button>
255
+ <Button type="button" size="sm" disabled={disabled} data-testid="filter-builder-apply" onclick={apply}><Check size={14} aria-hidden="true" />{i18n.t('common.confirm')}</Button>
256
+ </div>
257
+ </div>
258
+ {#if issues.length}<div role="alert" class="errors" data-testid="filter-builder-errors">{#each issues as issue, index (`${issue.path}-${issue.code}-${index}`)}<p>{issue.path}: {message(issue.code)}</p>{/each}</div>{/if}
259
+ {#if !loadIssues.length}{@render groupEditor(root, root.wrapped || logicalOperator === 'or' ? 1 : 0, true)}{/if}
321
260
  </div>
261
+
262
+ <style>
263
+ .svadmin-filter-builder { color: var(--foreground); background: var(--card, var(--background)); border: 1px solid var(--border); border-radius: var(--radius, .5rem); padding: .75rem; font-size: .875rem; }
264
+ .builder-header, .group-actions, .nested-group { display: flex; align-items: center; gap: .5rem; }
265
+ .builder-header { justify-content: space-between; flex-wrap: wrap; padding-block-end: .75rem; }
266
+ .builder-header > span { display: inline-flex; align-items: center; gap: .5rem; font-weight: 600; }
267
+ .group-actions { flex-wrap: wrap; }
268
+ .filter-group { min-width: 0; width: 100%; border: 1px solid var(--border); border-radius: var(--radius, .5rem); padding: .75rem; margin: .5rem 0 0; }
269
+ legend { padding-inline: .25rem; color: var(--muted-foreground); }
270
+ .filter-rule { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 2fr) auto; align-items: end; gap: .5rem; margin-block-start: .75rem; }
271
+ .filter-rule > div { min-width: 0; }
272
+ .filter-rule > output { grid-column: 1 / -2; overflow-wrap: anywhere; }
273
+ .filter-rule label { display: block; margin-block-end: .25rem; color: var(--muted-foreground); }
274
+ .empty { padding-block: .75rem; color: var(--muted-foreground); }
275
+ .errors { color: var(--destructive); overflow-wrap: anywhere; }
276
+ .nested-group { align-items: flex-start; }
277
+ @media (max-width: 640px) { .filter-rule { grid-template-columns: minmax(0, 1fr); } .nested-group { gap: .25rem; } }
278
+ </style>
@@ -1,4 +1,5 @@
1
1
  import type { CrudOperator, FieldDefinition, Filter } from '@svadmin/core';
2
+ import { type FilterEditorIssue } from './enterprise/filter-tree.js';
2
3
  export interface FilterRuleItem {
3
4
  id: string;
4
5
  field: string;
@@ -9,9 +10,11 @@ interface Props {
9
10
  fields?: FieldDefinition[];
10
11
  filters?: Filter[];
11
12
  logicalOperator?: 'and' | 'or';
13
+ disabled?: boolean;
12
14
  class?: string;
13
15
  onApply?: (filters: Filter[]) => void;
14
16
  onReset?: () => void;
17
+ onInvalid?: (issues: FilterEditorIssue[]) => void;
15
18
  }
16
19
  declare const FilterBuilder: import("svelte").Component<Props, {
17
20
  addRule: () => void;