@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": "drawer",
3
+ "type": "ui",
4
+ "description": "- **Feature parity** with the shadcn original: same exported names, same props surface where Ark supports it, same visual result, same variants. - **Ark's tree wins** where the structure differs (e.g.",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1"
7
+ },
8
+ "registryDependencies": [
9
+ "utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/drawer.tsx",
14
+ "content": "import * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Drawer as DrawerPrimitive, Portal as PortalPrimitive } from \"@ark-ui/react\"\n\ntype DrawerDirection = \"top\" | \"bottom\" | \"left\" | \"right\"\n\nconst swipeDirectionByDirection = {\n bottom: \"down\",\n top: \"up\",\n left: \"start\",\n right: \"end\",\n} as const\n\nfunction Drawer({\n direction = \"bottom\",\n swipeDirection,\n lazyMount = true,\n unmountOnExit = true,\n ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Root> & {\n /** Edge the drawer slides in from. Maps to Ark's `swipeDirection`. */\n direction?: DrawerDirection\n}) {\n return (\n <DrawerPrimitive.Root\n swipeDirection={swipeDirection ?? swipeDirectionByDirection[direction]}\n lazyMount={lazyMount}\n unmountOnExit={unmountOnExit}\n {...props}\n />\n )\n}\n\nfunction DrawerTrigger({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {\n return <DrawerPrimitive.Trigger data-slot=\"drawer-trigger\" {...props} />\n}\n\nfunction DrawerPortal({ ...props }: React.ComponentProps<typeof PortalPrimitive>) {\n return <PortalPrimitive {...props} />\n}\n\nfunction DrawerContext({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Context>) {\n return <DrawerPrimitive.Context {...props} />\n}\n\nfunction DrawerClose({ ...props }: React.ComponentProps<typeof DrawerPrimitive.CloseTrigger>) {\n return <DrawerPrimitive.CloseTrigger data-slot=\"drawer-close\" {...props} />\n}\n\nfunction DrawerOverlay({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Backdrop>) {\n return (\n <DrawerPrimitive.Backdrop\n data-slot=\"drawer-overlay\"\n className={cn(\n \"fixed inset-0 z-50 bg-black/10 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DrawerPositioner({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Positioner>) {\n return (\n <DrawerPrimitive.Positioner\n data-slot=\"drawer-positioner\"\n className={cn(\"fixed inset-0 z-50\", className)}\n {...props}\n />\n )\n}\n\nfunction DrawerGrabber({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Grabber>) {\n return (\n <DrawerPrimitive.Grabber\n data-slot=\"drawer-grabber\"\n className={cn(\n \"mx-auto mt-4 hidden shrink-0 cursor-grab touch-none group-data-[swipe-direction=down]/drawer-content:block\",\n className\n )}\n {...props}\n >\n <DrawerPrimitive.GrabberIndicator\n data-slot=\"drawer-grabber-indicator\"\n className=\"block h-1 w-25 rounded-full bg-muted\"\n />\n </DrawerPrimitive.Grabber>\n )\n}\n\nfunction DrawerContent({ className, children, ...props }: React.ComponentProps<typeof DrawerPrimitive.Content>) {\n return (\n <DrawerPortal>\n <DrawerOverlay />\n <DrawerPositioner>\n <DrawerPrimitive.Content\n data-slot=\"drawer-content\"\n className={cn(\n \"group/drawer-content absolute flex h-auto flex-col bg-popover text-sm text-popover-foreground outline-hidden transition-transform duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] data-[swipe-direction=down]:inset-x-0 data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:mt-24 data-[swipe-direction=down]:max-h-[80vh] data-[swipe-direction=down]:rounded-t-xl data-[swipe-direction=down]:border-t data-[swipe-direction=left]:inset-y-0 data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:w-3/4 data-[swipe-direction=left]:rounded-r-xl data-[swipe-direction=left]:border-r data-[swipe-direction=right]:inset-y-0 data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:w-3/4 data-[swipe-direction=right]:rounded-l-xl data-[swipe-direction=right]:border-l data-[swipe-direction=up]:inset-x-0 data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:mb-24 data-[swipe-direction=up]:max-h-[80vh] data-[swipe-direction=up]:rounded-b-xl data-[swipe-direction=up]:border-b data-[swipe-direction=left]:sm:max-w-sm data-[swipe-direction=right]:sm:max-w-sm data-open:animate-in data-[swipe-direction=down]:data-open:slide-in-from-bottom-full data-[swipe-direction=left]:data-open:slide-in-from-left-full data-[swipe-direction=right]:data-open:slide-in-from-right-full data-[swipe-direction=up]:data-open:slide-in-from-top-full data-closed:animate-out data-[swipe-direction=down]:data-closed:slide-out-to-bottom-full data-[swipe-direction=left]:data-closed:slide-out-to-left-full data-[swipe-direction=right]:data-closed:slide-out-to-right-full data-[swipe-direction=up]:data-closed:slide-out-to-top-full\",\n className\n )}\n {...props}\n >\n <DrawerGrabber />\n {children}\n </DrawerPrimitive.Content>\n </DrawerPositioner>\n </DrawerPortal>\n )\n}\n\nfunction DrawerHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"drawer-header\"\n className={cn(\n \"flex flex-col gap-0.5 p-4 group-data-[swipe-direction=down]/drawer-content:text-center group-data-[swipe-direction=up]/drawer-content:text-center md:gap-0.5 md:text-left\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DrawerFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"drawer-footer\" className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)} {...props} />\n}\n\nfunction DrawerTitle({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Title>) {\n return (\n <DrawerPrimitive.Title\n data-slot=\"drawer-title\"\n className={cn(\"font-heading text-base font-medium text-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction DrawerDescription({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Description>) {\n return (\n <DrawerPrimitive.Description\n data-slot=\"drawer-description\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction DrawerSwipeArea({ ...props }: React.ComponentProps<typeof DrawerPrimitive.SwipeArea>) {\n return <DrawerPrimitive.SwipeArea data-slot=\"drawer-swipe-area\" {...props} />\n}\n\n/** Wrap page content in a stack so nested drawers push it back (`Drawer` parts inside read the stack). */\nfunction DrawerStack({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Stack>) {\n return <DrawerPrimitive.Stack data-slot=\"drawer-stack\" {...props} />\n}\n\n/** The layer that scales and shifts back when a drawer opens over it. */\nfunction DrawerIndent({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Indent>) {\n return (\n <DrawerPrimitive.Indent\n data-slot=\"drawer-indent\"\n className={cn(\"origin-center transition-transform duration-300 ease-out\", className)}\n {...props}\n />\n )\n}\n\nfunction DrawerIndentBackground({\n className,\n ...props\n}: React.ComponentProps<typeof DrawerPrimitive.IndentBackground>) {\n return (\n <DrawerPrimitive.IndentBackground\n data-slot=\"drawer-indent-background\"\n className={cn(\"fixed inset-0 bg-black\", className)}\n {...props}\n />\n )\n}\n\nexport {\n Drawer,\n DrawerClose,\n DrawerContent,\n DrawerContext,\n DrawerDescription,\n DrawerFooter,\n DrawerGrabber,\n DrawerHeader,\n DrawerOverlay,\n DrawerPortal,\n DrawerPositioner,\n DrawerSwipeArea,\n DrawerTitle,\n DrawerTrigger,\n DrawerIndent,\n DrawerIndentBackground,\n DrawerStack,\n}\n"
15
+ }
16
+ ],
17
+ "docs": "## Porting rules\n- **Feature parity** with the shadcn original: same exported names, same props surface\n where Ark supports it, same visual result, same variants.\n- **Ark's tree wins** where the structure differs (e.g. `Positioner` wrappers,\n `Control`/`HiddenInput` splits, `ItemGroup`/`ItemGroupLabel`, `Backdrop` vs `Overlay`).\n Export the extra Ark parts as their own composable pieces; do not fold them away.\n- **Stay compositional.** Every part is its own exported component. No collapsing\n parts into a single \"smart\" component, no dropping sub-parts to simplify.\n- **Styling:** `cn` from `@/lib/utils` and `cva` from `class-variance-authority`.\n Style state via Ark's data attributes (`data-state`, `data-open`, `data-disabled`,\n `data-highlighted`, `data-placement`, `data-orientation`, ...). Keep `data-slot`\n attributes on every part, as shadcn does.\n- `command.tsx` runs on Ark's Listbox and `drawer.tsx` on Ark's Drawer; `cmdk`, `vaul`,\n `radix-ui`, `@base-ui/react`, and every shadcn package (`shadcn` CLI, `@shadcn/react`,\n `cn`) are removed. Do not reintroduce them. `cn` is clsx + tailwind-merge in\n `src/lib/utils.ts`. Base variants, keyframes, and utilities live in `src/index.css`\n (keyframes at the end of the `@theme inline` block); there is no `components.json`.\n- `questionnaire.tsx` and `message-scroller.tsx` were removed because they depended on\n the `@shadcn/react` runtime. Rebuild from scratch if they are wanted.\n- Components with no Ark equivalent (calendar, carousel, chart, resizable, input-otp,\n sonner/toast) keep their current libraries. Separator, Label, AspectRatio, Direction\n are plain React."
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "dropdown-menu",
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/dropdown-menu.tsx",
15
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Menu as DropdownMenuPrimitive, Portal as PortalPrimitive } from \"@ark-ui/react\"\nimport { CheckIcon, ChevronRightIcon, ChevronDownIcon } from \"lucide-react\"\n\nfunction DropdownMenu({\n positioning,\n lazyMount = true,\n unmountOnExit = true,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n return (\n <DropdownMenuPrimitive.Root\n positioning={{ placement: \"bottom-start\", gutter: 4, ...positioning }}\n lazyMount={lazyMount}\n unmountOnExit={unmountOnExit}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuPortal({ ...props }: React.ComponentProps<typeof PortalPrimitive>) {\n return <PortalPrimitive {...props} />\n}\n\nfunction DropdownMenuContext({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Context>) {\n return <DropdownMenuPrimitive.Context {...props} />\n}\n\nfunction DropdownMenuTrigger({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n return <DropdownMenuPrimitive.Trigger data-slot=\"dropdown-menu-trigger\" {...props} />\n}\n\nfunction DropdownMenuPositioner({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Positioner>) {\n return (\n <DropdownMenuPrimitive.Positioner\n data-slot=\"dropdown-menu-positioner\"\n className={cn(\"[--z-index:50]\", className)}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuContent({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n return (\n <DropdownMenuPortal>\n <DropdownMenuPositioner>\n <DropdownMenuPrimitive.Content\n data-slot=\"dropdown-menu-content\"\n className={cn(\n \"z-50 max-h-(--available-height) w-(--reference-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 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-[state=closed]:overflow-hidden 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 </DropdownMenuPositioner>\n </DropdownMenuPortal>\n )\n}\n\nfunction DropdownMenuArrow({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Arrow>) {\n return (\n <DropdownMenuPrimitive.Arrow\n data-slot=\"dropdown-menu-arrow\"\n className={cn(\"[--arrow-background:var(--color-popover)] [--arrow-size:0.625rem]\", className)}\n {...props}\n >\n <DropdownMenuPrimitive.ArrowTip\n data-slot=\"dropdown-menu-arrow-tip\"\n className=\"border-t border-l border-foreground/10\"\n />\n </DropdownMenuPrimitive.Arrow>\n )\n}\n\nfunction DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.ItemGroup>) {\n return <DropdownMenuPrimitive.ItemGroup data-slot=\"dropdown-menu-group\" {...props} />\n}\n\nconst itemClassName =\n \"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 data-[variant=destructive]:data-highlighted:text-destructive dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive\"\n\nfunction DropdownMenuItem({\n className,\n inset,\n variant = \"default\",\n value,\n ...props\n}: Omit<React.ComponentProps<typeof DropdownMenuPrimitive.Item>, \"value\"> & {\n value?: string\n inset?: boolean\n variant?: \"default\" | \"destructive\"\n}) {\n const id = React.useId()\n return (\n <DropdownMenuPrimitive.Item\n data-slot=\"dropdown-menu-item\"\n data-inset={inset}\n data-variant={variant}\n value={value ?? id}\n className={cn(itemClassName, className)}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuItemText({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.ItemText>) {\n return <DropdownMenuPrimitive.ItemText data-slot=\"dropdown-menu-item-text\" {...props} />\n}\n\nfunction DropdownMenuItemIndicator({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.ItemIndicator>) {\n return (\n <DropdownMenuPrimitive.ItemIndicator\n data-slot=\"dropdown-menu-item-indicator\"\n className={cn(\"pointer-events-none absolute right-2 flex items-center justify-center\", className)}\n {...props}\n />\n )\n}\n\nconst optionItemClassName =\n \"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-highlighted:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n\nfunction DropdownMenuCheckboxItem({\n className,\n children,\n checked = false,\n inset,\n value,\n ...props\n}: Omit<React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>, \"checked\" | \"value\"> & {\n checked?: boolean\n value?: string\n inset?: boolean\n}) {\n const id = React.useId()\n return (\n <DropdownMenuPrimitive.CheckboxItem\n data-slot=\"dropdown-menu-checkbox-item\"\n data-inset={inset}\n className={cn(optionItemClassName, className)}\n checked={checked}\n value={value ?? id}\n {...props}\n >\n <DropdownMenuItemIndicator data-slot=\"dropdown-menu-checkbox-item-indicator\">\n <CheckIcon />\n </DropdownMenuItemIndicator>\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n )\n}\n\nfunction DropdownMenuRadioGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItemGroup>) {\n return <DropdownMenuPrimitive.RadioItemGroup data-slot=\"dropdown-menu-radio-group\" {...props} />\n}\n\nfunction DropdownMenuRadioItem({\n className,\n children,\n inset,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {\n inset?: boolean\n}) {\n return (\n <DropdownMenuPrimitive.RadioItem\n data-slot=\"dropdown-menu-radio-item\"\n data-inset={inset}\n className={cn(optionItemClassName, className)}\n {...props}\n >\n <DropdownMenuItemIndicator data-slot=\"dropdown-menu-radio-item-indicator\">\n <CheckIcon />\n </DropdownMenuItemIndicator>\n {children}\n </DropdownMenuPrimitive.RadioItem>\n )\n}\n\nfunction DropdownMenuLabel({\n className,\n inset,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.ItemGroupLabel> & {\n inset?: boolean\n}) {\n return (\n <DropdownMenuPrimitive.ItemGroupLabel\n data-slot=\"dropdown-menu-label\"\n data-inset={inset}\n className={cn(\"px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7\", className)}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n return (\n <DropdownMenuPrimitive.Separator\n data-slot=\"dropdown-menu-separator\"\n className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"dropdown-menu-shortcut\"\n className={cn(\n \"ml-auto text-xs tracking-widest text-muted-foreground group-data-highlighted/dropdown-menu-item:text-accent-foreground\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuSub({\n lazyMount = true,\n unmountOnExit = true,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n return <DropdownMenuPrimitive.Root lazyMount={lazyMount} unmountOnExit={unmountOnExit} {...props} />\n}\n\nfunction DropdownMenuSubTrigger({\n className,\n inset,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.TriggerItem> & {\n inset?: boolean\n}) {\n return (\n <DropdownMenuPrimitive.TriggerItem\n data-slot=\"dropdown-menu-sub-trigger\"\n data-inset={inset}\n className={cn(\n \"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronRightIcon className=\"ml-auto\" />\n </DropdownMenuPrimitive.TriggerItem>\n )\n}\n\nfunction DropdownMenuSubContent({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n return (\n <DropdownMenuPortal>\n <DropdownMenuPositioner>\n <DropdownMenuPrimitive.Content\n data-slot=\"dropdown-menu-sub-content\"\n className={cn(\n \"z-50 min-w-24 origin-(--transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg 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 </DropdownMenuPositioner>\n </DropdownMenuPortal>\n )\n}\n\n/** Render-prop access to one item's state (`{ selected, highlighted, disabled, ... }`). */\nfunction DropdownMenuItemContext({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.ItemContext>) {\n return <DropdownMenuPrimitive.ItemContext {...props} />\n}\n\n/** Opens the menu from a right-click (context-menu style) instead of a click. */\nfunction DropdownMenuContextTrigger({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.ContextTrigger>) {\n return <DropdownMenuPrimitive.ContextTrigger data-slot=\"dropdown-menu-context-trigger\" {...props} />\n}\n\n/** Open-state indicator for a trigger; rotates when open. */\nfunction DropdownMenuIndicator({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Indicator>) {\n return (\n <DropdownMenuPrimitive.Indicator\n data-slot=\"dropdown-menu-indicator\"\n className={cn(\"inline-flex transition-transform data-[state=open]:rotate-180 [&_svg]:size-4\", className)}\n {...props}\n >\n {children ?? <ChevronDownIcon />}\n </DropdownMenuPrimitive.Indicator>\n )\n}\n\nexport {\n DropdownMenu,\n DropdownMenuArrow,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuContext,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuItemIndicator,\n DropdownMenuItemText,\n DropdownMenuLabel,\n DropdownMenuPortal,\n DropdownMenuPositioner,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n DropdownMenuItemContext,\n DropdownMenuContextTrigger,\n DropdownMenuIndicator,\n}\n"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "editable",
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/editable.tsx",
14
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Editable as EditablePrimitive } from \"@ark-ui/react\"\n\nfunction Editable({ className, ...props }: React.ComponentProps<typeof EditablePrimitive.Root>) {\n return (\n <EditablePrimitive.Root data-slot=\"editable\" className={cn(\"flex w-full flex-col gap-1.5\", className)} {...props} />\n )\n}\n\nfunction EditableContext({ ...props }: React.ComponentProps<typeof EditablePrimitive.Context>) {\n return <EditablePrimitive.Context {...props} />\n}\n\nfunction EditableLabel({ className, ...props }: React.ComponentProps<typeof EditablePrimitive.Label>) {\n return (\n <EditablePrimitive.Label\n data-slot=\"editable-label\"\n className={cn(\"text-sm font-medium select-none\", className)}\n {...props}\n />\n )\n}\n\nfunction EditableArea({ className, ...props }: React.ComponentProps<typeof EditablePrimitive.Area>) {\n return <EditablePrimitive.Area data-slot=\"editable-area\" className={cn(\"relative w-full\", className)} {...props} />\n}\n\nfunction EditablePreview({ className, ...props }: React.ComponentProps<typeof EditablePrimitive.Preview>) {\n return (\n <EditablePrimitive.Preview\n data-slot=\"editable-preview\"\n className={cn(\n \"inline-flex h-8 w-full items-center rounded-lg px-2.5 text-sm transition-colors hover:bg-muted data-placeholder-shown:text-muted-foreground data-disabled:opacity-50\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction EditableInput({ className, ...props }: React.ComponentProps<typeof EditablePrimitive.Input>) {\n return (\n <EditablePrimitive.Input\n data-slot=\"editable-input\"\n className={cn(\n \"h-8 w-full rounded-lg border border-input bg-transparent px-2.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 data-invalid:border-destructive dark:bg-input/30\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction EditableControl({ className, ...props }: React.ComponentProps<typeof EditablePrimitive.Control>) {\n return (\n <EditablePrimitive.Control\n data-slot=\"editable-control\"\n className={cn(\"flex items-center gap-1\", className)}\n {...props}\n />\n )\n}\n\nfunction EditableEditTrigger({ ...props }: React.ComponentProps<typeof EditablePrimitive.EditTrigger>) {\n return <EditablePrimitive.EditTrigger data-slot=\"editable-edit-trigger\" {...props} />\n}\n\nfunction EditableSubmitTrigger({ ...props }: React.ComponentProps<typeof EditablePrimitive.SubmitTrigger>) {\n return <EditablePrimitive.SubmitTrigger data-slot=\"editable-submit-trigger\" {...props} />\n}\n\nfunction EditableCancelTrigger({ ...props }: React.ComponentProps<typeof EditablePrimitive.CancelTrigger>) {\n return <EditablePrimitive.CancelTrigger data-slot=\"editable-cancel-trigger\" {...props} />\n}\n\nexport {\n Editable,\n EditableArea,\n EditableCancelTrigger,\n EditableContext,\n EditableControl,\n EditableEditTrigger,\n EditableInput,\n EditableLabel,\n EditablePreview,\n EditableSubmitTrigger,\n}\n"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "empty",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "class-variance-authority": "^0.7.1"
7
+ },
8
+ "registryDependencies": [
9
+ "utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/empty.tsx",
14
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"@/lib/utils\"\n\nfunction Empty({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"empty\"\n className={cn(\n \"flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction EmptyHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div data-slot=\"empty-header\" className={cn(\"flex max-w-sm flex-col items-center gap-2\", className)} {...props} />\n )\n}\n\nconst emptyMediaVariants = cva(\n \"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n icon: \"flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction EmptyMedia({\n className,\n variant = \"default\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof emptyMediaVariants>) {\n return (\n <div\n data-slot=\"empty-icon\"\n data-variant={variant}\n className={cn(emptyMediaVariants({ variant, className }))}\n {...props}\n />\n )\n}\n\nfunction EmptyTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"empty-title\"\n className={cn(\"font-heading text-sm font-medium tracking-tight\", className)}\n {...props}\n />\n )\n}\n\nfunction EmptyDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <div\n data-slot=\"empty-description\"\n className={cn(\n \"text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction EmptyContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"empty-content\"\n className={cn(\"flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance\", className)}\n {...props}\n />\n )\n}\n\nexport { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia }\n"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "field",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "class-variance-authority": "^0.7.1"
7
+ },
8
+ "registryDependencies": [
9
+ "label",
10
+ "separator",
11
+ "utils"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/ui/field.tsx",
16
+ "content": "\"use client\"\n\nimport { useMemo } from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"@/lib/utils\"\n\nimport { Label } from \"@/components/ui/label\"\nimport { Separator } from \"@/components/ui/separator\"\n\nfunction FieldSet({ className, ...props }: React.ComponentProps<\"fieldset\">) {\n return (\n <fieldset\n data-slot=\"field-set\"\n className={cn(\n \"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldLegend({\n className,\n variant = \"legend\",\n ...props\n}: React.ComponentProps<\"legend\"> & { variant?: \"legend\" | \"label\" }) {\n return (\n <legend\n data-slot=\"field-legend\"\n data-variant={variant}\n className={cn(\"mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base\", className)}\n {...props}\n />\n )\n}\n\nfunction FieldGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"field-group\"\n className={cn(\n \"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nconst fieldVariants = cva(\"group/field flex w-full gap-2 data-[invalid=true]:text-destructive\", {\n variants: {\n orientation: {\n vertical: \"flex-col *:w-full [&>.sr-only]:w-auto\",\n horizontal:\n \"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\",\n responsive:\n \"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\",\n },\n },\n defaultVariants: {\n orientation: \"vertical\",\n },\n})\n\nfunction Field({\n className,\n orientation = \"vertical\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof fieldVariants>) {\n return (\n <div\n role=\"group\"\n data-slot=\"field\"\n data-orientation={orientation}\n className={cn(fieldVariants({ orientation }), className)}\n {...props}\n />\n )\n}\n\nfunction FieldContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"field-content\"\n className={cn(\"group/field-content flex flex-1 flex-col gap-0.5 leading-snug\", className)}\n {...props}\n />\n )\n}\n\nfunction FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) {\n return (\n <Label\n data-slot=\"field-label\"\n className={cn(\n \"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border has-[>[data-slot=field]]:not-has-[:disabled,[data-disabled]]:hover:bg-muted/50 has-[>[data-slot=field]]:has-focus-visible:border-ring has-[>[data-slot=field]]:has-focus-visible:ring-3 has-[>[data-slot=field]]:has-focus-visible:ring-ring/50 *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10\",\n \"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"field-label\"\n className={cn(\n \"flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"field-description\"\n className={cn(\n \"text-left text-sm/normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5\",\n \"last:mt-0 nth-last-2:-mt-1\",\n \"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldSeparator({\n children,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n children?: React.ReactNode\n}) {\n return (\n <div\n data-slot=\"field-separator\"\n data-content={!!children}\n className={cn(\"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2\", className)}\n {...props}\n >\n <Separator className=\"absolute inset-0 top-1/2\" />\n {children && (\n <span\n className=\"relative mx-auto block w-fit bg-background px-2 text-muted-foreground\"\n data-slot=\"field-separator-content\"\n >\n {children}\n </span>\n )}\n </div>\n )\n}\n\nfunction FieldError({\n className,\n children,\n errors,\n ...props\n}: React.ComponentProps<\"div\"> & {\n errors?: Array<{ message?: string } | undefined>\n}) {\n const content = useMemo(() => {\n if (children) {\n return children\n }\n\n if (!errors?.length) {\n return null\n }\n\n const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()]\n\n if (uniqueErrors?.length == 1) {\n return uniqueErrors[0]?.message\n }\n\n return (\n <ul className=\"ml-4 flex list-disc flex-col gap-1\">\n {uniqueErrors.map((error, index) => error?.message && <li key={index}>{error.message}</li>)}\n </ul>\n )\n }, [children, errors])\n\n if (!content) {\n return null\n }\n\n return (\n <div\n role=\"alert\"\n data-slot=\"field-error\"\n className={cn(\"text-sm font-normal text-destructive\", className)}\n {...props}\n >\n {content}\n </div>\n )\n}\n\nexport {\n Field,\n FieldLabel,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLegend,\n FieldSeparator,\n FieldSet,\n FieldContent,\n FieldTitle,\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "file-upload",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "button",
11
+ "utils"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/ui/file-upload.tsx",
16
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { FileUpload as FileUploadPrimitive } from \"@ark-ui/react\"\nimport { FileIcon, UploadIcon, XIcon } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\n\nfunction FileUpload({ className, ...props }: React.ComponentProps<typeof FileUploadPrimitive.Root>) {\n return (\n <FileUploadPrimitive.Root\n data-slot=\"file-upload\"\n className={cn(\"flex w-full flex-col gap-3\", className)}\n {...props}\n />\n )\n}\n\nfunction FileUploadContext({ ...props }: React.ComponentProps<typeof FileUploadPrimitive.Context>) {\n return <FileUploadPrimitive.Context {...props} />\n}\n\nfunction FileUploadLabel({ className, ...props }: React.ComponentProps<typeof FileUploadPrimitive.Label>) {\n return (\n <FileUploadPrimitive.Label\n data-slot=\"file-upload-label\"\n className={cn(\"text-sm font-medium select-none\", className)}\n {...props}\n />\n )\n}\n\nfunction FileUploadDropzone({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof FileUploadPrimitive.Dropzone>) {\n return (\n <FileUploadPrimitive.Dropzone\n data-slot=\"file-upload-dropzone\"\n className={cn(\n \"flex min-h-32 cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border border-dashed border-input bg-transparent p-6 text-center text-sm text-muted-foreground transition-colors outline-none hover:bg-muted/50 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 data-dragging:border-ring data-dragging:bg-muted/50 data-invalid:border-destructive dark:bg-input/30 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-5\",\n className\n )}\n {...props}\n >\n {children ?? (\n <>\n <UploadIcon />\n <span>Drag your files here or click to browse</span>\n </>\n )}\n </FileUploadPrimitive.Dropzone>\n )\n}\n\nfunction FileUploadTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof FileUploadPrimitive.Trigger>) {\n return (\n <FileUploadPrimitive.Trigger data-slot=\"file-upload-trigger\" className={cn(className)} asChild {...props}>\n <Button variant=\"outline\" size=\"sm\">\n {children}\n </Button>\n </FileUploadPrimitive.Trigger>\n )\n}\n\nfunction FileUploadClearTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof FileUploadPrimitive.ClearTrigger>) {\n return (\n <FileUploadPrimitive.ClearTrigger\n data-slot=\"file-upload-clear-trigger\"\n className={cn(className)}\n asChild\n {...props}\n >\n <Button variant=\"ghost\" size=\"sm\">\n {children}\n </Button>\n </FileUploadPrimitive.ClearTrigger>\n )\n}\n\nfunction FileUploadItemGroup({ className, ...props }: React.ComponentProps<typeof FileUploadPrimitive.ItemGroup>) {\n return (\n <FileUploadPrimitive.ItemGroup\n data-slot=\"file-upload-item-group\"\n className={cn(\"flex flex-col gap-2\", className)}\n {...props}\n />\n )\n}\n\nfunction FileUploadItem({ className, ...props }: React.ComponentProps<typeof FileUploadPrimitive.Item>) {\n return (\n <FileUploadPrimitive.Item\n data-slot=\"file-upload-item\"\n className={cn(\"grid grid-cols-[auto_1fr_auto] items-center gap-x-3 rounded-lg border p-2 text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction FileUploadItemPreview({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof FileUploadPrimitive.ItemPreview>) {\n return (\n <FileUploadPrimitive.ItemPreview\n data-slot=\"file-upload-item-preview\"\n className={cn(\n \"row-span-2 flex size-10 items-center justify-center overflow-hidden rounded-md bg-muted text-muted-foreground [&_svg]:size-5\",\n className\n )}\n {...props}\n >\n {children ?? <FileIcon />}\n </FileUploadPrimitive.ItemPreview>\n )\n}\n\nfunction FileUploadItemPreviewImage({\n className,\n ...props\n}: React.ComponentProps<typeof FileUploadPrimitive.ItemPreviewImage>) {\n return (\n <FileUploadPrimitive.ItemPreviewImage\n data-slot=\"file-upload-item-preview-image\"\n className={cn(\"size-full object-cover\", className)}\n {...props}\n />\n )\n}\n\nfunction FileUploadItemName({ className, ...props }: React.ComponentProps<typeof FileUploadPrimitive.ItemName>) {\n return (\n <FileUploadPrimitive.ItemName\n data-slot=\"file-upload-item-name\"\n className={cn(\"truncate font-medium\", className)}\n {...props}\n />\n )\n}\n\nfunction FileUploadItemSizeText({\n className,\n ...props\n}: React.ComponentProps<typeof FileUploadPrimitive.ItemSizeText>) {\n return (\n <FileUploadPrimitive.ItemSizeText\n data-slot=\"file-upload-item-size-text\"\n className={cn(\"col-start-2 text-xs text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction FileUploadItemDeleteTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof FileUploadPrimitive.ItemDeleteTrigger>) {\n return (\n <FileUploadPrimitive.ItemDeleteTrigger\n data-slot=\"file-upload-item-delete-trigger\"\n className={cn(\"row-span-2\", className)}\n asChild\n {...props}\n >\n <Button variant=\"ghost\" size=\"icon-sm\">\n {children ?? <XIcon />}\n </Button>\n </FileUploadPrimitive.ItemDeleteTrigger>\n )\n}\n\nfunction FileUploadHiddenInput({ ...props }: React.ComponentProps<typeof FileUploadPrimitive.HiddenInput>) {\n return <FileUploadPrimitive.HiddenInput {...props} />\n}\n\nexport {\n FileUpload,\n FileUploadClearTrigger,\n FileUploadContext,\n FileUploadDropzone,\n FileUploadHiddenInput,\n FileUploadItem,\n FileUploadItemDeleteTrigger,\n FileUploadItemGroup,\n FileUploadItemName,\n FileUploadItemPreview,\n FileUploadItemPreviewImage,\n FileUploadItemSizeText,\n FileUploadLabel,\n FileUploadTrigger,\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "floating-panel",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "button",
11
+ "utils"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "components/ui/floating-panel.tsx",
16
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { FloatingPanel as FloatingPanelPrimitive, Portal as PortalPrimitive } from \"@ark-ui/react\"\nimport { Maximize2Icon, Minimize2Icon, MinusIcon, XIcon } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\n\nfunction FloatingPanel({\n lazyMount = true,\n unmountOnExit = true,\n ...props\n}: React.ComponentProps<typeof FloatingPanelPrimitive.Root>) {\n return <FloatingPanelPrimitive.Root lazyMount={lazyMount} unmountOnExit={unmountOnExit} {...props} />\n}\n\nfunction FloatingPanelContext({ ...props }: React.ComponentProps<typeof FloatingPanelPrimitive.Context>) {\n return <FloatingPanelPrimitive.Context {...props} />\n}\n\nfunction FloatingPanelTrigger({ ...props }: React.ComponentProps<typeof FloatingPanelPrimitive.Trigger>) {\n return <FloatingPanelPrimitive.Trigger data-slot=\"floating-panel-trigger\" {...props} />\n}\n\nfunction FloatingPanelPortal({ ...props }: React.ComponentProps<typeof PortalPrimitive>) {\n return <PortalPrimitive {...props} />\n}\n\nfunction FloatingPanelPositioner({\n className,\n ...props\n}: React.ComponentProps<typeof FloatingPanelPrimitive.Positioner>) {\n return (\n <FloatingPanelPrimitive.Positioner\n data-slot=\"floating-panel-positioner\"\n className={cn(\"[--z-index:50]\", className)}\n {...props}\n />\n )\n}\n\nfunction FloatingPanelContent({ className, ...props }: React.ComponentProps<typeof FloatingPanelPrimitive.Content>) {\n return (\n <FloatingPanelPortal>\n <FloatingPanelPositioner>\n <FloatingPanelPrimitive.Content\n data-slot=\"floating-panel-content\"\n className={cn(\n \"flex flex-col overflow-hidden rounded-xl bg-popover text-sm text-popover-foreground shadow-lg ring-1 ring-foreground/10 outline-hidden data-[stage=minimized]:h-auto 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 </FloatingPanelPositioner>\n </FloatingPanelPortal>\n )\n}\n\nfunction FloatingPanelDragTrigger({\n className,\n ...props\n}: React.ComponentProps<typeof FloatingPanelPrimitive.DragTrigger>) {\n return (\n <FloatingPanelPrimitive.DragTrigger\n data-slot=\"floating-panel-drag-trigger\"\n className={cn(\"flex-1 cursor-grab select-none data-dragging:cursor-grabbing\", className)}\n {...props}\n />\n )\n}\n\nfunction FloatingPanelHeader({ className, ...props }: React.ComponentProps<typeof FloatingPanelPrimitive.Header>) {\n return (\n <FloatingPanelPrimitive.Header\n data-slot=\"floating-panel-header\"\n className={cn(\"flex h-9 items-center gap-1 border-b bg-muted/50 pr-1 pl-3\", className)}\n {...props}\n />\n )\n}\n\nfunction FloatingPanelTitle({ className, ...props }: React.ComponentProps<typeof FloatingPanelPrimitive.Title>) {\n return (\n <FloatingPanelPrimitive.Title\n data-slot=\"floating-panel-title\"\n className={cn(\"font-heading text-sm font-medium\", className)}\n {...props}\n />\n )\n}\n\nfunction FloatingPanelControl({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof FloatingPanelPrimitive.Control>) {\n return (\n <FloatingPanelPrimitive.Control\n data-slot=\"floating-panel-control\"\n className={cn(\"flex items-center\", className)}\n {...props}\n >\n {children ?? (\n <>\n <FloatingPanelStageTrigger stage=\"minimized\">\n <MinusIcon />\n </FloatingPanelStageTrigger>\n <FloatingPanelStageTrigger stage=\"maximized\">\n <Maximize2Icon />\n </FloatingPanelStageTrigger>\n <FloatingPanelStageTrigger stage=\"default\">\n <Minimize2Icon />\n </FloatingPanelStageTrigger>\n <FloatingPanelCloseTrigger />\n </>\n )}\n </FloatingPanelPrimitive.Control>\n )\n}\n\nfunction FloatingPanelStageTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof FloatingPanelPrimitive.StageTrigger>) {\n return (\n <FloatingPanelPrimitive.StageTrigger\n data-slot=\"floating-panel-stage-trigger\"\n className={cn(className)}\n asChild\n {...props}\n >\n <Button variant=\"ghost\" size=\"icon-xs\">\n {children}\n </Button>\n </FloatingPanelPrimitive.StageTrigger>\n )\n}\n\nfunction FloatingPanelCloseTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof FloatingPanelPrimitive.CloseTrigger>) {\n return (\n <FloatingPanelPrimitive.CloseTrigger\n data-slot=\"floating-panel-close-trigger\"\n className={cn(className)}\n asChild\n {...props}\n >\n <Button variant=\"ghost\" size=\"icon-xs\">\n {children ?? <XIcon />}\n <span className=\"sr-only\">Close</span>\n </Button>\n </FloatingPanelPrimitive.CloseTrigger>\n )\n}\n\nfunction FloatingPanelBody({ className, ...props }: React.ComponentProps<typeof FloatingPanelPrimitive.Body>) {\n return (\n <FloatingPanelPrimitive.Body\n data-slot=\"floating-panel-body\"\n className={cn(\"flex-1 overflow-auto p-3\", className)}\n {...props}\n />\n )\n}\n\nfunction FloatingPanelResizeTrigger({\n className,\n ...props\n}: React.ComponentProps<typeof FloatingPanelPrimitive.ResizeTrigger>) {\n return (\n <FloatingPanelPrimitive.ResizeTrigger\n data-slot=\"floating-panel-resize-trigger\"\n className={cn(className)}\n {...props}\n />\n )\n}\n\nexport {\n FloatingPanel,\n FloatingPanelBody,\n FloatingPanelCloseTrigger,\n FloatingPanelContent,\n FloatingPanelContext,\n FloatingPanelControl,\n FloatingPanelDragTrigger,\n FloatingPanelHeader,\n FloatingPanelPortal,\n FloatingPanelPositioner,\n FloatingPanelResizeTrigger,\n FloatingPanelStageTrigger,\n FloatingPanelTitle,\n FloatingPanelTrigger,\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "floating-toolbar",
3
+ "type": "ui",
4
+ "description": "FloatingToolbar — the snackbar-style action bar that floats over a page (bulk actions on tables, pending edits on grids). Arrow keys move between its buttons, Home/End jump, and Escape calls `onEscape` unless focus is in a nested menu. Rend",
5
+ "dependencies": {},
6
+ "registryDependencies": [
7
+ "utils"
8
+ ],
9
+ "files": [
10
+ {
11
+ "path": "components/ui/floating-toolbar.tsx",
12
+ "content": "import * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\n/**\n * FloatingToolbar — the snackbar-style action bar that floats over a page\n * (bulk actions on tables, pending edits on grids). Arrow keys move between\n * its buttons, Home/End jump, and Escape calls `onEscape` unless focus is in a\n * nested menu. Renders nothing while `open` is false.\n */\nfunction FloatingToolbar({\n open = true,\n onEscape,\n className,\n children,\n onKeyDown,\n ...props\n}: React.ComponentProps<\"div\"> & { open?: boolean; onEscape?: () => void }) {\n const ref = React.useRef<HTMLDivElement>(null)\n const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event)\n if (event.defaultPrevented) return\n const list = Array.from(ref.current?.querySelectorAll<HTMLButtonElement>(\"button:not([disabled])\") ?? [])\n const currentIndex = list.findIndex((b) => b === document.activeElement)\n switch (event.key) {\n case \"ArrowRight\":\n event.preventDefault()\n list[(currentIndex + 1) % list.length]?.focus()\n break\n case \"ArrowLeft\":\n event.preventDefault()\n list[currentIndex <= 0 ? list.length - 1 : currentIndex - 1]?.focus()\n break\n case \"Home\":\n event.preventDefault()\n list[0]?.focus()\n break\n case \"End\":\n event.preventDefault()\n list[list.length - 1]?.focus()\n break\n case \"Escape\": {\n const target = event.target as HTMLElement\n const active = document.activeElement as HTMLElement | null\n const inMenu = [target, active].some(\n (el) =>\n el?.closest('[data-slot=\"dropdown-menu-trigger\"]') || el?.closest('[data-slot=\"dropdown-menu-content\"]')\n )\n if (inMenu || !onEscape) return\n event.preventDefault()\n onEscape()\n break\n }\n }\n }\n if (!open) return null\n return (\n <div\n ref={ref}\n role=\"toolbar\"\n data-slot=\"floating-toolbar\"\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n className={cn(\n \"fixed bottom-6 left-1/2 z-50 -translate-x-1/2 rounded-xl transition-all delay-100 duration-300 ease-out hover:scale-105 focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none\",\n className\n )}\n {...props}\n >\n <div\n data-slot=\"floating-toolbar-content\"\n className=\"flex items-center gap-2 rounded-xl border bg-background/95 p-2 shadow-xl backdrop-blur-lg supports-backdrop-filter:bg-background/60\"\n >\n {children}\n </div>\n </div>\n )\n}\n\nexport { FloatingToolbar }\n"
13
+ }
14
+ ],
15
+ "docs": "## Floating toolbar\n`src/components/ui/floating-toolbar.tsx` is the snackbar-style bar that floats at the bottom of a\npage (`open`, `onEscape`, arrow/Home/End roving between its buttons, Escape ignored while focus is\nin a nested menu). `DataTableBulkActions` renders on it; the data grid demo uses it for unsaved\nchanges. Use it for any \"N things pending\" action bar instead of a new fixed div."
16
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "form",
3
+ "type": "lib",
4
+ "description": "`src/lib/form.ts` exports `useForm(defaultValues, validate, onSubmit, { mode })`: a small, dependency-free hook (values, per-field errors, validating submit) that pairs with the `Field*` parts.",
5
+ "dependencies": {},
6
+ "registryDependencies": [],
7
+ "files": [
8
+ {
9
+ "path": "lib/form.ts",
10
+ "content": "import * as React from \"react\"\n\ntype Errors<T> = Partial<Record<keyof T, string>>\n\n/**\n * A small, dependency-free form hook: values, per-field errors, and a submit\n * handler that validates first. Pair it with the `Field*` parts\n * (`data-invalid` on `Field`, `aria-invalid` on the control, `FieldError` for\n * the message). Validation is a plain function so any schema library can be\n * adapted by returning its messages keyed by field.\n */\nexport function useForm<T extends object>(\n defaultValues: T,\n validate: (values: T) => Errors<T>,\n onSubmit: (values: T) => void,\n options: { mode?: \"onSubmit\" | \"onChange\" } = {}\n) {\n const [values, setValues] = React.useState<T>(defaultValues)\n const [errors, setErrors] = React.useState<Errors<T>>({})\n const [submitted, setSubmitted] = React.useState(false)\n\n const setValue = <K extends keyof T>(key: K, value: T[K]) => {\n setValues((prev) => {\n const next = { ...prev, [key]: value }\n if (options.mode === \"onChange\" || submitted) setErrors(validate(next))\n return next\n })\n }\n\n const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault()\n setSubmitted(true)\n const nextErrors = validate(values)\n setErrors(nextErrors)\n if (Object.keys(nextErrors).length === 0) onSubmit(values)\n }\n\n return { values, errors, setValue, setValues, handleSubmit }\n}\n"
11
+ }
12
+ ],
13
+ "docs": "## Forms\n`src/lib/form.ts` exports `useForm(defaultValues, validate, onSubmit, { mode })`: a small,\ndependency-free hook (values, per-field errors, validating submit) that pairs with the\n`Field*` parts. It is the toolkit's one form approach; do not add react-hook-form/zod. All\ndemo forms use it. Shared demo-only helpers live in `src/demo/lib/` (`showSubmittedData`,\n`DatePicker`)."
14
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "format",
3
+ "type": "ui",
4
+ "description": "Locale-aware formatting helpers from Ark, wrapping `Intl`.",
5
+ "dependencies": {
6
+ "@ark-ui/react": "^5.39.1"
7
+ },
8
+ "registryDependencies": [],
9
+ "files": [
10
+ {
11
+ "path": "components/ui/format.tsx",
12
+ "content": "import * as React from \"react\"\nimport { Format as FormatPrimitive } from \"@ark-ui/react\"\n\n/** Locale-aware formatting helpers from Ark, wrapping `Intl`. */\n\nfunction FormatNumber({ ...props }: React.ComponentProps<typeof FormatPrimitive.Number>) {\n return <FormatPrimitive.Number {...props} />\n}\n\nfunction FormatByte({ ...props }: React.ComponentProps<typeof FormatPrimitive.Byte>) {\n return <FormatPrimitive.Byte {...props} />\n}\n\nfunction FormatTime({ ...props }: React.ComponentProps<typeof FormatPrimitive.Time>) {\n return <FormatPrimitive.Time {...props} />\n}\n\nfunction FormatRelativeTime({ ...props }: React.ComponentProps<typeof FormatPrimitive.RelativeTime>) {\n return <FormatPrimitive.RelativeTime {...props} />\n}\n\nexport { FormatByte, FormatNumber, FormatRelativeTime, FormatTime }\n"
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "gantt",
3
+ "type": "ui",
4
+ "description": "`src/components/ui/gantt.tsx` is a compositional, data-agnostic timeline.",
5
+ "dependencies": {
6
+ "date-fns": "^4.4.0",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "button",
11
+ "controllable",
12
+ "live-region",
13
+ "utils"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "components/ui/gantt.tsx",
18
+ "content": "import * as React from \"react\"\nimport {\n addDays,\n differenceInCalendarDays,\n eachDayOfInterval,\n eachMonthOfInterval,\n eachWeekOfInterval,\n endOfMonth,\n endOfWeek,\n format,\n isSameDay,\n isWeekend,\n startOfDay,\n startOfMonth,\n startOfWeek,\n} from \"date-fns\"\nimport { CalendarIcon, MinusIcon, PlusIcon } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport { useControllable } from \"@/lib/controllable\"\nimport { LiveRegion, useLiveRegion } from \"@/components/ui/live-region\"\n\n/**\n * Gantt — a compositional, data-agnostic timeline. The consumer owns rows,\n * bars and dependencies and renders them with the parts; the root owns only\n * the time scale (`start`/`end`, `dayWidth`, header `scale`), the drag/resize\n * interaction and keyboard moves, and reports `onBarChange({ id, start, end })`.\n * Dates snap to whole days.\n *\n * Anatomy:\n * Gantt { start, end, dayWidth, scale, onBarChange }\n * GanttControls > GanttZoomOutTrigger / GanttZoomInTrigger / GanttTodayTrigger\n * GanttViewport single scroll container (x and y)\n * GanttHeader sticky two-tier date header (+ GanttHeaderCorner over the sidebar)\n * GanttBody\n * GanttRow value=… sidebar cell (GanttRowLabel, sticky) + track\n * GanttBar value=… start end progress > GanttBarLabel, GanttBarResizeHandle side=…\n * GanttMilestone value=… date\n * GanttDependencies svg arrows between registered bars\n * GanttToday vertical marker\n * GanttEmpty\n *\n * Keyboard on a bar: Space/Enter picks up, ←/→ move a day, Shift+←/→ change\n * the end, Alt+←/→ change the start, Space/Enter drops, Escape cancels.\n */\n\ntype GanttScale = \"day\" | \"week\" | \"month\"\ntype GanttBarChange = { id: string; start: Date; end: Date }\ntype BarRegistration = { id: string; el: HTMLElement | null; start: Date; end: Date; milestone?: boolean }\n\n/** Registered bars, outside React state so registering never re-renders the root. */\nfunction createBarStore() {\n const bars = new Map<string, BarRegistration>()\n const listeners = new Set<() => void>()\n let version = 0\n const notify = () => {\n version++\n listeners.forEach((l) => l())\n }\n return {\n bars,\n subscribe: (l: () => void) => {\n listeners.add(l)\n return () => {\n listeners.delete(l)\n }\n },\n getVersion: () => version,\n register(bar: BarRegistration) {\n bars.set(bar.id, bar)\n notify()\n return () => {\n if (bars.get(bar.id) === bar) bars.delete(bar.id)\n notify()\n }\n },\n }\n}\ntype BarStore = ReturnType<typeof createBarStore>\n\ntype Interaction = {\n id: string\n type: \"move\" | \"resize-start\" | \"resize-end\"\n start: Date\n end: Date\n}\n\ntype GanttContextValue = {\n start: Date\n end: Date\n days: number\n dayWidth: number\n rowHeight: number\n sidebarWidth: number\n scale: GanttScale\n editable: boolean\n interaction: Interaction | null\n grabbed: string | null\n viewportRef: React.RefObject<HTMLDivElement | null>\n xOf: (date: Date) => number\n dateAt: (x: number) => Date\n registerBar: (bar: BarRegistration) => () => void\n barStore: BarStore\n startInteraction: (bar: BarRegistration, type: Interaction[\"type\"], event: React.PointerEvent) => void\n grab: (id: string | null) => void\n cancelGrab: () => void\n nudge: (bar: BarRegistration, deltaDays: number, part: \"both\" | \"start\" | \"end\") => void\n zoomIn: () => void\n zoomOut: () => void\n scrollToDate: (date: Date, options?: { behavior?: ScrollBehavior }) => void\n announce: (message: string) => void\n}\n\nconst GanttContext = React.createContext<GanttContextValue | null>(null)\nconst RowContext = React.createContext<{ value: string } | null>(null)\n\nfunction useGantt() {\n const ctx = React.useContext(GanttContext)\n if (!ctx) throw new Error(\"Gantt parts must be used within <Gantt>\")\n return ctx\n}\nfunction useGanttRow() {\n const ctx = React.useContext(RowContext)\n if (!ctx) throw new Error(\"Row parts must be used within <GanttRow>\")\n return ctx\n}\n\nconst clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v))\n\n/* ---------------------------------------------------------------------------\n * Root\n * ------------------------------------------------------------------------- */\n\nfunction Gantt({\n start: startProp,\n end: endProp,\n dayWidth: dayWidthProp,\n defaultDayWidth = 40,\n onDayWidthChange,\n minDayWidth = 8,\n maxDayWidth = 160,\n scale: scaleProp,\n rowHeight = 40,\n sidebarWidth = 240,\n editable = true,\n onBarChange,\n className,\n children,\n ...props\n}: Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n start: Date\n end: Date\n /** Pixels per day; controllable. Zooming changes it. */\n dayWidth?: number\n defaultDayWidth?: number\n onDayWidthChange?: (dayWidth: number) => void\n minDayWidth?: number\n maxDayWidth?: number\n /** Header tiers. Defaults from `dayWidth`: month below 16px, week below 32px, else day. */\n scale?: GanttScale\n rowHeight?: number\n sidebarWidth?: number\n editable?: boolean\n onBarChange?: (change: GanttBarChange) => void\n}) {\n const viewportRef = React.useRef<HTMLDivElement>(null)\n const start = React.useMemo(() => startOfDay(startProp), [startProp])\n const end = React.useMemo(() => startOfDay(endProp), [endProp])\n const days = Math.max(1, differenceInCalendarDays(end, start) + 1)\n\n const [dayWidth, setDayWidthState] = useControllable(dayWidthProp, defaultDayWidth, onDayWidthChange)\n const setDayWidth = React.useCallback(\n (next: number) => setDayWidthState(clamp(next, minDayWidth, maxDayWidth)),\n [setDayWidthState, minDayWidth, maxDayWidth]\n )\n const scale: GanttScale = scaleProp ?? (dayWidth < 16 ? \"month\" : dayWidth < 32 ? \"week\" : \"day\")\n\n const xOf = React.useCallback(\n (date: Date) => differenceInCalendarDays(startOfDay(date), start) * dayWidth,\n [start, dayWidth]\n )\n const dateAt = React.useCallback((x: number) => addDays(start, Math.round(x / dayWidth)), [start, dayWidth])\n\n /* Bar registry for dependency lines */\n const barStore = React.useMemo(() => createBarStore(), [])\n const registerBar = React.useCallback((bar: BarRegistration) => barStore.register(bar), [barStore])\n\n /* Interaction */\n const [interaction, setInteraction] = React.useState<Interaction | null>(null)\n const [grabbed, setGrabbed] = React.useState<string | null>(null)\n const { message: announcement, announce } = useLiveRegion()\n const onBarChangeRef = React.useRef(onBarChange)\n React.useEffect(() => {\n onBarChangeRef.current = onBarChange\n }, [onBarChange])\n const grabOrigin = React.useRef<{ id: string; start: Date; end: Date } | null>(null)\n\n const startInteraction = React.useCallback(\n (bar: BarRegistration, type: Interaction[\"type\"], event: React.PointerEvent) => {\n if (!editable) return\n event.preventDefault()\n event.stopPropagation()\n const origin = event.clientX\n let current: Interaction = { id: bar.id, type, start: bar.start, end: bar.end }\n setInteraction(current)\n const move = (e: PointerEvent) => {\n const deltaDays = Math.round((e.clientX - origin) / dayWidth)\n const duration = differenceInCalendarDays(bar.end, bar.start)\n if (type === \"move\") {\n current = { id: bar.id, type, start: addDays(bar.start, deltaDays), end: addDays(bar.end, deltaDays) }\n } else if (type === \"resize-end\") {\n current = { id: bar.id, type, start: bar.start, end: addDays(bar.end, Math.max(deltaDays, -duration)) }\n } else {\n current = { id: bar.id, type, start: addDays(bar.start, Math.min(deltaDays, duration)), end: bar.end }\n }\n setInteraction(current)\n }\n const up = () => {\n window.removeEventListener(\"pointermove\", move)\n window.removeEventListener(\"pointerup\", up)\n window.removeEventListener(\"pointercancel\", up)\n setInteraction(null)\n if (!isSameDay(current.start, bar.start) || !isSameDay(current.end, bar.end)) {\n onBarChangeRef.current?.({ id: bar.id, start: current.start, end: current.end })\n }\n }\n window.addEventListener(\"pointermove\", move)\n window.addEventListener(\"pointerup\", up)\n window.addEventListener(\"pointercancel\", up)\n },\n [editable, dayWidth]\n )\n\n const grab = React.useCallback(\n (id: string | null) => {\n if (id) {\n const bar = barStore.bars.get(id)\n grabOrigin.current = bar ? { id, start: bar.start, end: bar.end } : null\n setGrabbed(id)\n announce(\n \"Picked up. Arrow keys move by a day, Shift changes the end, Alt the start. Space drops, Escape cancels.\"\n )\n } else {\n setGrabbed(null)\n grabOrigin.current = null\n announce(\"Dropped.\")\n }\n },\n [barStore, announce]\n )\n const cancelGrab = React.useCallback(() => {\n const origin = grabOrigin.current\n if (origin) onBarChangeRef.current?.(origin)\n setGrabbed(null)\n grabOrigin.current = null\n announce(\"Cancelled.\")\n }, [announce])\n\n const nudge = React.useCallback(\n (bar: BarRegistration, deltaDays: number, part: \"both\" | \"start\" | \"end\") => {\n const duration = differenceInCalendarDays(bar.end, bar.start)\n const next =\n part === \"both\"\n ? { start: addDays(bar.start, deltaDays), end: addDays(bar.end, deltaDays) }\n : part === \"end\"\n ? { start: bar.start, end: addDays(bar.end, Math.max(deltaDays, -duration)) }\n : { start: addDays(bar.start, Math.min(deltaDays, duration)), end: bar.end }\n onBarChangeRef.current?.({ id: bar.id, ...next })\n announce(`${format(next.start, \"MMM d\")} to ${format(next.end, \"MMM d\")}.`)\n },\n [announce]\n )\n\n const scrollToDate = React.useCallback(\n (date: Date, options?: { behavior?: ScrollBehavior }) => {\n const el = viewportRef.current\n if (!el) return\n const x = xOf(date)\n el.scrollTo({\n left: Math.max(0, x - (el.clientWidth - sidebarWidth) / 2),\n behavior: options?.behavior ?? \"smooth\",\n })\n },\n [xOf, sidebarWidth]\n )\n\n /** Zoom keeping the date at the viewport centre in place. */\n const zoomBy = React.useCallback(\n (factor: number) => {\n const el = viewportRef.current\n const centreX = el ? el.scrollLeft + (el.clientWidth - sidebarWidth) / 2 : 0\n const centreDays = centreX / dayWidth\n const next = clamp(dayWidth * factor, minDayWidth, maxDayWidth)\n setDayWidth(next)\n requestAnimationFrame(() => {\n if (el) el.scrollLeft = Math.max(0, centreDays * next - (el.clientWidth - sidebarWidth) / 2)\n })\n },\n [dayWidth, minDayWidth, maxDayWidth, setDayWidth, sidebarWidth]\n )\n const zoomIn = React.useCallback(() => zoomBy(1.5), [zoomBy])\n const zoomOut = React.useCallback(() => zoomBy(1 / 1.5), [zoomBy])\n\n const ctx = React.useMemo<GanttContextValue>(\n () => ({\n start,\n end,\n days,\n dayWidth,\n rowHeight,\n sidebarWidth,\n scale,\n editable,\n interaction,\n grabbed,\n viewportRef,\n xOf,\n dateAt,\n registerBar,\n barStore,\n startInteraction,\n grab,\n cancelGrab,\n nudge,\n zoomIn,\n zoomOut,\n scrollToDate,\n announce,\n }),\n [\n start,\n end,\n days,\n dayWidth,\n rowHeight,\n sidebarWidth,\n scale,\n editable,\n interaction,\n grabbed,\n xOf,\n dateAt,\n registerBar,\n barStore,\n startInteraction,\n grab,\n cancelGrab,\n nudge,\n zoomIn,\n zoomOut,\n scrollToDate,\n announce,\n ]\n )\n\n return (\n <GanttContext.Provider value={ctx}>\n <div\n data-slot=\"gantt\"\n data-scale={scale}\n data-editable={editable ? \"\" : undefined}\n data-interacting={interaction?.type}\n className={cn(\"relative flex min-h-0 flex-1 flex-col\", className)}\n style={{ [\"--gantt-sidebar\" as string]: `${sidebarWidth}px`, [\"--gantt-row\" as string]: `${rowHeight}px` }}\n {...props}\n >\n {children}\n <LiveRegion data-slot=\"gantt-live-region\" message={announcement} />\n </div>\n </GanttContext.Provider>\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Viewport, header, body\n * ------------------------------------------------------------------------- */\n\nfunction GanttViewport({ className, ...props }: React.ComponentProps<\"div\">) {\n const { viewportRef } = useGantt()\n return (\n <div\n ref={viewportRef}\n data-slot=\"gantt-viewport\"\n className={cn(\"relative min-h-0 flex-1 overflow-auto rounded-xl border bg-background text-sm\", className)}\n {...props}\n />\n )\n}\n\n/** Two-tier sticky header; the tiers depend on the scale. */\nfunction GanttHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n const { start, end, days, dayWidth, scale, sidebarWidth } = useGantt()\n const width = days * dayWidth\n const tiers = React.useMemo(() => {\n const clampX = (date: Date) => clamp(differenceInCalendarDays(date, start), 0, days)\n const span = (from: Date, to: Date) => ({\n left: clampX(from) * dayWidth,\n width: (clampX(addDays(to, 1)) - clampX(from)) * dayWidth,\n })\n const months = eachMonthOfInterval({ start, end }).map((m) => ({\n key: m.toISOString(),\n label: format(m, scale === \"month\" ? \"MMMM yyyy\" : \"MMM yyyy\"),\n ...span(startOfMonth(m), endOfMonth(m)),\n }))\n if (scale === \"day\") {\n const dayCells = eachDayOfInterval({ start, end }).map((d) => ({\n key: d.toISOString(),\n label: dayWidth >= 40 ? format(d, \"EEEEE d\") : format(d, \"d\"),\n weekend: isWeekend(d),\n today: isSameDay(d, new Date()),\n ...span(d, d),\n }))\n return { top: months, bottom: dayCells }\n }\n if (scale === \"week\") {\n const weeks = eachWeekOfInterval({ start, end }, { weekStartsOn: 1 }).map((w) => ({\n key: w.toISOString(),\n label: format(w, \"d MMM\"),\n weekend: false,\n today: false,\n ...span(startOfWeek(w, { weekStartsOn: 1 }), endOfWeek(w, { weekStartsOn: 1 })),\n }))\n return { top: months, bottom: weeks }\n }\n const years = Array.from(new Set(months.map((m) => m.label.slice(-4)))).map((y) => {\n const first = new Date(Number(y), 0, 1)\n const last = new Date(Number(y), 11, 31)\n return { key: y, label: y, ...span(first, last) }\n })\n return {\n top: years,\n bottom: months.map((m) => ({ ...m, label: format(new Date(m.key), \"MMM\"), weekend: false, today: false })),\n }\n }, [start, end, days, dayWidth, scale])\n\n return (\n <div\n data-slot=\"gantt-header\"\n className={cn(\"sticky top-0 z-30 flex bg-muted text-xs text-muted-foreground\", className)}\n style={{ width: sidebarWidth + width }}\n {...props}\n >\n <GanttHeaderCorner />\n <div className=\"relative shrink-0\" style={{ width }}>\n <div className=\"flex h-7 border-b\">\n {tiers.top.map((cell) => (\n <div\n key={cell.key}\n className=\"absolute inset-y-0 flex items-center truncate border-e px-2 font-medium\"\n style={{ left: cell.left, width: cell.width }}\n >\n {cell.label}\n </div>\n ))}\n </div>\n <div className=\"flex h-7 border-b\">\n {tiers.bottom.map((cell) => (\n <div\n key={cell.key}\n data-weekend={cell.weekend ? \"\" : undefined}\n data-today={cell.today ? \"\" : undefined}\n className=\"absolute flex h-7 items-center justify-center truncate border-e data-today:font-semibold data-today:text-foreground data-weekend:bg-foreground/3\"\n style={{ left: cell.left, width: cell.width, top: 28 }}\n >\n {cell.width >= 18 ? cell.label : \"\"}\n </div>\n ))}\n </div>\n </div>\n </div>\n )\n}\n\n/** The sticky cell above the sidebar; put a title or a search box in it. */\nfunction GanttHeaderCorner({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"gantt-header-corner\"\n className={cn(\n \"sticky left-0 z-40 flex h-14 shrink-0 items-end border-e border-b bg-muted px-3 pb-2 font-medium\",\n className\n )}\n style={{ width: \"var(--gantt-sidebar)\" }}\n {...props}\n />\n )\n}\n\nfunction GanttBody({ className, style, ...props }: React.ComponentProps<\"div\">) {\n const { days, dayWidth, sidebarWidth } = useGantt()\n return (\n <div\n data-slot=\"gantt-body\"\n className={cn(\"relative\", className)}\n style={{ width: sidebarWidth + days * dayWidth, ...style }}\n {...props}\n />\n )\n}\n\n/** Vertical day/week lines and weekend shading behind the bars. */\nfunction GanttGridLines({ className, ...props }: React.ComponentProps<\"div\">) {\n const { start, end, dayWidth, scale, sidebarWidth } = useGantt()\n const cells = React.useMemo(() => {\n if (scale === \"month\")\n return eachMonthOfInterval({ start, end }).map((d) => ({ date: startOfMonth(d), weekend: false }))\n if (scale === \"week\")\n return eachWeekOfInterval({ start, end }, { weekStartsOn: 1 }).map((d) => ({ date: d, weekend: false }))\n return eachDayOfInterval({ start, end }).map((d) => ({ date: d, weekend: isWeekend(d) }))\n }, [start, end, scale])\n return (\n <div\n data-slot=\"gantt-grid-lines\"\n aria-hidden\n className={cn(\"pointer-events-none absolute inset-y-0\", className)}\n style={{ left: sidebarWidth, right: 0 }}\n {...props}\n >\n {cells.map((cell) => (\n <div\n key={cell.date.toISOString()}\n data-weekend={cell.weekend ? \"\" : undefined}\n className=\"absolute inset-y-0 border-e border-border/60 data-weekend:bg-foreground/2.5\"\n style={{\n left: differenceInCalendarDays(cell.date, start) * dayWidth,\n width: scale === \"day\" ? dayWidth : undefined,\n }}\n />\n ))}\n </div>\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Rows\n * ------------------------------------------------------------------------- */\n\n/** Groups the rows; keep bars inside so dependency lines can find them. */\nfunction GanttRows({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"gantt-rows\" className={cn(\"relative\", className)} {...props} />\n}\n\nfunction GanttRow({ value, className, children, ...props }: React.ComponentProps<\"div\"> & { value: string }) {\n const { rowHeight } = useGantt()\n const ctx = React.useMemo(() => ({ value }), [value])\n return (\n <RowContext.Provider value={ctx}>\n <div\n data-slot=\"gantt-row\"\n data-value={value}\n className={cn(\"group/row flex border-b hover:bg-muted/30\", className)}\n style={{ height: rowHeight }}\n {...props}\n >\n {children}\n </div>\n </RowContext.Provider>\n )\n}\n\n/** Sticky sidebar cell for the row. */\nfunction GanttRowLabel({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"gantt-row-label\"\n className={cn(\n \"sticky left-0 z-20 flex shrink-0 items-center gap-2 border-e bg-background px-3 text-sm transition-colors\",\n \"group-hover/row:bg-[color-mix(in_oklab,var(--color-muted)_30%,var(--color-background))]\",\n className\n )}\n style={{ width: \"var(--gantt-sidebar)\" }}\n {...props}\n />\n )\n}\n\n/** The timeline part of a row; bars are positioned inside it. */\nfunction GanttRowTrack({ className, ...props }: React.ComponentProps<\"div\">) {\n const { days, dayWidth } = useGantt()\n return (\n <div\n data-slot=\"gantt-row-track\"\n className={cn(\"relative shrink-0\", className)}\n style={{ width: days * dayWidth }}\n {...props}\n />\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Bars\n * ------------------------------------------------------------------------- */\n\nconst BarContext = React.createContext<BarRegistration | null>(null)\n\nfunction GanttBar({\n value,\n start,\n end,\n progress,\n className,\n style,\n children,\n onPointerDown,\n onKeyDown,\n onBlur,\n ...props\n}: React.ComponentProps<\"div\"> & { value: string; start: Date; end: Date; progress?: number }) {\n const gantt = useGantt()\n useGanttRow()\n const ref = React.useRef<HTMLDivElement>(null)\n const live = gantt.interaction?.id === value ? gantt.interaction : null\n // Key on timestamps: callers often pass fresh Date objects every render.\n const startMs = (live ? live.start : startOfDay(start)).getTime()\n const endMs = (live ? live.end : startOfDay(end)).getTime()\n const registration = React.useMemo<BarRegistration>(\n () => ({ id: value, el: null, start: new Date(startMs), end: new Date(endMs) }),\n [value, startMs, endMs]\n )\n const shownStart = registration.start\n const shownEnd = registration.end\n const { registerBar } = gantt\n React.useLayoutEffect(() => registerBar({ ...registration, el: ref.current }), [registerBar, registration])\n const left = gantt.xOf(shownStart)\n const width = Math.max(gantt.dayWidth, (differenceInCalendarDays(shownEnd, shownStart) + 1) * gantt.dayWidth)\n const isGrabbed = gantt.grabbed === value\n\n return (\n <BarContext.Provider value={registration}>\n <div\n ref={ref}\n data-slot=\"gantt-bar\"\n data-value={value}\n data-dragging={live ? live.type : undefined}\n data-grabbed={isGrabbed ? \"\" : undefined}\n tabIndex={gantt.editable ? 0 : undefined}\n aria-pressed={gantt.editable ? isGrabbed : undefined}\n aria-label={`${format(shownStart, \"MMM d\")} to ${format(shownEnd, \"MMM d\")}`}\n className={cn(\n \"group/bar absolute top-1/2 z-10 flex h-7 -translate-y-1/2 items-center overflow-hidden rounded-md border border-primary/30 bg-primary/15 text-xs text-foreground outline-none select-none\",\n gantt.editable && \"cursor-grab active:cursor-grabbing\",\n \"focus-visible:ring-2 focus-visible:ring-ring/50 data-dragging:z-20 data-dragging:shadow-md data-grabbed:ring-2 data-grabbed:ring-primary\",\n className\n )}\n style={{ left, width, ...style }}\n onPointerDown={(event) => {\n onPointerDown?.(event)\n if (event.defaultPrevented || event.button !== 0) return\n if ((event.target as HTMLElement).closest(\"[data-slot=gantt-bar-resize-handle], button, a, input\")) return\n gantt.startInteraction(registration, \"move\", event)\n }}\n onKeyDown={(event) => {\n onKeyDown?.(event)\n if (event.defaultPrevented || !gantt.editable || event.target !== event.currentTarget) return\n if (event.key === \" \" || event.key === \"Enter\") {\n event.preventDefault()\n gantt.grab(isGrabbed ? null : value)\n return\n }\n if (!isGrabbed) return\n if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n event.preventDefault()\n const delta = event.key === \"ArrowLeft\" ? -1 : 1\n gantt.nudge(registration, delta, event.shiftKey ? \"end\" : event.altKey ? \"start\" : \"both\")\n } else if (event.key === \"Escape\") {\n event.preventDefault()\n gantt.cancelGrab()\n }\n }}\n onBlur={(event) => {\n onBlur?.(event)\n if (isGrabbed && !event.currentTarget.contains(event.relatedTarget as Node | null)) gantt.grab(null)\n }}\n {...props}\n >\n {progress !== undefined && (\n <div\n data-slot=\"gantt-bar-progress\"\n aria-hidden\n className=\"absolute inset-y-0 left-0 bg-primary/30\"\n style={{ width: `${clamp(progress, 0, 1) * 100}%` }}\n />\n )}\n {children}\n </div>\n </BarContext.Provider>\n )\n}\n\nfunction GanttBarLabel({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span data-slot=\"gantt-bar-label\" className={cn(\"relative z-10 truncate px-2 font-medium\", className)} {...props} />\n )\n}\n\nfunction GanttBarResizeHandle({\n side,\n className,\n onPointerDown,\n ...props\n}: React.ComponentProps<\"div\"> & { side: \"start\" | \"end\" }) {\n const gantt = useGantt()\n const bar = React.useContext(BarContext)\n if (!gantt.editable || !bar) return null\n return (\n <div\n data-slot=\"gantt-bar-resize-handle\"\n data-side={side}\n role=\"separator\"\n aria-orientation=\"vertical\"\n className={cn(\n \"absolute inset-y-0 z-10 w-2 cursor-ew-resize touch-none opacity-0 transition-opacity group-hover/bar:opacity-100 group-focus-visible/bar:opacity-100\",\n side === \"start\" ? \"left-0 rounded-l-md\" : \"right-0 rounded-r-md\",\n \"after:absolute after:inset-y-1.5 after:w-0.5 after:rounded-full after:bg-primary/60\",\n side === \"start\" ? \"after:left-0.5\" : \"after:right-0.5\",\n className\n )}\n onPointerDown={(event) => {\n onPointerDown?.(event)\n if (event.defaultPrevented || event.button !== 0) return\n gantt.startInteraction(bar, side === \"start\" ? \"resize-start\" : \"resize-end\", event)\n }}\n {...props}\n />\n )\n}\n\n/** A single-day marker rendered as a diamond. */\nfunction GanttMilestone({\n value,\n date,\n className,\n style,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & { value: string; date: Date }) {\n const gantt = useGantt()\n useGanttRow()\n const ref = React.useRef<HTMLDivElement>(null)\n const dayMs = startOfDay(date).getTime()\n const registration = React.useMemo<BarRegistration>(\n () => ({ id: value, el: null, start: new Date(dayMs), end: new Date(dayMs), milestone: true }),\n [value, dayMs]\n )\n const day = registration.start\n const { registerBar } = gantt\n React.useLayoutEffect(() => registerBar({ ...registration, el: ref.current }), [registerBar, registration])\n return (\n <div\n ref={ref}\n data-slot=\"gantt-milestone\"\n data-value={value}\n aria-label={format(day, \"MMM d\")}\n className={cn(\"absolute top-1/2 z-10 flex -translate-y-1/2 items-center gap-2\", className)}\n style={{ left: gantt.xOf(day) + gantt.dayWidth / 2 - 8, ...style }}\n {...props}\n >\n <span aria-hidden className=\"block size-4 rotate-45 rounded-sm bg-foreground\" />\n {children && <span className=\"text-xs font-medium whitespace-nowrap\">{children}</span>}\n </div>\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Dependencies, today, empty\n * ------------------------------------------------------------------------- */\n\n/** Arrows from the end of `from` to the start of `to`, measured from the registered bar elements. */\nfunction GanttDependencies({\n links,\n className,\n ...props\n}: React.ComponentProps<\"svg\"> & { links: { from: string; to: string }[] }) {\n const { barStore, dayWidth, xOf, sidebarWidth, rowHeight, days } = useGantt()\n React.useSyncExternalStore(barStore.subscribe, barStore.getVersion, barStore.getVersion)\n const svgRef = React.useRef<SVGSVGElement>(null)\n const id = React.useId()\n // Row centres come from the DOM so collapsed groups and custom row order need no bookkeeping.\n const origin = svgRef.current?.getBoundingClientRect()\n const centreY = (el: HTMLElement | null) => {\n if (!el || !origin) return null\n const r = el.getBoundingClientRect()\n return r.top + r.height / 2 - origin.top\n }\n const paths = links\n .map((link) => {\n const from = barStore.bars.get(link.from)\n const to = barStore.bars.get(link.to)\n if (!from || !to) return null\n const y1 = centreY(from.el)\n const y2 = centreY(to.el)\n if (y1 === null || y2 === null) return null\n const x1 = xOf(from.end) + (from.milestone ? dayWidth / 2 + 8 : dayWidth)\n const x2 = xOf(to.start) + (to.milestone ? dayWidth / 2 - 8 : 0)\n const gap = 10\n const d =\n x2 - gap > x1\n ? `M ${x1} ${y1} H ${x1 + gap} V ${y2} H ${x2}`\n : `M ${x1} ${y1} H ${x1 + gap} V ${y1 + rowHeight / 2 - 4} H ${x2 - gap} V ${y2} H ${x2}`\n return { key: `${link.from}->${link.to}`, d }\n })\n .filter((p): p is { key: string; d: string } => !!p)\n return (\n <svg\n ref={svgRef}\n data-slot=\"gantt-dependencies\"\n aria-hidden\n className={cn(\"pointer-events-none absolute top-0 z-5 overflow-hidden text-muted-foreground\", className)}\n style={{ left: sidebarWidth, width: days * dayWidth, height: \"100%\" }}\n {...props}\n >\n <defs>\n <marker id={`${id}-arrow`} viewBox=\"0 0 6 6\" refX=\"5\" refY=\"3\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto\">\n <path d=\"M 0 0 L 6 3 L 0 6 z\" fill=\"currentColor\" />\n </marker>\n </defs>\n {paths.map((p) => (\n <path key={p.key} d={p.d} fill=\"none\" stroke=\"currentColor\" strokeWidth={1.5} markerEnd={`url(#${id}-arrow)`} />\n ))}\n </svg>\n )\n}\n\nfunction GanttToday({ className, date = new Date(), ...props }: React.ComponentProps<\"div\"> & { date?: Date }) {\n const { xOf, dayWidth, sidebarWidth, start, end } = useGantt()\n const day = startOfDay(date)\n if (day < start || day > end) return null\n return (\n <div\n data-slot=\"gantt-today\"\n aria-hidden\n className={cn(\"pointer-events-none absolute inset-y-0 w-px bg-destructive\", className)}\n style={{ left: sidebarWidth + xOf(day) + dayWidth / 2 }}\n {...props}\n >\n <span className=\"absolute top-0 left-1 rounded-b bg-destructive px-1 text-[10px]/4 text-white\">Today</span>\n </div>\n )\n}\n\nfunction GanttEmpty({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"gantt-empty\"\n className={cn(\"flex h-32 items-center justify-center text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Controls\n * ------------------------------------------------------------------------- */\n\nfunction GanttControls({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"gantt-controls\" className={cn(\"flex items-center gap-1\", className)} {...props} />\n}\n\nfunction GanttZoomInTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { zoomIn } = useGantt()\n return (\n <Button\n data-slot=\"gantt-zoom-in-trigger\"\n variant=\"outline\"\n size=\"icon-sm\"\n aria-label=\"Zoom in\"\n asChild={asChild}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) zoomIn()\n }}\n {...props}\n >\n {asChild ? children : (children ?? <PlusIcon />)}\n </Button>\n )\n}\n\nfunction GanttZoomOutTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { zoomOut } = useGantt()\n return (\n <Button\n data-slot=\"gantt-zoom-out-trigger\"\n variant=\"outline\"\n size=\"icon-sm\"\n aria-label=\"Zoom out\"\n asChild={asChild}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) zoomOut()\n }}\n {...props}\n >\n {asChild ? children : (children ?? <MinusIcon />)}\n </Button>\n )\n}\n\nfunction GanttTodayTrigger({ asChild, children, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { scrollToDate } = useGantt()\n return (\n <Button\n data-slot=\"gantt-today-trigger\"\n variant=\"outline\"\n size=\"sm\"\n asChild={asChild}\n onClick={(event) => {\n onClick?.(event)\n if (!event.defaultPrevented) scrollToDate(new Date())\n }}\n {...props}\n >\n {asChild\n ? children\n : (children ?? (\n <>\n <CalendarIcon /> Today\n </>\n ))}\n </Button>\n )\n}\n\nexport {\n Gantt,\n GanttViewport,\n GanttHeader,\n GanttHeaderCorner,\n GanttBody,\n GanttGridLines,\n GanttRows,\n GanttRow,\n GanttRowLabel,\n GanttRowTrack,\n GanttBar,\n GanttBarLabel,\n GanttBarResizeHandle,\n GanttMilestone,\n GanttDependencies,\n GanttToday,\n GanttEmpty,\n GanttControls,\n GanttZoomInTrigger,\n GanttZoomOutTrigger,\n GanttTodayTrigger,\n useGantt,\n useGanttRow,\n type GanttScale,\n type GanttBarChange,\n}\n"
19
+ }
20
+ ],
21
+ "docs": "## Gantt\n`src/components/ui/gantt.tsx` is a compositional, data-agnostic timeline. The consumer owns rows,\nbars and dependency links; the root owns the time scale (`start`/`end` dates, `dayWidth`\ncontrollable + `onDayWidthChange`, `scale` day|week|month derived from `dayWidth` unless set,\n`rowHeight`, `sidebarWidth`, `editable`), the drag/resize interaction, keyboard moves, and\nreports `onBarChange({ id, start, end })` snapped to whole days. Parts: `GanttViewport` (one\nscroll container for both axes) > `GanttHeader` (sticky two-tier date header, tiers by scale;\n`GanttHeaderCorner` is the sticky cell over the sidebar) + `GanttBody` (`GanttGridLines` with\nweekend shading, `GanttToday`, `GanttRows` > `GanttRow value=` > `GanttRowLabel` (sticky sidebar\ncell) + `GanttRowTrack` > `GanttBar value= start end progress` (draggable, focusable; `data-dragging`\n= move|resize-start|resize-end, `data-grabbed`; children: `GanttBarLabel`, `GanttBarResizeHandle\nside=start|end`) or `GanttMilestone value= date` (diamond), `GanttDependencies links=[{ from, to }]`\n(orthogonal arrows measured from the registered bar elements through a small external store, so\ncollapsed groups and custom row order need no bookkeeping), `GanttEmpty`), `GanttControls` >\n`GanttZoomIn/ZoomOutTrigger` (zoom keeps the centre date) + `GanttTodayTrigger`, and `useGantt()`\n(`xOf`, `dateAt`, `scrollToDate`, ...). Keyboard on a bar: Space/Enter picks up, ←/→ move a day,\nShift+←/→ change the end, Alt+←/→ change the start, Escape cancels; a live region announces.\nGotcha: bars register with a memo keyed on timestamps; never key registrations on `Date` identity.\n`src/demo/gantt/` is the reference (`/projects/timeline`, \"Timeline\"): phases with collapsible groups and\nsummary spans, milestones, dependency arrows toggle, day/week/month `SegmentGroup`, undo/redo on\n`useHistory`, editable toggle, scroll to today on mount."
22
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "history",
3
+ "type": "lib",
4
+ "description": "Undo/redo state for any immutable value. `set` records a history entry unless `{ commit: false }` is passed (live drags, typing); `checkpoint` snapshots the present before such a sequence so one undo restores it.",
5
+ "dependencies": {},
6
+ "registryDependencies": [],
7
+ "files": [
8
+ {
9
+ "path": "lib/history.ts",
10
+ "content": "import * as React from \"react\"\n\n/**\n * Undo/redo state for any immutable value. `set` records a history entry\n * unless `{ commit: false }` is passed (live drags, typing); `checkpoint`\n * snapshots the present before such a sequence so one undo restores it.\n */\nexport function useHistory<T>(initial: T | (() => T), limit = 100) {\n const [state, setState] = React.useState(() => ({\n past: [] as T[],\n present: typeof initial === \"function\" ? (initial as () => T)() : initial,\n future: [] as T[],\n }))\n const set = React.useCallback(\n (update: T | ((prev: T) => T), options?: { commit?: boolean }) =>\n setState((s) => {\n const next = typeof update === \"function\" ? (update as (prev: T) => T)(s.present) : update\n if (Object.is(next, s.present)) return s\n if (options?.commit === false) return { ...s, present: next }\n return { past: [...s.past.slice(-(limit - 1)), s.present], present: next, future: [] }\n }),\n [limit]\n )\n const checkpoint = React.useCallback(\n () => setState((s) => ({ past: [...s.past.slice(-(limit - 1)), s.present], present: s.present, future: [] })),\n [limit]\n )\n const undo = React.useCallback(\n () =>\n setState((s) =>\n s.past.length\n ? { past: s.past.slice(0, -1), present: s.past[s.past.length - 1], future: [s.present, ...s.future] }\n : s\n ),\n []\n )\n const redo = React.useCallback(\n () =>\n setState((s) =>\n s.future.length ? { past: [...s.past, s.present], present: s.future[0], future: s.future.slice(1) } : s\n ),\n []\n )\n const reset = React.useCallback((value: T) => setState({ past: [], present: value, future: [] }), [])\n return {\n present: state.present,\n set,\n checkpoint,\n undo,\n redo,\n reset,\n canUndo: state.past.length > 0,\n canRedo: state.future.length > 0,\n }\n}\n"
11
+ }
12
+ ],
13
+ "docs": "## Shared hooks\n`src/lib/history.ts` exports `useHistory(initial, limit)` → `{ present, set(next, { commit }),\ncheckpoint, undo, redo, reset, canUndo, canRedo }`; the node graph and form builder use it. Never\nadd a local copy. There is no native `<select>` component any more (`native-select.tsx` was\nremoved); always use `Select`."
14
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "hotkeys",
3
+ "type": "ui",
4
+ "description": "-------------------------------- parsing --------------------------------",
5
+ "dependencies": {},
6
+ "registryDependencies": [
7
+ "dialog",
8
+ "kbd",
9
+ "utils"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "components/ui/hotkeys.tsx",
14
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"@/components/ui/dialog\"\nimport { Kbd, KbdGroup } from \"@/components/ui/kbd\"\n\n/* -------------------------------- parsing -------------------------------- */\n\nconst isMac = () => typeof navigator !== \"undefined\" && /mac|iphone|ipad|ipod/i.test(navigator.platform)\n\ntype Chord = { key: string; mod: boolean; ctrl: boolean; meta: boolean; alt: boolean; shift: boolean }\n\nconst keyAliases: Record<string, string> = {\n esc: \"escape\",\n return: \"enter\",\n space: \" \",\n up: \"arrowup\",\n down: \"arrowdown\",\n left: \"arrowleft\",\n right: \"arrowright\",\n plus: \"+\",\n del: \"delete\",\n}\n\n/** \"mod+shift+k\" → chord; \"g d\" → sequence of chords. */\nfunction parseHotkey(hotkey: string): Chord[] {\n return hotkey\n .trim()\n .split(/\\s+/)\n .map((part) => {\n const tokens = part.split(\"+\").map((t) => t.toLowerCase())\n // \"+\" itself is written as \"plus\"\n const key = tokens.pop() ?? \"\"\n const chord: Chord = {\n key: keyAliases[key] ?? key,\n mod: false,\n ctrl: false,\n meta: false,\n alt: false,\n shift: false,\n }\n for (const t of tokens) {\n if (t === \"mod\" || t === \"cmd\" || t === \"command\") chord.mod = true\n else if (t === \"ctrl\" || t === \"control\") chord.ctrl = true\n else if (t === \"meta\" || t === \"win\" || t === \"super\") chord.meta = true\n else if (t === \"alt\" || t === \"option\") chord.alt = true\n else if (t === \"shift\") chord.shift = true\n }\n return chord\n })\n}\n\nfunction matchesChord(event: KeyboardEvent, chord: Chord, mac: boolean) {\n const key = event.key.length === 1 ? event.key.toLowerCase() : event.key.toLowerCase()\n if (key !== chord.key) return false\n const wantMeta = chord.meta || (chord.mod && mac)\n const wantCtrl = chord.ctrl || (chord.mod && !mac)\n if (event.metaKey !== wantMeta || event.ctrlKey !== wantCtrl || event.altKey !== chord.alt) return false\n // Punctuation like \"?\" already implies Shift on most layouts, so only enforce Shift for letters.\n const letter = /^[a-z0-9]$/.test(chord.key) || chord.key.length > 1\n if (letter && event.shiftKey !== chord.shift) return false\n return true\n}\n\nconst symbol: Record<string, string> = {\n mod: \"⌘\",\n meta: \"⌘\",\n ctrl: \"⌃\",\n alt: \"⌥\",\n shift: \"⇧\",\n enter: \"↵\",\n escape: \"Esc\",\n backspace: \"⌫\",\n delete: \"⌦\",\n arrowup: \"↑\",\n arrowdown: \"↓\",\n arrowleft: \"←\",\n arrowright: \"→\",\n \" \": \"Space\",\n tab: \"Tab\",\n}\nconst winSymbol: Record<string, string> = {\n ...symbol,\n mod: \"Ctrl\",\n meta: \"Win\",\n ctrl: \"Ctrl\",\n alt: \"Alt\",\n shift: \"Shift\",\n}\n\n/** Platform-aware key labels: \"mod+k\" → [\"⌘\", \"K\"] on Mac, [\"Ctrl\", \"K\"] elsewhere. Sequences give one array per chord. */\nfunction formatHotkey(hotkey: string, mac = isMac()): string[][] {\n const table = mac ? symbol : winSymbol\n return parseHotkey(hotkey).map((chord) => {\n const parts: string[] = []\n if (chord.ctrl) parts.push(table.ctrl)\n if (chord.alt) parts.push(table.alt)\n if (chord.shift) parts.push(table.shift)\n if (chord.mod) parts.push(table.mod)\n if (chord.meta && !chord.mod) parts.push(table.meta)\n parts.push(table[chord.key] ?? (chord.key.length === 1 ? chord.key.toUpperCase() : chord.key))\n return parts\n })\n}\n\n/* -------------------------------- registry ------------------------------- */\n\ntype HotkeyEntry = {\n id: string\n hotkey: string\n label: string\n group: string\n enabled: boolean\n /** Fire even while typing in an input, textarea, or contenteditable. */\n allowInInput: boolean\n handler: (event: KeyboardEvent) => void\n}\n\ntype HotkeysContextValue = {\n entries: HotkeyEntry[]\n register: (entry: HotkeyEntry) => () => void\n dialogOpen: boolean\n setDialogOpen: (open: boolean) => void\n}\n\nconst HotkeysContext = React.createContext<HotkeysContextValue | null>(null)\n\nfunction useHotkeysContext() {\n const ctx = React.useContext(HotkeysContext)\n if (!ctx) throw new Error(\"Hotkeys parts must be used inside <HotkeysProvider>\")\n return ctx\n}\n\nconst isEditable = (target: EventTarget | null) => {\n const el = target as HTMLElement | null\n if (!el || !(\"tagName\" in el)) return false\n const tag = el.tagName\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\" || el.isContentEditable\n}\n\n/**\n * One document-level keydown listener for every registered hotkey. Supports chords\n * (\"mod+k\"), sequences (\"g d\", one second between keys), and a built-in \"?\" that opens\n * `HotkeysDialog` when one is rendered.\n */\nfunction HotkeysProvider({\n children,\n dialogHotkey = \"?\",\n sequenceTimeout = 1000,\n}: {\n children: React.ReactNode\n /** Opens the shortcuts dialog; `null` disables it. */\n dialogHotkey?: string | null\n sequenceTimeout?: number\n}) {\n const [entries, setEntries] = React.useState<HotkeyEntry[]>([])\n const [dialogOpen, setDialogOpen] = React.useState(false)\n const entriesRef = React.useRef(entries)\n entriesRef.current = entries\n const buffer = React.useRef<{ keys: KeyboardEvent[]; at: number }>({ keys: [], at: 0 })\n\n const register = React.useCallback((entry: HotkeyEntry) => {\n setEntries((prev) => [...prev.filter((e) => e.id !== entry.id), entry])\n return () => setEntries((prev) => prev.filter((e) => e.id !== entry.id))\n }, [])\n\n const mac = React.useMemo(isMac, [])\n React.useEffect(() => {\n const down = (event: KeyboardEvent) => {\n if (event.defaultPrevented || event.isComposing) return\n if ([\"Shift\", \"Control\", \"Alt\", \"Meta\"].includes(event.key)) return\n const editing = isEditable(event.target)\n const now = Date.now()\n const buf = buffer.current\n if (now - buf.at > sequenceTimeout) buf.keys = []\n buf.keys.push(event)\n buf.at = now\n if (buf.keys.length > 4) buf.keys.shift()\n\n const candidates = [...entriesRef.current]\n if (dialogHotkey) {\n candidates.push({\n id: \"hotkeys:dialog\",\n hotkey: dialogHotkey,\n label: \"Keyboard shortcuts\",\n group: \"General\",\n enabled: true,\n allowInInput: false,\n handler: () => setDialogOpen((open) => !open),\n })\n }\n let partial = false\n for (const entry of candidates) {\n if (!entry.enabled || (editing && !entry.allowInInput)) continue\n const chords = parseHotkey(entry.hotkey)\n const tail = buf.keys.slice(-chords.length)\n if (tail.length === chords.length && chords.every((c, i) => matchesChord(tail[i], c, mac))) {\n event.preventDefault()\n buf.keys = []\n entry.handler(event)\n return\n }\n // Is the buffer a prefix of a longer sequence? Then keep it.\n if (chords.length > 1 && buf.keys.length < chords.length) {\n const head = chords.slice(0, buf.keys.length)\n if (head.every((c, i) => matchesChord(buf.keys[i], c, mac))) partial = true\n }\n }\n if (!partial) buf.keys = []\n }\n document.addEventListener(\"keydown\", down)\n return () => document.removeEventListener(\"keydown\", down)\n }, [dialogHotkey, sequenceTimeout, mac])\n\n const ctx = React.useMemo<HotkeysContextValue>(\n () => ({ entries, register, dialogOpen, setDialogOpen }),\n [entries, register, dialogOpen]\n )\n return <HotkeysContext.Provider value={ctx}>{children}</HotkeysContext.Provider>\n}\n\n/** Register a hotkey for the lifetime of the component. Listed in `HotkeysDialog` when it has a label. */\nfunction useHotkey(\n hotkey: string,\n handler: (event: KeyboardEvent) => void,\n options: { label?: string; group?: string; enabled?: boolean; allowInInput?: boolean } = {}\n) {\n const { register } = useHotkeysContext()\n const { label = \"\", group = \"General\", enabled = true, allowInInput = false } = options\n const id = React.useId()\n const handlerRef = React.useRef(handler)\n handlerRef.current = handler\n React.useEffect(\n () =>\n register({\n id,\n hotkey,\n label,\n group,\n enabled,\n allowInInput,\n handler: (event) => handlerRef.current(event),\n }),\n [register, id, hotkey, label, group, enabled, allowInInput]\n )\n}\n\n/** Every registered hotkey with a label, grouped, for rendering a cheat sheet. */\nfunction useHotkeys() {\n const { entries } = useHotkeysContext()\n return React.useMemo(() => {\n const groups = new Map<string, HotkeyEntry[]>()\n for (const e of entries) {\n if (!e.label) continue\n const list = groups.get(e.group) ?? []\n list.push(e)\n groups.set(e.group, list)\n }\n return Array.from(groups, ([group, hotkeys]) => ({ group, hotkeys }))\n }, [entries])\n}\n\n/* ---------------------------------- parts -------------------------------- */\n\n/** Renders a hotkey as Kbd caps (\"mod+k\" → ⌘ K; \"g d\" → G then D). */\nfunction HotkeyKbd({ hotkey, className, ...props }: React.ComponentProps<\"span\"> & { hotkey: string }) {\n const chords = formatHotkey(hotkey)\n return (\n <span data-slot=\"hotkey-kbd\" className={cn(\"inline-flex items-center gap-1\", className)} {...props}>\n {chords.map((keys, i) => (\n <React.Fragment key={i}>\n {i > 0 && <span className=\"text-xs text-muted-foreground\">then</span>}\n <KbdGroup>\n {keys.map((k, j) => (\n <Kbd key={j}>{k}</Kbd>\n ))}\n </KbdGroup>\n </React.Fragment>\n ))}\n </span>\n )\n}\n\n/** The shortcuts cheat sheet. Opens from the provider's `dialogHotkey` (\"?\" by default) or `useHotkeysDialog()`. */\nfunction HotkeysDialog({\n title = \"Keyboard shortcuts\",\n description = \"Shortcuts work anywhere on the page except while typing.\",\n children,\n className,\n}: {\n title?: React.ReactNode\n description?: React.ReactNode\n children?: React.ReactNode\n className?: string\n}) {\n const { dialogOpen, setDialogOpen } = useHotkeysContext()\n const registered = useHotkeys()\n const groups = React.useMemo(() => {\n const self = { id: \"hotkeys:dialog\", hotkey: \"?\", label: \"Show this dialog\" }\n const general = registered.find((g) => g.group === \"General\")\n if (general) return registered.map((g) => (g === general ? { ...g, hotkeys: [...g.hotkeys, self] } : g))\n return [...registered, { group: \"General\", hotkeys: [self] }]\n }, [registered])\n return (\n <Dialog open={dialogOpen} onOpenChange={({ open }) => setDialogOpen(open)}>\n <DialogContent data-slot=\"hotkeys-dialog\" className={cn(\"sm:max-w-lg\", className)}>\n <DialogHeader>\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n <div className=\"grid gap-5 sm:grid-cols-2\">\n {groups.map(({ group, hotkeys }) => (\n <div key={group} data-slot=\"hotkeys-dialog-group\" className=\"flex flex-col gap-1.5\">\n <h4 className=\"text-xs font-medium text-muted-foreground uppercase\">{group}</h4>\n {hotkeys.map((h) => (\n <div\n key={h.id}\n data-slot=\"hotkeys-dialog-item\"\n className=\"flex items-center justify-between gap-3 text-sm\"\n >\n <span className=\"truncate\">{h.label}</span>\n <HotkeyKbd hotkey={h.hotkey} />\n </div>\n ))}\n </div>\n ))}\n </div>\n {children}\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction useHotkeysDialog() {\n const { dialogOpen, setDialogOpen } = useHotkeysContext()\n return { open: dialogOpen, setOpen: setDialogOpen }\n}\n\nexport { HotkeyKbd, HotkeysDialog, HotkeysProvider, formatHotkey, parseHotkey, useHotkey, useHotkeys, useHotkeysDialog }\n"
15
+ }
16
+ ],
17
+ "docs": "## Shared infrastructure\n- `src/lib/controllable.ts` `useControllable(controlled, default, onChange)` is the one\n controllable-state hook (updater functions accepted; `onChange` fires in both modes, not on\n no-op sets). Every primitive and demo composition uses it (chat, menubar, comment thread, gantt,\n scheduler, data grid, query builder, tree select, cascader, transfer list, the table engine,\n apps view, settings layout). Never hand-roll `valueProp ?? internal` again.\n- `src/components/ui/live-region.tsx`: `useLiveRegion({ clearAfter })` → `{ message, announce }`\n (re-fires identical text) and `<LiveRegion message= assertive? />` (sr-only `role=\"status\"`).\n Kanban, canvas, gantt, scheduler, node graph, data grid, data table bulk actions, and transfer\n list all announce through it; a primitive renders its own `LiveRegion` (with its `data-slot`)\n so it works without any provider.\n- `src/components/ui/hotkeys.tsx`: `HotkeysProvider` (one document keydown listener; chords like\n `mod+k` where `mod` is ⌘ on Mac and Ctrl elsewhere, sequences like `g d` with a 1s window,\n ignores typing in inputs unless `allowInInput`, built-in `?` toggles the dialog;\n `dialogHotkey={null}` disables), `useHotkey(hotkey, handler, { label, group, enabled,\n allowInInput })` registers for the component's lifetime (labelled ones appear in the dialog),\n `useHotkeys()` (grouped list), `HotkeysDialog` (the cheat sheet on `Dialog`; `useHotkeysDialog()`\n opens it), `HotkeyKbd hotkey=` (platform-aware `Kbd` caps, \"then\" between sequence steps),\n `formatHotkey`/`parseHotkey`. The shell demo wraps everything in the provider with\n `AppShell searchShortcut={false}` and registers ⌘K, ⌘J (theme), and `g` + letter navigation in\n `ShellHotkeys`; the user menu has a \"Keyboard shortcuts\" item. Pages register their own with\n `useHotkey` and a `group`."
18
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "hover-card",
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/hover-card.tsx",
14
+ "content": "import * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { HoverCard as HoverCardPrimitive, Portal as PortalPrimitive } from \"@ark-ui/react\"\n\nfunction HoverCard({\n positioning,\n lazyMount = true,\n unmountOnExit = true,\n ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {\n return (\n <HoverCardPrimitive.Root\n positioning={{ placement: \"bottom\", gutter: 4, ...positioning }}\n lazyMount={lazyMount}\n unmountOnExit={unmountOnExit}\n {...props}\n />\n )\n}\n\nfunction HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {\n return <HoverCardPrimitive.Trigger data-slot=\"hover-card-trigger\" {...props} />\n}\n\nfunction HoverCardPortal({ ...props }: React.ComponentProps<typeof PortalPrimitive>) {\n return <PortalPrimitive {...props} />\n}\n\nfunction HoverCardContext({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Context>) {\n return <HoverCardPrimitive.Context {...props} />\n}\n\nfunction HoverCardPositioner({ className, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Positioner>) {\n return (\n <HoverCardPrimitive.Positioner\n data-slot=\"hover-card-positioner\"\n className={cn(\"[--z-index:50]\", className)}\n {...props}\n />\n )\n}\n\nfunction HoverCardArrow({ className, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Arrow>) {\n return (\n <HoverCardPrimitive.Arrow\n data-slot=\"hover-card-arrow\"\n className={cn(\"[--arrow-background:var(--color-popover)] [--arrow-size:0.625rem]\", className)}\n {...props}\n >\n <HoverCardPrimitive.ArrowTip\n data-slot=\"hover-card-arrow-tip\"\n className=\"border-t border-l border-foreground/10\"\n />\n </HoverCardPrimitive.Arrow>\n )\n}\n\nfunction HoverCardContent({ className, ...props }: React.ComponentProps<typeof HoverCardPrimitive.Content>) {\n return (\n <HoverCardPortal>\n <HoverCardPositioner>\n <HoverCardPrimitive.Content\n data-slot=\"hover-card-content\"\n className={cn(\n \"z-50 w-64 origin-(--transform-origin) 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 </HoverCardPositioner>\n </HoverCardPortal>\n )\n}\n\nexport {\n HoverCard,\n HoverCardArrow,\n HoverCardContent,\n HoverCardContext,\n HoverCardPortal,\n HoverCardPositioner,\n HoverCardTrigger,\n}\n"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "image-cropper",
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/image-cropper.tsx",
14
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { ImageCropper as ImageCropperPrimitive } from \"@ark-ui/react\"\n\nconst handlePositions = [\"n\", \"e\", \"s\", \"w\", \"ne\", \"se\", \"sw\", \"nw\"] as const\n\nfunction ImageCropper({ className, ...props }: React.ComponentProps<typeof ImageCropperPrimitive.Root>) {\n return (\n <ImageCropperPrimitive.Root\n data-slot=\"image-cropper\"\n className={cn(\"flex w-full flex-col gap-2\", className)}\n {...props}\n />\n )\n}\n\nfunction ImageCropperContext({ ...props }: React.ComponentProps<typeof ImageCropperPrimitive.Context>) {\n return <ImageCropperPrimitive.Context {...props} />\n}\n\nfunction ImageCropperViewport({ className, ...props }: React.ComponentProps<typeof ImageCropperPrimitive.Viewport>) {\n return (\n <ImageCropperPrimitive.Viewport\n data-slot=\"image-cropper-viewport\"\n className={cn(\n \"relative h-72 w-full touch-none overflow-hidden rounded-xl border bg-muted select-none\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ImageCropperImage({ className, ...props }: React.ComponentProps<typeof ImageCropperPrimitive.Image>) {\n return (\n <ImageCropperPrimitive.Image data-slot=\"image-cropper-image\" className={cn(\"max-w-none\", className)} {...props} />\n )\n}\n\nfunction ImageCropperSelection({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof ImageCropperPrimitive.Selection>) {\n return (\n <ImageCropperPrimitive.Selection\n data-slot=\"image-cropper-selection\"\n className={cn(\n \"shadow-[0_0_0_9999px_rgba(0,0,0,0.45)] outline-1 outline-white/80 data-[shape=circle]:rounded-full\",\n className\n )}\n {...props}\n >\n {children ?? (\n <>\n <ImageCropperGrid axis=\"horizontal\" />\n <ImageCropperGrid axis=\"vertical\" />\n {handlePositions.map((position) => (\n <ImageCropperHandle key={position} position={position} />\n ))}\n </>\n )}\n </ImageCropperPrimitive.Selection>\n )\n}\n\nfunction ImageCropperGrid({ className, ...props }: React.ComponentProps<typeof ImageCropperPrimitive.Grid>) {\n return (\n <ImageCropperPrimitive.Grid\n data-slot=\"image-cropper-grid\"\n className={cn(\"stroke-white/50\", className)}\n {...props}\n />\n )\n}\n\nfunction ImageCropperHandle({ className, ...props }: React.ComponentProps<typeof ImageCropperPrimitive.Handle>) {\n return (\n <ImageCropperPrimitive.Handle\n data-slot=\"image-cropper-handle\"\n className={cn(\n \"after:absolute after:top-1/2 after:left-1/2 after:size-2.5 after:-translate-1/2 after:rounded-full after:border after:border-ring after:bg-white data-[axis=x]:after:h-4 data-[axis=x]:after:w-1 data-[axis=y]:after:h-1 data-[axis=y]:after:w-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n ImageCropper,\n ImageCropperContext,\n ImageCropperGrid,\n ImageCropperHandle,\n ImageCropperImage,\n ImageCropperSelection,\n ImageCropperViewport,\n}\n"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "input-group",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "class-variance-authority": "^0.7.1"
7
+ },
8
+ "registryDependencies": [
9
+ "button",
10
+ "input",
11
+ "textarea",
12
+ "utils"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "components/ui/input-group.tsx",
17
+ "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"@/lib/utils\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport { Textarea } from \"@/components/ui/textarea\"\n\nfunction InputGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"input-group\"\n role=\"group\"\n className={cn(\n \"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5\",\n className\n )}\n {...props}\n />\n )\n}\n\nconst inputGroupAddonVariants = cva(\n \"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n align: {\n \"inline-start\": \"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]\",\n \"inline-end\": \"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]\",\n \"block-start\":\n \"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2\",\n \"block-end\": \"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2\",\n },\n },\n defaultVariants: {\n align: \"inline-start\",\n },\n }\n)\n\nfunction InputGroupAddon({\n className,\n align = \"inline-start\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof inputGroupAddonVariants>) {\n return (\n <div\n role=\"group\"\n data-slot=\"input-group-addon\"\n data-align={align}\n className={cn(inputGroupAddonVariants({ align }), className)}\n onClick={(e) => {\n if ((e.target as HTMLElement).closest(\"button\")) {\n return\n }\n e.currentTarget.parentElement?.querySelector(\"input\")?.focus()\n }}\n {...props}\n />\n )\n}\n\nconst inputGroupTriggerVariants = cva(\"flex items-center gap-2 text-sm shadow-none\", {\n variants: {\n size: {\n xs: \"h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5\",\n sm: \"\",\n \"icon-xs\": \"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0\",\n \"icon-sm\": \"size-8 p-0 has-[>svg]:p-0\",\n },\n },\n defaultVariants: {\n size: \"xs\",\n },\n})\n\nfunction InputGroupTrigger({\n className,\n type = \"button\",\n variant = \"ghost\",\n size = \"xs\",\n ...props\n}: Omit<React.ComponentProps<typeof Button>, \"size\"> & VariantProps<typeof inputGroupTriggerVariants>) {\n return (\n <Button\n type={type}\n data-size={size}\n variant={variant}\n className={cn(inputGroupTriggerVariants({ size }), className)}\n {...props}\n />\n )\n}\n\nfunction InputGroupText({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n className={cn(\n \"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction InputGroupInput({ className, ...props }: React.ComponentProps<\"input\">) {\n return (\n <Input\n data-slot=\"input-group-control\"\n className={cn(\n \"flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction InputGroupTextarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n return (\n <Textarea\n data-slot=\"input-group-control\"\n className={cn(\n \"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { InputGroup, InputGroupAddon, InputGroupTrigger, InputGroupText, InputGroupInput, InputGroupTextarea }\n"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "input-otp",
3
+ "type": "ui",
4
+ "description": "",
5
+ "dependencies": {
6
+ "input-otp": "^1.5.0",
7
+ "lucide-react": "^1.41.0"
8
+ },
9
+ "registryDependencies": [
10
+ "utils"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/input-otp.tsx",
15
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\nimport { OTPInput, OTPInputContext } from \"input-otp\"\nimport { MinusIcon } from \"lucide-react\"\n\nfunction InputOTP({\n className,\n containerClassName,\n ...props\n}: React.ComponentProps<typeof OTPInput> & {\n containerClassName?: string\n}) {\n return (\n <OTPInput\n data-slot=\"input-otp\"\n containerClassName={cn(\"flex items-center has-disabled:opacity-50\", containerClassName)}\n spellCheck={false}\n className={cn(\"disabled:cursor-not-allowed\", className)}\n {...props}\n />\n )\n}\n\nfunction InputOTPGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"input-otp-group\"\n className={cn(\n \"flex items-center rounded-lg has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction InputOTPSlot({\n index,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n index: number\n}) {\n const inputOTPContext = React.useContext(OTPInputContext)\n const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}\n\n return (\n <div\n data-slot=\"input-otp-slot\"\n data-active={isActive}\n className={cn(\n \"relative flex size-8 items-center justify-center border-y border-r border-input text-sm transition-all outline-none first:rounded-l-lg first:border-l last:rounded-r-lg aria-invalid:border-destructive data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-3 data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-destructive/40\",\n className\n )}\n {...props}\n >\n {char}\n {hasFakeCaret && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <div className=\"h-4 w-px animate-caret-blink bg-foreground duration-1000\" />\n </div>\n )}\n </div>\n )\n}\n\nfunction InputOTPSeparator({ ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"input-otp-separator\"\n className=\"flex items-center [&_svg:not([class*='size-'])]:size-4\"\n role=\"separator\"\n {...props}\n >\n <MinusIcon />\n </div>\n )\n}\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }\n"
16
+ }
17
+ ]
18
+ }