@multicomma/arkcn 0.1.2 → 0.1.3
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.
- package/package.json +1 -1
- package/registry/index.json +1 -1
- package/registry/items/accordion.json +302 -0
- package/registry/items/activity-feed.json +408 -0
- package/registry/items/alert-dialog.json +676 -0
- package/registry/items/alert.json +113 -0
- package/registry/items/angle-slider.json +378 -0
- package/registry/items/app-shell.json +2654 -1
- package/registry/items/aspect-ratio.json +36 -0
- package/registry/items/attachment.json +299 -1
- package/registry/items/avatar.json +213 -0
- package/registry/items/badge.json +44 -0
- package/registry/items/breadcrumb.json +191 -0
- package/registry/items/bubble.json +146 -0
- package/registry/items/button-group.json +110 -0
- package/registry/items/button.json +52 -0
- package/registry/items/calendar.json +499 -1
- package/registry/items/canvas.json +446 -1
- package/registry/items/card.json +192 -0
- package/registry/items/carousel.json +208 -1
- package/registry/items/cascader.json +797 -1
- package/registry/items/chart.json +1764 -1
- package/registry/items/chat.json +993 -0
- package/registry/items/checkbox.json +503 -0
- package/registry/items/clipboard.json +308 -0
- package/registry/items/collapsible.json +226 -0
- package/registry/items/color-picker.json +1261 -0
- package/registry/items/combobox.json +1123 -1
- package/registry/items/command.json +837 -1
- package/registry/items/comment-thread.json +952 -1
- package/registry/items/context-menu.json +1344 -1
- package/registry/items/data-grid.json +449 -1
- package/registry/items/data-table.json +1310 -1
- package/registry/items/date-input.json +476 -0
- package/registry/items/dialog.json +592 -0
- package/registry/items/direction.json +29 -1
- package/registry/items/drawer.json +797 -0
- package/registry/items/dropdown-menu.json +1311 -1
- package/registry/items/editable.json +503 -0
- package/registry/items/empty.json +166 -0
- package/registry/items/field.json +286 -1
- package/registry/items/file-upload.json +638 -0
- package/registry/items/floating-panel.json +720 -0
- package/registry/items/floating-toolbar.json +43 -0
- package/registry/items/format.json +109 -0
- package/registry/items/gantt.json +773 -1
- package/registry/items/hooks/use-mobile.json +1 -0
- package/registry/items/hotkeys.json +99 -1
- package/registry/items/hover-card.json +357 -0
- package/registry/items/image-cropper.json +431 -0
- package/registry/items/input-group.json +189 -0
- package/registry/items/input-otp.json +181 -0
- package/registry/items/input.json +28 -0
- package/registry/items/item.json +308 -0
- package/registry/items/json-tree-view.json +336 -0
- package/registry/items/kanban.json +624 -1
- package/registry/items/kbd.json +54 -0
- package/registry/items/label.json +28 -0
- package/registry/items/lib/controllable.json +1 -0
- package/registry/items/lib/data-table-adapter.json +1 -0
- package/registry/items/lib/drag-preview.json +1 -0
- package/registry/items/lib/form.json +1 -0
- package/registry/items/lib/history.json +1 -0
- package/registry/items/lib/row-layout.json +1 -0
- package/registry/items/lib/table.json +1 -0
- package/registry/items/lib/time.json +1 -0
- package/registry/items/lib/utils.json +1 -0
- package/registry/items/live-region.json +43 -0
- package/registry/items/marker.json +96 -0
- package/registry/items/marquee.json +300 -0
- package/registry/items/menubar.json +1282 -1
- package/registry/items/message.json +166 -0
- package/registry/items/navigation-menu.json +561 -1
- package/registry/items/node-graph.json +1037 -1
- package/registry/items/number-input.json +519 -0
- package/registry/items/pagination.json +244 -1
- package/registry/items/password-input.json +311 -0
- package/registry/items/popconfirm.json +566 -0
- package/registry/items/popover.json +615 -0
- package/registry/items/progress.json +516 -0
- package/registry/items/qr-code.json +249 -0
- package/registry/items/query-builder.json +1368 -1
- package/registry/items/radio-group.json +362 -0
- package/registry/items/rating-group.json +304 -0
- package/registry/items/resizable.json +242 -1
- package/registry/items/rich-text-editor.json +934 -1
- package/registry/items/scheduler.json +1013 -1
- package/registry/items/scroll-area.json +227 -0
- package/registry/items/segment-group.json +363 -0
- package/registry/items/select.json +956 -0
- package/registry/items/separator.json +45 -1
- package/registry/items/sheet.json +592 -0
- package/registry/items/sidebar.json +785 -1
- package/registry/items/signature-pad.json +330 -0
- package/registry/items/skeleton.json +28 -0
- package/registry/items/slider.json +799 -0
- package/registry/items/sonner.json +135 -0
- package/registry/items/spinner.json +28 -0
- package/registry/items/steps.json +483 -0
- package/registry/items/swap.json +107 -0
- package/registry/items/switch.json +362 -0
- package/registry/items/table.json +210 -0
- package/registry/items/tabs.json +326 -0
- package/registry/items/tags-input.json +640 -0
- package/registry/items/textarea.json +28 -0
- package/registry/items/timer.json +289 -0
- package/registry/items/toc.json +372 -0
- package/registry/items/toggle-group.json +214 -1
- package/registry/items/toggle.json +124 -0
- package/registry/items/tooltip.json +417 -0
- package/registry/items/tour.json +498 -0
- package/registry/items/transfer-list.json +609 -1
- package/registry/items/tree-select.json +742 -1
- package/registry/items/tree-view.json +794 -0
- package/registry/items/virtual-list.json +214 -1
- package/src/components/ui/app-shell.tsx +11 -0
- package/src/components/ui/attachment.tsx +1 -0
- package/src/components/ui/calendar.tsx +1 -0
- package/src/components/ui/canvas.tsx +8 -0
- package/src/components/ui/carousel.tsx +3 -0
- package/src/components/ui/cascader.tsx +1 -0
- package/src/components/ui/chart.tsx +10 -0
- package/src/components/ui/combobox.tsx +3 -0
- package/src/components/ui/command.tsx +1 -0
- package/src/components/ui/comment-thread.tsx +11 -0
- package/src/components/ui/context-menu.tsx +6 -0
- package/src/components/ui/data-grid.tsx +8 -0
- package/src/components/ui/data-table.tsx +6 -0
- package/src/components/ui/direction.tsx +2 -0
- package/src/components/ui/dropdown-menu.tsx +6 -0
- package/src/components/ui/field.tsx +1 -0
- package/src/components/ui/gantt.tsx +15 -0
- package/src/components/ui/hotkeys.tsx +3 -0
- package/src/components/ui/kanban.tsx +3 -0
- package/src/components/ui/menubar.tsx +6 -0
- package/src/components/ui/navigation-menu.tsx +1 -0
- package/src/components/ui/node-graph.tsx +23 -0
- package/src/components/ui/pagination.tsx +1 -0
- package/src/components/ui/query-builder.tsx +7 -0
- package/src/components/ui/resizable.tsx +1 -0
- package/src/components/ui/rich-text-editor.tsx +54 -0
- package/src/components/ui/scheduler.tsx +26 -0
- package/src/components/ui/separator.tsx +1 -0
- package/src/components/ui/sidebar.tsx +5 -0
- package/src/components/ui/toggle-group.tsx +3 -0
- package/src/components/ui/transfer-list.tsx +11 -0
- package/src/components/ui/tree-select.tsx +5 -0
- package/src/components/ui/virtual-list.tsx +7 -0
|
@@ -31,6 +31,1373 @@
|
|
|
31
31
|
"describeQuery",
|
|
32
32
|
"pruneQuery"
|
|
33
33
|
],
|
|
34
|
+
"parts": [
|
|
35
|
+
{
|
|
36
|
+
"name": "QueryBuilder",
|
|
37
|
+
"props": [
|
|
38
|
+
{
|
|
39
|
+
"name": "fields",
|
|
40
|
+
"type": "QueryField[]",
|
|
41
|
+
"required": true,
|
|
42
|
+
"description": "Fields the user can filter on: `{ name, label, type, options, operators, placeholder }`.",
|
|
43
|
+
"from": "arkcn"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "defaultValue",
|
|
47
|
+
"type": "QueryGroup",
|
|
48
|
+
"required": false,
|
|
49
|
+
"description": "The initial value when uncontrolled.",
|
|
50
|
+
"from": "arkcn"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "disabled",
|
|
54
|
+
"type": "boolean",
|
|
55
|
+
"required": false,
|
|
56
|
+
"default": "false",
|
|
57
|
+
"description": "Disables the part and everything inside it.",
|
|
58
|
+
"from": "arkcn"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "maxDepth",
|
|
62
|
+
"type": "number",
|
|
63
|
+
"required": false,
|
|
64
|
+
"default": "3",
|
|
65
|
+
"description": "Nesting limit for \"Add group\"; the trigger hides past it.",
|
|
66
|
+
"from": "arkcn"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "onValueChange",
|
|
70
|
+
"type": "((value: QueryGroup) => void)",
|
|
71
|
+
"required": false,
|
|
72
|
+
"description": "Called with the new value whenever it changes.",
|
|
73
|
+
"from": "arkcn"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "operators",
|
|
77
|
+
"type": "QueryOperator[]",
|
|
78
|
+
"required": false,
|
|
79
|
+
"default": "defaultOperators",
|
|
80
|
+
"description": "Operator definitions; defaults to `defaultOperators`.",
|
|
81
|
+
"from": "arkcn"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "value",
|
|
85
|
+
"type": "QueryGroup",
|
|
86
|
+
"required": false,
|
|
87
|
+
"description": "The controlled value.",
|
|
88
|
+
"from": "arkcn"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "children",
|
|
92
|
+
"type": "ReactNode",
|
|
93
|
+
"required": false,
|
|
94
|
+
"description": "Content rendered inside the part.",
|
|
95
|
+
"from": "@types/react"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "className",
|
|
99
|
+
"type": "string",
|
|
100
|
+
"required": false,
|
|
101
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
102
|
+
"from": "@types/react"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"name": "style",
|
|
106
|
+
"type": "CSSProperties",
|
|
107
|
+
"required": false,
|
|
108
|
+
"description": "Inline styles applied to the rendered element.",
|
|
109
|
+
"from": "@types/react"
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "QueryBuilderGroup",
|
|
115
|
+
"props": [
|
|
116
|
+
{
|
|
117
|
+
"name": "group",
|
|
118
|
+
"type": "QueryGroup",
|
|
119
|
+
"required": true,
|
|
120
|
+
"description": "The group node this part renders.",
|
|
121
|
+
"from": "arkcn"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"name": "children",
|
|
125
|
+
"type": "ReactNode",
|
|
126
|
+
"required": false,
|
|
127
|
+
"description": "Content rendered inside the part.",
|
|
128
|
+
"from": "@types/react"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "className",
|
|
132
|
+
"type": "string",
|
|
133
|
+
"required": false,
|
|
134
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
135
|
+
"from": "@types/react"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"name": "style",
|
|
139
|
+
"type": "CSSProperties",
|
|
140
|
+
"required": false,
|
|
141
|
+
"description": "Inline styles applied to the rendered element.",
|
|
142
|
+
"from": "@types/react"
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "QueryBuilderGroupHeader",
|
|
148
|
+
"props": [
|
|
149
|
+
{
|
|
150
|
+
"name": "children",
|
|
151
|
+
"type": "ReactNode",
|
|
152
|
+
"required": false,
|
|
153
|
+
"description": "Content rendered inside the part.",
|
|
154
|
+
"from": "@types/react"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "className",
|
|
158
|
+
"type": "string",
|
|
159
|
+
"required": false,
|
|
160
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
161
|
+
"from": "@types/react"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"name": "style",
|
|
165
|
+
"type": "CSSProperties",
|
|
166
|
+
"required": false,
|
|
167
|
+
"description": "Inline styles applied to the rendered element.",
|
|
168
|
+
"from": "@types/react"
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "QueryBuilderMatch",
|
|
174
|
+
"props": [
|
|
175
|
+
{
|
|
176
|
+
"name": "after",
|
|
177
|
+
"type": "ReactNode",
|
|
178
|
+
"required": false,
|
|
179
|
+
"default": "\"of the following\"",
|
|
180
|
+
"description": "Text rendered after the match select.",
|
|
181
|
+
"from": "arkcn"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"name": "before",
|
|
185
|
+
"type": "ReactNode",
|
|
186
|
+
"required": false,
|
|
187
|
+
"description": "Text before the select. Defaults to \"Match\" at the top level and \"Where\" in nested groups.",
|
|
188
|
+
"from": "arkcn"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "asChild",
|
|
192
|
+
"type": "boolean",
|
|
193
|
+
"required": false,
|
|
194
|
+
"description": "Use the provided child element as the default rendered element, combining their props and behavior.",
|
|
195
|
+
"from": "@ark-ui/react"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"name": "autoComplete",
|
|
199
|
+
"type": "string",
|
|
200
|
+
"required": false,
|
|
201
|
+
"description": "The autocomplete attribute for the hidden select. Enables browser autofill (e.g. \"address-level1\" for state).",
|
|
202
|
+
"from": "@zag-js/select"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"name": "children",
|
|
206
|
+
"type": "ReactNode",
|
|
207
|
+
"required": false,
|
|
208
|
+
"description": "Content rendered inside the part.",
|
|
209
|
+
"from": "@types/react"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"name": "className",
|
|
213
|
+
"type": "string",
|
|
214
|
+
"required": false,
|
|
215
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
216
|
+
"from": "@types/react"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"name": "closeOnSelect",
|
|
220
|
+
"type": "boolean",
|
|
221
|
+
"required": false,
|
|
222
|
+
"default": "true",
|
|
223
|
+
"description": "Whether the select should close after an item is selected",
|
|
224
|
+
"from": "@zag-js/select"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"name": "composite",
|
|
228
|
+
"type": "boolean",
|
|
229
|
+
"required": false,
|
|
230
|
+
"default": "true",
|
|
231
|
+
"description": "Whether the select is a composed with other composite widgets like tabs or combobox",
|
|
232
|
+
"from": "@zag-js/select"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"name": "defaultHighlightedValue",
|
|
236
|
+
"type": "string | null",
|
|
237
|
+
"required": false,
|
|
238
|
+
"description": "The initial value of the highlighted item when opened.\nUse when you don't need to control the highlighted value of the select.",
|
|
239
|
+
"from": "@zag-js/select"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"name": "defaultOpen",
|
|
243
|
+
"type": "boolean",
|
|
244
|
+
"required": false,
|
|
245
|
+
"description": "Whether the select's open state is controlled by the user",
|
|
246
|
+
"from": "@zag-js/select"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"name": "defaultValue",
|
|
250
|
+
"type": "string[]",
|
|
251
|
+
"required": false,
|
|
252
|
+
"description": "The initial default value of the select when rendered.\nUse when you don't need to control the value of the select.",
|
|
253
|
+
"from": "@zag-js/select"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"name": "deselectable",
|
|
257
|
+
"type": "boolean",
|
|
258
|
+
"required": false,
|
|
259
|
+
"description": "Whether the value can be cleared by clicking the selected item.\n\n**Note:** this is only applicable for single selection",
|
|
260
|
+
"from": "@zag-js/select"
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"name": "disabled",
|
|
264
|
+
"type": "boolean",
|
|
265
|
+
"required": false,
|
|
266
|
+
"description": "Whether the select is disabled",
|
|
267
|
+
"from": "@zag-js/select"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"name": "form",
|
|
271
|
+
"type": "string",
|
|
272
|
+
"required": false,
|
|
273
|
+
"description": "The associate form of the underlying select.",
|
|
274
|
+
"from": "@zag-js/select"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"name": "hideMode",
|
|
278
|
+
"type": "HideMode",
|
|
279
|
+
"required": false,
|
|
280
|
+
"default": "'display-none'",
|
|
281
|
+
"description": "How to hide content when mounted but not present.\n- `'display-none'`: HTML `hidden` attribute. Effects stay alive.\n- `'activity'`: React 19 `<Activity mode=\"hidden\">`. Effects pause. Requires React 19+.",
|
|
282
|
+
"from": "@ark-ui/react"
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"name": "highlightedValue",
|
|
286
|
+
"type": "string | null",
|
|
287
|
+
"required": false,
|
|
288
|
+
"description": "The controlled key of the highlighted item",
|
|
289
|
+
"from": "@zag-js/select"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"name": "id",
|
|
293
|
+
"type": "string",
|
|
294
|
+
"required": false,
|
|
295
|
+
"description": "The unique identifier of the machine.",
|
|
296
|
+
"from": "@zag-js/types"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"name": "ids",
|
|
300
|
+
"type": "ElementIds",
|
|
301
|
+
"required": false,
|
|
302
|
+
"description": "The ids of the elements in the select. Useful for composition.",
|
|
303
|
+
"from": "@zag-js/select"
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"name": "immediate",
|
|
307
|
+
"type": "boolean",
|
|
308
|
+
"required": false,
|
|
309
|
+
"description": "Whether to synchronize the present change immediately or defer it to the next frame",
|
|
310
|
+
"from": "@zag-js/presence"
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"name": "invalid",
|
|
314
|
+
"type": "boolean",
|
|
315
|
+
"required": false,
|
|
316
|
+
"description": "Whether the select is invalid",
|
|
317
|
+
"from": "@zag-js/select"
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"name": "lazyMount",
|
|
321
|
+
"type": "boolean",
|
|
322
|
+
"required": false,
|
|
323
|
+
"default": "false",
|
|
324
|
+
"description": "Whether to enable lazy mounting",
|
|
325
|
+
"from": "@ark-ui/react"
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"name": "loopFocus",
|
|
329
|
+
"type": "boolean",
|
|
330
|
+
"required": false,
|
|
331
|
+
"default": "false",
|
|
332
|
+
"description": "Whether to loop the keyboard navigation through the options",
|
|
333
|
+
"from": "@zag-js/select"
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"name": "multiple",
|
|
337
|
+
"type": "boolean",
|
|
338
|
+
"required": false,
|
|
339
|
+
"description": "Whether to allow multiple selection",
|
|
340
|
+
"from": "@zag-js/select"
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"name": "name",
|
|
344
|
+
"type": "string",
|
|
345
|
+
"required": false,
|
|
346
|
+
"description": "The `name` attribute of the underlying select.",
|
|
347
|
+
"from": "@zag-js/select"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"name": "onEnterComplete",
|
|
351
|
+
"type": "VoidFunction",
|
|
352
|
+
"required": false,
|
|
353
|
+
"description": "Function called when the animation ends in the open state",
|
|
354
|
+
"from": "@zag-js/presence"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"name": "onExitComplete",
|
|
358
|
+
"type": "VoidFunction",
|
|
359
|
+
"required": false,
|
|
360
|
+
"description": "Function called when the animation ends in the closed state",
|
|
361
|
+
"from": "@zag-js/presence"
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
"name": "onFocusOutside",
|
|
365
|
+
"type": "((event: FocusOutsideEvent) => void)",
|
|
366
|
+
"required": false,
|
|
367
|
+
"description": "Function called when the focus is moved outside the component",
|
|
368
|
+
"from": "@zag-js/interact-outside"
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
"name": "onHighlightChange",
|
|
372
|
+
"type": "((details: HighlightChangeDetails<unknown>) => void)",
|
|
373
|
+
"required": false,
|
|
374
|
+
"description": "The callback fired when the highlighted item changes.",
|
|
375
|
+
"from": "@zag-js/select"
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"name": "onInteractOutside",
|
|
379
|
+
"type": "((event: InteractOutsideEvent) => void)",
|
|
380
|
+
"required": false,
|
|
381
|
+
"description": "Function called when an interaction happens outside the component",
|
|
382
|
+
"from": "@zag-js/interact-outside"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"name": "onOpenChange",
|
|
386
|
+
"type": "((details: OpenChangeDetails) => void)",
|
|
387
|
+
"required": false,
|
|
388
|
+
"description": "Function called when the popup is opened",
|
|
389
|
+
"from": "@zag-js/select"
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
"name": "onPointerDownOutside",
|
|
393
|
+
"type": "((event: PointerDownOutsideEvent) => void)",
|
|
394
|
+
"required": false,
|
|
395
|
+
"description": "Function called when the pointer is pressed down outside the component",
|
|
396
|
+
"from": "@zag-js/interact-outside"
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
"name": "onSelect",
|
|
400
|
+
"type": "((details: SelectionDetails) => void)",
|
|
401
|
+
"required": false,
|
|
402
|
+
"description": "Function called when an item is selected",
|
|
403
|
+
"from": "@zag-js/select"
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"name": "open",
|
|
407
|
+
"type": "boolean",
|
|
408
|
+
"required": false,
|
|
409
|
+
"description": "Whether the select menu is open",
|
|
410
|
+
"from": "@zag-js/select"
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"name": "positioning",
|
|
414
|
+
"type": "PositioningOptions",
|
|
415
|
+
"required": false,
|
|
416
|
+
"description": "The positioning options of the menu.",
|
|
417
|
+
"from": "@zag-js/select"
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
"name": "present",
|
|
421
|
+
"type": "boolean",
|
|
422
|
+
"required": false,
|
|
423
|
+
"description": "Whether the node is present (controlled by the user)",
|
|
424
|
+
"from": "@zag-js/presence"
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
"name": "readOnly",
|
|
428
|
+
"type": "boolean",
|
|
429
|
+
"required": false,
|
|
430
|
+
"description": "Whether the select is read-only",
|
|
431
|
+
"from": "@zag-js/select"
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
"name": "required",
|
|
435
|
+
"type": "boolean",
|
|
436
|
+
"required": false,
|
|
437
|
+
"description": "Whether the select is required",
|
|
438
|
+
"from": "@zag-js/select"
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"name": "scrollToIndexFn",
|
|
442
|
+
"type": "((details: ScrollToIndexDetails) => void)",
|
|
443
|
+
"required": false,
|
|
444
|
+
"description": "Function to scroll to a specific index",
|
|
445
|
+
"from": "@zag-js/select"
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
"name": "skipAnimationOnMount",
|
|
449
|
+
"type": "boolean",
|
|
450
|
+
"required": false,
|
|
451
|
+
"default": "false",
|
|
452
|
+
"description": "Whether to allow the initial presence animation.",
|
|
453
|
+
"from": "@ark-ui/react"
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
"name": "style",
|
|
457
|
+
"type": "CSSProperties",
|
|
458
|
+
"required": false,
|
|
459
|
+
"description": "Inline styles applied to the rendered element.",
|
|
460
|
+
"from": "@types/react"
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
"name": "translations",
|
|
464
|
+
"type": "IntlTranslations",
|
|
465
|
+
"required": false,
|
|
466
|
+
"description": "Specifies the localized strings that identifies the accessibility elements and their states",
|
|
467
|
+
"from": "@zag-js/select"
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
"name": "unmountOnExit",
|
|
471
|
+
"type": "boolean",
|
|
472
|
+
"required": false,
|
|
473
|
+
"default": "false",
|
|
474
|
+
"description": "Whether to unmount on exit.",
|
|
475
|
+
"from": "@ark-ui/react"
|
|
476
|
+
}
|
|
477
|
+
]
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
"name": "QueryBuilderGroupBody",
|
|
481
|
+
"props": [
|
|
482
|
+
{
|
|
483
|
+
"name": "children",
|
|
484
|
+
"type": "ReactNode",
|
|
485
|
+
"required": false,
|
|
486
|
+
"description": "Content rendered inside the part.",
|
|
487
|
+
"from": "@types/react"
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
"name": "className",
|
|
491
|
+
"type": "string",
|
|
492
|
+
"required": false,
|
|
493
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
494
|
+
"from": "@types/react"
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
"name": "style",
|
|
498
|
+
"type": "CSSProperties",
|
|
499
|
+
"required": false,
|
|
500
|
+
"description": "Inline styles applied to the rendered element.",
|
|
501
|
+
"from": "@types/react"
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
"name": "QueryBuilderGroupFooter",
|
|
507
|
+
"props": [
|
|
508
|
+
{
|
|
509
|
+
"name": "children",
|
|
510
|
+
"type": "ReactNode",
|
|
511
|
+
"required": false,
|
|
512
|
+
"description": "Content rendered inside the part.",
|
|
513
|
+
"from": "@types/react"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
"name": "className",
|
|
517
|
+
"type": "string",
|
|
518
|
+
"required": false,
|
|
519
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
520
|
+
"from": "@types/react"
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
"name": "style",
|
|
524
|
+
"type": "CSSProperties",
|
|
525
|
+
"required": false,
|
|
526
|
+
"description": "Inline styles applied to the rendered element.",
|
|
527
|
+
"from": "@types/react"
|
|
528
|
+
}
|
|
529
|
+
]
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
"name": "QueryBuilderRule",
|
|
533
|
+
"props": [
|
|
534
|
+
{
|
|
535
|
+
"name": "rule",
|
|
536
|
+
"type": "QueryRule",
|
|
537
|
+
"required": true,
|
|
538
|
+
"description": "The rule node this part renders.",
|
|
539
|
+
"from": "arkcn"
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
"name": "children",
|
|
543
|
+
"type": "ReactNode",
|
|
544
|
+
"required": false,
|
|
545
|
+
"description": "Content rendered inside the part.",
|
|
546
|
+
"from": "@types/react"
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
"name": "className",
|
|
550
|
+
"type": "string",
|
|
551
|
+
"required": false,
|
|
552
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
553
|
+
"from": "@types/react"
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
"name": "style",
|
|
557
|
+
"type": "CSSProperties",
|
|
558
|
+
"required": false,
|
|
559
|
+
"description": "Inline styles applied to the rendered element.",
|
|
560
|
+
"from": "@types/react"
|
|
561
|
+
}
|
|
562
|
+
]
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
"name": "QueryBuilderFieldSelect",
|
|
566
|
+
"props": [
|
|
567
|
+
{
|
|
568
|
+
"name": "asChild",
|
|
569
|
+
"type": "boolean",
|
|
570
|
+
"required": false,
|
|
571
|
+
"description": "Use the provided child element as the default rendered element, combining their props and behavior.",
|
|
572
|
+
"from": "@ark-ui/react"
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
"name": "autoComplete",
|
|
576
|
+
"type": "string",
|
|
577
|
+
"required": false,
|
|
578
|
+
"description": "The autocomplete attribute for the hidden select. Enables browser autofill (e.g. \"address-level1\" for state).",
|
|
579
|
+
"from": "@zag-js/select"
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
"name": "children",
|
|
583
|
+
"type": "ReactNode",
|
|
584
|
+
"required": false,
|
|
585
|
+
"description": "Content rendered inside the part.",
|
|
586
|
+
"from": "@types/react"
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
"name": "className",
|
|
590
|
+
"type": "string",
|
|
591
|
+
"required": false,
|
|
592
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
593
|
+
"from": "@types/react"
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
"name": "closeOnSelect",
|
|
597
|
+
"type": "boolean",
|
|
598
|
+
"required": false,
|
|
599
|
+
"default": "true",
|
|
600
|
+
"description": "Whether the select should close after an item is selected",
|
|
601
|
+
"from": "@zag-js/select"
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
"name": "composite",
|
|
605
|
+
"type": "boolean",
|
|
606
|
+
"required": false,
|
|
607
|
+
"default": "true",
|
|
608
|
+
"description": "Whether the select is a composed with other composite widgets like tabs or combobox",
|
|
609
|
+
"from": "@zag-js/select"
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
"name": "defaultHighlightedValue",
|
|
613
|
+
"type": "string | null",
|
|
614
|
+
"required": false,
|
|
615
|
+
"description": "The initial value of the highlighted item when opened.\nUse when you don't need to control the highlighted value of the select.",
|
|
616
|
+
"from": "@zag-js/select"
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
"name": "defaultOpen",
|
|
620
|
+
"type": "boolean",
|
|
621
|
+
"required": false,
|
|
622
|
+
"description": "Whether the select's open state is controlled by the user",
|
|
623
|
+
"from": "@zag-js/select"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
"name": "defaultValue",
|
|
627
|
+
"type": "string[]",
|
|
628
|
+
"required": false,
|
|
629
|
+
"description": "The initial default value of the select when rendered.\nUse when you don't need to control the value of the select.",
|
|
630
|
+
"from": "@zag-js/select"
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
"name": "deselectable",
|
|
634
|
+
"type": "boolean",
|
|
635
|
+
"required": false,
|
|
636
|
+
"description": "Whether the value can be cleared by clicking the selected item.\n\n**Note:** this is only applicable for single selection",
|
|
637
|
+
"from": "@zag-js/select"
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
"name": "disabled",
|
|
641
|
+
"type": "boolean",
|
|
642
|
+
"required": false,
|
|
643
|
+
"description": "Whether the select is disabled",
|
|
644
|
+
"from": "@zag-js/select"
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
"name": "form",
|
|
648
|
+
"type": "string",
|
|
649
|
+
"required": false,
|
|
650
|
+
"description": "The associate form of the underlying select.",
|
|
651
|
+
"from": "@zag-js/select"
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
"name": "hideMode",
|
|
655
|
+
"type": "HideMode",
|
|
656
|
+
"required": false,
|
|
657
|
+
"default": "'display-none'",
|
|
658
|
+
"description": "How to hide content when mounted but not present.\n- `'display-none'`: HTML `hidden` attribute. Effects stay alive.\n- `'activity'`: React 19 `<Activity mode=\"hidden\">`. Effects pause. Requires React 19+.",
|
|
659
|
+
"from": "@ark-ui/react"
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
"name": "highlightedValue",
|
|
663
|
+
"type": "string | null",
|
|
664
|
+
"required": false,
|
|
665
|
+
"description": "The controlled key of the highlighted item",
|
|
666
|
+
"from": "@zag-js/select"
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
"name": "id",
|
|
670
|
+
"type": "string",
|
|
671
|
+
"required": false,
|
|
672
|
+
"description": "The unique identifier of the machine.",
|
|
673
|
+
"from": "@zag-js/types"
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
"name": "ids",
|
|
677
|
+
"type": "ElementIds",
|
|
678
|
+
"required": false,
|
|
679
|
+
"description": "The ids of the elements in the select. Useful for composition.",
|
|
680
|
+
"from": "@zag-js/select"
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
"name": "immediate",
|
|
684
|
+
"type": "boolean",
|
|
685
|
+
"required": false,
|
|
686
|
+
"description": "Whether to synchronize the present change immediately or defer it to the next frame",
|
|
687
|
+
"from": "@zag-js/presence"
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
"name": "invalid",
|
|
691
|
+
"type": "boolean",
|
|
692
|
+
"required": false,
|
|
693
|
+
"description": "Whether the select is invalid",
|
|
694
|
+
"from": "@zag-js/select"
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
"name": "lazyMount",
|
|
698
|
+
"type": "boolean",
|
|
699
|
+
"required": false,
|
|
700
|
+
"default": "false",
|
|
701
|
+
"description": "Whether to enable lazy mounting",
|
|
702
|
+
"from": "@ark-ui/react"
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
"name": "loopFocus",
|
|
706
|
+
"type": "boolean",
|
|
707
|
+
"required": false,
|
|
708
|
+
"default": "false",
|
|
709
|
+
"description": "Whether to loop the keyboard navigation through the options",
|
|
710
|
+
"from": "@zag-js/select"
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
"name": "multiple",
|
|
714
|
+
"type": "boolean",
|
|
715
|
+
"required": false,
|
|
716
|
+
"description": "Whether to allow multiple selection",
|
|
717
|
+
"from": "@zag-js/select"
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
"name": "name",
|
|
721
|
+
"type": "string",
|
|
722
|
+
"required": false,
|
|
723
|
+
"description": "The `name` attribute of the underlying select.",
|
|
724
|
+
"from": "@zag-js/select"
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
"name": "onEnterComplete",
|
|
728
|
+
"type": "VoidFunction",
|
|
729
|
+
"required": false,
|
|
730
|
+
"description": "Function called when the animation ends in the open state",
|
|
731
|
+
"from": "@zag-js/presence"
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
"name": "onExitComplete",
|
|
735
|
+
"type": "VoidFunction",
|
|
736
|
+
"required": false,
|
|
737
|
+
"description": "Function called when the animation ends in the closed state",
|
|
738
|
+
"from": "@zag-js/presence"
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
"name": "onFocusOutside",
|
|
742
|
+
"type": "((event: FocusOutsideEvent) => void)",
|
|
743
|
+
"required": false,
|
|
744
|
+
"description": "Function called when the focus is moved outside the component",
|
|
745
|
+
"from": "@zag-js/interact-outside"
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
"name": "onHighlightChange",
|
|
749
|
+
"type": "((details: HighlightChangeDetails<unknown>) => void)",
|
|
750
|
+
"required": false,
|
|
751
|
+
"description": "The callback fired when the highlighted item changes.",
|
|
752
|
+
"from": "@zag-js/select"
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
"name": "onInteractOutside",
|
|
756
|
+
"type": "((event: InteractOutsideEvent) => void)",
|
|
757
|
+
"required": false,
|
|
758
|
+
"description": "Function called when an interaction happens outside the component",
|
|
759
|
+
"from": "@zag-js/interact-outside"
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
"name": "onOpenChange",
|
|
763
|
+
"type": "((details: OpenChangeDetails) => void)",
|
|
764
|
+
"required": false,
|
|
765
|
+
"description": "Function called when the popup is opened",
|
|
766
|
+
"from": "@zag-js/select"
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
"name": "onPointerDownOutside",
|
|
770
|
+
"type": "((event: PointerDownOutsideEvent) => void)",
|
|
771
|
+
"required": false,
|
|
772
|
+
"description": "Function called when the pointer is pressed down outside the component",
|
|
773
|
+
"from": "@zag-js/interact-outside"
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
"name": "onSelect",
|
|
777
|
+
"type": "((details: SelectionDetails) => void)",
|
|
778
|
+
"required": false,
|
|
779
|
+
"description": "Function called when an item is selected",
|
|
780
|
+
"from": "@zag-js/select"
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
"name": "open",
|
|
784
|
+
"type": "boolean",
|
|
785
|
+
"required": false,
|
|
786
|
+
"description": "Whether the select menu is open",
|
|
787
|
+
"from": "@zag-js/select"
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
"name": "positioning",
|
|
791
|
+
"type": "PositioningOptions",
|
|
792
|
+
"required": false,
|
|
793
|
+
"description": "The positioning options of the menu.",
|
|
794
|
+
"from": "@zag-js/select"
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
"name": "present",
|
|
798
|
+
"type": "boolean",
|
|
799
|
+
"required": false,
|
|
800
|
+
"description": "Whether the node is present (controlled by the user)",
|
|
801
|
+
"from": "@zag-js/presence"
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
"name": "readOnly",
|
|
805
|
+
"type": "boolean",
|
|
806
|
+
"required": false,
|
|
807
|
+
"description": "Whether the select is read-only",
|
|
808
|
+
"from": "@zag-js/select"
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
"name": "required",
|
|
812
|
+
"type": "boolean",
|
|
813
|
+
"required": false,
|
|
814
|
+
"description": "Whether the select is required",
|
|
815
|
+
"from": "@zag-js/select"
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
"name": "scrollToIndexFn",
|
|
819
|
+
"type": "((details: ScrollToIndexDetails) => void)",
|
|
820
|
+
"required": false,
|
|
821
|
+
"description": "Function to scroll to a specific index",
|
|
822
|
+
"from": "@zag-js/select"
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
"name": "skipAnimationOnMount",
|
|
826
|
+
"type": "boolean",
|
|
827
|
+
"required": false,
|
|
828
|
+
"default": "false",
|
|
829
|
+
"description": "Whether to allow the initial presence animation.",
|
|
830
|
+
"from": "@ark-ui/react"
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
"name": "style",
|
|
834
|
+
"type": "CSSProperties",
|
|
835
|
+
"required": false,
|
|
836
|
+
"description": "Inline styles applied to the rendered element.",
|
|
837
|
+
"from": "@types/react"
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
"name": "translations",
|
|
841
|
+
"type": "IntlTranslations",
|
|
842
|
+
"required": false,
|
|
843
|
+
"description": "Specifies the localized strings that identifies the accessibility elements and their states",
|
|
844
|
+
"from": "@zag-js/select"
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
"name": "unmountOnExit",
|
|
848
|
+
"type": "boolean",
|
|
849
|
+
"required": false,
|
|
850
|
+
"default": "false",
|
|
851
|
+
"description": "Whether to unmount on exit.",
|
|
852
|
+
"from": "@ark-ui/react"
|
|
853
|
+
}
|
|
854
|
+
]
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
"name": "QueryBuilderOperatorSelect",
|
|
858
|
+
"props": [
|
|
859
|
+
{
|
|
860
|
+
"name": "asChild",
|
|
861
|
+
"type": "boolean",
|
|
862
|
+
"required": false,
|
|
863
|
+
"description": "Use the provided child element as the default rendered element, combining their props and behavior.",
|
|
864
|
+
"from": "@ark-ui/react"
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
"name": "autoComplete",
|
|
868
|
+
"type": "string",
|
|
869
|
+
"required": false,
|
|
870
|
+
"description": "The autocomplete attribute for the hidden select. Enables browser autofill (e.g. \"address-level1\" for state).",
|
|
871
|
+
"from": "@zag-js/select"
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
"name": "children",
|
|
875
|
+
"type": "ReactNode",
|
|
876
|
+
"required": false,
|
|
877
|
+
"description": "Content rendered inside the part.",
|
|
878
|
+
"from": "@types/react"
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
"name": "className",
|
|
882
|
+
"type": "string",
|
|
883
|
+
"required": false,
|
|
884
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
885
|
+
"from": "@types/react"
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
"name": "closeOnSelect",
|
|
889
|
+
"type": "boolean",
|
|
890
|
+
"required": false,
|
|
891
|
+
"default": "true",
|
|
892
|
+
"description": "Whether the select should close after an item is selected",
|
|
893
|
+
"from": "@zag-js/select"
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
"name": "composite",
|
|
897
|
+
"type": "boolean",
|
|
898
|
+
"required": false,
|
|
899
|
+
"default": "true",
|
|
900
|
+
"description": "Whether the select is a composed with other composite widgets like tabs or combobox",
|
|
901
|
+
"from": "@zag-js/select"
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
"name": "defaultHighlightedValue",
|
|
905
|
+
"type": "string | null",
|
|
906
|
+
"required": false,
|
|
907
|
+
"description": "The initial value of the highlighted item when opened.\nUse when you don't need to control the highlighted value of the select.",
|
|
908
|
+
"from": "@zag-js/select"
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
"name": "defaultOpen",
|
|
912
|
+
"type": "boolean",
|
|
913
|
+
"required": false,
|
|
914
|
+
"description": "Whether the select's open state is controlled by the user",
|
|
915
|
+
"from": "@zag-js/select"
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
"name": "defaultValue",
|
|
919
|
+
"type": "string[]",
|
|
920
|
+
"required": false,
|
|
921
|
+
"description": "The initial default value of the select when rendered.\nUse when you don't need to control the value of the select.",
|
|
922
|
+
"from": "@zag-js/select"
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
"name": "deselectable",
|
|
926
|
+
"type": "boolean",
|
|
927
|
+
"required": false,
|
|
928
|
+
"description": "Whether the value can be cleared by clicking the selected item.\n\n**Note:** this is only applicable for single selection",
|
|
929
|
+
"from": "@zag-js/select"
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
"name": "disabled",
|
|
933
|
+
"type": "boolean",
|
|
934
|
+
"required": false,
|
|
935
|
+
"description": "Whether the select is disabled",
|
|
936
|
+
"from": "@zag-js/select"
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
"name": "form",
|
|
940
|
+
"type": "string",
|
|
941
|
+
"required": false,
|
|
942
|
+
"description": "The associate form of the underlying select.",
|
|
943
|
+
"from": "@zag-js/select"
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
"name": "hideMode",
|
|
947
|
+
"type": "HideMode",
|
|
948
|
+
"required": false,
|
|
949
|
+
"default": "'display-none'",
|
|
950
|
+
"description": "How to hide content when mounted but not present.\n- `'display-none'`: HTML `hidden` attribute. Effects stay alive.\n- `'activity'`: React 19 `<Activity mode=\"hidden\">`. Effects pause. Requires React 19+.",
|
|
951
|
+
"from": "@ark-ui/react"
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
"name": "highlightedValue",
|
|
955
|
+
"type": "string | null",
|
|
956
|
+
"required": false,
|
|
957
|
+
"description": "The controlled key of the highlighted item",
|
|
958
|
+
"from": "@zag-js/select"
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
"name": "id",
|
|
962
|
+
"type": "string",
|
|
963
|
+
"required": false,
|
|
964
|
+
"description": "The unique identifier of the machine.",
|
|
965
|
+
"from": "@zag-js/types"
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
"name": "ids",
|
|
969
|
+
"type": "ElementIds",
|
|
970
|
+
"required": false,
|
|
971
|
+
"description": "The ids of the elements in the select. Useful for composition.",
|
|
972
|
+
"from": "@zag-js/select"
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
"name": "immediate",
|
|
976
|
+
"type": "boolean",
|
|
977
|
+
"required": false,
|
|
978
|
+
"description": "Whether to synchronize the present change immediately or defer it to the next frame",
|
|
979
|
+
"from": "@zag-js/presence"
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
"name": "invalid",
|
|
983
|
+
"type": "boolean",
|
|
984
|
+
"required": false,
|
|
985
|
+
"description": "Whether the select is invalid",
|
|
986
|
+
"from": "@zag-js/select"
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
"name": "lazyMount",
|
|
990
|
+
"type": "boolean",
|
|
991
|
+
"required": false,
|
|
992
|
+
"default": "false",
|
|
993
|
+
"description": "Whether to enable lazy mounting",
|
|
994
|
+
"from": "@ark-ui/react"
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
"name": "loopFocus",
|
|
998
|
+
"type": "boolean",
|
|
999
|
+
"required": false,
|
|
1000
|
+
"default": "false",
|
|
1001
|
+
"description": "Whether to loop the keyboard navigation through the options",
|
|
1002
|
+
"from": "@zag-js/select"
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
"name": "multiple",
|
|
1006
|
+
"type": "boolean",
|
|
1007
|
+
"required": false,
|
|
1008
|
+
"description": "Whether to allow multiple selection",
|
|
1009
|
+
"from": "@zag-js/select"
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
"name": "name",
|
|
1013
|
+
"type": "string",
|
|
1014
|
+
"required": false,
|
|
1015
|
+
"description": "The `name` attribute of the underlying select.",
|
|
1016
|
+
"from": "@zag-js/select"
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
"name": "onEnterComplete",
|
|
1020
|
+
"type": "VoidFunction",
|
|
1021
|
+
"required": false,
|
|
1022
|
+
"description": "Function called when the animation ends in the open state",
|
|
1023
|
+
"from": "@zag-js/presence"
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
"name": "onExitComplete",
|
|
1027
|
+
"type": "VoidFunction",
|
|
1028
|
+
"required": false,
|
|
1029
|
+
"description": "Function called when the animation ends in the closed state",
|
|
1030
|
+
"from": "@zag-js/presence"
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
"name": "onFocusOutside",
|
|
1034
|
+
"type": "((event: FocusOutsideEvent) => void)",
|
|
1035
|
+
"required": false,
|
|
1036
|
+
"description": "Function called when the focus is moved outside the component",
|
|
1037
|
+
"from": "@zag-js/interact-outside"
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
"name": "onHighlightChange",
|
|
1041
|
+
"type": "((details: HighlightChangeDetails<unknown>) => void)",
|
|
1042
|
+
"required": false,
|
|
1043
|
+
"description": "The callback fired when the highlighted item changes.",
|
|
1044
|
+
"from": "@zag-js/select"
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
"name": "onInteractOutside",
|
|
1048
|
+
"type": "((event: InteractOutsideEvent) => void)",
|
|
1049
|
+
"required": false,
|
|
1050
|
+
"description": "Function called when an interaction happens outside the component",
|
|
1051
|
+
"from": "@zag-js/interact-outside"
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
"name": "onOpenChange",
|
|
1055
|
+
"type": "((details: OpenChangeDetails) => void)",
|
|
1056
|
+
"required": false,
|
|
1057
|
+
"description": "Function called when the popup is opened",
|
|
1058
|
+
"from": "@zag-js/select"
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
"name": "onPointerDownOutside",
|
|
1062
|
+
"type": "((event: PointerDownOutsideEvent) => void)",
|
|
1063
|
+
"required": false,
|
|
1064
|
+
"description": "Function called when the pointer is pressed down outside the component",
|
|
1065
|
+
"from": "@zag-js/interact-outside"
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
"name": "onSelect",
|
|
1069
|
+
"type": "((details: SelectionDetails) => void)",
|
|
1070
|
+
"required": false,
|
|
1071
|
+
"description": "Function called when an item is selected",
|
|
1072
|
+
"from": "@zag-js/select"
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
"name": "open",
|
|
1076
|
+
"type": "boolean",
|
|
1077
|
+
"required": false,
|
|
1078
|
+
"description": "Whether the select menu is open",
|
|
1079
|
+
"from": "@zag-js/select"
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
"name": "positioning",
|
|
1083
|
+
"type": "PositioningOptions",
|
|
1084
|
+
"required": false,
|
|
1085
|
+
"description": "The positioning options of the menu.",
|
|
1086
|
+
"from": "@zag-js/select"
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
"name": "present",
|
|
1090
|
+
"type": "boolean",
|
|
1091
|
+
"required": false,
|
|
1092
|
+
"description": "Whether the node is present (controlled by the user)",
|
|
1093
|
+
"from": "@zag-js/presence"
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
"name": "readOnly",
|
|
1097
|
+
"type": "boolean",
|
|
1098
|
+
"required": false,
|
|
1099
|
+
"description": "Whether the select is read-only",
|
|
1100
|
+
"from": "@zag-js/select"
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
"name": "required",
|
|
1104
|
+
"type": "boolean",
|
|
1105
|
+
"required": false,
|
|
1106
|
+
"description": "Whether the select is required",
|
|
1107
|
+
"from": "@zag-js/select"
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
"name": "scrollToIndexFn",
|
|
1111
|
+
"type": "((details: ScrollToIndexDetails) => void)",
|
|
1112
|
+
"required": false,
|
|
1113
|
+
"description": "Function to scroll to a specific index",
|
|
1114
|
+
"from": "@zag-js/select"
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
"name": "skipAnimationOnMount",
|
|
1118
|
+
"type": "boolean",
|
|
1119
|
+
"required": false,
|
|
1120
|
+
"default": "false",
|
|
1121
|
+
"description": "Whether to allow the initial presence animation.",
|
|
1122
|
+
"from": "@ark-ui/react"
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
"name": "style",
|
|
1126
|
+
"type": "CSSProperties",
|
|
1127
|
+
"required": false,
|
|
1128
|
+
"description": "Inline styles applied to the rendered element.",
|
|
1129
|
+
"from": "@types/react"
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
"name": "translations",
|
|
1133
|
+
"type": "IntlTranslations",
|
|
1134
|
+
"required": false,
|
|
1135
|
+
"description": "Specifies the localized strings that identifies the accessibility elements and their states",
|
|
1136
|
+
"from": "@zag-js/select"
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
"name": "unmountOnExit",
|
|
1140
|
+
"type": "boolean",
|
|
1141
|
+
"required": false,
|
|
1142
|
+
"default": "false",
|
|
1143
|
+
"description": "Whether to unmount on exit.",
|
|
1144
|
+
"from": "@ark-ui/react"
|
|
1145
|
+
}
|
|
1146
|
+
]
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
"name": "QueryBuilderValueEditor",
|
|
1150
|
+
"props": [
|
|
1151
|
+
{
|
|
1152
|
+
"name": "children",
|
|
1153
|
+
"type": "((props: ValueEditorRenderProps) => ReactNode)",
|
|
1154
|
+
"required": false,
|
|
1155
|
+
"description": "Content rendered inside the part.",
|
|
1156
|
+
"from": "arkcn"
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
"name": "className",
|
|
1160
|
+
"type": "string",
|
|
1161
|
+
"required": false,
|
|
1162
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
1163
|
+
"from": "@types/react"
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
"name": "style",
|
|
1167
|
+
"type": "CSSProperties",
|
|
1168
|
+
"required": false,
|
|
1169
|
+
"description": "Inline styles applied to the rendered element.",
|
|
1170
|
+
"from": "@types/react"
|
|
1171
|
+
}
|
|
1172
|
+
]
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
"name": "QueryBuilderRuleActions",
|
|
1176
|
+
"props": [
|
|
1177
|
+
{
|
|
1178
|
+
"name": "children",
|
|
1179
|
+
"type": "ReactNode",
|
|
1180
|
+
"required": false,
|
|
1181
|
+
"description": "Content rendered inside the part.",
|
|
1182
|
+
"from": "@types/react"
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
"name": "className",
|
|
1186
|
+
"type": "string",
|
|
1187
|
+
"required": false,
|
|
1188
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
1189
|
+
"from": "@types/react"
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
"name": "style",
|
|
1193
|
+
"type": "CSSProperties",
|
|
1194
|
+
"required": false,
|
|
1195
|
+
"description": "Inline styles applied to the rendered element.",
|
|
1196
|
+
"from": "@types/react"
|
|
1197
|
+
}
|
|
1198
|
+
]
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
"name": "QueryBuilderAddRuleTrigger",
|
|
1202
|
+
"props": [
|
|
1203
|
+
{
|
|
1204
|
+
"name": "asChild",
|
|
1205
|
+
"type": "boolean",
|
|
1206
|
+
"required": false,
|
|
1207
|
+
"description": "Render the child element instead of the default one, merging props and behaviour onto it.",
|
|
1208
|
+
"from": "arkcn"
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
"name": "size",
|
|
1212
|
+
"type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
|
|
1213
|
+
"required": false,
|
|
1214
|
+
"description": "Size variant; sets a `data-size` attribute.",
|
|
1215
|
+
"from": "arkcn"
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
"name": "variant",
|
|
1219
|
+
"type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
|
|
1220
|
+
"required": false,
|
|
1221
|
+
"description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
|
|
1222
|
+
"from": "arkcn"
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
"name": "children",
|
|
1226
|
+
"type": "ReactNode",
|
|
1227
|
+
"required": false,
|
|
1228
|
+
"description": "Content rendered inside the part.",
|
|
1229
|
+
"from": "@types/react"
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
"name": "className",
|
|
1233
|
+
"type": "string",
|
|
1234
|
+
"required": false,
|
|
1235
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
1236
|
+
"from": "@types/react"
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
"name": "style",
|
|
1240
|
+
"type": "CSSProperties",
|
|
1241
|
+
"required": false,
|
|
1242
|
+
"description": "Inline styles applied to the rendered element.",
|
|
1243
|
+
"from": "@types/react"
|
|
1244
|
+
}
|
|
1245
|
+
]
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
"name": "QueryBuilderAddGroupTrigger",
|
|
1249
|
+
"props": [
|
|
1250
|
+
{
|
|
1251
|
+
"name": "asChild",
|
|
1252
|
+
"type": "boolean",
|
|
1253
|
+
"required": false,
|
|
1254
|
+
"description": "Render the child element instead of the default one, merging props and behaviour onto it.",
|
|
1255
|
+
"from": "arkcn"
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
"name": "size",
|
|
1259
|
+
"type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
|
|
1260
|
+
"required": false,
|
|
1261
|
+
"description": "Size variant; sets a `data-size` attribute.",
|
|
1262
|
+
"from": "arkcn"
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
"name": "variant",
|
|
1266
|
+
"type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
|
|
1267
|
+
"required": false,
|
|
1268
|
+
"description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
|
|
1269
|
+
"from": "arkcn"
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
"name": "children",
|
|
1273
|
+
"type": "ReactNode",
|
|
1274
|
+
"required": false,
|
|
1275
|
+
"description": "Content rendered inside the part.",
|
|
1276
|
+
"from": "@types/react"
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
"name": "className",
|
|
1280
|
+
"type": "string",
|
|
1281
|
+
"required": false,
|
|
1282
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
1283
|
+
"from": "@types/react"
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
"name": "style",
|
|
1287
|
+
"type": "CSSProperties",
|
|
1288
|
+
"required": false,
|
|
1289
|
+
"description": "Inline styles applied to the rendered element.",
|
|
1290
|
+
"from": "@types/react"
|
|
1291
|
+
}
|
|
1292
|
+
]
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
"name": "QueryBuilderRemoveTrigger",
|
|
1296
|
+
"props": [
|
|
1297
|
+
{
|
|
1298
|
+
"name": "asChild",
|
|
1299
|
+
"type": "boolean",
|
|
1300
|
+
"required": false,
|
|
1301
|
+
"description": "Render the child element instead of the default one, merging props and behaviour onto it.",
|
|
1302
|
+
"from": "arkcn"
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
"name": "size",
|
|
1306
|
+
"type": "\"default\" | \"xs\" | \"sm\" | \"lg\" | \"icon\" | \"icon-xs\" | \"icon-sm\" | \"icon-lg\" | null",
|
|
1307
|
+
"required": false,
|
|
1308
|
+
"description": "Size variant; sets a `data-size` attribute.",
|
|
1309
|
+
"from": "arkcn"
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
"name": "variant",
|
|
1313
|
+
"type": "\"link\" | \"default\" | \"outline\" | \"secondary\" | \"ghost\" | \"destructive\" | null",
|
|
1314
|
+
"required": false,
|
|
1315
|
+
"description": "Visual variant, styled with `cva`; sets a `data-variant` attribute.",
|
|
1316
|
+
"from": "arkcn"
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
"name": "children",
|
|
1320
|
+
"type": "ReactNode",
|
|
1321
|
+
"required": false,
|
|
1322
|
+
"description": "Content rendered inside the part.",
|
|
1323
|
+
"from": "@types/react"
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
"name": "className",
|
|
1327
|
+
"type": "string",
|
|
1328
|
+
"required": false,
|
|
1329
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
1330
|
+
"from": "@types/react"
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
"name": "style",
|
|
1334
|
+
"type": "CSSProperties",
|
|
1335
|
+
"required": false,
|
|
1336
|
+
"description": "Inline styles applied to the rendered element.",
|
|
1337
|
+
"from": "@types/react"
|
|
1338
|
+
}
|
|
1339
|
+
]
|
|
1340
|
+
},
|
|
1341
|
+
{
|
|
1342
|
+
"name": "QueryBuilderSummary",
|
|
1343
|
+
"props": [
|
|
1344
|
+
{
|
|
1345
|
+
"name": "children",
|
|
1346
|
+
"type": "ReactNode",
|
|
1347
|
+
"required": false,
|
|
1348
|
+
"description": "Content rendered inside the part.",
|
|
1349
|
+
"from": "@types/react"
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
"name": "className",
|
|
1353
|
+
"type": "string",
|
|
1354
|
+
"required": false,
|
|
1355
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
1356
|
+
"from": "@types/react"
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
"name": "style",
|
|
1360
|
+
"type": "CSSProperties",
|
|
1361
|
+
"required": false,
|
|
1362
|
+
"description": "Inline styles applied to the rendered element.",
|
|
1363
|
+
"from": "@types/react"
|
|
1364
|
+
}
|
|
1365
|
+
]
|
|
1366
|
+
},
|
|
1367
|
+
{
|
|
1368
|
+
"name": "QueryBuilderEmpty",
|
|
1369
|
+
"props": [
|
|
1370
|
+
{
|
|
1371
|
+
"name": "asChild",
|
|
1372
|
+
"type": "boolean",
|
|
1373
|
+
"required": false,
|
|
1374
|
+
"description": "Use the provided child element as the default rendered element, combining their props and behavior.",
|
|
1375
|
+
"from": "@ark-ui/react"
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
"name": "children",
|
|
1379
|
+
"type": "ReactNode",
|
|
1380
|
+
"required": false,
|
|
1381
|
+
"description": "Content rendered inside the part.",
|
|
1382
|
+
"from": "@types/react"
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
"name": "className",
|
|
1386
|
+
"type": "string",
|
|
1387
|
+
"required": false,
|
|
1388
|
+
"description": "Extra classes merged into the part's own classes with `cn`.",
|
|
1389
|
+
"from": "@types/react"
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
"name": "style",
|
|
1393
|
+
"type": "CSSProperties",
|
|
1394
|
+
"required": false,
|
|
1395
|
+
"description": "Inline styles applied to the rendered element.",
|
|
1396
|
+
"from": "@types/react"
|
|
1397
|
+
}
|
|
1398
|
+
]
|
|
1399
|
+
}
|
|
1400
|
+
],
|
|
34
1401
|
"dependencies": {
|
|
35
1402
|
"@ark-ui/react": "^5.39.1",
|
|
36
1403
|
"lucide-react": "^1.41.0"
|
|
@@ -45,7 +1412,7 @@
|
|
|
45
1412
|
"files": [
|
|
46
1413
|
{
|
|
47
1414
|
"path": "components/ui/query-builder.tsx",
|
|
48
|
-
"content": "import * as React from \"react\"\nimport { ark } from \"@ark-ui/react\"\nimport { createListCollection } from \"@ark-ui/react/collection\"\nimport { PlusIcon, XIcon } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n Select,\n SelectContent,\n SelectControl,\n SelectItem,\n SelectItemIndicator,\n SelectItemText,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\"\nimport { cn } from \"@/lib/utils\"\nimport { useControllable } from \"@/lib/controllable\"\n\n/**\n * QueryBuilder — a compositional filter builder that reads like a sentence.\n *\n * Every group opens with \"Match all / any of the following\" and each condition\n * reads field → plain-language operator → value. Incomplete conditions (no\n * value yet) are flagged with `data-incomplete` and dropped by `pruneQuery`\n * instead of erroring, negation lives in the operator wording (\"is not\",\n * \"does not contain\") and order is irrelevant, so there is no NOT toggle and\n * no drag-reorder to learn.\n *\n * The consumer owns the query tree (`value`/`onValueChange`) and renders the\n * parts recursively; the root applies all edits (add, remove, update). The\n * primitive never interprets the tree: turning it into SQL, an API filter, or\n * an in-memory predicate is the consumer's job (see `src/demo/query-builder`).\n *\n * Anatomy:\n * QueryBuilder\n * QueryBuilderGroup { group }\n * QueryBuilderGroupHeader\n * QueryBuilderMatch \"all\" | \"any\" select\n * QueryBuilderRemoveTrigger (nested groups)\n * QueryBuilderGroupBody\n * QueryBuilderRule { rule } → FieldSelect + OperatorSelect + ValueEditor + RuleActions\n * QueryBuilderGroup (nested)\n * QueryBuilderGroupFooter\n * QueryBuilderAddRuleTrigger / QueryBuilderAddGroupTrigger\n * QueryBuilderSummary natural-language sentence\n */\n\n/* ---------------------------------------------------------------------------\n * Model\n * ------------------------------------------------------------------------- */\n\ntype QueryFieldType = \"text\" | \"number\" | \"date\" | \"boolean\" | \"select\"\ntype QueryOption = { value: string; label: string }\ntype QueryField = {\n name: string\n label: string\n type?: QueryFieldType\n /** For `select` fields. */\n options?: QueryOption[]\n /** Restrict the operators offered (names from `operators`). */\n operators?: string[]\n /** Placeholder for the value editor. */\n placeholder?: string\n}\n/** How many values an operator takes. */\ntype QueryArity = \"none\" | \"one\" | \"two\" | \"many\"\ntype QueryOperator = { name: string; label: string; arity: QueryArity; types: QueryFieldType[] }\n\ntype QueryRule = { id: string; field: string; operator: string; value?: unknown }\ntype QueryGroup = { id: string; match: \"all\" | \"any\"; rules: QueryNode[] }\ntype QueryNode = QueryRule | QueryGroup\n\nconst isGroup = (node: QueryNode): node is QueryGroup => \"rules\" in node\n\nlet seq = 0\nconst uid = (prefix: string) => `${prefix}-${(seq++).toString(36)}${Date.now().toString(36).slice(-3)}`\n\nconst op = (name: string, label: string, arity: QueryArity, types: QueryFieldType[]): QueryOperator => ({\n name,\n label,\n arity,\n types,\n})\n\n/** Plain-language operators; negation is part of the wording. */\nconst defaultOperators: QueryOperator[] = [\n op(\"is\", \"is\", \"one\", [\"text\", \"number\", \"date\", \"select\"]),\n op(\"isNot\", \"is not\", \"one\", [\"text\", \"number\", \"date\", \"select\"]),\n op(\"contains\", \"contains\", \"one\", [\"text\"]),\n op(\"doesNotContain\", \"does not contain\", \"one\", [\"text\"]),\n op(\"startsWith\", \"starts with\", \"one\", [\"text\"]),\n op(\"endsWith\", \"ends with\", \"one\", [\"text\"]),\n op(\"greaterThan\", \"is greater than\", \"one\", [\"number\"]),\n op(\"lessThan\", \"is less than\", \"one\", [\"number\"]),\n op(\"atLeast\", \"is at least\", \"one\", [\"number\"]),\n op(\"atMost\", \"is at most\", \"one\", [\"number\"]),\n op(\"before\", \"is before\", \"one\", [\"date\"]),\n op(\"after\", \"is after\", \"one\", [\"date\"]),\n op(\"between\", \"is between\", \"two\", [\"number\", \"date\"]),\n op(\"anyOf\", \"is any of\", \"many\", [\"select\", \"text\"]),\n op(\"noneOf\", \"is none of\", \"many\", [\"select\", \"text\"]),\n op(\"isTrue\", \"is true\", \"none\", [\"boolean\"]),\n op(\"isFalse\", \"is false\", \"none\", [\"boolean\"]),\n op(\"isEmpty\", \"is empty\", \"none\", [\"text\", \"number\", \"date\", \"select\"]),\n op(\"isNotEmpty\", \"is not empty\", \"none\", [\"text\", \"number\", \"date\", \"select\"]),\n]\n\nfunction operatorsFor(field: QueryField | undefined, operators: QueryOperator[]) {\n if (!field) return []\n const type = field.type ?? \"text\"\n const allowed = operators.filter((o) => o.types.includes(type))\n return field.operators\n ? (field.operators.map((n) => allowed.find((o) => o.name === n)).filter(Boolean) as QueryOperator[])\n : allowed\n}\n\nfunction createRule(fields: QueryField[], operators = defaultOperators, patch?: Partial<QueryRule>): QueryRule {\n const field = fields.find((f) => f.name === patch?.field) ?? fields[0]\n const operator = patch?.operator ?? operatorsFor(field, operators)[0]?.name ?? \"\"\n return { id: uid(\"rule\"), field: field?.name ?? \"\", operator, value: undefined, ...patch }\n}\n\nfunction createGroup(match: QueryGroup[\"match\"] = \"all\", rules: QueryNode[] = []): QueryGroup {\n return { id: uid(\"group\"), match, rules }\n}\n\nconst hasValue = (value: unknown) =>\n value !== undefined && value !== null && value !== \"\" && !(Array.isArray(value) && value.length === 0)\n\n/** A rule counts once it has everything its operator needs. */\nfunction isComplete(rule: QueryRule, operators = defaultOperators) {\n const operator = operators.find((o) => o.name === rule.operator)\n if (!operator || !rule.field) return false\n if (operator.arity === \"none\") return true\n if (operator.arity === \"two\") return Array.isArray(rule.value) && hasValue(rule.value[0]) && hasValue(rule.value[1])\n return hasValue(rule.value)\n}\n\n/* Tree edits (pure) */\n\nfunction mapTree(node: QueryGroup, fn: (node: QueryNode) => QueryNode | null): QueryGroup {\n const rules = node.rules\n .map((child) => {\n const next = fn(child)\n if (next === null) return null\n return isGroup(next) ? mapTree(next, fn) : next\n })\n .filter((n): n is QueryNode => n !== null)\n return { ...node, rules }\n}\n\nfunction updateNode(root: QueryGroup, id: string, patch: Partial<QueryRule> | Partial<QueryGroup>): QueryGroup {\n if (root.id === id) return { ...root, ...(patch as Partial<QueryGroup>) }\n return mapTree(root, (n) => (n.id === id ? ({ ...n, ...patch } as QueryNode) : n))\n}\n\nfunction removeNode(root: QueryGroup, id: string): QueryGroup {\n return mapTree(root, (n) => (n.id === id ? null : n))\n}\n\nfunction insertNode(root: QueryGroup, groupId: string, node: QueryNode): QueryGroup {\n if (root.id === groupId) return { ...root, rules: [...root.rules, node] }\n return mapTree(root, (n) => (isGroup(n) && n.id === groupId ? { ...n, rules: [...n.rules, node] } : n))\n}\n\n/* ---------------------------------------------------------------------------\n * Describing the tree (presentation only; interpreting it is the consumer's job)\n * ------------------------------------------------------------------------- */\n\ntype DescribeOptions = { fields: QueryField[]; operators?: QueryOperator[] }\n\nfunction valueLabel(rule: QueryRule, field: QueryField | undefined): string {\n const label = (v: unknown) => field?.options?.find((o) => o.value === String(v))?.label ?? String(v)\n if (Array.isArray(rule.value)) {\n const values = rule.value.map(label)\n if (rule.operator === \"between\") return `${values[0]} and ${values[1]}`\n if (values.length <= 3) return values.join(\", \")\n return `${values.slice(0, 2).join(\", \")} and ${values.length - 2} more`\n }\n return label(rule.value)\n}\n\n/** Natural-language sentence, e.g. \"Status is Active and Created is after 2026-01-01\". */\nfunction describeQuery(\n group: QueryGroup,\n { fields, operators = defaultOperators }: DescribeOptions,\n depth = 0\n): string {\n const parts = group.rules\n .map((n) => {\n if (isGroup(n)) return describeQuery(n, { fields, operators }, depth + 1)\n if (!isComplete(n, operators)) return \"\"\n const field = fields.find((f) => f.name === n.field)\n const operator = operators.find((o) => o.name === n.operator)\n const value = operator?.arity === \"none\" ? \"\" : ` ${valueLabel(n, field)}`\n return `${field?.label ?? n.field} ${operator?.label ?? n.operator}${value}`\n })\n .filter(Boolean)\n if (!parts.length) return \"\"\n const joined = parts.join(group.match === \"all\" ? \" and \" : \" or \")\n return depth > 0 && parts.length > 1 ? `(${joined})` : joined\n}\n\n/** The tree without incomplete rules and empty groups: what an interpreter should consume. */\nfunction pruneQuery(group: QueryGroup, operators = defaultOperators): QueryGroup {\n const rules = group.rules\n .map((n) => (isGroup(n) ? pruneQuery(n, operators) : isComplete(n, operators) ? n : null))\n .filter((n): n is QueryNode => n !== null && (!isGroup(n) || n.rules.length > 0))\n return { ...group, rules }\n}\n\n/* ---------------------------------------------------------------------------\n * Context\n * ------------------------------------------------------------------------- */\n\ntype QueryBuilderContextValue = {\n fields: QueryField[]\n operators: QueryOperator[]\n value: QueryGroup\n disabled: boolean\n maxDepth: number\n addRule: (groupId: string, patch?: Partial<QueryRule>) => void\n addGroup: (groupId: string) => void\n remove: (id: string) => void\n updateRule: (id: string, patch: Partial<QueryRule>) => void\n setMatch: (groupId: string, match: QueryGroup[\"match\"]) => void\n}\n\nconst QueryBuilderContext = React.createContext<QueryBuilderContextValue | null>(null)\nconst GroupContext = React.createContext<{ group: QueryGroup; depth: number } | null>(null)\nconst RuleContext = React.createContext<{ rule: QueryRule } | null>(null)\n\nfunction useQueryBuilder() {\n const ctx = React.useContext(QueryBuilderContext)\n if (!ctx) throw new Error(\"QueryBuilder parts must be used within <QueryBuilder>\")\n return ctx\n}\nfunction useQueryBuilderGroup() {\n const ctx = React.useContext(GroupContext)\n if (!ctx) throw new Error(\"Group parts must be used within <QueryBuilderGroup>\")\n return ctx\n}\nfunction useQueryBuilderRule() {\n const ctx = React.useContext(RuleContext)\n if (!ctx) throw new Error(\"Rule parts must be used within <QueryBuilderRule>\")\n return ctx\n}\n\n/* ---------------------------------------------------------------------------\n * Root\n * ------------------------------------------------------------------------- */\n\nfunction QueryBuilder({\n fields,\n operators = defaultOperators,\n value: valueProp,\n defaultValue,\n onValueChange,\n disabled = false,\n maxDepth = 3,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & {\n fields: QueryField[]\n operators?: QueryOperator[]\n value?: QueryGroup\n defaultValue?: QueryGroup\n onValueChange?: (value: QueryGroup) => void\n disabled?: boolean\n /** Nesting limit for \"Add group\"; the trigger hides past it. */\n maxDepth?: number\n}) {\n const [initial] = React.useState<QueryGroup>(\n () => defaultValue ?? createGroup(\"all\", [createRule(fields, operators)])\n )\n const [value, setValue] = useControllable<QueryGroup>(valueProp, initial, onValueChange)\n const update = React.useCallback((fn: (prev: QueryGroup) => QueryGroup) => setValue(fn), [setValue])\n\n const ctx = React.useMemo<QueryBuilderContextValue>(\n () => ({\n fields,\n operators,\n value,\n disabled,\n maxDepth,\n addRule: (groupId, patch) => update((prev) => insertNode(prev, groupId, createRule(fields, operators, patch))),\n addGroup: (groupId) =>\n update((prev) => {\n // A nested group starts with the opposite match so it adds meaning (\"…and any of these\").\n const match = groupIn(prev, groupId)?.match === \"any\" ? \"all\" : \"any\"\n return insertNode(prev, groupId, createGroup(match, [createRule(fields, operators)]))\n }),\n remove: (id) => update((prev) => removeNode(prev, id)),\n updateRule: (id, patch) =>\n update((prev) => {\n if (patch.field !== undefined) {\n // Changing the field resets the operator to the first valid one and clears the value.\n const field = fields.find((f) => f.name === patch.field)\n const current = operators.find((o) => o.name === patch.operator)\n const options = operatorsFor(field, operators)\n const operator = current && options.includes(current) ? current.name : (options[0]?.name ?? \"\")\n return updateNode(prev, id, { field: patch.field, operator, value: undefined })\n }\n if (patch.operator !== undefined && patch.value === undefined) {\n const prevRule = findRule(prev, id)\n const before = operators.find((o) => o.name === prevRule?.operator)?.arity\n const after = operators.find((o) => o.name === patch.operator)?.arity\n return updateNode(prev, id, {\n operator: patch.operator,\n value: before === after ? prevRule?.value : undefined,\n })\n }\n return updateNode(prev, id, patch)\n }),\n setMatch: (groupId, match) => update((prev) => updateNode(prev, groupId, { match })),\n }),\n [fields, operators, value, disabled, maxDepth, update]\n )\n\n return (\n <QueryBuilderContext.Provider value={ctx}>\n <div\n data-slot=\"query-builder\"\n data-disabled={disabled ? \"\" : undefined}\n className={cn(\"flex flex-col gap-3 text-sm\", className)}\n {...props}\n >\n {children}\n </div>\n </QueryBuilderContext.Provider>\n )\n}\n\nfunction groupIn(root: QueryGroup, id: string): QueryGroup | null {\n if (root.id === id) return root\n for (const child of root.rules) {\n if (isGroup(child)) {\n const found = groupIn(child, id)\n if (found) return found\n }\n }\n return null\n}\n\nfunction findRule(root: QueryGroup, id: string): QueryRule | null {\n for (const child of root.rules) {\n if (isGroup(child)) {\n const found = findRule(child, id)\n if (found) return found\n } else if (child.id === id) return child\n }\n return null\n}\n\n/* ---------------------------------------------------------------------------\n * Group parts\n * ------------------------------------------------------------------------- */\n\nfunction QueryBuilderGroup({\n group,\n className,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & { group: QueryGroup }) {\n const parent = React.useContext(GroupContext)\n const depth = parent ? parent.depth + 1 : 0\n const ctx = React.useMemo(() => ({ group, depth }), [group, depth])\n return (\n <GroupContext.Provider value={ctx}>\n <div\n data-slot=\"query-builder-group\"\n data-match={group.match}\n data-depth={depth}\n data-empty={group.rules.length === 0 ? \"\" : undefined}\n className={cn(\n \"group/query-group flex flex-col gap-2\",\n depth > 0 && \"rounded-lg border border-dashed bg-muted/30 p-3\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n </GroupContext.Provider>\n )\n}\n\nfunction QueryBuilderGroupHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"query-builder-group-header\"\n className={cn(\"flex flex-wrap items-center gap-2\", className)}\n {...props}\n />\n )\n}\n\nconst matchOptions: QueryOption[] = [\n { value: \"all\", label: \"all\" },\n { value: \"any\", label: \"any\" },\n]\nconst matchCollection = createListCollection({ items: matchOptions, itemToValue: (o) => o.value })\n\n/** \"Match [all ▾] of the following\" — the only combinator control, once per group. */\nfunction QueryBuilderMatch({\n className,\n before,\n after = \"of the following\",\n ...props\n}: Omit<React.ComponentProps<typeof Select>, \"collection\" | \"value\" | \"onValueChange\"> & {\n /** Text before the select. Defaults to \"Match\" at the top level and \"Where\" in nested groups. */\n before?: React.ReactNode\n after?: React.ReactNode\n}) {\n const { setMatch, disabled } = useQueryBuilder()\n const { group, depth } = useQueryBuilderGroup()\n return (\n <span data-slot=\"query-builder-match\" className=\"inline-flex flex-wrap items-center gap-1.5\">\n <span className=\"text-muted-foreground\">{before ?? (depth === 0 ? \"Match\" : \"Where\")}</span>\n <Select\n collection={matchCollection}\n value={[group.match]}\n onValueChange={({ value }) => value[0] && setMatch(group.id, value[0] as QueryGroup[\"match\"])}\n disabled={disabled}\n positioning={{ sameWidth: false }}\n {...props}\n >\n <SelectControl>\n <SelectTrigger size=\"sm\" className={cn(\"font-medium\", className)} aria-label=\"Match all or any\">\n <SelectValue>{group.match}</SelectValue>\n </SelectTrigger>\n </SelectControl>\n <SelectContent className=\"min-w-24\">\n {matchOptions.map((o) => (\n <SelectItem key={o.value} item={o}>\n <SelectItemText>{o.label}</SelectItemText>\n <SelectItemIndicator />\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <span className=\"text-muted-foreground\">{after}</span>\n </span>\n )\n}\n\n/** Indented list of conditions with a guide line. */\nfunction QueryBuilderGroupBody({ className, ...props }: React.ComponentProps<\"div\">) {\n const { group } = useQueryBuilderGroup()\n return (\n <div\n data-slot=\"query-builder-group-body\"\n data-empty={group.rules.length === 0 ? \"\" : undefined}\n className={cn(\n \"relative flex flex-col gap-2 ps-4 before:absolute before:inset-y-1 before:inset-s-1 before:w-px before:bg-border\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction QueryBuilderGroupFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"query-builder-group-footer\"\n className={cn(\"flex flex-wrap items-center gap-1 ps-4\", className)}\n {...props}\n />\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Rule parts\n * ------------------------------------------------------------------------- */\n\nfunction QueryBuilderRule({ rule, className, children, ...props }: React.ComponentProps<\"div\"> & { rule: QueryRule }) {\n const { operators } = useQueryBuilder()\n const complete = isComplete(rule, operators)\n const ctx = React.useMemo(() => ({ rule }), [rule])\n return (\n <RuleContext.Provider value={ctx}>\n <div\n data-slot=\"query-builder-rule\"\n data-incomplete={complete ? undefined : \"\"}\n className={cn(\n \"group/query-rule -ms-1 flex flex-wrap items-center gap-1.5 rounded-lg py-0.5 ps-1 pe-1 hover:bg-muted/40 has-focus-visible:bg-muted/40\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n </RuleContext.Provider>\n )\n}\n\ntype PickerProps = Omit<React.ComponentProps<typeof Select>, \"collection\" | \"value\" | \"onValueChange\">\n\nfunction QueryBuilderFieldSelect({ className, ...props }: PickerProps & { className?: string }) {\n const { fields, updateRule, disabled } = useQueryBuilder()\n const { rule } = useQueryBuilderRule()\n const collection = React.useMemo(\n () => createListCollection({ items: fields, itemToValue: (f) => f.name, itemToString: (f) => f.label }),\n [fields]\n )\n const field = fields.find((f) => f.name === rule.field)\n return (\n <Select\n collection={collection}\n value={[rule.field]}\n onValueChange={({ value }) => value[0] && updateRule(rule.id, { field: value[0] })}\n disabled={disabled}\n positioning={{ sameWidth: false }}\n {...props}\n >\n <SelectControl>\n <SelectTrigger\n size=\"sm\"\n className={cn(\"font-medium\", className)}\n data-slot=\"query-builder-field-select\"\n aria-label=\"Field\"\n >\n <SelectValue placeholder=\"Field\">{field?.label}</SelectValue>\n </SelectTrigger>\n </SelectControl>\n <SelectContent>\n {fields.map((f) => (\n <SelectItem key={f.name} item={f}>\n <SelectItemText>{f.label}</SelectItemText>\n <SelectItemIndicator />\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\nfunction QueryBuilderOperatorSelect({ className, ...props }: PickerProps & { className?: string }) {\n const { fields, operators, updateRule, disabled } = useQueryBuilder()\n const { rule } = useQueryBuilderRule()\n const field = fields.find((f) => f.name === rule.field)\n const options = React.useMemo(() => operatorsFor(field, operators), [field, operators])\n const collection = React.useMemo(\n () => createListCollection({ items: options, itemToValue: (o) => o.name, itemToString: (o) => o.label }),\n [options]\n )\n const current = options.find((o) => o.name === rule.operator)\n return (\n <Select\n collection={collection}\n value={[rule.operator]}\n onValueChange={({ value }) => value[0] && updateRule(rule.id, { operator: value[0] })}\n disabled={disabled}\n positioning={{ sameWidth: false }}\n {...props}\n >\n <SelectControl>\n <SelectTrigger\n size=\"sm\"\n className={cn(\"text-muted-foreground\", className)}\n data-slot=\"query-builder-operator-select\"\n aria-label=\"Operator\"\n >\n <SelectValue placeholder=\"Operator\">{current?.label}</SelectValue>\n </SelectTrigger>\n </SelectControl>\n <SelectContent>\n {options.map((o) => (\n <SelectItem key={o.name} item={o}>\n <SelectItemText>{o.label}</SelectItemText>\n <SelectItemIndicator />\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\ntype ValueEditorRenderProps = {\n rule: QueryRule\n field: QueryField | undefined\n operator: QueryOperator | undefined\n value: unknown\n setValue: (value: unknown) => void\n disabled: boolean\n}\n\n/**\n * Renders an editor for the rule's value based on the field type and operator\n * arity. Pass a render-prop child to take over for specific cases and fall back\n * to the default by returning `undefined`.\n */\nfunction QueryBuilderValueEditor({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & {\n children?: (props: ValueEditorRenderProps) => React.ReactNode | undefined\n}) {\n const { fields, operators, updateRule, disabled } = useQueryBuilder()\n const { rule } = useQueryBuilderRule()\n const field = fields.find((f) => f.name === rule.field)\n const operator = operators.find((o) => o.name === rule.operator)\n const setValue = React.useCallback((value: unknown) => updateRule(rule.id, { value }), [updateRule, rule.id])\n const custom = children?.({ rule, field, operator, value: rule.value, setValue, disabled })\n if (custom !== undefined) return <>{custom}</>\n if (!operator || operator.arity === \"none\") return null\n const type = field?.type ?? \"text\"\n const inputType = type === \"number\" ? \"number\" : type === \"date\" ? \"date\" : \"text\"\n const inputClass = cn(\"h-7 w-40 text-sm md:text-sm\", type === \"number\" && \"w-24\", type === \"date\" && \"w-36\")\n\n if (operator.arity === \"two\") {\n const pair = Array.isArray(rule.value) ? (rule.value as unknown[]) : [undefined, undefined]\n const set = (index: 0 | 1, v: string) => {\n const next = [...pair]\n next[index] = v\n setValue(next)\n }\n return (\n <div\n data-slot=\"query-builder-value-editor\"\n className={cn(\"inline-flex items-center gap-1.5\", className)}\n {...props}\n >\n <Input\n type={inputType}\n value={String(pair[0] ?? \"\")}\n onChange={(e) => set(0, e.target.value)}\n disabled={disabled}\n aria-label=\"From\"\n className={inputClass}\n />\n <span className=\"text-muted-foreground\">and</span>\n <Input\n type={inputType}\n value={String(pair[1] ?? \"\")}\n onChange={(e) => set(1, e.target.value)}\n disabled={disabled}\n aria-label=\"To\"\n className={inputClass}\n />\n </div>\n )\n }\n\n if (type === \"select\" && field?.options) {\n const many = operator.arity === \"many\"\n const selected = many ? ((rule.value as string[] | undefined) ?? []) : rule.value ? [String(rule.value)] : []\n return (\n <div data-slot=\"query-builder-value-editor\" className={cn(\"inline-flex\", className)} {...props}>\n <OptionPicker\n options={field.options}\n value={selected}\n multiple={many}\n disabled={disabled}\n placeholder=\"Choose…\"\n onChange={(values) => setValue(many ? values : values[0])}\n />\n </div>\n )\n }\n\n if (operator.arity === \"many\") {\n // Free-text lists: comma separated, shown as-is.\n const list = Array.isArray(rule.value) ? (rule.value as string[]) : []\n return (\n <div data-slot=\"query-builder-value-editor\" className={cn(\"inline-flex\", className)} {...props}>\n <Input\n value={list.join(\", \")}\n onChange={(e) =>\n setValue(\n e.target.value\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean)\n )\n }\n placeholder={field?.placeholder ?? \"Comma separated\"}\n disabled={disabled}\n aria-label=\"Values\"\n className={cn(inputClass, \"w-56\")}\n />\n </div>\n )\n }\n\n return (\n <div data-slot=\"query-builder-value-editor\" className={cn(\"inline-flex\", className)} {...props}>\n <Input\n type={inputType}\n value={String(rule.value ?? \"\")}\n onChange={(e) => setValue(e.target.value)}\n placeholder={field?.placeholder ?? (type === \"text\" ? \"Value\" : undefined)}\n disabled={disabled}\n aria-label=\"Value\"\n className={inputClass}\n />\n </div>\n )\n}\n\nfunction OptionPicker({\n options,\n value,\n multiple,\n disabled,\n placeholder,\n onChange,\n}: {\n options: QueryOption[]\n value: string[]\n multiple: boolean\n disabled: boolean\n placeholder: string\n onChange: (values: string[]) => void\n}) {\n const collection = React.useMemo(\n () => createListCollection({ items: options, itemToValue: (o) => o.value, itemToString: (o) => o.label }),\n [options]\n )\n const labels = value.map((v) => options.find((o) => o.value === v)?.label ?? v)\n return (\n <Select\n collection={collection}\n value={value}\n multiple={multiple}\n closeOnSelect={!multiple}\n onValueChange={({ value }) => onChange(value)}\n disabled={disabled}\n positioning={{ sameWidth: false }}\n >\n <SelectControl>\n <SelectTrigger size=\"sm\" className=\"max-w-64\" aria-label=\"Value\">\n <SelectValue placeholder={placeholder}>\n {labels.length\n ? labels.length > 3\n ? `${labels.slice(0, 3).join(\", \")} +${labels.length - 3}`\n : labels.join(\", \")\n : undefined}\n </SelectValue>\n </SelectTrigger>\n </SelectControl>\n <SelectContent>\n {options.map((o) => (\n <SelectItem key={o.value} item={o}>\n <SelectItemText>{o.label}</SelectItemText>\n <SelectItemIndicator />\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\n/** Hover-revealed slot at the end of a rule row (remove trigger, extras). */\nfunction QueryBuilderRuleActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"query-builder-rule-actions\"\n className={cn(\n \"ms-auto inline-flex items-center gap-0.5 opacity-0 transition-opacity group-focus-within/query-rule:opacity-100 group-hover/query-rule:opacity-100\",\n className\n )}\n {...props}\n />\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Triggers (polymorphic via asChild)\n * ------------------------------------------------------------------------- */\n\nfunction QueryBuilderAddRuleTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { addRule, disabled } = useQueryBuilder()\n const { group } = useQueryBuilderGroup()\n return (\n <Button\n data-slot=\"query-builder-add-rule-trigger\"\n variant=\"ghost\"\n size=\"sm\"\n asChild={asChild}\n disabled={disabled}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) addRule(group.id)\n }}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <PlusIcon /> Add condition\n </>\n ))}\n </Button>\n )\n}\n\nfunction QueryBuilderAddGroupTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { addGroup, disabled, maxDepth } = useQueryBuilder()\n const { group, depth } = useQueryBuilderGroup()\n if (depth >= maxDepth) return null\n return (\n <Button\n data-slot=\"query-builder-add-group-trigger\"\n variant=\"ghost\"\n size=\"sm\"\n asChild={asChild}\n disabled={disabled}\n className=\"text-muted-foreground\"\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) addGroup(group.id)\n }}\n {...props}\n >\n {asChild ? children : (children ?? <>Add a nested group</>)}\n </Button>\n )\n}\n\n/** Removes the enclosing rule, or the enclosing group when used in a group header. */\nfunction QueryBuilderRemoveTrigger({\n asChild,\n children,\n onClick,\n className,\n ...props\n}: React.ComponentProps<typeof Button>) {\n const { remove, disabled } = useQueryBuilder()\n const rule = React.useContext(RuleContext)\n const group = React.useContext(GroupContext)\n const target = rule?.rule.id ?? group?.group.id\n const label = rule ? \"Remove condition\" : \"Remove group\"\n return (\n <Button\n data-slot=\"query-builder-remove-trigger\"\n variant=\"ghost\"\n size=\"icon-xs\"\n asChild={asChild}\n disabled={disabled}\n aria-label={label}\n className={cn(\"text-muted-foreground hover:text-destructive\", className)}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented && target) remove(target)\n }}\n {...props}\n >\n {asChild ? children : (children ?? <XIcon />)}\n </Button>\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Summary and empty state\n * ------------------------------------------------------------------------- */\n\n/** The query as a sentence; `children` overrides the fallback when nothing is set. */\nfunction QueryBuilderSummary({\n className,\n children,\n prefix = \"Showing results where \",\n ...props\n}: React.ComponentProps<\"p\"> & { prefix?: React.ReactNode }) {\n const { value, fields, operators } = useQueryBuilder()\n const text = describeQuery(value, { fields, operators })\n return (\n <p\n data-slot=\"query-builder-summary\"\n data-empty={text ? undefined : \"\"}\n className={cn(\"text-sm\", className)}\n {...props}\n >\n {text ? (\n <>\n <span className=\"text-muted-foreground\">{prefix}</span>\n <span className=\"font-medium\">{text}</span>\n </>\n ) : (\n (children ?? (\n <span className=\"text-muted-foreground\">Showing everything. Add a condition to narrow it down.</span>\n ))\n )}\n </p>\n )\n}\n\nfunction QueryBuilderEmpty({ className, ...props }: React.ComponentProps<typeof ark.div>) {\n return (\n <ark.div\n data-slot=\"query-builder-empty\"\n className={cn(\"rounded-lg border border-dashed px-3 py-2 text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nexport {\n QueryBuilder,\n QueryBuilderGroup,\n QueryBuilderGroupHeader,\n QueryBuilderMatch,\n QueryBuilderGroupBody,\n QueryBuilderGroupFooter,\n QueryBuilderRule,\n QueryBuilderFieldSelect,\n QueryBuilderOperatorSelect,\n QueryBuilderValueEditor,\n QueryBuilderRuleActions,\n QueryBuilderAddRuleTrigger,\n QueryBuilderAddGroupTrigger,\n QueryBuilderRemoveTrigger,\n QueryBuilderSummary,\n QueryBuilderEmpty,\n useQueryBuilder,\n useQueryBuilderGroup,\n useQueryBuilderRule,\n createRule,\n createGroup,\n isGroup,\n isComplete,\n operatorsFor,\n defaultOperators,\n describeQuery,\n pruneQuery,\n type QueryField,\n type QueryFieldType,\n type QueryOption,\n type QueryOperator,\n type QueryRule,\n type QueryGroup,\n type QueryNode,\n}\n"
|
|
1415
|
+
"content": "import * as React from \"react\"\nimport { ark } from \"@ark-ui/react\"\nimport { createListCollection } from \"@ark-ui/react/collection\"\nimport { PlusIcon, XIcon } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n Select,\n SelectContent,\n SelectControl,\n SelectItem,\n SelectItemIndicator,\n SelectItemText,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\"\nimport { cn } from \"@/lib/utils\"\nimport { useControllable } from \"@/lib/controllable\"\n\n/**\n * QueryBuilder — a compositional filter builder that reads like a sentence.\n *\n * Every group opens with \"Match all / any of the following\" and each condition\n * reads field → plain-language operator → value. Incomplete conditions (no\n * value yet) are flagged with `data-incomplete` and dropped by `pruneQuery`\n * instead of erroring, negation lives in the operator wording (\"is not\",\n * \"does not contain\") and order is irrelevant, so there is no NOT toggle and\n * no drag-reorder to learn.\n *\n * The consumer owns the query tree (`value`/`onValueChange`) and renders the\n * parts recursively; the root applies all edits (add, remove, update). The\n * primitive never interprets the tree: turning it into SQL, an API filter, or\n * an in-memory predicate is the consumer's job (see `src/demo/query-builder`).\n *\n * Anatomy:\n * QueryBuilder\n * QueryBuilderGroup { group }\n * QueryBuilderGroupHeader\n * QueryBuilderMatch \"all\" | \"any\" select\n * QueryBuilderRemoveTrigger (nested groups)\n * QueryBuilderGroupBody\n * QueryBuilderRule { rule } → FieldSelect + OperatorSelect + ValueEditor + RuleActions\n * QueryBuilderGroup (nested)\n * QueryBuilderGroupFooter\n * QueryBuilderAddRuleTrigger / QueryBuilderAddGroupTrigger\n * QueryBuilderSummary natural-language sentence\n */\n\n/* ---------------------------------------------------------------------------\n * Model\n * ------------------------------------------------------------------------- */\n\ntype QueryFieldType = \"text\" | \"number\" | \"date\" | \"boolean\" | \"select\"\ntype QueryOption = { value: string; label: string }\ntype QueryField = {\n name: string\n label: string\n type?: QueryFieldType\n /** For `select` fields. */\n options?: QueryOption[]\n /** Restrict the operators offered (names from `operators`). */\n operators?: string[]\n /** Placeholder for the value editor. */\n placeholder?: string\n}\n/** How many values an operator takes. */\ntype QueryArity = \"none\" | \"one\" | \"two\" | \"many\"\ntype QueryOperator = { name: string; label: string; arity: QueryArity; types: QueryFieldType[] }\n\ntype QueryRule = { id: string; field: string; operator: string; value?: unknown }\ntype QueryGroup = { id: string; match: \"all\" | \"any\"; rules: QueryNode[] }\ntype QueryNode = QueryRule | QueryGroup\n\nconst isGroup = (node: QueryNode): node is QueryGroup => \"rules\" in node\n\nlet seq = 0\nconst uid = (prefix: string) => `${prefix}-${(seq++).toString(36)}${Date.now().toString(36).slice(-3)}`\n\nconst op = (name: string, label: string, arity: QueryArity, types: QueryFieldType[]): QueryOperator => ({\n name,\n label,\n arity,\n types,\n})\n\n/** Plain-language operators; negation is part of the wording. */\nconst defaultOperators: QueryOperator[] = [\n op(\"is\", \"is\", \"one\", [\"text\", \"number\", \"date\", \"select\"]),\n op(\"isNot\", \"is not\", \"one\", [\"text\", \"number\", \"date\", \"select\"]),\n op(\"contains\", \"contains\", \"one\", [\"text\"]),\n op(\"doesNotContain\", \"does not contain\", \"one\", [\"text\"]),\n op(\"startsWith\", \"starts with\", \"one\", [\"text\"]),\n op(\"endsWith\", \"ends with\", \"one\", [\"text\"]),\n op(\"greaterThan\", \"is greater than\", \"one\", [\"number\"]),\n op(\"lessThan\", \"is less than\", \"one\", [\"number\"]),\n op(\"atLeast\", \"is at least\", \"one\", [\"number\"]),\n op(\"atMost\", \"is at most\", \"one\", [\"number\"]),\n op(\"before\", \"is before\", \"one\", [\"date\"]),\n op(\"after\", \"is after\", \"one\", [\"date\"]),\n op(\"between\", \"is between\", \"two\", [\"number\", \"date\"]),\n op(\"anyOf\", \"is any of\", \"many\", [\"select\", \"text\"]),\n op(\"noneOf\", \"is none of\", \"many\", [\"select\", \"text\"]),\n op(\"isTrue\", \"is true\", \"none\", [\"boolean\"]),\n op(\"isFalse\", \"is false\", \"none\", [\"boolean\"]),\n op(\"isEmpty\", \"is empty\", \"none\", [\"text\", \"number\", \"date\", \"select\"]),\n op(\"isNotEmpty\", \"is not empty\", \"none\", [\"text\", \"number\", \"date\", \"select\"]),\n]\n\nfunction operatorsFor(field: QueryField | undefined, operators: QueryOperator[]) {\n if (!field) return []\n const type = field.type ?? \"text\"\n const allowed = operators.filter((o) => o.types.includes(type))\n return field.operators\n ? (field.operators.map((n) => allowed.find((o) => o.name === n)).filter(Boolean) as QueryOperator[])\n : allowed\n}\n\nfunction createRule(fields: QueryField[], operators = defaultOperators, patch?: Partial<QueryRule>): QueryRule {\n const field = fields.find((f) => f.name === patch?.field) ?? fields[0]\n const operator = patch?.operator ?? operatorsFor(field, operators)[0]?.name ?? \"\"\n return { id: uid(\"rule\"), field: field?.name ?? \"\", operator, value: undefined, ...patch }\n}\n\nfunction createGroup(match: QueryGroup[\"match\"] = \"all\", rules: QueryNode[] = []): QueryGroup {\n return { id: uid(\"group\"), match, rules }\n}\n\nconst hasValue = (value: unknown) =>\n value !== undefined && value !== null && value !== \"\" && !(Array.isArray(value) && value.length === 0)\n\n/** A rule counts once it has everything its operator needs. */\nfunction isComplete(rule: QueryRule, operators = defaultOperators) {\n const operator = operators.find((o) => o.name === rule.operator)\n if (!operator || !rule.field) return false\n if (operator.arity === \"none\") return true\n if (operator.arity === \"two\") return Array.isArray(rule.value) && hasValue(rule.value[0]) && hasValue(rule.value[1])\n return hasValue(rule.value)\n}\n\n/* Tree edits (pure) */\n\nfunction mapTree(node: QueryGroup, fn: (node: QueryNode) => QueryNode | null): QueryGroup {\n const rules = node.rules\n .map((child) => {\n const next = fn(child)\n if (next === null) return null\n return isGroup(next) ? mapTree(next, fn) : next\n })\n .filter((n): n is QueryNode => n !== null)\n return { ...node, rules }\n}\n\nfunction updateNode(root: QueryGroup, id: string, patch: Partial<QueryRule> | Partial<QueryGroup>): QueryGroup {\n if (root.id === id) return { ...root, ...(patch as Partial<QueryGroup>) }\n return mapTree(root, (n) => (n.id === id ? ({ ...n, ...patch } as QueryNode) : n))\n}\n\nfunction removeNode(root: QueryGroup, id: string): QueryGroup {\n return mapTree(root, (n) => (n.id === id ? null : n))\n}\n\nfunction insertNode(root: QueryGroup, groupId: string, node: QueryNode): QueryGroup {\n if (root.id === groupId) return { ...root, rules: [...root.rules, node] }\n return mapTree(root, (n) => (isGroup(n) && n.id === groupId ? { ...n, rules: [...n.rules, node] } : n))\n}\n\n/* ---------------------------------------------------------------------------\n * Describing the tree (presentation only; interpreting it is the consumer's job)\n * ------------------------------------------------------------------------- */\n\ntype DescribeOptions = { fields: QueryField[]; operators?: QueryOperator[] }\n\nfunction valueLabel(rule: QueryRule, field: QueryField | undefined): string {\n const label = (v: unknown) => field?.options?.find((o) => o.value === String(v))?.label ?? String(v)\n if (Array.isArray(rule.value)) {\n const values = rule.value.map(label)\n if (rule.operator === \"between\") return `${values[0]} and ${values[1]}`\n if (values.length <= 3) return values.join(\", \")\n return `${values.slice(0, 2).join(\", \")} and ${values.length - 2} more`\n }\n return label(rule.value)\n}\n\n/** Natural-language sentence, e.g. \"Status is Active and Created is after 2026-01-01\". */\nfunction describeQuery(\n /** The group node this part renders. */\n group: QueryGroup,\n { fields, operators = defaultOperators }: DescribeOptions,\n depth = 0\n): string {\n const parts = group.rules\n .map((n) => {\n if (isGroup(n)) return describeQuery(n, { fields, operators }, depth + 1)\n if (!isComplete(n, operators)) return \"\"\n const field = fields.find((f) => f.name === n.field)\n const operator = operators.find((o) => o.name === n.operator)\n const value = operator?.arity === \"none\" ? \"\" : ` ${valueLabel(n, field)}`\n return `${field?.label ?? n.field} ${operator?.label ?? n.operator}${value}`\n })\n .filter(Boolean)\n if (!parts.length) return \"\"\n const joined = parts.join(group.match === \"all\" ? \" and \" : \" or \")\n return depth > 0 && parts.length > 1 ? `(${joined})` : joined\n}\n\n/** The tree without incomplete rules and empty groups: what an interpreter should consume. */\nfunction pruneQuery(group: QueryGroup, operators = defaultOperators): QueryGroup {\n const rules = group.rules\n .map((n) => (isGroup(n) ? pruneQuery(n, operators) : isComplete(n, operators) ? n : null))\n .filter((n): n is QueryNode => n !== null && (!isGroup(n) || n.rules.length > 0))\n return { ...group, rules }\n}\n\n/* ---------------------------------------------------------------------------\n * Context\n * ------------------------------------------------------------------------- */\n\ntype QueryBuilderContextValue = {\n /** Fields the user can filter on: `{ name, label, type, options, operators, placeholder }`. */\n fields: QueryField[]\n /** Operator definitions; defaults to `defaultOperators`. */\n operators: QueryOperator[]\n value: QueryGroup\n disabled: boolean\n maxDepth: number\n addRule: (groupId: string, patch?: Partial<QueryRule>) => void\n addGroup: (groupId: string) => void\n remove: (id: string) => void\n updateRule: (id: string, patch: Partial<QueryRule>) => void\n setMatch: (groupId: string, match: QueryGroup[\"match\"]) => void\n}\n\nconst QueryBuilderContext = React.createContext<QueryBuilderContextValue | null>(null)\nconst GroupContext = React.createContext<{ group: QueryGroup; depth: number } | null>(null)\nconst RuleContext = React.createContext<{ rule: QueryRule } | null>(null)\n\nfunction useQueryBuilder() {\n const ctx = React.useContext(QueryBuilderContext)\n if (!ctx) throw new Error(\"QueryBuilder parts must be used within <QueryBuilder>\")\n return ctx\n}\nfunction useQueryBuilderGroup() {\n const ctx = React.useContext(GroupContext)\n if (!ctx) throw new Error(\"Group parts must be used within <QueryBuilderGroup>\")\n return ctx\n}\nfunction useQueryBuilderRule() {\n const ctx = React.useContext(RuleContext)\n if (!ctx) throw new Error(\"Rule parts must be used within <QueryBuilderRule>\")\n return ctx\n}\n\n/* ---------------------------------------------------------------------------\n * Root\n * ------------------------------------------------------------------------- */\n\nfunction QueryBuilder({\n fields,\n operators = defaultOperators,\n value: valueProp,\n defaultValue,\n onValueChange,\n disabled = false,\n maxDepth = 3,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & {\n /** Fields the user can filter on: `{ name, label, type, options, operators, placeholder }`. */\n fields: QueryField[]\n /** Operator definitions; defaults to `defaultOperators`. */\n operators?: QueryOperator[]\n value?: QueryGroup\n defaultValue?: QueryGroup\n onValueChange?: (value: QueryGroup) => void\n disabled?: boolean\n /** Nesting limit for \"Add group\"; the trigger hides past it. */\n maxDepth?: number\n}) {\n const [initial] = React.useState<QueryGroup>(\n () => defaultValue ?? createGroup(\"all\", [createRule(fields, operators)])\n )\n const [value, setValue] = useControllable<QueryGroup>(valueProp, initial, onValueChange)\n const update = React.useCallback((fn: (prev: QueryGroup) => QueryGroup) => setValue(fn), [setValue])\n\n const ctx = React.useMemo<QueryBuilderContextValue>(\n () => ({\n fields,\n operators,\n value,\n disabled,\n maxDepth,\n addRule: (groupId, patch) => update((prev) => insertNode(prev, groupId, createRule(fields, operators, patch))),\n addGroup: (groupId) =>\n update((prev) => {\n // A nested group starts with the opposite match so it adds meaning (\"…and any of these\").\n const match = groupIn(prev, groupId)?.match === \"any\" ? \"all\" : \"any\"\n return insertNode(prev, groupId, createGroup(match, [createRule(fields, operators)]))\n }),\n remove: (id) => update((prev) => removeNode(prev, id)),\n updateRule: (id, patch) =>\n update((prev) => {\n if (patch.field !== undefined) {\n // Changing the field resets the operator to the first valid one and clears the value.\n const field = fields.find((f) => f.name === patch.field)\n const current = operators.find((o) => o.name === patch.operator)\n const options = operatorsFor(field, operators)\n const operator = current && options.includes(current) ? current.name : (options[0]?.name ?? \"\")\n return updateNode(prev, id, { field: patch.field, operator, value: undefined })\n }\n if (patch.operator !== undefined && patch.value === undefined) {\n const prevRule = findRule(prev, id)\n const before = operators.find((o) => o.name === prevRule?.operator)?.arity\n const after = operators.find((o) => o.name === patch.operator)?.arity\n return updateNode(prev, id, {\n operator: patch.operator,\n value: before === after ? prevRule?.value : undefined,\n })\n }\n return updateNode(prev, id, patch)\n }),\n setMatch: (groupId, match) => update((prev) => updateNode(prev, groupId, { match })),\n }),\n [fields, operators, value, disabled, maxDepth, update]\n )\n\n return (\n <QueryBuilderContext.Provider value={ctx}>\n <div\n data-slot=\"query-builder\"\n data-disabled={disabled ? \"\" : undefined}\n className={cn(\"flex flex-col gap-3 text-sm\", className)}\n {...props}\n >\n {children}\n </div>\n </QueryBuilderContext.Provider>\n )\n}\n\nfunction groupIn(root: QueryGroup, id: string): QueryGroup | null {\n if (root.id === id) return root\n for (const child of root.rules) {\n if (isGroup(child)) {\n const found = groupIn(child, id)\n if (found) return found\n }\n }\n return null\n}\n\nfunction findRule(root: QueryGroup, id: string): QueryRule | null {\n for (const child of root.rules) {\n if (isGroup(child)) {\n const found = findRule(child, id)\n if (found) return found\n } else if (child.id === id) return child\n }\n return null\n}\n\n/* ---------------------------------------------------------------------------\n * Group parts\n * ------------------------------------------------------------------------- */\n\nfunction QueryBuilderGroup({\n group,\n className,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & { group: QueryGroup }) {\n const parent = React.useContext(GroupContext)\n const depth = parent ? parent.depth + 1 : 0\n const ctx = React.useMemo(() => ({ group, depth }), [group, depth])\n return (\n <GroupContext.Provider value={ctx}>\n <div\n data-slot=\"query-builder-group\"\n data-match={group.match}\n data-depth={depth}\n data-empty={group.rules.length === 0 ? \"\" : undefined}\n className={cn(\n \"group/query-group flex flex-col gap-2\",\n depth > 0 && \"rounded-lg border border-dashed bg-muted/30 p-3\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n </GroupContext.Provider>\n )\n}\n\nfunction QueryBuilderGroupHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"query-builder-group-header\"\n className={cn(\"flex flex-wrap items-center gap-2\", className)}\n {...props}\n />\n )\n}\n\nconst matchOptions: QueryOption[] = [\n { value: \"all\", label: \"all\" },\n { value: \"any\", label: \"any\" },\n]\nconst matchCollection = createListCollection({ items: matchOptions, itemToValue: (o) => o.value })\n\n/** \"Match [all ▾] of the following\" — the only combinator control, once per group. */\nfunction QueryBuilderMatch({\n className,\n before,\n after = \"of the following\",\n ...props\n}: Omit<React.ComponentProps<typeof Select>, \"collection\" | \"value\" | \"onValueChange\"> & {\n /** Text before the select. Defaults to \"Match\" at the top level and \"Where\" in nested groups. */\n before?: React.ReactNode\n /** Text rendered after the match select. */\n after?: React.ReactNode\n}) {\n const { setMatch, disabled } = useQueryBuilder()\n const { group, depth } = useQueryBuilderGroup()\n return (\n <span data-slot=\"query-builder-match\" className=\"inline-flex flex-wrap items-center gap-1.5\">\n <span className=\"text-muted-foreground\">{before ?? (depth === 0 ? \"Match\" : \"Where\")}</span>\n <Select\n collection={matchCollection}\n value={[group.match]}\n onValueChange={({ value }) => value[0] && setMatch(group.id, value[0] as QueryGroup[\"match\"])}\n disabled={disabled}\n positioning={{ sameWidth: false }}\n {...props}\n >\n <SelectControl>\n <SelectTrigger size=\"sm\" className={cn(\"font-medium\", className)} aria-label=\"Match all or any\">\n <SelectValue>{group.match}</SelectValue>\n </SelectTrigger>\n </SelectControl>\n <SelectContent className=\"min-w-24\">\n {matchOptions.map((o) => (\n <SelectItem key={o.value} item={o}>\n <SelectItemText>{o.label}</SelectItemText>\n <SelectItemIndicator />\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <span className=\"text-muted-foreground\">{after}</span>\n </span>\n )\n}\n\n/** Indented list of conditions with a guide line. */\nfunction QueryBuilderGroupBody({ className, ...props }: React.ComponentProps<\"div\">) {\n const { group } = useQueryBuilderGroup()\n return (\n <div\n data-slot=\"query-builder-group-body\"\n data-empty={group.rules.length === 0 ? \"\" : undefined}\n className={cn(\n \"relative flex flex-col gap-2 ps-4 before:absolute before:inset-y-1 before:inset-s-1 before:w-px before:bg-border\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction QueryBuilderGroupFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"query-builder-group-footer\"\n className={cn(\"flex flex-wrap items-center gap-1 ps-4\", className)}\n {...props}\n />\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Rule parts\n * ------------------------------------------------------------------------- */\n\nfunction QueryBuilderRule({ rule, className, children, ...props }: React.ComponentProps<\"div\"> & { rule: QueryRule }) {\n const { operators } = useQueryBuilder()\n const complete = isComplete(rule, operators)\n const ctx = React.useMemo(() => ({ rule }), [rule])\n return (\n <RuleContext.Provider value={ctx}>\n <div\n data-slot=\"query-builder-rule\"\n data-incomplete={complete ? undefined : \"\"}\n className={cn(\n \"group/query-rule -ms-1 flex flex-wrap items-center gap-1.5 rounded-lg py-0.5 ps-1 pe-1 hover:bg-muted/40 has-focus-visible:bg-muted/40\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n </RuleContext.Provider>\n )\n}\n\ntype PickerProps = Omit<React.ComponentProps<typeof Select>, \"collection\" | \"value\" | \"onValueChange\">\n\nfunction QueryBuilderFieldSelect({ className, ...props }: PickerProps & { className?: string }) {\n const { fields, updateRule, disabled } = useQueryBuilder()\n const { rule } = useQueryBuilderRule()\n const collection = React.useMemo(\n () => createListCollection({ items: fields, itemToValue: (f) => f.name, itemToString: (f) => f.label }),\n [fields]\n )\n const field = fields.find((f) => f.name === rule.field)\n return (\n <Select\n collection={collection}\n value={[rule.field]}\n onValueChange={({ value }) => value[0] && updateRule(rule.id, { field: value[0] })}\n disabled={disabled}\n positioning={{ sameWidth: false }}\n {...props}\n >\n <SelectControl>\n <SelectTrigger\n size=\"sm\"\n className={cn(\"font-medium\", className)}\n data-slot=\"query-builder-field-select\"\n aria-label=\"Field\"\n >\n <SelectValue placeholder=\"Field\">{field?.label}</SelectValue>\n </SelectTrigger>\n </SelectControl>\n <SelectContent>\n {fields.map((f) => (\n <SelectItem key={f.name} item={f}>\n <SelectItemText>{f.label}</SelectItemText>\n <SelectItemIndicator />\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\nfunction QueryBuilderOperatorSelect({ className, ...props }: PickerProps & { className?: string }) {\n const { fields, operators, updateRule, disabled } = useQueryBuilder()\n const { rule } = useQueryBuilderRule()\n const field = fields.find((f) => f.name === rule.field)\n const options = React.useMemo(() => operatorsFor(field, operators), [field, operators])\n const collection = React.useMemo(\n () => createListCollection({ items: options, itemToValue: (o) => o.name, itemToString: (o) => o.label }),\n [options]\n )\n const current = options.find((o) => o.name === rule.operator)\n return (\n <Select\n collection={collection}\n value={[rule.operator]}\n onValueChange={({ value }) => value[0] && updateRule(rule.id, { operator: value[0] })}\n disabled={disabled}\n positioning={{ sameWidth: false }}\n {...props}\n >\n <SelectControl>\n <SelectTrigger\n size=\"sm\"\n className={cn(\"text-muted-foreground\", className)}\n data-slot=\"query-builder-operator-select\"\n aria-label=\"Operator\"\n >\n <SelectValue placeholder=\"Operator\">{current?.label}</SelectValue>\n </SelectTrigger>\n </SelectControl>\n <SelectContent>\n {options.map((o) => (\n <SelectItem key={o.name} item={o}>\n <SelectItemText>{o.label}</SelectItemText>\n <SelectItemIndicator />\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\ntype ValueEditorRenderProps = {\n /** The rule node this part renders. */\n rule: QueryRule\n field: QueryField | undefined\n operator: QueryOperator | undefined\n value: unknown\n setValue: (value: unknown) => void\n disabled: boolean\n}\n\n/**\n * Renders an editor for the rule's value based on the field type and operator\n * arity. Pass a render-prop child to take over for specific cases and fall back\n * to the default by returning `undefined`.\n */\nfunction QueryBuilderValueEditor({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"children\"> & {\n children?: (props: ValueEditorRenderProps) => React.ReactNode | undefined\n}) {\n const { fields, operators, updateRule, disabled } = useQueryBuilder()\n const { rule } = useQueryBuilderRule()\n const field = fields.find((f) => f.name === rule.field)\n const operator = operators.find((o) => o.name === rule.operator)\n const setValue = React.useCallback((value: unknown) => updateRule(rule.id, { value }), [updateRule, rule.id])\n const custom = children?.({ rule, field, operator, value: rule.value, setValue, disabled })\n if (custom !== undefined) return <>{custom}</>\n if (!operator || operator.arity === \"none\") return null\n const type = field?.type ?? \"text\"\n const inputType = type === \"number\" ? \"number\" : type === \"date\" ? \"date\" : \"text\"\n const inputClass = cn(\"h-7 w-40 text-sm md:text-sm\", type === \"number\" && \"w-24\", type === \"date\" && \"w-36\")\n\n if (operator.arity === \"two\") {\n const pair = Array.isArray(rule.value) ? (rule.value as unknown[]) : [undefined, undefined]\n const set = (index: 0 | 1, v: string) => {\n const next = [...pair]\n next[index] = v\n setValue(next)\n }\n return (\n <div\n data-slot=\"query-builder-value-editor\"\n className={cn(\"inline-flex items-center gap-1.5\", className)}\n {...props}\n >\n <Input\n type={inputType}\n value={String(pair[0] ?? \"\")}\n onChange={(e) => set(0, e.target.value)}\n disabled={disabled}\n aria-label=\"From\"\n className={inputClass}\n />\n <span className=\"text-muted-foreground\">and</span>\n <Input\n type={inputType}\n value={String(pair[1] ?? \"\")}\n onChange={(e) => set(1, e.target.value)}\n disabled={disabled}\n aria-label=\"To\"\n className={inputClass}\n />\n </div>\n )\n }\n\n if (type === \"select\" && field?.options) {\n const many = operator.arity === \"many\"\n const selected = many ? ((rule.value as string[] | undefined) ?? []) : rule.value ? [String(rule.value)] : []\n return (\n <div data-slot=\"query-builder-value-editor\" className={cn(\"inline-flex\", className)} {...props}>\n <OptionPicker\n options={field.options}\n value={selected}\n multiple={many}\n disabled={disabled}\n placeholder=\"Choose…\"\n onChange={(values) => setValue(many ? values : values[0])}\n />\n </div>\n )\n }\n\n if (operator.arity === \"many\") {\n // Free-text lists: comma separated, shown as-is.\n const list = Array.isArray(rule.value) ? (rule.value as string[]) : []\n return (\n <div data-slot=\"query-builder-value-editor\" className={cn(\"inline-flex\", className)} {...props}>\n <Input\n value={list.join(\", \")}\n onChange={(e) =>\n setValue(\n e.target.value\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean)\n )\n }\n placeholder={field?.placeholder ?? \"Comma separated\"}\n disabled={disabled}\n aria-label=\"Values\"\n className={cn(inputClass, \"w-56\")}\n />\n </div>\n )\n }\n\n return (\n <div data-slot=\"query-builder-value-editor\" className={cn(\"inline-flex\", className)} {...props}>\n <Input\n type={inputType}\n value={String(rule.value ?? \"\")}\n onChange={(e) => setValue(e.target.value)}\n placeholder={field?.placeholder ?? (type === \"text\" ? \"Value\" : undefined)}\n disabled={disabled}\n aria-label=\"Value\"\n className={inputClass}\n />\n </div>\n )\n}\n\nfunction OptionPicker({\n options,\n value,\n multiple,\n disabled,\n placeholder,\n onChange,\n}: {\n options: QueryOption[]\n value: string[]\n multiple: boolean\n disabled: boolean\n placeholder: string\n onChange: (values: string[]) => void\n}) {\n const collection = React.useMemo(\n () => createListCollection({ items: options, itemToValue: (o) => o.value, itemToString: (o) => o.label }),\n [options]\n )\n const labels = value.map((v) => options.find((o) => o.value === v)?.label ?? v)\n return (\n <Select\n collection={collection}\n value={value}\n multiple={multiple}\n closeOnSelect={!multiple}\n onValueChange={({ value }) => onChange(value)}\n disabled={disabled}\n positioning={{ sameWidth: false }}\n >\n <SelectControl>\n <SelectTrigger size=\"sm\" className=\"max-w-64\" aria-label=\"Value\">\n <SelectValue placeholder={placeholder}>\n {labels.length\n ? labels.length > 3\n ? `${labels.slice(0, 3).join(\", \")} +${labels.length - 3}`\n : labels.join(\", \")\n : undefined}\n </SelectValue>\n </SelectTrigger>\n </SelectControl>\n <SelectContent>\n {options.map((o) => (\n <SelectItem key={o.value} item={o}>\n <SelectItemText>{o.label}</SelectItemText>\n <SelectItemIndicator />\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n\n/** Hover-revealed slot at the end of a rule row (remove trigger, extras). */\nfunction QueryBuilderRuleActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"query-builder-rule-actions\"\n className={cn(\n \"ms-auto inline-flex items-center gap-0.5 opacity-0 transition-opacity group-focus-within/query-rule:opacity-100 group-hover/query-rule:opacity-100\",\n className\n )}\n {...props}\n />\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Triggers (polymorphic via asChild)\n * ------------------------------------------------------------------------- */\n\nfunction QueryBuilderAddRuleTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { addRule, disabled } = useQueryBuilder()\n const { group } = useQueryBuilderGroup()\n return (\n <Button\n data-slot=\"query-builder-add-rule-trigger\"\n variant=\"ghost\"\n size=\"sm\"\n asChild={asChild}\n disabled={disabled}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) addRule(group.id)\n }}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <PlusIcon /> Add condition\n </>\n ))}\n </Button>\n )\n}\n\nfunction QueryBuilderAddGroupTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { addGroup, disabled, maxDepth } = useQueryBuilder()\n const { group, depth } = useQueryBuilderGroup()\n if (depth >= maxDepth) return null\n return (\n <Button\n data-slot=\"query-builder-add-group-trigger\"\n variant=\"ghost\"\n size=\"sm\"\n asChild={asChild}\n disabled={disabled}\n className=\"text-muted-foreground\"\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) addGroup(group.id)\n }}\n {...props}\n >\n {asChild ? children : (children ?? <>Add a nested group</>)}\n </Button>\n )\n}\n\n/** Removes the enclosing rule, or the enclosing group when used in a group header. */\nfunction QueryBuilderRemoveTrigger({\n asChild,\n children,\n onClick,\n className,\n ...props\n}: React.ComponentProps<typeof Button>) {\n const { remove, disabled } = useQueryBuilder()\n const rule = React.useContext(RuleContext)\n const group = React.useContext(GroupContext)\n const target = rule?.rule.id ?? group?.group.id\n const label = rule ? \"Remove condition\" : \"Remove group\"\n return (\n <Button\n data-slot=\"query-builder-remove-trigger\"\n variant=\"ghost\"\n size=\"icon-xs\"\n asChild={asChild}\n disabled={disabled}\n aria-label={label}\n className={cn(\"text-muted-foreground hover:text-destructive\", className)}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented && target) remove(target)\n }}\n {...props}\n >\n {asChild ? children : (children ?? <XIcon />)}\n </Button>\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Summary and empty state\n * ------------------------------------------------------------------------- */\n\n/** The query as a sentence; `children` overrides the fallback when nothing is set. */\nfunction QueryBuilderSummary({\n className,\n children,\n prefix = \"Showing results where \",\n ...props\n}: React.ComponentProps<\"p\"> & { prefix?: React.ReactNode }) {\n const { value, fields, operators } = useQueryBuilder()\n const text = describeQuery(value, { fields, operators })\n return (\n <p\n data-slot=\"query-builder-summary\"\n data-empty={text ? undefined : \"\"}\n className={cn(\"text-sm\", className)}\n {...props}\n >\n {text ? (\n <>\n <span className=\"text-muted-foreground\">{prefix}</span>\n <span className=\"font-medium\">{text}</span>\n </>\n ) : (\n (children ?? (\n <span className=\"text-muted-foreground\">Showing everything. Add a condition to narrow it down.</span>\n ))\n )}\n </p>\n )\n}\n\nfunction QueryBuilderEmpty({ className, ...props }: React.ComponentProps<typeof ark.div>) {\n return (\n <ark.div\n data-slot=\"query-builder-empty\"\n className={cn(\"rounded-lg border border-dashed px-3 py-2 text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nexport {\n QueryBuilder,\n QueryBuilderGroup,\n QueryBuilderGroupHeader,\n QueryBuilderMatch,\n QueryBuilderGroupBody,\n QueryBuilderGroupFooter,\n QueryBuilderRule,\n QueryBuilderFieldSelect,\n QueryBuilderOperatorSelect,\n QueryBuilderValueEditor,\n QueryBuilderRuleActions,\n QueryBuilderAddRuleTrigger,\n QueryBuilderAddGroupTrigger,\n QueryBuilderRemoveTrigger,\n QueryBuilderSummary,\n QueryBuilderEmpty,\n useQueryBuilder,\n useQueryBuilderGroup,\n useQueryBuilderRule,\n createRule,\n createGroup,\n isGroup,\n isComplete,\n operatorsFor,\n defaultOperators,\n describeQuery,\n pruneQuery,\n type QueryField,\n type QueryFieldType,\n type QueryOption,\n type QueryOperator,\n type QueryRule,\n type QueryGroup,\n type QueryNode,\n}\n"
|
|
49
1416
|
}
|
|
50
1417
|
],
|
|
51
1418
|
"docs": "## Query builder\n`src/components/ui/query-builder.tsx` is a compositional filter builder designed for low cognitive\nload: it reads like a sentence instead of the usual AND/OR toggles per row. One `QueryBuilderMatch`\n(\"Match all/any of the following\") per group is the only combinator control, each condition reads\nfield → plain-language operator → value (`is`, `is not`, `contains`, `is after`, `is any of`,\n`is empty`, ...), negation lives in the operator wording (no NOT toggle), order never matters (no\ndrag-reorder), and incomplete conditions are flagged with `data-incomplete` and dropped by\n`pruneQuery` instead of erroring. **The primitive never interprets the tree**: SQL, API payloads,\nand in-memory evaluation are the consumer's job (the demo's `interpret.ts` has `toSql`, `toJson`,\n`matches`). The consumer owns the tree (`value`/`onValueChange`,\n`QueryGroup = { id, match: \"all\"|\"any\", rules: (QueryRule|QueryGroup)[] }`) and renders parts\nrecursively; the root applies edits. Fields are data (`{ name, label, type: text|number|date|\nboolean|select, options, operators, placeholder }`); operators are `{ name, label, arity:\nnone|one|two|many, types }` with `defaultOperators` exported. Parts: `QueryBuilderGroup` (`group`,\n`data-match`/`data-depth`, nested groups get a dashed panel), `GroupHeader` > `QueryBuilderMatch`\n(`before`/`after` text) + `QueryBuilderRemoveTrigger` (removes the enclosing rule, or group when\nused in a header), `GroupBody` (guide line), `QueryBuilderRule` (`rule`) > `FieldSelect`\n(changing the field resets operator/value) + `OperatorSelect` (keeps the value when the arity\nmatches) + `ValueEditor` (by type/arity: text/number/date inputs, two inputs for \"between\",\nsingle/multiple Select for select fields, comma list for free-text lists; render-prop child\noverrides, return `undefined` to fall back) + `RuleActions` (hover-revealed), `GroupFooter` >\n`AddRuleTrigger` / `AddGroupTrigger` (hidden past `maxDepth`; a new nested group starts with the\nopposite match), `QueryBuilderSummary` (natural-language sentence; `prefix` prop, default\n\"Showing results where \"), `QueryBuilderEmpty`. Helpers: `createRule`, `createGroup`, `isGroup`,\n`isComplete`, `operatorsFor`, `describeQuery(tree, { fields })`, `pruneQuery`. The form builder\nreuses these parts for per-field visibility conditions. `src/demo/query-builder/` is the reference\n(`/segments`): filters the users dataset with quick-start presets, the sentence summary, a\nlive matching-rows table, and SQL/JSON output."
|