@multicomma/arkcn 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +78 -0
  2. package/bin/arkcn.mjs +78 -0
  3. package/cli/commands.mjs +251 -0
  4. package/cli/project.mjs +168 -0
  5. package/cli/registry.mjs +50 -0
  6. package/cli/ui.mjs +46 -0
  7. package/mcp/server.mjs +149 -0
  8. package/package.json +201 -0
  9. package/registry/base.css +680 -0
  10. package/registry/index.json +1833 -0
  11. package/registry/items/accordion.json +18 -0
  12. package/registry/items/activity-feed.json +18 -0
  13. package/registry/items/alert-dialog.json +18 -0
  14. package/registry/items/alert.json +17 -0
  15. package/registry/items/angle-slider.json +17 -0
  16. package/registry/items/app-shell.json +28 -0
  17. package/registry/items/aspect-ratio.json +13 -0
  18. package/registry/items/attachment.json +19 -0
  19. package/registry/items/avatar.json +17 -0
  20. package/registry/items/badge.json +18 -0
  21. package/registry/items/breadcrumb.json +18 -0
  22. package/registry/items/bubble.json +18 -0
  23. package/registry/items/button-group.json +19 -0
  24. package/registry/items/button.json +18 -0
  25. package/registry/items/calendar.json +19 -0
  26. package/registry/items/canvas.json +24 -0
  27. package/registry/items/card.json +15 -0
  28. package/registry/items/carousel.json +19 -0
  29. package/registry/items/cascader.json +23 -0
  30. package/registry/items/chart.json +17 -0
  31. package/registry/items/chat.json +23 -0
  32. package/registry/items/checkbox.json +18 -0
  33. package/registry/items/clipboard.json +19 -0
  34. package/registry/items/collapsible.json +15 -0
  35. package/registry/items/color-picker.json +19 -0
  36. package/registry/items/combobox.json +20 -0
  37. package/registry/items/command.json +21 -0
  38. package/registry/items/comment-thread.json +25 -0
  39. package/registry/items/context-menu.json +18 -0
  40. package/registry/items/controllable.json +14 -0
  41. package/registry/items/data-grid.json +24 -0
  42. package/registry/items/data-table-adapter.json +14 -0
  43. package/registry/items/data-table.json +32 -0
  44. package/registry/items/date-input.json +17 -0
  45. package/registry/items/dialog.json +19 -0
  46. package/registry/items/direction.json +13 -0
  47. package/registry/items/drag-preview.json +16 -0
  48. package/registry/items/drawer.json +18 -0
  49. package/registry/items/dropdown-menu.json +18 -0
  50. package/registry/items/editable.json +17 -0
  51. package/registry/items/empty.json +17 -0
  52. package/registry/items/field.json +19 -0
  53. package/registry/items/file-upload.json +19 -0
  54. package/registry/items/floating-panel.json +19 -0
  55. package/registry/items/floating-toolbar.json +16 -0
  56. package/registry/items/form.json +14 -0
  57. package/registry/items/format.json +15 -0
  58. package/registry/items/gantt.json +22 -0
  59. package/registry/items/history.json +14 -0
  60. package/registry/items/hotkeys.json +18 -0
  61. package/registry/items/hover-card.json +17 -0
  62. package/registry/items/image-cropper.json +17 -0
  63. package/registry/items/input-group.json +20 -0
  64. package/registry/items/input-otp.json +18 -0
  65. package/registry/items/input.json +15 -0
  66. package/registry/items/item.json +19 -0
  67. package/registry/items/json-tree-view.json +17 -0
  68. package/registry/items/kanban.json +26 -0
  69. package/registry/items/kbd.json +15 -0
  70. package/registry/items/label.json +15 -0
  71. package/registry/items/live-region.json +16 -0
  72. package/registry/items/marker.json +18 -0
  73. package/registry/items/marquee.json +17 -0
  74. package/registry/items/menubar.json +19 -0
  75. package/registry/items/message.json +15 -0
  76. package/registry/items/navigation-menu.json +19 -0
  77. package/registry/items/node-graph.json +22 -0
  78. package/registry/items/number-input.json +18 -0
  79. package/registry/items/pagination.json +18 -0
  80. package/registry/items/password-input.json +18 -0
  81. package/registry/items/popconfirm.json +20 -0
  82. package/registry/items/popover.json +18 -0
  83. package/registry/items/progress.json +17 -0
  84. package/registry/items/qr-code.json +17 -0
  85. package/registry/items/query-builder.json +23 -0
  86. package/registry/items/radio-group.json +17 -0
  87. package/registry/items/rating-group.json +18 -0
  88. package/registry/items/resizable.json +17 -0
  89. package/registry/items/rich-text-editor.json +37 -0
  90. package/registry/items/row-layout.json +15 -0
  91. package/registry/items/scheduler.json +23 -0
  92. package/registry/items/scroll-area.json +17 -0
  93. package/registry/items/segment-group.json +17 -0
  94. package/registry/items/select.json +19 -0
  95. package/registry/items/separator.json +15 -0
  96. package/registry/items/sheet.json +19 -0
  97. package/registry/items/sidebar.json +27 -0
  98. package/registry/items/signature-pad.json +19 -0
  99. package/registry/items/skeleton.json +15 -0
  100. package/registry/items/slider.json +17 -0
  101. package/registry/items/sonner.json +17 -0
  102. package/registry/items/spinner.json +17 -0
  103. package/registry/items/steps.json +19 -0
  104. package/registry/items/swap.json +17 -0
  105. package/registry/items/switch.json +17 -0
  106. package/registry/items/table.json +16 -0
  107. package/registry/items/tabs.json +18 -0
  108. package/registry/items/tags-input.json +18 -0
  109. package/registry/items/textarea.json +15 -0
  110. package/registry/items/time.json +14 -0
  111. package/registry/items/timer.json +18 -0
  112. package/registry/items/toc.json +17 -0
  113. package/registry/items/toggle-group.json +19 -0
  114. package/registry/items/toggle.json +18 -0
  115. package/registry/items/tooltip.json +17 -0
  116. package/registry/items/tour.json +19 -0
  117. package/registry/items/transfer-list.json +24 -0
  118. package/registry/items/tree-select.json +24 -0
  119. package/registry/items/tree-view.json +18 -0
  120. package/registry/items/use-mobile.json +13 -0
  121. package/registry/items/utils.json +17 -0
  122. package/registry/items/virtual-list.json +16 -0
  123. package/src/components/ui/accordion.tsx +111 -0
  124. package/src/components/ui/activity-feed.tsx +228 -0
  125. package/src/components/ui/alert-dialog.tsx +204 -0
  126. package/src/components/ui/alert.tsx +55 -0
  127. package/src/components/ui/angle-slider.tsx +117 -0
  128. package/src/components/ui/app-shell.tsx +985 -0
  129. package/src/components/ui/aspect-ratio.tsx +35 -0
  130. package/src/components/ui/attachment.tsx +193 -0
  131. package/src/components/ui/avatar.tsx +94 -0
  132. package/src/components/ui/badge.tsx +45 -0
  133. package/src/components/ui/breadcrumb.tsx +93 -0
  134. package/src/components/ui/bubble.tsx +119 -0
  135. package/src/components/ui/button-group.tsx +79 -0
  136. package/src/components/ui/button.tsx +65 -0
  137. package/src/components/ui/calendar.tsx +172 -0
  138. package/src/components/ui/canvas.tsx +723 -0
  139. package/src/components/ui/card.tsx +69 -0
  140. package/src/components/ui/carousel.tsx +228 -0
  141. package/src/components/ui/cascader.tsx +754 -0
  142. package/src/components/ui/chart.tsx +303 -0
  143. package/src/components/ui/chat.tsx +544 -0
  144. package/src/components/ui/checkbox.tsx +94 -0
  145. package/src/components/ui/clipboard.tsx +94 -0
  146. package/src/components/ui/collapsible.tsx +32 -0
  147. package/src/components/ui/color-picker.tsx +441 -0
  148. package/src/components/ui/combobox.tsx +400 -0
  149. package/src/components/ui/command.tsx +292 -0
  150. package/src/components/ui/comment-thread.tsx +923 -0
  151. package/src/components/ui/context-menu.tsx +331 -0
  152. package/src/components/ui/data-grid.tsx +1227 -0
  153. package/src/components/ui/data-table.tsx +993 -0
  154. package/src/components/ui/date-input.tsx +103 -0
  155. package/src/components/ui/dialog.tsx +159 -0
  156. package/src/components/ui/direction.tsx +32 -0
  157. package/src/components/ui/drawer.tsx +200 -0
  158. package/src/components/ui/dropdown-menu.tsx +329 -0
  159. package/src/components/ui/editable.tsx +90 -0
  160. package/src/components/ui/empty.tsx +86 -0
  161. package/src/components/ui/field.tsx +218 -0
  162. package/src/components/ui/file-upload.tsx +204 -0
  163. package/src/components/ui/floating-panel.tsx +199 -0
  164. package/src/components/ui/floating-toolbar.tsx +79 -0
  165. package/src/components/ui/format.tsx +22 -0
  166. package/src/components/ui/gantt.tsx +950 -0
  167. package/src/components/ui/hotkeys.tsx +340 -0
  168. package/src/components/ui/hover-card.tsx +83 -0
  169. package/src/components/ui/image-cropper.tsx +100 -0
  170. package/src/components/ui/input-group.tsx +133 -0
  171. package/src/components/ui/input-otp.tsx +82 -0
  172. package/src/components/ui/input.tsx +18 -0
  173. package/src/components/ui/item.tsx +174 -0
  174. package/src/components/ui/json-tree-view.tsx +37 -0
  175. package/src/components/ui/kanban.tsx +884 -0
  176. package/src/components/ui/kbd.tsx +20 -0
  177. package/src/components/ui/label.tsx +30 -0
  178. package/src/components/ui/live-region.tsx +54 -0
  179. package/src/components/ui/marker.tsx +66 -0
  180. package/src/components/ui/marquee.tsx +61 -0
  181. package/src/components/ui/menubar.tsx +456 -0
  182. package/src/components/ui/message.tsx +74 -0
  183. package/src/components/ui/navigation-menu.tsx +202 -0
  184. package/src/components/ui/node-graph.tsx +1723 -0
  185. package/src/components/ui/number-input.tsx +148 -0
  186. package/src/components/ui/pagination.tsx +88 -0
  187. package/src/components/ui/password-input.tsx +104 -0
  188. package/src/components/ui/popconfirm.tsx +149 -0
  189. package/src/components/ui/popover.tsx +126 -0
  190. package/src/components/ui/progress.tsx +114 -0
  191. package/src/components/ui/qr-code.tsx +51 -0
  192. package/src/components/ui/query-builder.tsx +938 -0
  193. package/src/components/ui/radio-group.tsx +108 -0
  194. package/src/components/ui/rating-group.tsx +91 -0
  195. package/src/components/ui/resizable.tsx +41 -0
  196. package/src/components/ui/rich-text-editor.tsx +845 -0
  197. package/src/components/ui/scheduler.tsx +1164 -0
  198. package/src/components/ui/scroll-area.tsx +82 -0
  199. package/src/components/ui/segment-group.tsx +95 -0
  200. package/src/components/ui/select.tsx +354 -0
  201. package/src/components/ui/separator.tsx +32 -0
  202. package/src/components/ui/sheet.tsx +130 -0
  203. package/src/components/ui/sidebar.tsx +665 -0
  204. package/src/components/ui/signature-pad.tsx +112 -0
  205. package/src/components/ui/skeleton.tsx +7 -0
  206. package/src/components/ui/slider.tsx +183 -0
  207. package/src/components/ui/sonner.tsx +37 -0
  208. package/src/components/ui/spinner.tsx +16 -0
  209. package/src/components/ui/steps.tsx +152 -0
  210. package/src/components/ui/swap.tsx +33 -0
  211. package/src/components/ui/switch.tsx +83 -0
  212. package/src/components/ui/table.tsx +72 -0
  213. package/src/components/ui/tabs.tsx +100 -0
  214. package/src/components/ui/tags-input.tsx +153 -0
  215. package/src/components/ui/textarea.tsx +17 -0
  216. package/src/components/ui/timer.tsx +67 -0
  217. package/src/components/ui/toc.tsx +64 -0
  218. package/src/components/ui/toggle-group.tsx +89 -0
  219. package/src/components/ui/toggle.tsx +46 -0
  220. package/src/components/ui/tooltip.tsx +113 -0
  221. package/src/components/ui/tour.tsx +168 -0
  222. package/src/components/ui/transfer-list.tsx +700 -0
  223. package/src/components/ui/tree-select.tsx +638 -0
  224. package/src/components/ui/tree-view.tsx +217 -0
  225. package/src/components/ui/virtual-list.tsx +343 -0
  226. package/src/hooks/use-mobile.ts +19 -0
  227. package/src/lib/controllable.ts +31 -0
  228. package/src/lib/data-table-adapter.ts +67 -0
  229. package/src/lib/drag-preview.ts +39 -0
  230. package/src/lib/form.ts +39 -0
  231. package/src/lib/history.ts +55 -0
  232. package/src/lib/row-layout.ts +81 -0
  233. package/src/lib/table.ts +483 -0
  234. package/src/lib/time.ts +37 -0
  235. package/src/lib/utils.ts +6 -0
  236. package/src/styles/arkcn.css +764 -0
  237. package/src/styles/index.css +3 -0
@@ -0,0 +1,938 @@
1
+ import * as React from "react"
2
+ import { ark } from "@ark-ui/react"
3
+ import { createListCollection } from "@ark-ui/react/collection"
4
+ import { PlusIcon, XIcon } from "lucide-react"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Input } from "@/components/ui/input"
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectControl,
11
+ SelectItem,
12
+ SelectItemIndicator,
13
+ SelectItemText,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from "@/components/ui/select"
17
+ import { cn } from "@/lib/utils"
18
+ import { useControllable } from "@/lib/controllable"
19
+
20
+ /**
21
+ * QueryBuilder — a compositional filter builder that reads like a sentence.
22
+ *
23
+ * Every group opens with "Match all / any of the following" and each condition
24
+ * reads field → plain-language operator → value. Incomplete conditions (no
25
+ * value yet) are flagged with `data-incomplete` and dropped by `pruneQuery`
26
+ * instead of erroring, negation lives in the operator wording ("is not",
27
+ * "does not contain") and order is irrelevant, so there is no NOT toggle and
28
+ * no drag-reorder to learn.
29
+ *
30
+ * The consumer owns the query tree (`value`/`onValueChange`) and renders the
31
+ * parts recursively; the root applies all edits (add, remove, update). The
32
+ * primitive never interprets the tree: turning it into SQL, an API filter, or
33
+ * an in-memory predicate is the consumer's job (see `src/demo/query-builder`).
34
+ *
35
+ * Anatomy:
36
+ * QueryBuilder
37
+ * QueryBuilderGroup { group }
38
+ * QueryBuilderGroupHeader
39
+ * QueryBuilderMatch "all" | "any" select
40
+ * QueryBuilderRemoveTrigger (nested groups)
41
+ * QueryBuilderGroupBody
42
+ * QueryBuilderRule { rule } → FieldSelect + OperatorSelect + ValueEditor + RuleActions
43
+ * QueryBuilderGroup (nested)
44
+ * QueryBuilderGroupFooter
45
+ * QueryBuilderAddRuleTrigger / QueryBuilderAddGroupTrigger
46
+ * QueryBuilderSummary natural-language sentence
47
+ */
48
+
49
+ /* ---------------------------------------------------------------------------
50
+ * Model
51
+ * ------------------------------------------------------------------------- */
52
+
53
+ type QueryFieldType = "text" | "number" | "date" | "boolean" | "select"
54
+ type QueryOption = { value: string; label: string }
55
+ type QueryField = {
56
+ name: string
57
+ label: string
58
+ type?: QueryFieldType
59
+ /** For `select` fields. */
60
+ options?: QueryOption[]
61
+ /** Restrict the operators offered (names from `operators`). */
62
+ operators?: string[]
63
+ /** Placeholder for the value editor. */
64
+ placeholder?: string
65
+ }
66
+ /** How many values an operator takes. */
67
+ type QueryArity = "none" | "one" | "two" | "many"
68
+ type QueryOperator = { name: string; label: string; arity: QueryArity; types: QueryFieldType[] }
69
+
70
+ type QueryRule = { id: string; field: string; operator: string; value?: unknown }
71
+ type QueryGroup = { id: string; match: "all" | "any"; rules: QueryNode[] }
72
+ type QueryNode = QueryRule | QueryGroup
73
+
74
+ const isGroup = (node: QueryNode): node is QueryGroup => "rules" in node
75
+
76
+ let seq = 0
77
+ const uid = (prefix: string) => `${prefix}-${(seq++).toString(36)}${Date.now().toString(36).slice(-3)}`
78
+
79
+ const op = (name: string, label: string, arity: QueryArity, types: QueryFieldType[]): QueryOperator => ({
80
+ name,
81
+ label,
82
+ arity,
83
+ types,
84
+ })
85
+
86
+ /** Plain-language operators; negation is part of the wording. */
87
+ const defaultOperators: QueryOperator[] = [
88
+ op("is", "is", "one", ["text", "number", "date", "select"]),
89
+ op("isNot", "is not", "one", ["text", "number", "date", "select"]),
90
+ op("contains", "contains", "one", ["text"]),
91
+ op("doesNotContain", "does not contain", "one", ["text"]),
92
+ op("startsWith", "starts with", "one", ["text"]),
93
+ op("endsWith", "ends with", "one", ["text"]),
94
+ op("greaterThan", "is greater than", "one", ["number"]),
95
+ op("lessThan", "is less than", "one", ["number"]),
96
+ op("atLeast", "is at least", "one", ["number"]),
97
+ op("atMost", "is at most", "one", ["number"]),
98
+ op("before", "is before", "one", ["date"]),
99
+ op("after", "is after", "one", ["date"]),
100
+ op("between", "is between", "two", ["number", "date"]),
101
+ op("anyOf", "is any of", "many", ["select", "text"]),
102
+ op("noneOf", "is none of", "many", ["select", "text"]),
103
+ op("isTrue", "is true", "none", ["boolean"]),
104
+ op("isFalse", "is false", "none", ["boolean"]),
105
+ op("isEmpty", "is empty", "none", ["text", "number", "date", "select"]),
106
+ op("isNotEmpty", "is not empty", "none", ["text", "number", "date", "select"]),
107
+ ]
108
+
109
+ function operatorsFor(field: QueryField | undefined, operators: QueryOperator[]) {
110
+ if (!field) return []
111
+ const type = field.type ?? "text"
112
+ const allowed = operators.filter((o) => o.types.includes(type))
113
+ return field.operators
114
+ ? (field.operators.map((n) => allowed.find((o) => o.name === n)).filter(Boolean) as QueryOperator[])
115
+ : allowed
116
+ }
117
+
118
+ function createRule(fields: QueryField[], operators = defaultOperators, patch?: Partial<QueryRule>): QueryRule {
119
+ const field = fields.find((f) => f.name === patch?.field) ?? fields[0]
120
+ const operator = patch?.operator ?? operatorsFor(field, operators)[0]?.name ?? ""
121
+ return { id: uid("rule"), field: field?.name ?? "", operator, value: undefined, ...patch }
122
+ }
123
+
124
+ function createGroup(match: QueryGroup["match"] = "all", rules: QueryNode[] = []): QueryGroup {
125
+ return { id: uid("group"), match, rules }
126
+ }
127
+
128
+ const hasValue = (value: unknown) =>
129
+ value !== undefined && value !== null && value !== "" && !(Array.isArray(value) && value.length === 0)
130
+
131
+ /** A rule counts once it has everything its operator needs. */
132
+ function isComplete(rule: QueryRule, operators = defaultOperators) {
133
+ const operator = operators.find((o) => o.name === rule.operator)
134
+ if (!operator || !rule.field) return false
135
+ if (operator.arity === "none") return true
136
+ if (operator.arity === "two") return Array.isArray(rule.value) && hasValue(rule.value[0]) && hasValue(rule.value[1])
137
+ return hasValue(rule.value)
138
+ }
139
+
140
+ /* Tree edits (pure) */
141
+
142
+ function mapTree(node: QueryGroup, fn: (node: QueryNode) => QueryNode | null): QueryGroup {
143
+ const rules = node.rules
144
+ .map((child) => {
145
+ const next = fn(child)
146
+ if (next === null) return null
147
+ return isGroup(next) ? mapTree(next, fn) : next
148
+ })
149
+ .filter((n): n is QueryNode => n !== null)
150
+ return { ...node, rules }
151
+ }
152
+
153
+ function updateNode(root: QueryGroup, id: string, patch: Partial<QueryRule> | Partial<QueryGroup>): QueryGroup {
154
+ if (root.id === id) return { ...root, ...(patch as Partial<QueryGroup>) }
155
+ return mapTree(root, (n) => (n.id === id ? ({ ...n, ...patch } as QueryNode) : n))
156
+ }
157
+
158
+ function removeNode(root: QueryGroup, id: string): QueryGroup {
159
+ return mapTree(root, (n) => (n.id === id ? null : n))
160
+ }
161
+
162
+ function insertNode(root: QueryGroup, groupId: string, node: QueryNode): QueryGroup {
163
+ if (root.id === groupId) return { ...root, rules: [...root.rules, node] }
164
+ return mapTree(root, (n) => (isGroup(n) && n.id === groupId ? { ...n, rules: [...n.rules, node] } : n))
165
+ }
166
+
167
+ /* ---------------------------------------------------------------------------
168
+ * Describing the tree (presentation only; interpreting it is the consumer's job)
169
+ * ------------------------------------------------------------------------- */
170
+
171
+ type DescribeOptions = { fields: QueryField[]; operators?: QueryOperator[] }
172
+
173
+ function valueLabel(rule: QueryRule, field: QueryField | undefined): string {
174
+ const label = (v: unknown) => field?.options?.find((o) => o.value === String(v))?.label ?? String(v)
175
+ if (Array.isArray(rule.value)) {
176
+ const values = rule.value.map(label)
177
+ if (rule.operator === "between") return `${values[0]} and ${values[1]}`
178
+ if (values.length <= 3) return values.join(", ")
179
+ return `${values.slice(0, 2).join(", ")} and ${values.length - 2} more`
180
+ }
181
+ return label(rule.value)
182
+ }
183
+
184
+ /** Natural-language sentence, e.g. "Status is Active and Created is after 2026-01-01". */
185
+ function describeQuery(
186
+ group: QueryGroup,
187
+ { fields, operators = defaultOperators }: DescribeOptions,
188
+ depth = 0
189
+ ): string {
190
+ const parts = group.rules
191
+ .map((n) => {
192
+ if (isGroup(n)) return describeQuery(n, { fields, operators }, depth + 1)
193
+ if (!isComplete(n, operators)) return ""
194
+ const field = fields.find((f) => f.name === n.field)
195
+ const operator = operators.find((o) => o.name === n.operator)
196
+ const value = operator?.arity === "none" ? "" : ` ${valueLabel(n, field)}`
197
+ return `${field?.label ?? n.field} ${operator?.label ?? n.operator}${value}`
198
+ })
199
+ .filter(Boolean)
200
+ if (!parts.length) return ""
201
+ const joined = parts.join(group.match === "all" ? " and " : " or ")
202
+ return depth > 0 && parts.length > 1 ? `(${joined})` : joined
203
+ }
204
+
205
+ /** The tree without incomplete rules and empty groups: what an interpreter should consume. */
206
+ function pruneQuery(group: QueryGroup, operators = defaultOperators): QueryGroup {
207
+ const rules = group.rules
208
+ .map((n) => (isGroup(n) ? pruneQuery(n, operators) : isComplete(n, operators) ? n : null))
209
+ .filter((n): n is QueryNode => n !== null && (!isGroup(n) || n.rules.length > 0))
210
+ return { ...group, rules }
211
+ }
212
+
213
+ /* ---------------------------------------------------------------------------
214
+ * Context
215
+ * ------------------------------------------------------------------------- */
216
+
217
+ type QueryBuilderContextValue = {
218
+ fields: QueryField[]
219
+ operators: QueryOperator[]
220
+ value: QueryGroup
221
+ disabled: boolean
222
+ maxDepth: number
223
+ addRule: (groupId: string, patch?: Partial<QueryRule>) => void
224
+ addGroup: (groupId: string) => void
225
+ remove: (id: string) => void
226
+ updateRule: (id: string, patch: Partial<QueryRule>) => void
227
+ setMatch: (groupId: string, match: QueryGroup["match"]) => void
228
+ }
229
+
230
+ const QueryBuilderContext = React.createContext<QueryBuilderContextValue | null>(null)
231
+ const GroupContext = React.createContext<{ group: QueryGroup; depth: number } | null>(null)
232
+ const RuleContext = React.createContext<{ rule: QueryRule } | null>(null)
233
+
234
+ function useQueryBuilder() {
235
+ const ctx = React.useContext(QueryBuilderContext)
236
+ if (!ctx) throw new Error("QueryBuilder parts must be used within <QueryBuilder>")
237
+ return ctx
238
+ }
239
+ function useQueryBuilderGroup() {
240
+ const ctx = React.useContext(GroupContext)
241
+ if (!ctx) throw new Error("Group parts must be used within <QueryBuilderGroup>")
242
+ return ctx
243
+ }
244
+ function useQueryBuilderRule() {
245
+ const ctx = React.useContext(RuleContext)
246
+ if (!ctx) throw new Error("Rule parts must be used within <QueryBuilderRule>")
247
+ return ctx
248
+ }
249
+
250
+ /* ---------------------------------------------------------------------------
251
+ * Root
252
+ * ------------------------------------------------------------------------- */
253
+
254
+ function QueryBuilder({
255
+ fields,
256
+ operators = defaultOperators,
257
+ value: valueProp,
258
+ defaultValue,
259
+ onValueChange,
260
+ disabled = false,
261
+ maxDepth = 3,
262
+ className,
263
+ children,
264
+ ...props
265
+ }: Omit<React.ComponentProps<"div">, "defaultValue" | "onChange"> & {
266
+ fields: QueryField[]
267
+ operators?: QueryOperator[]
268
+ value?: QueryGroup
269
+ defaultValue?: QueryGroup
270
+ onValueChange?: (value: QueryGroup) => void
271
+ disabled?: boolean
272
+ /** Nesting limit for "Add group"; the trigger hides past it. */
273
+ maxDepth?: number
274
+ }) {
275
+ const [initial] = React.useState<QueryGroup>(
276
+ () => defaultValue ?? createGroup("all", [createRule(fields, operators)])
277
+ )
278
+ const [value, setValue] = useControllable<QueryGroup>(valueProp, initial, onValueChange)
279
+ const update = React.useCallback((fn: (prev: QueryGroup) => QueryGroup) => setValue(fn), [setValue])
280
+
281
+ const ctx = React.useMemo<QueryBuilderContextValue>(
282
+ () => ({
283
+ fields,
284
+ operators,
285
+ value,
286
+ disabled,
287
+ maxDepth,
288
+ addRule: (groupId, patch) => update((prev) => insertNode(prev, groupId, createRule(fields, operators, patch))),
289
+ addGroup: (groupId) =>
290
+ update((prev) => {
291
+ // A nested group starts with the opposite match so it adds meaning ("…and any of these").
292
+ const match = groupIn(prev, groupId)?.match === "any" ? "all" : "any"
293
+ return insertNode(prev, groupId, createGroup(match, [createRule(fields, operators)]))
294
+ }),
295
+ remove: (id) => update((prev) => removeNode(prev, id)),
296
+ updateRule: (id, patch) =>
297
+ update((prev) => {
298
+ if (patch.field !== undefined) {
299
+ // Changing the field resets the operator to the first valid one and clears the value.
300
+ const field = fields.find((f) => f.name === patch.field)
301
+ const current = operators.find((o) => o.name === patch.operator)
302
+ const options = operatorsFor(field, operators)
303
+ const operator = current && options.includes(current) ? current.name : (options[0]?.name ?? "")
304
+ return updateNode(prev, id, { field: patch.field, operator, value: undefined })
305
+ }
306
+ if (patch.operator !== undefined && patch.value === undefined) {
307
+ const prevRule = findRule(prev, id)
308
+ const before = operators.find((o) => o.name === prevRule?.operator)?.arity
309
+ const after = operators.find((o) => o.name === patch.operator)?.arity
310
+ return updateNode(prev, id, {
311
+ operator: patch.operator,
312
+ value: before === after ? prevRule?.value : undefined,
313
+ })
314
+ }
315
+ return updateNode(prev, id, patch)
316
+ }),
317
+ setMatch: (groupId, match) => update((prev) => updateNode(prev, groupId, { match })),
318
+ }),
319
+ [fields, operators, value, disabled, maxDepth, update]
320
+ )
321
+
322
+ return (
323
+ <QueryBuilderContext.Provider value={ctx}>
324
+ <div
325
+ data-slot="query-builder"
326
+ data-disabled={disabled ? "" : undefined}
327
+ className={cn("flex flex-col gap-3 text-sm", className)}
328
+ {...props}
329
+ >
330
+ {children}
331
+ </div>
332
+ </QueryBuilderContext.Provider>
333
+ )
334
+ }
335
+
336
+ function groupIn(root: QueryGroup, id: string): QueryGroup | null {
337
+ if (root.id === id) return root
338
+ for (const child of root.rules) {
339
+ if (isGroup(child)) {
340
+ const found = groupIn(child, id)
341
+ if (found) return found
342
+ }
343
+ }
344
+ return null
345
+ }
346
+
347
+ function findRule(root: QueryGroup, id: string): QueryRule | null {
348
+ for (const child of root.rules) {
349
+ if (isGroup(child)) {
350
+ const found = findRule(child, id)
351
+ if (found) return found
352
+ } else if (child.id === id) return child
353
+ }
354
+ return null
355
+ }
356
+
357
+ /* ---------------------------------------------------------------------------
358
+ * Group parts
359
+ * ------------------------------------------------------------------------- */
360
+
361
+ function QueryBuilderGroup({
362
+ group,
363
+ className,
364
+ children,
365
+ ...props
366
+ }: React.ComponentProps<"div"> & { group: QueryGroup }) {
367
+ const parent = React.useContext(GroupContext)
368
+ const depth = parent ? parent.depth + 1 : 0
369
+ const ctx = React.useMemo(() => ({ group, depth }), [group, depth])
370
+ return (
371
+ <GroupContext.Provider value={ctx}>
372
+ <div
373
+ data-slot="query-builder-group"
374
+ data-match={group.match}
375
+ data-depth={depth}
376
+ data-empty={group.rules.length === 0 ? "" : undefined}
377
+ className={cn(
378
+ "group/query-group flex flex-col gap-2",
379
+ depth > 0 && "rounded-lg border border-dashed bg-muted/30 p-3",
380
+ className
381
+ )}
382
+ {...props}
383
+ >
384
+ {children}
385
+ </div>
386
+ </GroupContext.Provider>
387
+ )
388
+ }
389
+
390
+ function QueryBuilderGroupHeader({ className, ...props }: React.ComponentProps<"div">) {
391
+ return (
392
+ <div
393
+ data-slot="query-builder-group-header"
394
+ className={cn("flex flex-wrap items-center gap-2", className)}
395
+ {...props}
396
+ />
397
+ )
398
+ }
399
+
400
+ const matchOptions: QueryOption[] = [
401
+ { value: "all", label: "all" },
402
+ { value: "any", label: "any" },
403
+ ]
404
+ const matchCollection = createListCollection({ items: matchOptions, itemToValue: (o) => o.value })
405
+
406
+ /** "Match [all ▾] of the following" — the only combinator control, once per group. */
407
+ function QueryBuilderMatch({
408
+ className,
409
+ before,
410
+ after = "of the following",
411
+ ...props
412
+ }: Omit<React.ComponentProps<typeof Select>, "collection" | "value" | "onValueChange"> & {
413
+ /** Text before the select. Defaults to "Match" at the top level and "Where" in nested groups. */
414
+ before?: React.ReactNode
415
+ after?: React.ReactNode
416
+ }) {
417
+ const { setMatch, disabled } = useQueryBuilder()
418
+ const { group, depth } = useQueryBuilderGroup()
419
+ return (
420
+ <span data-slot="query-builder-match" className="inline-flex flex-wrap items-center gap-1.5">
421
+ <span className="text-muted-foreground">{before ?? (depth === 0 ? "Match" : "Where")}</span>
422
+ <Select
423
+ collection={matchCollection}
424
+ value={[group.match]}
425
+ onValueChange={({ value }) => value[0] && setMatch(group.id, value[0] as QueryGroup["match"])}
426
+ disabled={disabled}
427
+ positioning={{ sameWidth: false }}
428
+ {...props}
429
+ >
430
+ <SelectControl>
431
+ <SelectTrigger size="sm" className={cn("font-medium", className)} aria-label="Match all or any">
432
+ <SelectValue>{group.match}</SelectValue>
433
+ </SelectTrigger>
434
+ </SelectControl>
435
+ <SelectContent className="min-w-24">
436
+ {matchOptions.map((o) => (
437
+ <SelectItem key={o.value} item={o}>
438
+ <SelectItemText>{o.label}</SelectItemText>
439
+ <SelectItemIndicator />
440
+ </SelectItem>
441
+ ))}
442
+ </SelectContent>
443
+ </Select>
444
+ <span className="text-muted-foreground">{after}</span>
445
+ </span>
446
+ )
447
+ }
448
+
449
+ /** Indented list of conditions with a guide line. */
450
+ function QueryBuilderGroupBody({ className, ...props }: React.ComponentProps<"div">) {
451
+ const { group } = useQueryBuilderGroup()
452
+ return (
453
+ <div
454
+ data-slot="query-builder-group-body"
455
+ data-empty={group.rules.length === 0 ? "" : undefined}
456
+ className={cn(
457
+ "relative flex flex-col gap-2 ps-4 before:absolute before:inset-y-1 before:inset-s-1 before:w-px before:bg-border",
458
+ className
459
+ )}
460
+ {...props}
461
+ />
462
+ )
463
+ }
464
+
465
+ function QueryBuilderGroupFooter({ className, ...props }: React.ComponentProps<"div">) {
466
+ return (
467
+ <div
468
+ data-slot="query-builder-group-footer"
469
+ className={cn("flex flex-wrap items-center gap-1 ps-4", className)}
470
+ {...props}
471
+ />
472
+ )
473
+ }
474
+
475
+ /* ---------------------------------------------------------------------------
476
+ * Rule parts
477
+ * ------------------------------------------------------------------------- */
478
+
479
+ function QueryBuilderRule({ rule, className, children, ...props }: React.ComponentProps<"div"> & { rule: QueryRule }) {
480
+ const { operators } = useQueryBuilder()
481
+ const complete = isComplete(rule, operators)
482
+ const ctx = React.useMemo(() => ({ rule }), [rule])
483
+ return (
484
+ <RuleContext.Provider value={ctx}>
485
+ <div
486
+ data-slot="query-builder-rule"
487
+ data-incomplete={complete ? undefined : ""}
488
+ className={cn(
489
+ "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",
490
+ className
491
+ )}
492
+ {...props}
493
+ >
494
+ {children}
495
+ </div>
496
+ </RuleContext.Provider>
497
+ )
498
+ }
499
+
500
+ type PickerProps = Omit<React.ComponentProps<typeof Select>, "collection" | "value" | "onValueChange">
501
+
502
+ function QueryBuilderFieldSelect({ className, ...props }: PickerProps & { className?: string }) {
503
+ const { fields, updateRule, disabled } = useQueryBuilder()
504
+ const { rule } = useQueryBuilderRule()
505
+ const collection = React.useMemo(
506
+ () => createListCollection({ items: fields, itemToValue: (f) => f.name, itemToString: (f) => f.label }),
507
+ [fields]
508
+ )
509
+ const field = fields.find((f) => f.name === rule.field)
510
+ return (
511
+ <Select
512
+ collection={collection}
513
+ value={[rule.field]}
514
+ onValueChange={({ value }) => value[0] && updateRule(rule.id, { field: value[0] })}
515
+ disabled={disabled}
516
+ positioning={{ sameWidth: false }}
517
+ {...props}
518
+ >
519
+ <SelectControl>
520
+ <SelectTrigger
521
+ size="sm"
522
+ className={cn("font-medium", className)}
523
+ data-slot="query-builder-field-select"
524
+ aria-label="Field"
525
+ >
526
+ <SelectValue placeholder="Field">{field?.label}</SelectValue>
527
+ </SelectTrigger>
528
+ </SelectControl>
529
+ <SelectContent>
530
+ {fields.map((f) => (
531
+ <SelectItem key={f.name} item={f}>
532
+ <SelectItemText>{f.label}</SelectItemText>
533
+ <SelectItemIndicator />
534
+ </SelectItem>
535
+ ))}
536
+ </SelectContent>
537
+ </Select>
538
+ )
539
+ }
540
+
541
+ function QueryBuilderOperatorSelect({ className, ...props }: PickerProps & { className?: string }) {
542
+ const { fields, operators, updateRule, disabled } = useQueryBuilder()
543
+ const { rule } = useQueryBuilderRule()
544
+ const field = fields.find((f) => f.name === rule.field)
545
+ const options = React.useMemo(() => operatorsFor(field, operators), [field, operators])
546
+ const collection = React.useMemo(
547
+ () => createListCollection({ items: options, itemToValue: (o) => o.name, itemToString: (o) => o.label }),
548
+ [options]
549
+ )
550
+ const current = options.find((o) => o.name === rule.operator)
551
+ return (
552
+ <Select
553
+ collection={collection}
554
+ value={[rule.operator]}
555
+ onValueChange={({ value }) => value[0] && updateRule(rule.id, { operator: value[0] })}
556
+ disabled={disabled}
557
+ positioning={{ sameWidth: false }}
558
+ {...props}
559
+ >
560
+ <SelectControl>
561
+ <SelectTrigger
562
+ size="sm"
563
+ className={cn("text-muted-foreground", className)}
564
+ data-slot="query-builder-operator-select"
565
+ aria-label="Operator"
566
+ >
567
+ <SelectValue placeholder="Operator">{current?.label}</SelectValue>
568
+ </SelectTrigger>
569
+ </SelectControl>
570
+ <SelectContent>
571
+ {options.map((o) => (
572
+ <SelectItem key={o.name} item={o}>
573
+ <SelectItemText>{o.label}</SelectItemText>
574
+ <SelectItemIndicator />
575
+ </SelectItem>
576
+ ))}
577
+ </SelectContent>
578
+ </Select>
579
+ )
580
+ }
581
+
582
+ type ValueEditorRenderProps = {
583
+ rule: QueryRule
584
+ field: QueryField | undefined
585
+ operator: QueryOperator | undefined
586
+ value: unknown
587
+ setValue: (value: unknown) => void
588
+ disabled: boolean
589
+ }
590
+
591
+ /**
592
+ * Renders an editor for the rule's value based on the field type and operator
593
+ * arity. Pass a render-prop child to take over for specific cases and fall back
594
+ * to the default by returning `undefined`.
595
+ */
596
+ function QueryBuilderValueEditor({
597
+ className,
598
+ children,
599
+ ...props
600
+ }: Omit<React.ComponentProps<"div">, "children"> & {
601
+ children?: (props: ValueEditorRenderProps) => React.ReactNode | undefined
602
+ }) {
603
+ const { fields, operators, updateRule, disabled } = useQueryBuilder()
604
+ const { rule } = useQueryBuilderRule()
605
+ const field = fields.find((f) => f.name === rule.field)
606
+ const operator = operators.find((o) => o.name === rule.operator)
607
+ const setValue = React.useCallback((value: unknown) => updateRule(rule.id, { value }), [updateRule, rule.id])
608
+ const custom = children?.({ rule, field, operator, value: rule.value, setValue, disabled })
609
+ if (custom !== undefined) return <>{custom}</>
610
+ if (!operator || operator.arity === "none") return null
611
+ const type = field?.type ?? "text"
612
+ const inputType = type === "number" ? "number" : type === "date" ? "date" : "text"
613
+ const inputClass = cn("h-7 w-40 text-sm md:text-sm", type === "number" && "w-24", type === "date" && "w-36")
614
+
615
+ if (operator.arity === "two") {
616
+ const pair = Array.isArray(rule.value) ? (rule.value as unknown[]) : [undefined, undefined]
617
+ const set = (index: 0 | 1, v: string) => {
618
+ const next = [...pair]
619
+ next[index] = v
620
+ setValue(next)
621
+ }
622
+ return (
623
+ <div
624
+ data-slot="query-builder-value-editor"
625
+ className={cn("inline-flex items-center gap-1.5", className)}
626
+ {...props}
627
+ >
628
+ <Input
629
+ type={inputType}
630
+ value={String(pair[0] ?? "")}
631
+ onChange={(e) => set(0, e.target.value)}
632
+ disabled={disabled}
633
+ aria-label="From"
634
+ className={inputClass}
635
+ />
636
+ <span className="text-muted-foreground">and</span>
637
+ <Input
638
+ type={inputType}
639
+ value={String(pair[1] ?? "")}
640
+ onChange={(e) => set(1, e.target.value)}
641
+ disabled={disabled}
642
+ aria-label="To"
643
+ className={inputClass}
644
+ />
645
+ </div>
646
+ )
647
+ }
648
+
649
+ if (type === "select" && field?.options) {
650
+ const many = operator.arity === "many"
651
+ const selected = many ? ((rule.value as string[] | undefined) ?? []) : rule.value ? [String(rule.value)] : []
652
+ return (
653
+ <div data-slot="query-builder-value-editor" className={cn("inline-flex", className)} {...props}>
654
+ <OptionPicker
655
+ options={field.options}
656
+ value={selected}
657
+ multiple={many}
658
+ disabled={disabled}
659
+ placeholder="Choose…"
660
+ onChange={(values) => setValue(many ? values : values[0])}
661
+ />
662
+ </div>
663
+ )
664
+ }
665
+
666
+ if (operator.arity === "many") {
667
+ // Free-text lists: comma separated, shown as-is.
668
+ const list = Array.isArray(rule.value) ? (rule.value as string[]) : []
669
+ return (
670
+ <div data-slot="query-builder-value-editor" className={cn("inline-flex", className)} {...props}>
671
+ <Input
672
+ value={list.join(", ")}
673
+ onChange={(e) =>
674
+ setValue(
675
+ e.target.value
676
+ .split(",")
677
+ .map((s) => s.trim())
678
+ .filter(Boolean)
679
+ )
680
+ }
681
+ placeholder={field?.placeholder ?? "Comma separated"}
682
+ disabled={disabled}
683
+ aria-label="Values"
684
+ className={cn(inputClass, "w-56")}
685
+ />
686
+ </div>
687
+ )
688
+ }
689
+
690
+ return (
691
+ <div data-slot="query-builder-value-editor" className={cn("inline-flex", className)} {...props}>
692
+ <Input
693
+ type={inputType}
694
+ value={String(rule.value ?? "")}
695
+ onChange={(e) => setValue(e.target.value)}
696
+ placeholder={field?.placeholder ?? (type === "text" ? "Value" : undefined)}
697
+ disabled={disabled}
698
+ aria-label="Value"
699
+ className={inputClass}
700
+ />
701
+ </div>
702
+ )
703
+ }
704
+
705
+ function OptionPicker({
706
+ options,
707
+ value,
708
+ multiple,
709
+ disabled,
710
+ placeholder,
711
+ onChange,
712
+ }: {
713
+ options: QueryOption[]
714
+ value: string[]
715
+ multiple: boolean
716
+ disabled: boolean
717
+ placeholder: string
718
+ onChange: (values: string[]) => void
719
+ }) {
720
+ const collection = React.useMemo(
721
+ () => createListCollection({ items: options, itemToValue: (o) => o.value, itemToString: (o) => o.label }),
722
+ [options]
723
+ )
724
+ const labels = value.map((v) => options.find((o) => o.value === v)?.label ?? v)
725
+ return (
726
+ <Select
727
+ collection={collection}
728
+ value={value}
729
+ multiple={multiple}
730
+ closeOnSelect={!multiple}
731
+ onValueChange={({ value }) => onChange(value)}
732
+ disabled={disabled}
733
+ positioning={{ sameWidth: false }}
734
+ >
735
+ <SelectControl>
736
+ <SelectTrigger size="sm" className="max-w-64" aria-label="Value">
737
+ <SelectValue placeholder={placeholder}>
738
+ {labels.length
739
+ ? labels.length > 3
740
+ ? `${labels.slice(0, 3).join(", ")} +${labels.length - 3}`
741
+ : labels.join(", ")
742
+ : undefined}
743
+ </SelectValue>
744
+ </SelectTrigger>
745
+ </SelectControl>
746
+ <SelectContent>
747
+ {options.map((o) => (
748
+ <SelectItem key={o.value} item={o}>
749
+ <SelectItemText>{o.label}</SelectItemText>
750
+ <SelectItemIndicator />
751
+ </SelectItem>
752
+ ))}
753
+ </SelectContent>
754
+ </Select>
755
+ )
756
+ }
757
+
758
+ /** Hover-revealed slot at the end of a rule row (remove trigger, extras). */
759
+ function QueryBuilderRuleActions({ className, ...props }: React.ComponentProps<"div">) {
760
+ return (
761
+ <div
762
+ data-slot="query-builder-rule-actions"
763
+ className={cn(
764
+ "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",
765
+ className
766
+ )}
767
+ {...props}
768
+ />
769
+ )
770
+ }
771
+
772
+ /* ---------------------------------------------------------------------------
773
+ * Triggers (polymorphic via asChild)
774
+ * ------------------------------------------------------------------------- */
775
+
776
+ function QueryBuilderAddRuleTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {
777
+ const { addRule, disabled } = useQueryBuilder()
778
+ const { group } = useQueryBuilderGroup()
779
+ return (
780
+ <Button
781
+ data-slot="query-builder-add-rule-trigger"
782
+ variant="ghost"
783
+ size="sm"
784
+ asChild={asChild}
785
+ disabled={disabled}
786
+ onClick={(event) => {
787
+ onClick?.(event)
788
+ if (!event.defaultPrevented) addRule(group.id)
789
+ }}
790
+ {...props}
791
+ >
792
+ {asChild
793
+ ? children
794
+ : (children ?? (
795
+ <>
796
+ <PlusIcon /> Add condition
797
+ </>
798
+ ))}
799
+ </Button>
800
+ )
801
+ }
802
+
803
+ function QueryBuilderAddGroupTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {
804
+ const { addGroup, disabled, maxDepth } = useQueryBuilder()
805
+ const { group, depth } = useQueryBuilderGroup()
806
+ if (depth >= maxDepth) return null
807
+ return (
808
+ <Button
809
+ data-slot="query-builder-add-group-trigger"
810
+ variant="ghost"
811
+ size="sm"
812
+ asChild={asChild}
813
+ disabled={disabled}
814
+ className="text-muted-foreground"
815
+ onClick={(event) => {
816
+ onClick?.(event)
817
+ if (!event.defaultPrevented) addGroup(group.id)
818
+ }}
819
+ {...props}
820
+ >
821
+ {asChild ? children : (children ?? <>Add a nested group</>)}
822
+ </Button>
823
+ )
824
+ }
825
+
826
+ /** Removes the enclosing rule, or the enclosing group when used in a group header. */
827
+ function QueryBuilderRemoveTrigger({
828
+ asChild,
829
+ children,
830
+ onClick,
831
+ className,
832
+ ...props
833
+ }: React.ComponentProps<typeof Button>) {
834
+ const { remove, disabled } = useQueryBuilder()
835
+ const rule = React.useContext(RuleContext)
836
+ const group = React.useContext(GroupContext)
837
+ const target = rule?.rule.id ?? group?.group.id
838
+ const label = rule ? "Remove condition" : "Remove group"
839
+ return (
840
+ <Button
841
+ data-slot="query-builder-remove-trigger"
842
+ variant="ghost"
843
+ size="icon-xs"
844
+ asChild={asChild}
845
+ disabled={disabled}
846
+ aria-label={label}
847
+ className={cn("text-muted-foreground hover:text-destructive", className)}
848
+ onClick={(event) => {
849
+ onClick?.(event)
850
+ if (!event.defaultPrevented && target) remove(target)
851
+ }}
852
+ {...props}
853
+ >
854
+ {asChild ? children : (children ?? <XIcon />)}
855
+ </Button>
856
+ )
857
+ }
858
+
859
+ /* ---------------------------------------------------------------------------
860
+ * Summary and empty state
861
+ * ------------------------------------------------------------------------- */
862
+
863
+ /** The query as a sentence; `children` overrides the fallback when nothing is set. */
864
+ function QueryBuilderSummary({
865
+ className,
866
+ children,
867
+ prefix = "Showing results where ",
868
+ ...props
869
+ }: React.ComponentProps<"p"> & { prefix?: React.ReactNode }) {
870
+ const { value, fields, operators } = useQueryBuilder()
871
+ const text = describeQuery(value, { fields, operators })
872
+ return (
873
+ <p
874
+ data-slot="query-builder-summary"
875
+ data-empty={text ? undefined : ""}
876
+ className={cn("text-sm", className)}
877
+ {...props}
878
+ >
879
+ {text ? (
880
+ <>
881
+ <span className="text-muted-foreground">{prefix}</span>
882
+ <span className="font-medium">{text}</span>
883
+ </>
884
+ ) : (
885
+ (children ?? (
886
+ <span className="text-muted-foreground">Showing everything. Add a condition to narrow it down.</span>
887
+ ))
888
+ )}
889
+ </p>
890
+ )
891
+ }
892
+
893
+ function QueryBuilderEmpty({ className, ...props }: React.ComponentProps<typeof ark.div>) {
894
+ return (
895
+ <ark.div
896
+ data-slot="query-builder-empty"
897
+ className={cn("rounded-lg border border-dashed px-3 py-2 text-sm text-muted-foreground", className)}
898
+ {...props}
899
+ />
900
+ )
901
+ }
902
+
903
+ export {
904
+ QueryBuilder,
905
+ QueryBuilderGroup,
906
+ QueryBuilderGroupHeader,
907
+ QueryBuilderMatch,
908
+ QueryBuilderGroupBody,
909
+ QueryBuilderGroupFooter,
910
+ QueryBuilderRule,
911
+ QueryBuilderFieldSelect,
912
+ QueryBuilderOperatorSelect,
913
+ QueryBuilderValueEditor,
914
+ QueryBuilderRuleActions,
915
+ QueryBuilderAddRuleTrigger,
916
+ QueryBuilderAddGroupTrigger,
917
+ QueryBuilderRemoveTrigger,
918
+ QueryBuilderSummary,
919
+ QueryBuilderEmpty,
920
+ useQueryBuilder,
921
+ useQueryBuilderGroup,
922
+ useQueryBuilderRule,
923
+ createRule,
924
+ createGroup,
925
+ isGroup,
926
+ isComplete,
927
+ operatorsFor,
928
+ defaultOperators,
929
+ describeQuery,
930
+ pruneQuery,
931
+ type QueryField,
932
+ type QueryFieldType,
933
+ type QueryOption,
934
+ type QueryOperator,
935
+ type QueryRule,
936
+ type QueryGroup,
937
+ type QueryNode,
938
+ }