@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,18 @@
1
+ {
2
+ "name": "number-input",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "utils"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/number-input.tsx",
15
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { NumberInput as NumberInputPrimitive } from \"@ark-ui/react\"\nimport { ChevronDownIcon, ChevronUpIcon } from \"lucide-react\"\n\nfunction NumberInput({ className, children, ...props }: React.ComponentProps<typeof NumberInputPrimitive.Root>) {\n return (\n <NumberInputPrimitive.Root\n data-slot=\"number-input\"\n className={cn(\"flex w-full flex-col gap-1.5\", className)}\n {...props}\n >\n {children ?? (\n <NumberInputControl>\n <NumberInputInput />\n <NumberInputTriggers />\n </NumberInputControl>\n )}\n </NumberInputPrimitive.Root>\n )\n}\n\nfunction NumberInputContext({ ...props }: React.ComponentProps<typeof NumberInputPrimitive.Context>) {\n return <NumberInputPrimitive.Context {...props} />\n}\n\nfunction NumberInputLabel({ className, ...props }: React.ComponentProps<typeof NumberInputPrimitive.Label>) {\n return (\n <NumberInputPrimitive.Label\n data-slot=\"number-input-label\"\n className={cn(\"text-sm font-medium select-none\", className)}\n {...props}\n />\n )\n}\n\nfunction NumberInputControl({ className, ...props }: React.ComponentProps<typeof NumberInputPrimitive.Control>) {\n return (\n <NumberInputPrimitive.Control\n data-slot=\"number-input-control\"\n className={cn(\n \"flex h-8 w-full min-w-0 items-stretch overflow-hidden rounded-lg border border-input bg-transparent transition-colors has-focus-visible:border-ring has-focus-visible:ring-3 has-focus-visible:ring-ring/50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 dark:bg-input/30 dark:data-invalid:border-destructive/50 dark:data-invalid:ring-destructive/40 data-disabled:pointer-events-none data-disabled:opacity-50\",\n className\n )}\n {...props}\n />\n )\n}\n\n/** `id` is omitted: set part ids through the root `ids` prop so Ark's internal lookups keep working. */\nfunction NumberInputInput({\n className,\n ...props\n}: Omit<React.ComponentProps<typeof NumberInputPrimitive.Input>, \"id\">) {\n return (\n <NumberInputPrimitive.Input\n data-slot=\"number-input-input\"\n className={cn(\n \"min-w-0 flex-1 bg-transparent px-2.5 py-1 text-base outline-none placeholder:text-muted-foreground md:text-sm\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction NumberInputTriggers({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"number-input-triggers\"\n className={cn(\"flex w-6 flex-col border-l border-input\", className)}\n {...props}\n >\n <NumberInputIncrementTrigger />\n <NumberInputDecrementTrigger />\n </div>\n )\n}\n\nconst triggerClassName =\n \"flex flex-1 items-center justify-center text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-3\"\n\nfunction NumberInputIncrementTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof NumberInputPrimitive.IncrementTrigger>) {\n return (\n <NumberInputPrimitive.IncrementTrigger\n data-slot=\"number-input-increment-trigger\"\n className={cn(triggerClassName, \"border-b border-input\", className)}\n {...props}\n >\n {children ?? <ChevronUpIcon />}\n </NumberInputPrimitive.IncrementTrigger>\n )\n}\n\nfunction NumberInputDecrementTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof NumberInputPrimitive.DecrementTrigger>) {\n return (\n <NumberInputPrimitive.DecrementTrigger\n data-slot=\"number-input-decrement-trigger\"\n className={cn(triggerClassName, className)}\n {...props}\n >\n {children ?? <ChevronDownIcon />}\n </NumberInputPrimitive.DecrementTrigger>\n )\n}\n\nfunction NumberInputScrubber({ className, ...props }: React.ComponentProps<typeof NumberInputPrimitive.Scrubber>) {\n return (\n <NumberInputPrimitive.Scrubber\n data-slot=\"number-input-scrubber\"\n className={cn(\"cursor-ew-resize select-none\", className)}\n {...props}\n />\n )\n}\n\nfunction NumberInputValueText({ className, ...props }: React.ComponentProps<typeof NumberInputPrimitive.ValueText>) {\n return (\n <NumberInputPrimitive.ValueText\n data-slot=\"number-input-value-text\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nexport {\n NumberInput,\n NumberInputContext,\n NumberInputControl,\n NumberInputDecrementTrigger,\n NumberInputIncrementTrigger,\n NumberInputInput,\n NumberInputLabel,\n NumberInputScrubber,\n NumberInputTriggers,\n NumberInputValueText,\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "pagination",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "lucide-react": "^1.41.0"
7
+ },
8
+ "registryDependencies": [
9
+ "button",
10
+ "utils"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/pagination.tsx",
15
+ "content": "import * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from \"lucide-react\"\n\nfunction Pagination({ className, ...props }: React.ComponentProps<\"nav\">) {\n return (\n <nav\n role=\"navigation\"\n aria-label=\"pagination\"\n data-slot=\"pagination\"\n className={cn(\"mx-auto flex w-full justify-center\", className)}\n {...props}\n />\n )\n}\n\nfunction PaginationContent({ className, ...props }: React.ComponentProps<\"ul\">) {\n return <ul data-slot=\"pagination-content\" className={cn(\"flex items-center gap-0.5\", className)} {...props} />\n}\n\nfunction PaginationItem({ ...props }: React.ComponentProps<\"li\">) {\n return <li data-slot=\"pagination-item\" {...props} />\n}\n\ntype PaginationLinkProps = {\n isActive?: boolean\n} & Pick<React.ComponentProps<typeof Button>, \"size\"> &\n React.ComponentProps<\"a\">\n\nfunction PaginationLink({ className, isActive, size = \"icon\", ...props }: PaginationLinkProps) {\n return (\n <Button asChild variant={isActive ? \"outline\" : \"ghost\"} size={size} className={cn(className)}>\n <a aria-current={isActive ? \"page\" : undefined} data-slot=\"pagination-link\" data-active={isActive} {...props} />\n </Button>\n )\n}\n\nfunction PaginationPrevious({\n className,\n text = \"Previous\",\n ...props\n}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {\n return (\n <PaginationLink aria-label=\"Go to previous page\" size=\"default\" className={cn(\"pl-1.5!\", className)} {...props}>\n <ChevronLeftIcon data-icon=\"inline-start\" />\n <span className=\"hidden sm:block\">{text}</span>\n </PaginationLink>\n )\n}\n\nfunction PaginationNext({\n className,\n text = \"Next\",\n ...props\n}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {\n return (\n <PaginationLink aria-label=\"Go to next page\" size=\"default\" className={cn(\"pr-1.5!\", className)} {...props}>\n <span className=\"hidden sm:block\">{text}</span>\n <ChevronRightIcon data-icon=\"inline-end\" />\n </PaginationLink>\n )\n}\n\nfunction PaginationEllipsis({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n aria-hidden\n data-slot=\"pagination-ellipsis\"\n className={cn(\"flex size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4\", className)}\n {...props}\n >\n <MoreHorizontalIcon />\n <span className=\"sr-only\">More pages</span>\n </span>\n )\n}\n\nexport {\n Pagination,\n PaginationContent,\n PaginationEllipsis,\n PaginationItem,\n PaginationLink,\n PaginationNext,\n PaginationPrevious,\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "password-input",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "utils"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/password-input.tsx",
15
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { PasswordInput as PasswordInputPrimitive } from \"@ark-ui/react\"\nimport { EyeIcon, EyeOffIcon } from \"lucide-react\"\n\nfunction PasswordInput({ className, children, ...props }: React.ComponentProps<typeof PasswordInputPrimitive.Root>) {\n return (\n <PasswordInputPrimitive.Root\n data-slot=\"password-input\"\n className={cn(\"flex w-full flex-col gap-1.5\", className)}\n {...props}\n >\n {children ?? (\n <PasswordInputControl>\n <PasswordInputInput />\n <PasswordInputVisibilityTrigger />\n </PasswordInputControl>\n )}\n </PasswordInputPrimitive.Root>\n )\n}\n\nfunction PasswordInputContext({ ...props }: React.ComponentProps<typeof PasswordInputPrimitive.Context>) {\n return <PasswordInputPrimitive.Context {...props} />\n}\n\nfunction PasswordInputLabel({ className, ...props }: React.ComponentProps<typeof PasswordInputPrimitive.Label>) {\n return (\n <PasswordInputPrimitive.Label\n data-slot=\"password-input-label\"\n className={cn(\"text-sm font-medium select-none\", className)}\n {...props}\n />\n )\n}\n\nfunction PasswordInputControl({ className, ...props }: React.ComponentProps<typeof PasswordInputPrimitive.Control>) {\n return (\n <PasswordInputPrimitive.Control\n data-slot=\"password-input-control\"\n className={cn(\n \"flex h-8 w-full min-w-0 items-center rounded-lg border border-input bg-transparent pr-1 transition-colors has-focus-visible:border-ring has-focus-visible:ring-3 has-focus-visible:ring-ring/50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 dark:bg-input/30 dark:data-invalid:border-destructive/50 dark:data-invalid:ring-destructive/40 data-disabled:pointer-events-none data-disabled:opacity-50\",\n className\n )}\n {...props}\n />\n )\n}\n\n/** `id` is omitted: set part ids through the root `ids` prop so Ark's internal lookups keep working. */\nfunction PasswordInputInput({\n className,\n ...props\n}: Omit<React.ComponentProps<typeof PasswordInputPrimitive.Input>, \"id\">) {\n return (\n <PasswordInputPrimitive.Input\n data-slot=\"password-input-input\"\n className={cn(\n \"min-w-0 flex-1 bg-transparent px-2.5 py-1 text-base outline-none placeholder:text-muted-foreground md:text-sm\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction PasswordInputVisibilityTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof PasswordInputPrimitive.VisibilityTrigger>) {\n return (\n <PasswordInputPrimitive.VisibilityTrigger\n data-slot=\"password-input-visibility-trigger\"\n className={cn(\n \"inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none [&_svg]:size-4\",\n className\n )}\n {...props}\n >\n {children ?? (\n <PasswordInputIndicator fallback={<EyeOffIcon />}>\n <EyeIcon />\n </PasswordInputIndicator>\n )}\n </PasswordInputPrimitive.VisibilityTrigger>\n )\n}\n\nfunction PasswordInputIndicator({ ...props }: React.ComponentProps<typeof PasswordInputPrimitive.Indicator>) {\n return <PasswordInputPrimitive.Indicator data-slot=\"password-input-indicator\" {...props} />\n}\n\nexport {\n PasswordInput,\n PasswordInputContext,\n PasswordInputControl,\n PasswordInputIndicator,\n PasswordInputInput,\n PasswordInputLabel,\n PasswordInputVisibilityTrigger,\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "popconfirm",
3
+ "type": "ui",
4
+ "description": "Popconfirm — a lightweight inline confirmation on Ark's Popover, for actions that need a \"are you sure?\" without a modal dialog. Compose it: `Popconfirm` > `PopconfirmTrigger` + `PopconfirmContent` > (`PopconfirmHeader` > `PopconfirmIcon` +",
5
+ "dependencies": {
6
+ "lucide-react": "^1.41.0"
7
+ },
8
+ "registryDependencies": [
9
+ "button",
10
+ "popover",
11
+ "utils"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/ui/popconfirm.tsx",
16
+ "content": "import * as React from \"react\"\nimport { AlertTriangleIcon } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Popover, PopoverArrow, PopoverClose, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\"\nimport { cn } from \"@/lib/utils\"\n\n/**\n * Popconfirm — a lightweight inline confirmation on Ark's Popover, for\n * actions that need a \"are you sure?\" without a modal dialog. Compose it:\n * `Popconfirm` > `PopconfirmTrigger` + `PopconfirmContent` > (`PopconfirmHeader` >\n * `PopconfirmIcon` + `PopconfirmTitle` + `PopconfirmDescription`) + `PopconfirmFooter` >\n * `PopconfirmCancelTrigger` + `PopconfirmConfirmTrigger`. `PopconfirmConfirmTrigger` closes after `onConfirm`\n * unless the handler calls `event.preventDefault()`.\n */\n\nfunction Popconfirm({ positioning, ...props }: React.ComponentProps<typeof Popover>) {\n return <Popover data-slot=\"popconfirm\" positioning={{ placement: \"top\", gutter: 8, ...positioning }} {...props} />\n}\n\nfunction PopconfirmTrigger({ ...props }: React.ComponentProps<typeof PopoverTrigger>) {\n return <PopoverTrigger data-slot=\"popconfirm-trigger\" {...props} />\n}\n\nfunction PopconfirmContent({\n className,\n children,\n showArrow = true,\n ...props\n}: React.ComponentProps<typeof PopoverContent> & { showArrow?: boolean }) {\n return (\n <PopoverContent\n data-slot=\"popconfirm-content\"\n role=\"alertdialog\"\n className={cn(\"w-72 gap-3 p-3\", className)}\n {...props}\n >\n {showArrow && <PopoverArrow />}\n {children}\n </PopoverContent>\n )\n}\n\nfunction PopconfirmHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"popconfirm-header\"\n className={cn(\n \"grid grid-cols-[auto_1fr] gap-x-0 gap-y-0.5 has-[>[data-slot=popconfirm-icon]]:gap-x-2\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction PopconfirmIcon({ className, children, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"popconfirm-icon\"\n className={cn(\n \"row-span-2 mt-0.5 flex size-5 items-center justify-center text-destructive [&_svg]:size-4\",\n className\n )}\n {...props}\n >\n {children ?? <AlertTriangleIcon />}\n </span>\n )\n}\n\nfunction PopconfirmTitle({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p data-slot=\"popconfirm-title\" className={cn(\"col-start-2 text-sm/tight font-semibold\", className)} {...props} />\n )\n}\n\nfunction PopconfirmDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"popconfirm-description\"\n className={cn(\"col-start-2 text-xs text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction PopconfirmFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div data-slot=\"popconfirm-footer\" className={cn(\"flex items-center justify-end gap-2\", className)} {...props} />\n )\n}\n\nfunction PopconfirmCancelTrigger({\n variant = \"outline\",\n size = \"sm\",\n children = \"Cancel\",\n ...props\n}: React.ComponentProps<typeof Button>) {\n return (\n <PopoverClose asChild>\n <Button data-slot=\"popconfirm-cancel-trigger\" variant={variant} size={size} {...props}>\n {children}\n </Button>\n </PopoverClose>\n )\n}\n\nfunction PopconfirmConfirmTrigger({\n variant = \"destructive\",\n size = \"sm\",\n onConfirm,\n onClick,\n children = \"Confirm\",\n ...props\n}: React.ComponentProps<typeof Button> & {\n /** Runs on click; call `event.preventDefault()` to keep the popconfirm open. */\n onConfirm?: (event: React.MouseEvent<HTMLButtonElement>) => void\n}) {\n const [stayOpen, setStayOpen] = React.useState(false)\n const button = (\n <Button\n data-slot=\"popconfirm-confirm-trigger\"\n variant={variant}\n size={size}\n onClick={(event) => {\n onClick?.(event)\n onConfirm?.(event)\n setStayOpen(event.defaultPrevented)\n }}\n {...props}\n >\n {children}\n </Button>\n )\n return stayOpen ? button : <PopoverClose asChild>{button}</PopoverClose>\n}\n\nexport {\n Popconfirm,\n PopconfirmCancelTrigger,\n PopconfirmConfirmTrigger,\n PopconfirmContent,\n PopconfirmDescription,\n PopconfirmFooter,\n PopconfirmHeader,\n PopconfirmIcon,\n PopconfirmTitle,\n PopconfirmTrigger,\n}\n"
17
+ }
18
+ ],
19
+ "docs": "## Popconfirm\n`src/components/ui/popconfirm.tsx` is an inline confirmation on the Popover (no modal):\n`Popconfirm` (placement `top`), `PopconfirmTrigger`, `PopconfirmContent` (`role=\"alertdialog\"`,\narrow by default), `PopconfirmHeader` > `PopconfirmIcon` (destructive triangle by default) +\n`PopconfirmTitle` + `PopconfirmDescription`, `PopconfirmFooter` > `PopconfirmCancelTrigger` (closes)\n+ `PopconfirmConfirmTrigger` (`onConfirm`, destructive by default, closes unless the handler calls\n`preventDefault`). The form builder uses it for field removal."
20
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "popover",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "utils"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/popover.tsx",
15
+ "content": "import * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { ChevronDownIcon } from \"lucide-react\"\nimport { Popover as PopoverPrimitive, Portal as PortalPrimitive } from \"@ark-ui/react\"\n\nfunction Popover({\n positioning,\n lazyMount = true,\n unmountOnExit = true,\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n return (\n <PopoverPrimitive.Root\n positioning={{ placement: \"bottom\", gutter: 4, ...positioning }}\n lazyMount={lazyMount}\n unmountOnExit={unmountOnExit}\n {...props}\n />\n )\n}\n\nfunction PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />\n}\n\nfunction PopoverPortal({ ...props }: React.ComponentProps<typeof PortalPrimitive>) {\n return <PortalPrimitive {...props} />\n}\n\nfunction PopoverContext({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Context>) {\n return <PopoverPrimitive.Context {...props} />\n}\n\nfunction PopoverPositioner({ className, ...props }: React.ComponentProps<typeof PopoverPrimitive.Positioner>) {\n return (\n <PopoverPrimitive.Positioner\n data-slot=\"popover-positioner\"\n className={cn(\"[--z-index:50]\", className)}\n {...props}\n />\n )\n}\n\nfunction PopoverContent({ className, ...props }: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n return (\n <PopoverPortal>\n <PopoverPositioner>\n <PopoverPrimitive.Content\n data-slot=\"popover-content\"\n className={cn(\n \"z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n className\n )}\n {...props}\n />\n </PopoverPositioner>\n </PopoverPortal>\n )\n}\n\nfunction PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />\n}\n\nfunction PopoverArrow({ className, ...props }: React.ComponentProps<typeof PopoverPrimitive.Arrow>) {\n return (\n <PopoverPrimitive.Arrow\n data-slot=\"popover-arrow\"\n className={cn(\"[--arrow-background:var(--color-popover)] [--arrow-size:0.625rem]\", className)}\n {...props}\n >\n <PopoverPrimitive.ArrowTip data-slot=\"popover-arrow-tip\" className=\"border-t border-l border-foreground/10\" />\n </PopoverPrimitive.Arrow>\n )\n}\n\nfunction PopoverClose({ ...props }: React.ComponentProps<typeof PopoverPrimitive.CloseTrigger>) {\n return <PopoverPrimitive.CloseTrigger data-slot=\"popover-close\" {...props} />\n}\n\nfunction PopoverHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"popover-header\" className={cn(\"flex flex-col gap-0.5 text-sm\", className)} {...props} />\n}\n\nfunction PopoverTitle({ className, ...props }: React.ComponentProps<typeof PopoverPrimitive.Title>) {\n return <PopoverPrimitive.Title data-slot=\"popover-title\" className={cn(\"font-medium\", className)} {...props} />\n}\n\nfunction PopoverDescription({ className, ...props }: React.ComponentProps<typeof PopoverPrimitive.Description>) {\n return (\n <PopoverPrimitive.Description\n data-slot=\"popover-description\"\n className={cn(\"text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\n/** Open-state indicator for a trigger; rotates when open. */\nfunction PopoverIndicator({ className, children, ...props }: React.ComponentProps<typeof PopoverPrimitive.Indicator>) {\n return (\n <PopoverPrimitive.Indicator\n data-slot=\"popover-indicator\"\n className={cn(\"inline-flex transition-transform data-[state=open]:rotate-180 [&_svg]:size-4\", className)}\n {...props}\n >\n {children ?? <ChevronDownIcon />}\n </PopoverPrimitive.Indicator>\n )\n}\n\nexport {\n Popover,\n PopoverAnchor,\n PopoverArrow,\n PopoverClose,\n PopoverContent,\n PopoverContext,\n PopoverDescription,\n PopoverHeader,\n PopoverPortal,\n PopoverPositioner,\n PopoverTitle,\n PopoverTrigger,\n PopoverIndicator,\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "progress",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1"
7
+ },
8
+ "registryDependencies": [
9
+ "utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/progress.tsx",
14
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Progress as ProgressPrimitive } from \"@ark-ui/react\"\n\nfunction Progress({ className, children, ...props }: React.ComponentProps<typeof ProgressPrimitive.Root>) {\n return (\n <ProgressPrimitive.Root data-slot=\"progress\" className={cn(\"flex w-full flex-col gap-1\", className)} {...props}>\n {children}\n <ProgressTrack>\n <ProgressRange />\n </ProgressTrack>\n </ProgressPrimitive.Root>\n )\n}\n\nfunction ProgressRoot({ className, ...props }: React.ComponentProps<typeof ProgressPrimitive.Root>) {\n return (\n <ProgressPrimitive.Root data-slot=\"progress\" className={cn(\"flex w-full flex-col gap-1\", className)} {...props} />\n )\n}\n\nfunction ProgressContext({ ...props }: React.ComponentProps<typeof ProgressPrimitive.Context>) {\n return <ProgressPrimitive.Context {...props} />\n}\n\nfunction ProgressLabel({ className, ...props }: React.ComponentProps<typeof ProgressPrimitive.Label>) {\n return (\n <ProgressPrimitive.Label data-slot=\"progress-label\" className={cn(\"text-sm font-medium\", className)} {...props} />\n )\n}\n\nfunction ProgressValueText({ className, ...props }: React.ComponentProps<typeof ProgressPrimitive.ValueText>) {\n return (\n <ProgressPrimitive.ValueText\n data-slot=\"progress-value-text\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction ProgressTrack({ className, ...props }: React.ComponentProps<typeof ProgressPrimitive.Track>) {\n return (\n <ProgressPrimitive.Track\n data-slot=\"progress-track\"\n className={cn(\"relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-muted\", className)}\n {...props}\n />\n )\n}\n\nfunction ProgressRange({ className, ...props }: React.ComponentProps<typeof ProgressPrimitive.Range>) {\n return (\n <ProgressPrimitive.Range\n data-slot=\"progress-indicator\"\n className={cn(\n \"h-full bg-primary transition-all data-[state=indeterminate]:w-1/3 data-[state=indeterminate]:animate-pulse\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ProgressCircle({ className, ...props }: React.ComponentProps<typeof ProgressPrimitive.Circle>) {\n return (\n <ProgressPrimitive.Circle\n data-slot=\"progress-circle\"\n className={cn(\"[--size:2.5rem] [--thickness:3px]\", className)}\n {...props}\n />\n )\n}\n\nfunction ProgressCircleTrack({ className, ...props }: React.ComponentProps<typeof ProgressPrimitive.CircleTrack>) {\n return (\n <ProgressPrimitive.CircleTrack\n data-slot=\"progress-circle-track\"\n className={cn(\"stroke-muted\", className)}\n {...props}\n />\n )\n}\n\nfunction ProgressCircleRange({ className, ...props }: React.ComponentProps<typeof ProgressPrimitive.CircleRange>) {\n return (\n <ProgressPrimitive.CircleRange\n data-slot=\"progress-circle-range\"\n className={cn(\"stroke-primary transition-all\", className)}\n {...props}\n />\n )\n}\n\n/** Renders only while the progress is in the given `state` (`loading`, `complete`, `indeterminate`). */\nfunction ProgressView({ ...props }: React.ComponentProps<typeof ProgressPrimitive.View>) {\n return <ProgressPrimitive.View data-slot=\"progress-view\" {...props} />\n}\n\nexport {\n Progress,\n ProgressCircle,\n ProgressCircleRange,\n ProgressCircleTrack,\n ProgressContext,\n ProgressLabel,\n ProgressRange,\n ProgressRoot,\n ProgressTrack,\n ProgressValueText,\n ProgressView,\n}\n"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "qr-code",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1"
7
+ },
8
+ "registryDependencies": [
9
+ "utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/qr-code.tsx",
14
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { QrCode as QrCodePrimitive } from \"@ark-ui/react\"\n\nfunction QrCode({ className, children, ...props }: React.ComponentProps<typeof QrCodePrimitive.Root>) {\n return (\n <QrCodePrimitive.Root data-slot=\"qr-code\" className={cn(\"inline-flex\", className)} {...props}>\n {children ?? (\n <QrCodeFrame>\n <QrCodePattern />\n </QrCodeFrame>\n )}\n </QrCodePrimitive.Root>\n )\n}\n\nfunction QrCodeContext({ ...props }: React.ComponentProps<typeof QrCodePrimitive.Context>) {\n return <QrCodePrimitive.Context {...props} />\n}\n\nfunction QrCodeFrame({ className, ...props }: React.ComponentProps<typeof QrCodePrimitive.Frame>) {\n return (\n <QrCodePrimitive.Frame\n data-slot=\"qr-code-frame\"\n className={cn(\"size-40 rounded-lg border bg-background fill-foreground p-2\", className)}\n {...props}\n />\n )\n}\n\nfunction QrCodePattern({ ...props }: React.ComponentProps<typeof QrCodePrimitive.Pattern>) {\n return <QrCodePrimitive.Pattern data-slot=\"qr-code-pattern\" {...props} />\n}\n\nfunction QrCodeOverlay({ className, ...props }: React.ComponentProps<typeof QrCodePrimitive.Overlay>) {\n return (\n <QrCodePrimitive.Overlay\n data-slot=\"qr-code-overlay\"\n className={cn(\"flex items-center justify-center rounded-md bg-background p-1\", className)}\n {...props}\n />\n )\n}\n\nfunction QrCodeDownloadTrigger({ ...props }: React.ComponentProps<typeof QrCodePrimitive.DownloadTrigger>) {\n return <QrCodePrimitive.DownloadTrigger data-slot=\"qr-code-download-trigger\" {...props} />\n}\n\nexport { QrCode, QrCodeContext, QrCodeDownloadTrigger, QrCodeFrame, QrCodeOverlay, QrCodePattern }\n"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "query-builder",
3
+ "type": "ui",
4
+ "description": "`src/components/ui/query-builder.tsx` is a compositional filter builder designed for low cognitive load: it reads like a sentence instead of the usual AND/OR toggles per row.",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "button",
11
+ "controllable",
12
+ "input",
13
+ "select",
14
+ "utils"
15
+ ],
16
+ "files": [
17
+ {
18
+ "path": "components/ui/query-builder.tsx",
19
+ "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"
20
+ }
21
+ ],
22
+ "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."
23
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "radio-group",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1"
7
+ },
8
+ "registryDependencies": [
9
+ "utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/radio-group.tsx",
14
+ "content": "import * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { RadioGroup as RadioGroupPrimitive } from \"@ark-ui/react\"\n\nfunction RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n return <RadioGroupPrimitive.Root data-slot=\"radio-group\" className={cn(\"grid w-full gap-2\", className)} {...props} />\n}\n\nfunction RadioGroupContext({ ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Context>) {\n return <RadioGroupPrimitive.Context {...props} />\n}\n\nfunction RadioGroupLabel({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Label>) {\n return (\n <RadioGroupPrimitive.Label\n data-slot=\"radio-group-label\"\n className={cn(\"text-sm font-medium\", className)}\n {...props}\n />\n )\n}\n\nfunction RadioGroupIndicator({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Indicator>) {\n return (\n <RadioGroupPrimitive.Indicator\n data-slot=\"radio-group-indicator\"\n className={cn(\"rounded-md bg-background shadow-sm\", className)}\n {...props}\n />\n )\n}\n\n/** `id` is omitted: set part ids through the root `ids` prop so Ark's internal lookups keep working. */\nfunction RadioGroupItem({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof RadioGroupPrimitive.Item>, \"id\">) {\n return (\n <RadioGroupPrimitive.Item\n data-slot=\"radio-group-item\"\n className=\"group/radio-group-item peer inline-flex items-center gap-2\"\n {...props}\n >\n <RadioGroupItemControl className={className} />\n {children && <RadioGroupItemText>{children}</RadioGroupItemText>}\n <RadioGroupItemHiddenInput />\n </RadioGroupPrimitive.Item>\n )\n}\n\nfunction RadioGroupItemContext({ ...props }: React.ComponentProps<typeof RadioGroupPrimitive.ItemContext>) {\n return <RadioGroupPrimitive.ItemContext {...props} />\n}\n\n/** `id` is omitted: set part ids through the root `ids` prop so Ark's internal lookups keep working. */\nfunction RadioGroupItemControl({\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<typeof RadioGroupPrimitive.ItemControl>, \"id\">) {\n return (\n <RadioGroupPrimitive.ItemControl\n data-slot=\"radio-group-item-control\"\n className={cn(\n \"group/radio-group-item-control relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none group-has-focus-visible/field-label:ring-0 group-has-focus-visible/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 data-focus-visible:border-ring data-focus-visible:ring-3 data-focus-visible:ring-ring/50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 dark:bg-input/30 dark:data-invalid:border-destructive/50 dark:data-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-focus-visible/field-label:data-checked:border-primary data-invalid:data-checked:border-primary dark:data-checked:bg-primary data-disabled:cursor-not-allowed data-disabled:opacity-50\",\n className\n )}\n {...props}\n >\n {children ?? (\n <span\n data-slot=\"radio-group-indicator\"\n className=\"absolute top-1/2 left-1/2 hidden size-2 -translate-1/2 rounded-full bg-primary-foreground group-data-checked/radio-group-item-control:block\"\n />\n )}\n </RadioGroupPrimitive.ItemControl>\n )\n}\n\nfunction RadioGroupItemText({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.ItemText>) {\n return (\n <RadioGroupPrimitive.ItemText\n data-slot=\"radio-group-item-text\"\n className={cn(\"text-sm leading-none select-none data-disabled:opacity-50\", className)}\n {...props}\n />\n )\n}\n\n/** `id` is omitted: set part ids through the root `ids` prop so Ark's internal lookups keep working. */\nfunction RadioGroupItemHiddenInput({\n ...props\n}: Omit<React.ComponentProps<typeof RadioGroupPrimitive.ItemHiddenInput>, \"id\">) {\n return <RadioGroupPrimitive.ItemHiddenInput {...props} />\n}\n\nexport {\n RadioGroup,\n RadioGroupContext,\n RadioGroupIndicator,\n RadioGroupItem,\n RadioGroupItemContext,\n RadioGroupItemControl,\n RadioGroupItemHiddenInput,\n RadioGroupItemText,\n RadioGroupLabel,\n}\n"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "rating-group",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "utils"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/rating-group.tsx",
15
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { RatingGroup as RatingGroupPrimitive } from \"@ark-ui/react\"\nimport { StarIcon, StarHalfIcon } from \"lucide-react\"\n\nfunction RatingGroup({ className, children, ...props }: React.ComponentProps<typeof RatingGroupPrimitive.Root>) {\n return (\n <RatingGroupPrimitive.Root data-slot=\"rating-group\" className={cn(\"flex flex-col gap-1.5\", className)} {...props}>\n {children ?? (\n <RatingGroupControl>\n <RatingGroupContext>\n {({ items }) => items.map((index) => <RatingGroupItem key={index} index={index} />)}\n </RatingGroupContext>\n <RatingGroupHiddenInput />\n </RatingGroupControl>\n )}\n </RatingGroupPrimitive.Root>\n )\n}\n\nfunction RatingGroupContext({ ...props }: React.ComponentProps<typeof RatingGroupPrimitive.Context>) {\n return <RatingGroupPrimitive.Context {...props} />\n}\n\nfunction RatingGroupLabel({ className, ...props }: React.ComponentProps<typeof RatingGroupPrimitive.Label>) {\n return (\n <RatingGroupPrimitive.Label\n data-slot=\"rating-group-label\"\n className={cn(\"text-sm font-medium select-none\", className)}\n {...props}\n />\n )\n}\n\nfunction RatingGroupControl({ className, ...props }: React.ComponentProps<typeof RatingGroupPrimitive.Control>) {\n return (\n <RatingGroupPrimitive.Control\n data-slot=\"rating-group-control\"\n className={cn(\n \"inline-flex items-center gap-0.5 rounded-md outline-none focus-visible:ring-3 focus-visible:ring-ring/50\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction RatingGroupItem({ className, children, ...props }: React.ComponentProps<typeof RatingGroupPrimitive.Item>) {\n return (\n <RatingGroupPrimitive.Item\n data-slot=\"rating-group-item\"\n className={cn(\n \"group/rating-group-item inline-flex cursor-pointer text-muted-foreground/40 transition-colors data-highlighted:text-primary data-readonly:cursor-default data-disabled:cursor-not-allowed [&_svg]:size-5\",\n className\n )}\n {...props}\n >\n {children ?? (\n <RatingGroupPrimitive.ItemContext>\n {({ half, highlighted }) =>\n half ? (\n <StarHalfIcon className=\"fill-current\" />\n ) : (\n <StarIcon className={highlighted ? \"fill-current\" : undefined} />\n )\n }\n </RatingGroupPrimitive.ItemContext>\n )}\n </RatingGroupPrimitive.Item>\n )\n}\n\nfunction RatingGroupItemContext({ ...props }: React.ComponentProps<typeof RatingGroupPrimitive.ItemContext>) {\n return <RatingGroupPrimitive.ItemContext {...props} />\n}\n\nfunction RatingGroupHiddenInput({ ...props }: React.ComponentProps<typeof RatingGroupPrimitive.HiddenInput>) {\n return <RatingGroupPrimitive.HiddenInput {...props} />\n}\n\nexport {\n RatingGroup,\n RatingGroupContext,\n RatingGroupControl,\n RatingGroupHiddenInput,\n RatingGroupItem,\n RatingGroupItemContext,\n RatingGroupLabel,\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "resizable",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "react-resizable-panels": "^4.12.3"
7
+ },
8
+ "registryDependencies": [
9
+ "utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/resizable.tsx",
14
+ "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport * as ResizablePrimitive from \"react-resizable-panels\"\n\nfunction ResizablePanelGroup({ className, ...props }: ResizablePrimitive.GroupProps) {\n return (\n <ResizablePrimitive.Group\n data-slot=\"resizable-panel-group\"\n className={cn(\"flex size-full aria-[orientation=vertical]:flex-col\", className)}\n {...props}\n />\n )\n}\n\nfunction ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {\n return <ResizablePrimitive.Panel data-slot=\"resizable-panel\" {...props} />\n}\n\nfunction ResizableHandle({\n withHandle,\n className,\n ...props\n}: ResizablePrimitive.SeparatorProps & {\n withHandle?: boolean\n}) {\n return (\n <ResizablePrimitive.Separator\n data-slot=\"resizable-handle\"\n className={cn(\n \"relative flex w-px items-center justify-center bg-border ring-offset-background after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90\",\n className\n )}\n {...props}\n >\n {withHandle && <div className=\"z-10 flex h-6 w-1 shrink-0 rounded-lg bg-border\" />}\n </ResizablePrimitive.Separator>\n )\n}\n\nexport { ResizableHandle, ResizablePanel, ResizablePanelGroup }\n"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "rich-text-editor",
3
+ "type": "ui",
4
+ "description": "`src/components/ui/rich-text-editor.tsx` is a compositional editor on **tiptap** (`@tiptap/react`, StarterKit with link/underline, `@tiptap/extensions` Placeholder + CharacterCount, task list, highlight, text align, image, TableKit; `extens",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "@tiptap/extension-highlight": "^3.31.3",
8
+ "@tiptap/extension-image": "^3.31.3",
9
+ "@tiptap/extension-table": "^3.31.3",
10
+ "@tiptap/extension-task-item": "^3.31.3",
11
+ "@tiptap/extension-task-list": "^3.31.3",
12
+ "@tiptap/extension-text-align": "^3.31.3",
13
+ "@tiptap/extensions": "^3.31.3",
14
+ "@tiptap/react": "^3.31.3",
15
+ "@tiptap/starter-kit": "^3.31.3",
16
+ "lucide-react": "^1.41.0"
17
+ },
18
+ "registryDependencies": [
19
+ "button",
20
+ "input",
21
+ "kbd",
22
+ "popover",
23
+ "select",
24
+ "separator",
25
+ "toggle",
26
+ "tooltip",
27
+ "utils"
28
+ ],
29
+ "files": [
30
+ {
31
+ "path": "components/ui/rich-text-editor.tsx",
32
+ "content": "import * as React from \"react\"\nimport { ark } from \"@ark-ui/react\"\nimport { createListCollection } from \"@ark-ui/react/collection\"\nimport { Highlight } from \"@tiptap/extension-highlight\"\nimport { Image } from \"@tiptap/extension-image\"\nimport { TableKit } from \"@tiptap/extension-table\"\nimport { TaskItem } from \"@tiptap/extension-task-item\"\nimport { TaskList } from \"@tiptap/extension-task-list\"\nimport { TextAlign } from \"@tiptap/extension-text-align\"\nimport { CharacterCount, Placeholder } from \"@tiptap/extensions\"\nimport {\n EditorContent,\n useEditor,\n useEditorState,\n type Editor,\n type Extensions,\n type JSONContent,\n type UseEditorOptions,\n} from \"@tiptap/react\"\nimport { BubbleMenu, type BubbleMenuProps } from \"@tiptap/react/menus\"\nimport { StarterKit } from \"@tiptap/starter-kit\"\nimport {\n AlignCenterIcon,\n AlignJustifyIcon,\n AlignLeftIcon,\n AlignRightIcon,\n BoldIcon,\n CodeIcon,\n Heading1Icon,\n Heading2Icon,\n Heading3Icon,\n HighlighterIcon,\n ImageIcon,\n ItalicIcon,\n LinkIcon,\n ListChecksIcon,\n ListIcon,\n ListOrderedIcon,\n MinusIcon,\n PilcrowIcon,\n QuoteIcon,\n Redo2Icon,\n RemoveFormattingIcon,\n SquareCodeIcon,\n StrikethroughIcon,\n TableIcon,\n UnderlineIcon,\n Undo2Icon,\n UnlinkIcon,\n} from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport { Kbd } from \"@/components/ui/kbd\"\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\"\nimport {\n Select,\n SelectContent,\n SelectControl,\n SelectItem,\n SelectItemIndicator,\n SelectItemText,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Toggle } from \"@/components/ui/toggle\"\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\"\nimport { cn } from \"@/lib/utils\"\n\n/**\n * RichTextEditor — compositional editor parts on tiptap. The root owns the\n * tiptap `Editor` (content is controllable as HTML) and reports changes as\n * `onChange({ html, json, text })`. Every toolbar control is its own part and\n * reads the editor from context, so a consumer composes exactly the toolbar\n * it wants; `RichTextEditorToggle name=\"bold\"` covers the common actions and\n * `isActive`/`onToggle` take over for custom ones.\n *\n * Anatomy:\n * RichTextEditor { content, onChange, placeholder, editable }\n * RichTextEditorToolbar > ToolbarGroup > Toggle name=… / Trigger name=… / BlockSelect / LinkTrigger / ImageTrigger\n * RichTextEditorContent the prose area\n * RichTextEditorBubbleMenu selection toolbar (same toggles)\n * RichTextEditorFooter > RichTextEditorCharacterCount\n */\n\ntype RichTextChange = { html: string; json: JSONContent; text: string }\n\ntype RichTextEditorContextValue = { editor: Editor | null; editable: boolean }\nconst RichTextEditorContext = React.createContext<RichTextEditorContextValue | null>(null)\n\nfunction useRichTextEditor() {\n const ctx = React.useContext(RichTextEditorContext)\n if (!ctx) throw new Error(\"RichTextEditor parts must be used within <RichTextEditor>\")\n return ctx\n}\n\n/* ---------------------------------------------------------------------------\n * Root\n * ------------------------------------------------------------------------- */\n\nfunction RichTextEditor({\n content,\n defaultContent = \"\",\n onChange,\n placeholder = \"Write something…\",\n editable = true,\n autofocus = false,\n extensions = [],\n characterLimit,\n editorOptions,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"content\" | \"onChange\"> & {\n /** Controlled HTML. Prefer `defaultContent` + `onChange` unless you need to replace the document. */\n content?: string\n defaultContent?: string\n onChange?: (change: RichTextChange) => void\n placeholder?: string\n editable?: boolean\n autofocus?: boolean\n /** Extra tiptap extensions appended to the built-in set. */\n extensions?: Extensions\n characterLimit?: number\n /** Escape hatch for any other `useEditor` option. */\n editorOptions?: Partial<UseEditorOptions>\n}) {\n const onChangeRef = React.useRef(onChange)\n React.useEffect(() => {\n onChangeRef.current = onChange\n }, [onChange])\n\n const editor = useEditor({\n extensions: [\n StarterKit.configure({\n heading: { levels: [1, 2, 3] },\n link: { openOnClick: false, autolink: true, defaultProtocol: \"https\" },\n }),\n Placeholder.configure({ placeholder }),\n CharacterCount.configure({ limit: characterLimit }),\n TaskList,\n TaskItem.configure({ nested: true }),\n Highlight,\n TextAlign.configure({ types: [\"heading\", \"paragraph\"] }),\n Image.configure({ inline: false }),\n TableKit.configure({ table: { resizable: false } }),\n ...extensions,\n ],\n content: content ?? defaultContent,\n editable,\n autofocus,\n editorProps: {\n attributes: { class: \"outline-none\" },\n ...editorOptions?.editorProps,\n },\n onUpdate: ({ editor }) => {\n onChangeRef.current?.({ html: editor.getHTML(), json: editor.getJSON(), text: editor.getText() })\n editorOptions?.onUpdate?.({ editor, transaction: editor.state.tr, appendedTransactions: [] })\n },\n ...editorOptions,\n })\n\n // Controlled content: replace the document only when the outside value actually differs.\n React.useEffect(() => {\n if (!editor || content === undefined) return\n if (content !== editor.getHTML()) editor.commands.setContent(content, { emitUpdate: false })\n }, [editor, content])\n\n React.useEffect(() => {\n if (editor && editor.isEditable !== editable) editor.setEditable(editable)\n }, [editor, editable])\n\n const ctx = React.useMemo(() => ({ editor, editable }), [editor, editable])\n\n return (\n <RichTextEditorContext.Provider value={ctx}>\n <div\n data-slot=\"rich-text-editor\"\n data-editable={editable ? \"\" : undefined}\n className={cn(\n \"flex min-h-0 flex-col rounded-xl border bg-background text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-focus-visible:border-ring\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n </RichTextEditorContext.Provider>\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Actions registry\n * ------------------------------------------------------------------------- */\n\ntype ActionDef = {\n label: string\n icon: React.ComponentType<{ className?: string }>\n shortcut?: string\n isActive?: (editor: Editor) => boolean\n canRun?: (editor: Editor) => boolean\n run: (editor: Editor) => void\n}\n\nconst chain = (editor: Editor) => editor.chain().focus()\n\nconst actions = {\n bold: {\n label: \"Bold\",\n icon: BoldIcon,\n shortcut: \"⌘B\",\n isActive: (e) => e.isActive(\"bold\"),\n run: (e) => chain(e).toggleBold().run(),\n },\n italic: {\n label: \"Italic\",\n icon: ItalicIcon,\n shortcut: \"⌘I\",\n isActive: (e) => e.isActive(\"italic\"),\n run: (e) => chain(e).toggleItalic().run(),\n },\n underline: {\n label: \"Underline\",\n icon: UnderlineIcon,\n shortcut: \"⌘U\",\n isActive: (e) => e.isActive(\"underline\"),\n run: (e) => chain(e).toggleUnderline().run(),\n },\n strike: {\n label: \"Strikethrough\",\n icon: StrikethroughIcon,\n shortcut: \"⌘⇧S\",\n isActive: (e) => e.isActive(\"strike\"),\n run: (e) => chain(e).toggleStrike().run(),\n },\n code: {\n label: \"Inline code\",\n icon: CodeIcon,\n shortcut: \"⌘E\",\n isActive: (e) => e.isActive(\"code\"),\n run: (e) => chain(e).toggleCode().run(),\n },\n highlight: {\n label: \"Highlight\",\n icon: HighlighterIcon,\n shortcut: \"⌘⇧H\",\n isActive: (e) => e.isActive(\"highlight\"),\n run: (e) => chain(e).toggleHighlight().run(),\n },\n paragraph: {\n label: \"Paragraph\",\n icon: PilcrowIcon,\n shortcut: \"⌘⌥0\",\n isActive: (e) => e.isActive(\"paragraph\"),\n run: (e) => chain(e).setParagraph().run(),\n },\n heading1: {\n label: \"Heading 1\",\n icon: Heading1Icon,\n shortcut: \"⌘⌥1\",\n isActive: (e) => e.isActive(\"heading\", { level: 1 }),\n run: (e) => chain(e).toggleHeading({ level: 1 }).run(),\n },\n heading2: {\n label: \"Heading 2\",\n icon: Heading2Icon,\n shortcut: \"⌘⌥2\",\n isActive: (e) => e.isActive(\"heading\", { level: 2 }),\n run: (e) => chain(e).toggleHeading({ level: 2 }).run(),\n },\n heading3: {\n label: \"Heading 3\",\n icon: Heading3Icon,\n shortcut: \"⌘⌥3\",\n isActive: (e) => e.isActive(\"heading\", { level: 3 }),\n run: (e) => chain(e).toggleHeading({ level: 3 }).run(),\n },\n bulletList: {\n label: \"Bullet list\",\n icon: ListIcon,\n shortcut: \"⌘⇧8\",\n isActive: (e) => e.isActive(\"bulletList\"),\n run: (e) => chain(e).toggleBulletList().run(),\n },\n orderedList: {\n label: \"Numbered list\",\n icon: ListOrderedIcon,\n shortcut: \"⌘⇧7\",\n isActive: (e) => e.isActive(\"orderedList\"),\n run: (e) => chain(e).toggleOrderedList().run(),\n },\n taskList: {\n label: \"Task list\",\n icon: ListChecksIcon,\n shortcut: \"⌘⇧9\",\n isActive: (e) => e.isActive(\"taskList\"),\n run: (e) => chain(e).toggleTaskList().run(),\n },\n blockquote: {\n label: \"Quote\",\n icon: QuoteIcon,\n shortcut: \"⌘⇧B\",\n isActive: (e) => e.isActive(\"blockquote\"),\n run: (e) => chain(e).toggleBlockquote().run(),\n },\n codeBlock: {\n label: \"Code block\",\n icon: SquareCodeIcon,\n shortcut: \"⌘⌥C\",\n isActive: (e) => e.isActive(\"codeBlock\"),\n run: (e) => chain(e).toggleCodeBlock().run(),\n },\n alignLeft: {\n label: \"Align left\",\n icon: AlignLeftIcon,\n shortcut: \"⌘⇧L\",\n isActive: (e) => e.isActive({ textAlign: \"left\" }),\n run: (e) => chain(e).setTextAlign(\"left\").run(),\n },\n alignCenter: {\n label: \"Align center\",\n icon: AlignCenterIcon,\n shortcut: \"⌘⇧E\",\n isActive: (e) => e.isActive({ textAlign: \"center\" }),\n run: (e) => chain(e).setTextAlign(\"center\").run(),\n },\n alignRight: {\n label: \"Align right\",\n icon: AlignRightIcon,\n shortcut: \"⌘⇧R\",\n isActive: (e) => e.isActive({ textAlign: \"right\" }),\n run: (e) => chain(e).setTextAlign(\"right\").run(),\n },\n alignJustify: {\n label: \"Justify\",\n icon: AlignJustifyIcon,\n shortcut: \"⌘⇧J\",\n isActive: (e) => e.isActive({ textAlign: \"justify\" }),\n run: (e) => chain(e).setTextAlign(\"justify\").run(),\n },\n horizontalRule: { label: \"Divider\", icon: MinusIcon, run: (e) => chain(e).setHorizontalRule().run() },\n clearFormatting: {\n label: \"Clear formatting\",\n icon: RemoveFormattingIcon,\n run: (e) => chain(e).unsetAllMarks().clearNodes().run(),\n },\n undo: {\n label: \"Undo\",\n icon: Undo2Icon,\n shortcut: \"⌘Z\",\n canRun: (e) => e.can().undo(),\n run: (e) => chain(e).undo().run(),\n },\n redo: {\n label: \"Redo\",\n icon: Redo2Icon,\n shortcut: \"⌘⇧Z\",\n canRun: (e) => e.can().redo(),\n run: (e) => chain(e).redo().run(),\n },\n unsetLink: {\n label: \"Remove link\",\n icon: UnlinkIcon,\n isActive: (e) => e.isActive(\"link\"),\n canRun: (e) => e.isActive(\"link\"),\n run: (e) => chain(e).unsetLink().run(),\n },\n insertTable: {\n label: \"Insert table\",\n icon: TableIcon,\n run: (e) => chain(e).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),\n },\n} satisfies Record<string, ActionDef>\n\ntype ActionName = keyof typeof actions\n\n/** Subscribe to the bits of editor state a control needs; re-renders on selection and doc changes. */\nfunction useActionState(isActive?: (e: Editor) => boolean, canRun?: (e: Editor) => boolean) {\n const { editor, editable } = useRichTextEditor()\n const state = useEditorState({\n editor,\n selector: ({ editor }) => ({\n active: !!editor && !!isActive?.(editor),\n enabled: !!editor && editable && (canRun ? canRun(editor) : true),\n }),\n })\n return state ?? { active: false, enabled: false }\n}\n\n/* ---------------------------------------------------------------------------\n * Toolbar\n * ------------------------------------------------------------------------- */\n\nfunction RichTextEditorToolbar({ className, onKeyDown, ...props }: React.ComponentProps<\"div\">) {\n const ref = React.useRef<HTMLDivElement>(null)\n return (\n <div\n ref={ref}\n data-slot=\"rich-text-editor-toolbar\"\n role=\"toolbar\"\n aria-label=\"Formatting\"\n className={cn(\"flex flex-wrap items-center gap-1 border-b p-1.5\", className)}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented) return\n if (event.key !== \"ArrowRight\" && event.key !== \"ArrowLeft\") return\n const buttons = Array.from(ref.current?.querySelectorAll<HTMLElement>(\"button:not([disabled])\") ?? [])\n const index = buttons.indexOf(document.activeElement as HTMLElement)\n if (index === -1) return\n event.preventDefault()\n const next =\n event.key === \"ArrowRight\" ? (index + 1) % buttons.length : (index - 1 + buttons.length) % buttons.length\n buttons[next]?.focus()\n }}\n {...props}\n />\n )\n}\n\nfunction RichTextEditorToolbarGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div data-slot=\"rich-text-editor-toolbar-group\" className={cn(\"flex items-center gap-0.5\", className)} {...props} />\n )\n}\n\nfunction RichTextEditorToolbarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {\n return (\n <Separator\n data-slot=\"rich-text-editor-toolbar-separator\"\n orientation=\"vertical\"\n className={cn(\"mx-1 h-5!\", className)}\n {...props}\n />\n )\n}\n\nfunction ControlTooltip({\n label,\n shortcut,\n children,\n}: {\n label: React.ReactNode\n shortcut?: string\n children: React.ReactElement\n}) {\n return (\n <Tooltip>\n <TooltipTrigger asChild>{children}</TooltipTrigger>\n <TooltipContent>\n {label}\n {shortcut && <Kbd className=\"ms-1.5\">{shortcut}</Kbd>}\n </TooltipContent>\n </Tooltip>\n )\n}\n\ntype ControlProps = {\n /** A built-in action; supplies icon, label, shortcut and behaviour. */\n name?: ActionName\n label?: string\n shortcut?: string\n isActive?: (editor: Editor) => boolean\n canRun?: (editor: Editor) => boolean\n onToggle?: (editor: Editor) => void\n tooltip?: boolean\n}\n\n/** A pressed/unpressed formatting control (marks, lists, alignment, block types). */\nfunction RichTextEditorToggle({\n name,\n label,\n shortcut,\n isActive,\n canRun,\n onToggle,\n tooltip = true,\n className,\n children,\n asChild,\n ...props\n}: Omit<React.ComponentProps<typeof Toggle>, \"pressed\" | \"onPressedChange\"> & ControlProps) {\n const { editor } = useRichTextEditor()\n const def: ActionDef | undefined = name ? actions[name] : undefined\n const activeFn = isActive ?? def?.isActive\n const canFn = canRun ?? def?.canRun\n const { active, enabled } = useActionState(activeFn, canFn)\n const text = label ?? def?.label ?? name ?? \"\"\n const Icon = def?.icon\n const control = (\n <Toggle\n data-slot=\"rich-text-editor-toggle\"\n data-action={name}\n size=\"sm\"\n aria-label={text}\n pressed={active}\n disabled={!enabled}\n asChild={asChild}\n className={cn(\"size-7 px-0\", className)}\n onPressedChange={() => {\n if (!editor) return\n if (onToggle) onToggle(editor)\n else def?.run(editor)\n }}\n {...props}\n >\n {asChild ? children : (children ?? (Icon ? <Icon /> : text))}\n </Toggle>\n )\n return tooltip ? (\n <ControlTooltip label={text} shortcut={shortcut ?? def?.shortcut}>\n {control}\n </ControlTooltip>\n ) : (\n control\n )\n}\n\n/** A one-shot action (undo, redo, divider, clear formatting, insert table). */\nfunction RichTextEditorTrigger({\n name,\n label,\n shortcut,\n canRun,\n onToggle,\n tooltip = true,\n className,\n children,\n asChild,\n onClick,\n ...props\n}: React.ComponentProps<typeof Button> & Omit<ControlProps, \"isActive\">) {\n const { editor } = useRichTextEditor()\n const def: ActionDef | undefined = name ? actions[name] : undefined\n const { enabled } = useActionState(undefined, canRun ?? def?.canRun)\n const text = label ?? def?.label ?? name ?? \"\"\n const Icon = def?.icon\n const control = (\n <Button\n data-slot=\"rich-text-editor-trigger\"\n data-action={name}\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={text}\n disabled={!enabled}\n asChild={asChild}\n className={className}\n onClick={(event) => {\n onClick?.(event)\n if (event.defaultPrevented || !editor) return\n if (onToggle) onToggle(editor)\n else def?.run(editor)\n }}\n {...props}\n >\n {asChild ? children : (children ?? (Icon ? <Icon /> : text))}\n </Button>\n )\n return tooltip ? (\n <ControlTooltip label={text} shortcut={shortcut ?? def?.shortcut}>\n {control}\n </ControlTooltip>\n ) : (\n control\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Block type select, link, image\n * ------------------------------------------------------------------------- */\n\nconst blockOptions: { value: ActionName; label: string }[] = [\n { value: \"paragraph\", label: \"Text\" },\n { value: \"heading1\", label: \"Heading 1\" },\n { value: \"heading2\", label: \"Heading 2\" },\n { value: \"heading3\", label: \"Heading 3\" },\n { value: \"blockquote\", label: \"Quote\" },\n { value: \"codeBlock\", label: \"Code block\" },\n]\nconst blockCollection = createListCollection({ items: blockOptions, itemToValue: (o) => o.value })\n\nfunction RichTextEditorBlockSelect({\n className,\n ...props\n}: Omit<React.ComponentProps<typeof Select>, \"collection\" | \"value\" | \"onValueChange\">) {\n const { editor, editable } = useRichTextEditor()\n const current = useEditorState({\n editor,\n selector: ({ editor }) =>\n editor\n ? (blockOptions.find((o) => (actions[o.value] as ActionDef).isActive?.(editor))?.value ?? \"paragraph\")\n : \"paragraph\",\n })\n const label = blockOptions.find((o) => o.value === current)?.label\n return (\n <Select\n collection={blockCollection}\n value={[current ?? \"paragraph\"]}\n onValueChange={({ value }) => {\n const name = value[0] as ActionName | undefined\n if (editor && name) actions[name].run(editor)\n }}\n disabled={!editable}\n positioning={{ sameWidth: false }}\n {...props}\n >\n <SelectControl>\n <SelectTrigger\n size=\"sm\"\n className={cn(\"w-32\", className)}\n data-slot=\"rich-text-editor-block-select\"\n aria-label=\"Block type\"\n >\n <SelectValue>{label}</SelectValue>\n </SelectTrigger>\n </SelectControl>\n <SelectContent>\n {blockOptions.map((o) => {\n const Icon = actions[o.value].icon\n return (\n <SelectItem key={o.value} item={o}>\n <Icon className=\"size-4 text-muted-foreground\" />\n <SelectItemText>{o.label}</SelectItemText>\n <SelectItemIndicator />\n </SelectItem>\n )\n })}\n </SelectContent>\n </Select>\n )\n}\n\n/** Set or edit a link on the selection through a small popover form. */\nfunction RichTextEditorLinkTrigger({ className, ...props }: React.ComponentProps<typeof Toggle>) {\n const { editor, editable } = useRichTextEditor()\n const { active } = useActionState((e) => e.isActive(\"link\"))\n const [open, setOpen] = React.useState(false)\n const [href, setHref] = React.useState(\"\")\n const apply = () => {\n if (!editor) return\n const url = href.trim()\n if (!url) editor.chain().focus().extendMarkRange(\"link\").unsetLink().run()\n else editor.chain().focus().extendMarkRange(\"link\").setLink({ href: url }).run()\n setOpen(false)\n }\n return (\n <Popover\n open={open}\n onOpenChange={({ open }) => {\n if (open) setHref((editor?.getAttributes(\"link\").href as string | undefined) ?? \"\")\n setOpen(open)\n }}\n positioning={{ placement: \"bottom-start\" }}\n >\n <ControlTooltip label=\"Link\" shortcut=\"⌘K\">\n <PopoverTrigger asChild>\n <Toggle\n data-slot=\"rich-text-editor-link-trigger\"\n size=\"sm\"\n aria-label=\"Link\"\n pressed={active}\n disabled={!editable}\n className={cn(\"size-7 px-0\", className)}\n {...props}\n >\n <LinkIcon />\n </Toggle>\n </PopoverTrigger>\n </ControlTooltip>\n <PopoverContent className=\"w-72\">\n <form\n className=\"flex items-center gap-2\"\n onSubmit={(event) => {\n event.preventDefault()\n apply()\n }}\n >\n <Input\n value={href}\n onChange={(e) => setHref(e.target.value)}\n placeholder=\"https://example.com\"\n aria-label=\"Link URL\"\n autoFocus\n className=\"h-8\"\n />\n <Button type=\"submit\" size=\"sm\">\n {href.trim() ? \"Apply\" : \"Remove\"}\n </Button>\n </form>\n </PopoverContent>\n </Popover>\n )\n}\n\n/** Insert an image by URL. */\nfunction RichTextEditorImageTrigger({ className, ...props }: React.ComponentProps<typeof Button>) {\n const { editor, editable } = useRichTextEditor()\n const [open, setOpen] = React.useState(false)\n const [src, setSrc] = React.useState(\"\")\n return (\n <Popover open={open} onOpenChange={({ open }) => setOpen(open)} positioning={{ placement: \"bottom-start\" }}>\n <ControlTooltip label=\"Image\">\n <PopoverTrigger asChild>\n <Button\n data-slot=\"rich-text-editor-image-trigger\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Image\"\n disabled={!editable}\n className={className}\n {...props}\n >\n <ImageIcon />\n </Button>\n </PopoverTrigger>\n </ControlTooltip>\n <PopoverContent className=\"w-72\">\n <form\n className=\"flex items-center gap-2\"\n onSubmit={(event) => {\n event.preventDefault()\n if (editor && src.trim()) editor.chain().focus().setImage({ src: src.trim() }).run()\n setSrc(\"\")\n setOpen(false)\n }}\n >\n <Input\n value={src}\n onChange={(e) => setSrc(e.target.value)}\n placeholder=\"https://…/image.png\"\n aria-label=\"Image URL\"\n autoFocus\n className=\"h-8\"\n />\n <Button type=\"submit\" size=\"sm\" disabled={!src.trim()}>\n Insert\n </Button>\n </form>\n </PopoverContent>\n </Popover>\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Content, bubble menu, footer\n * ------------------------------------------------------------------------- */\n\nfunction RichTextEditorContent({ className, ...props }: Omit<React.ComponentProps<typeof EditorContent>, \"editor\">) {\n const { editor } = useRichTextEditor()\n return (\n <EditorContent\n editor={editor}\n data-slot=\"rich-text-editor-content\"\n className={cn(\n \"prose prose-sm min-h-0 max-w-none flex-1 overflow-y-auto px-4 py-3 prose-neutral dark:prose-invert\",\n \"[&_.tiptap]:min-h-40 [&_.tiptap]:outline-none\",\n className\n )}\n {...props}\n />\n )\n}\n\n/** A floating toolbar over the current text selection. Compose it from the same toggles. */\nfunction RichTextEditorBubbleMenu({ className, children, ...props }: Omit<BubbleMenuProps, \"editor\">) {\n const { editor, editable } = useRichTextEditor()\n if (!editor || !editable) return null\n return (\n <BubbleMenu\n editor={editor}\n options={{ placement: \"top\", offset: 8 }}\n shouldShow={({ editor, from, to }) =>\n editable && from !== to && !editor.isActive(\"image\") && !editor.isActive(\"codeBlock\")\n }\n className={cn(\n \"z-50 flex items-center gap-0.5 rounded-lg border bg-popover p-1 text-popover-foreground shadow-md\",\n className\n )}\n {...props}\n >\n <div\n data-slot=\"rich-text-editor-bubble-menu\"\n role=\"toolbar\"\n aria-label=\"Selection formatting\"\n className=\"contents\"\n >\n {children}\n </div>\n </BubbleMenu>\n )\n}\n\nfunction RichTextEditorFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"rich-text-editor-footer\"\n className={cn(\"flex items-center gap-3 border-t px-3 py-1.5 text-xs text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction RichTextEditorCharacterCount({ className, ...props }: React.ComponentProps<typeof ark.span>) {\n const { editor } = useRichTextEditor()\n const stats = useEditorState({\n editor,\n selector: ({ editor }) => {\n const storage = editor?.storage.characterCount as { characters?: () => number; words?: () => number } | undefined\n return { characters: storage?.characters?.() ?? 0, words: storage?.words?.() ?? 0 }\n },\n })\n const limit = (\n editor?.extensionManager.extensions.find((e) => e.name === \"characterCount\")?.options as { limit?: number }\n )?.limit\n return (\n <ark.span\n data-slot=\"rich-text-editor-character-count\"\n data-over={limit && stats && stats.characters >= limit ? \"\" : undefined}\n className={cn(\"tabular-nums data-over:text-destructive\", className)}\n {...props}\n >\n {stats?.words ?? 0} words · {stats?.characters ?? 0}\n {limit ? ` / ${limit}` : \"\"} characters\n </ark.span>\n )\n}\n\nexport {\n RichTextEditor,\n RichTextEditorToolbar,\n RichTextEditorToolbarGroup,\n RichTextEditorToolbarSeparator,\n RichTextEditorToggle,\n RichTextEditorTrigger,\n RichTextEditorBlockSelect,\n RichTextEditorLinkTrigger,\n RichTextEditorImageTrigger,\n RichTextEditorContent,\n RichTextEditorBubbleMenu,\n RichTextEditorFooter,\n RichTextEditorCharacterCount,\n useRichTextEditor,\n actions as richTextActions,\n type RichTextChange,\n type ActionName as RichTextActionName,\n}\n"
33
+ }
34
+ ],
35
+ "css": "/* Rich text editor (tiptap) */\n[data-slot=\"rich-text-editor-content\"] .tiptap p.is-editor-empty:first-child::before {\n content: attr(data-placeholder);\n float: left;\n height: 0;\n pointer-events: none;\n color: var(--color-muted-foreground);\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap ul[data-type=\"taskList\"] {\n list-style: none;\n padding-inline-start: 0;\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap ul[data-type=\"taskList\"] li {\n display: flex;\n gap: 0.5rem;\n align-items: flex-start;\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap ul[data-type=\"taskList\"] li > label {\n flex: none;\n margin-top: 0.3em;\n user-select: none;\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap ul[data-type=\"taskList\"] li > div {\n flex: 1 1 auto;\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap ul[data-type=\"taskList\"] li > div > p {\n margin: 0;\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap ul[data-type=\"taskList\"] li[data-checked=\"true\"] > div {\n color: var(--color-muted-foreground);\n text-decoration: line-through;\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap ul[data-type=\"taskList\"] input[type=\"checkbox\"] {\n accent-color: var(--color-primary);\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap mark {\n background-color: color-mix(in oklab, var(--color-amber-400) 45%, transparent);\n color: inherit;\n border-radius: 0.2em;\n padding-inline: 0.1em;\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap table {\n table-layout: fixed;\n width: 100%;\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap table td,\n[data-slot=\"rich-text-editor-content\"] .tiptap table th {\n border: 1px solid var(--color-border);\n padding: 0.375rem 0.5rem;\n vertical-align: top;\n position: relative;\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap table th {\n background: color-mix(in oklab, var(--color-muted) 60%, transparent);\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap table .selectedCell::after {\n content: \"\";\n position: absolute;\n inset: 0;\n background: color-mix(in oklab, var(--color-primary) 12%, transparent);\n pointer-events: none;\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap img {\n border-radius: var(--radius-lg);\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap img.ProseMirror-selectednode,\n[data-slot=\"rich-text-editor-content\"] .tiptap .ProseMirror-selectednode {\n outline: 2px solid var(--color-ring);\n outline-offset: 2px;\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap a {\n color: var(--color-primary);\n text-decoration: underline;\n text-underline-offset: 0.2em;\n}\n[data-slot=\"rich-text-editor-content\"] .tiptap :where(h1, h2, h3):first-child {\n margin-top: 0;\n}\n",
36
+ "docs": "## Rich text editor\n`src/components/ui/rich-text-editor.tsx` is a compositional editor on **tiptap** (`@tiptap/react`,\nStarterKit with link/underline, `@tiptap/extensions` Placeholder + CharacterCount, task list,\nhighlight, text align, image, TableKit; `extensions` appends more, `editorOptions` is the escape\nhatch). The root owns the tiptap `Editor`: `defaultContent`/`content` (HTML, controlled content\nreplaces the doc only when it differs), `onChange({ html, json, text })`, `placeholder`,\n`editable`, `autofocus`, `characterLimit`; context exposes `useRichTextEditor()` → `{ editor,\neditable }`. Controls read the editor from context and re-render via `useEditorState`:\n`RichTextEditorToggle name=` (pressed controls: bold/italic/underline/strike/code/highlight,\nparagraph/heading1-3, bulletList/orderedList/taskList, blockquote/codeBlock, alignLeft/Center/\nRight/Justify, unsetLink) and `RichTextEditorTrigger name=` (one-shot: undo/redo/horizontalRule/\nclearFormatting/insertTable); both take `isActive`/`canRun`/`onToggle`/`label`/`shortcut` for\ncustom actions, wrap themselves in a Tooltip with a `Kbd` shortcut (`tooltip={false}` inside the\nbubble menu), and are polymorphic via `asChild`. The registry is exported as `richTextActions`.\nOther parts: `RichTextEditorToolbar` (role toolbar, arrow-key roving) > `ToolbarGroup` /\n`ToolbarSeparator`, `RichTextEditorBlockSelect` (toolkit Select: text/headings/quote/code block),\n`RichTextEditorLinkTrigger` (Popover with a URL form; empty removes), `RichTextEditorImageTrigger`\n(URL form), `RichTextEditorContent` (`EditorContent` with `prose prose-sm dark:prose-invert` from\n`@tailwindcss/typography`; placeholder, task list, highlight, table and selection styles live in\n`src/index.css` scoped to `[data-slot=\"rich-text-editor-content\"] .tiptap`),\n`RichTextEditorBubbleMenu` (tiptap `BubbleMenu` over text selections; compose the same toggles),\n`RichTextEditorFooter` + `RichTextEditorCharacterCount` (words · characters / limit, `data-over`).\n`src/demo/rich-text/` is the reference (`/documents`, \"Documents\"): full toolbar, bubble menu,\nread-only switch, reset, and an HTML/JSON/Text output card."
37
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "row-layout",
3
+ "type": "lib",
4
+ "description": "Rows-and-columns layout helpers for the Canvas primitive. A layout is `Row[]`, each row a list of items with a `width` (flex-grow weight). Drops above/below an item create a row, beside it a column; the resize handle shifts width between ne",
5
+ "dependencies": {},
6
+ "registryDependencies": [
7
+ "canvas"
8
+ ],
9
+ "files": [
10
+ {
11
+ "path": "lib/row-layout.ts",
12
+ "content": "import type { CanvasDropDetails } from \"@/components/ui/canvas\"\n\n/**\n * Rows-and-columns layout helpers for the Canvas primitive. A layout is\n * `Row[]`, each row a list of items with a `width` (flex-grow weight). Drops\n * above/below an item create a row, beside it a column; the resize handle\n * shifts width between neighbours. Shared by the form builder and the widgets\n * page.\n */\n\nexport type RowItem = { id: string; width: number }\nexport type Row<T extends RowItem> = { id: string; items: T[] }\n\nconst MIN_SHARE = 0.12\n\nexport function applyRowDrop<T extends RowItem>(\n rows: Row<T>[],\n details: CanvasDropDetails,\n options: { create: (data: unknown) => T | null; rowId: () => string }\n): Row<T>[] {\n let item: T | undefined\n let next = rows\n let sourceRowIndex = -1\n if (details.source.type === \"node\") {\n const id = details.source.id\n item = rows.flatMap((r) => r.items).find((i) => i.id === id)\n sourceRowIndex = rows.findIndex((r) => r.items.some((i) => i.id === id))\n next = rows.map((r) => ({ ...r, items: r.items.filter((i) => i.id !== id) })).filter((r) => r.items.length > 0)\n } else {\n item = options.create(details.source.data) ?? undefined\n }\n if (!item) return rows\n\n const target = details.target\n if (target.type === \"area\") return [...next, { id: options.rowId(), items: [{ ...item, width: 1 }] }]\n\n // Dropping a node on its own top/bottom edge (keyboard: \"move up/down\" while it has siblings)\n // pulls it out into a new row above or below its current row.\n if (details.source.type === \"node\" && target.id === details.source.id) {\n if (target.edge !== \"top\" && target.edge !== \"bottom\") return rows\n const stillThere = next.length === rows.length\n if (!stillThere) return rows\n const at = target.edge === \"top\" ? sourceRowIndex : sourceRowIndex + 1\n const newRow = { id: options.rowId(), items: [{ ...item, width: 1 }] }\n return [...next.slice(0, at), newRow, ...next.slice(at)]\n }\n\n const rowIndex = next.findIndex((r) => r.items.some((i) => i.id === target.id))\n if (rowIndex < 0) return rows\n const row = next[rowIndex]\n const index = row.items.findIndex((i) => i.id === target.id)\n if (target.edge === \"top\" || target.edge === \"bottom\") {\n const at = target.edge === \"top\" ? rowIndex : rowIndex + 1\n const newRow = { id: options.rowId(), items: [{ ...item, width: 1 }] }\n return [...next.slice(0, at), newRow, ...next.slice(at)]\n }\n const mean = row.items.reduce((s, i) => s + i.width, 0) / row.items.length\n const at = target.edge === \"left\" ? index : index + 1\n const items = [...row.items.slice(0, at), { ...item, width: mean }, ...row.items.slice(at)]\n return next.map((r, i) => (i === rowIndex ? { ...r, items } : r))\n}\n\n/** Move width from item `index` to `index + 1` by a fraction of the row. */\nexport function resizeRowItems<T extends RowItem>(row: Row<T>, index: number, deltaFraction: number): Row<T> {\n const total = row.items.reduce((s, i) => s + i.width, 0)\n const delta = deltaFraction * total\n const a = row.items[index]\n const b = row.items[index + 1]\n if (!a || !b) return row\n const min = MIN_SHARE * total\n const clamped = Math.max(min - a.width, Math.min(delta, b.width - min))\n if (clamped === 0) return row\n const items = row.items.map((i, k) =>\n k === index ? { ...i, width: a.width + clamped } : k === index + 1 ? { ...i, width: b.width - clamped } : i\n )\n return { ...row, items }\n}\n\nexport function removeRowItem<T extends RowItem>(rows: Row<T>[], id: string): Row<T>[] {\n return rows.map((r) => ({ ...r, items: r.items.filter((i) => i.id !== id) })).filter((r) => r.items.length > 0)\n}\n"
13
+ }
14
+ ]
15
+ }