@slexn/codecenter-ui 1.0.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 (472) hide show
  1. package/LICENSE +34 -0
  2. package/README.md +148 -0
  3. package/components.json +20 -0
  4. package/dist/codecenter-ui.cjs +10 -0
  5. package/dist/codecenter-ui.js +17995 -0
  6. package/dist/components/ui/accordion/Accordion.vue.d.ts +28 -0
  7. package/dist/components/ui/accordion/AccordionContent.vue.d.ts +22 -0
  8. package/dist/components/ui/accordion/AccordionItem.vue.d.ts +24 -0
  9. package/dist/components/ui/accordion/AccordionTrigger.vue.d.ts +22 -0
  10. package/dist/components/ui/accordion/index.d.ts +4 -0
  11. package/dist/components/ui/alert/Alert.vue.d.ts +32 -0
  12. package/dist/components/ui/alert/AlertDescription.vue.d.ts +24 -0
  13. package/dist/components/ui/alert/AlertTitle.vue.d.ts +24 -0
  14. package/dist/components/ui/alert/index.d.ts +4 -0
  15. package/dist/components/ui/alert/variants.d.ts +5 -0
  16. package/dist/components/ui/button/Button.vue.d.ts +27 -0
  17. package/dist/components/ui/button/index.d.ts +2 -0
  18. package/dist/components/ui/button/variants.d.ts +6 -0
  19. package/dist/components/ui/button-group/ButtonGroup.vue.d.ts +25 -0
  20. package/dist/components/ui/button-group/index.d.ts +2 -0
  21. package/dist/components/ui/card/Card.vue.d.ts +21 -0
  22. package/dist/components/ui/card/CardContent.vue.d.ts +21 -0
  23. package/dist/components/ui/card/CardDescription.vue.d.ts +21 -0
  24. package/dist/components/ui/card/CardFooter.vue.d.ts +21 -0
  25. package/dist/components/ui/card/CardHeader.vue.d.ts +21 -0
  26. package/dist/components/ui/card/CardTitle.vue.d.ts +21 -0
  27. package/dist/components/ui/card/index.d.ts +6 -0
  28. package/dist/components/ui/chart/Chart.vue.d.ts +92 -0
  29. package/dist/components/ui/chart/index.d.ts +2 -0
  30. package/dist/components/ui/chat/Chat.vue.d.ts +190 -0
  31. package/dist/components/ui/chat/ChatAttachments.vue.d.ts +11 -0
  32. package/dist/components/ui/chat/ChatCodeBlock.vue.d.ts +16 -0
  33. package/dist/components/ui/chat/code-block.d.ts +27 -0
  34. package/dist/components/ui/chat/index.d.ts +6 -0
  35. package/dist/components/ui/chat/types.d.ts +15 -0
  36. package/dist/components/ui/checkbox/Checkbox.vue.d.ts +29 -0
  37. package/dist/components/ui/checkbox/index.d.ts +1 -0
  38. package/dist/components/ui/commit/Commit.vue.d.ts +62 -0
  39. package/dist/components/ui/commit/index.d.ts +2 -0
  40. package/dist/components/ui/contribution-graph/ContributionGraph.vue.d.ts +87 -0
  41. package/dist/components/ui/contribution-graph/index.d.ts +2 -0
  42. package/dist/components/ui/data-table/DataTable.vue.d.ts +109 -0
  43. package/dist/components/ui/data-table/index.d.ts +2 -0
  44. package/dist/components/ui/date-picker/DatePicker.vue.d.ts +37 -0
  45. package/dist/components/ui/date-picker/index.d.ts +2 -0
  46. package/dist/components/ui/dialog/Dialog.vue.d.ts +25 -0
  47. package/dist/components/ui/dialog/DialogClose.vue.d.ts +18 -0
  48. package/dist/components/ui/dialog/DialogContent.vue.d.ts +39 -0
  49. package/dist/components/ui/dialog/DialogDescription.vue.d.ts +22 -0
  50. package/dist/components/ui/dialog/DialogFooter.vue.d.ts +21 -0
  51. package/dist/components/ui/dialog/DialogHeader.vue.d.ts +21 -0
  52. package/dist/components/ui/dialog/DialogOverlay.vue.d.ts +22 -0
  53. package/dist/components/ui/dialog/DialogScrollContent.vue.d.ts +36 -0
  54. package/dist/components/ui/dialog/DialogTitle.vue.d.ts +22 -0
  55. package/dist/components/ui/dialog/DialogTrigger.vue.d.ts +18 -0
  56. package/dist/components/ui/dialog/index.d.ts +10 -0
  57. package/dist/components/ui/diff/DiffTool.vue.d.ts +21 -0
  58. package/dist/components/ui/diff/diff-parser.d.ts +30 -0
  59. package/dist/components/ui/diff/diff-tool.d.ts +36 -0
  60. package/dist/components/ui/diff/index.d.ts +2 -0
  61. package/dist/components/ui/dropdown-menu/DropdownMenu.vue.d.ts +24 -0
  62. package/dist/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue.d.ts +29 -0
  63. package/dist/components/ui/dropdown-menu/DropdownMenuContent.vue.d.ts +36 -0
  64. package/dist/components/ui/dropdown-menu/DropdownMenuGroup.vue.d.ts +18 -0
  65. package/dist/components/ui/dropdown-menu/DropdownMenuItem.vue.d.ts +26 -0
  66. package/dist/components/ui/dropdown-menu/DropdownMenuLabel.vue.d.ts +23 -0
  67. package/dist/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue.d.ts +22 -0
  68. package/dist/components/ui/dropdown-menu/DropdownMenuRadioItem.vue.d.ts +27 -0
  69. package/dist/components/ui/dropdown-menu/DropdownMenuSeparator.vue.d.ts +7 -0
  70. package/dist/components/ui/dropdown-menu/DropdownMenuShortcut.vue.d.ts +21 -0
  71. package/dist/components/ui/dropdown-menu/DropdownMenuSub.vue.d.ts +22 -0
  72. package/dist/components/ui/dropdown-menu/DropdownMenuSubContent.vue.d.ts +38 -0
  73. package/dist/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue.d.ts +23 -0
  74. package/dist/components/ui/dropdown-menu/DropdownMenuTrigger.vue.d.ts +18 -0
  75. package/dist/components/ui/dropdown-menu/index.d.ts +15 -0
  76. package/dist/components/ui/gauge/Gauge.vue.d.ts +62 -0
  77. package/dist/components/ui/gauge/index.d.ts +2 -0
  78. package/dist/components/ui/git-graph/GitGraph.vue.d.ts +59 -0
  79. package/dist/components/ui/git-graph/index.d.ts +2 -0
  80. package/dist/components/ui/incident-timeline/IncidentTimeline.vue.d.ts +50 -0
  81. package/dist/components/ui/incident-timeline/index.d.ts +2 -0
  82. package/dist/components/ui/input/Input.vue.d.ts +14 -0
  83. package/dist/components/ui/input/InputControl.vue.d.ts +14 -0
  84. package/dist/components/ui/input/InputFieldGroup.vue.d.ts +21 -0
  85. package/dist/components/ui/input/index.d.ts +4 -0
  86. package/dist/components/ui/input/types.d.ts +31 -0
  87. package/dist/components/ui/kpi-card/KpiCard.vue.d.ts +46 -0
  88. package/dist/components/ui/kpi-card/index.d.ts +2 -0
  89. package/dist/components/ui/kpi-line-card/KpiLineCard.vue.d.ts +66 -0
  90. package/dist/components/ui/kpi-line-card/index.d.ts +2 -0
  91. package/dist/components/ui/model-selector/ModelSelector.vue.d.ts +41 -0
  92. package/dist/components/ui/model-selector/index.d.ts +2 -0
  93. package/dist/components/ui/model-selector/types.d.ts +12 -0
  94. package/dist/components/ui/network-graph/NetworkGraph.vue.d.ts +75 -0
  95. package/dist/components/ui/network-graph/index.d.ts +2 -0
  96. package/dist/components/ui/pagination/Pagination.vue.d.ts +29 -0
  97. package/dist/components/ui/pagination/PaginationContent.vue.d.ts +29 -0
  98. package/dist/components/ui/pagination/PaginationEllipsis.vue.d.ts +22 -0
  99. package/dist/components/ui/pagination/PaginationFirst.vue.d.ts +26 -0
  100. package/dist/components/ui/pagination/PaginationItem.vue.d.ts +28 -0
  101. package/dist/components/ui/pagination/PaginationLast.vue.d.ts +26 -0
  102. package/dist/components/ui/pagination/PaginationNext.vue.d.ts +26 -0
  103. package/dist/components/ui/pagination/PaginationPrevious.vue.d.ts +26 -0
  104. package/dist/components/ui/pagination/index.d.ts +8 -0
  105. package/dist/components/ui/profile/Profile.vue.d.ts +30 -0
  106. package/dist/components/ui/profile/ProfileGroup.vue.d.ts +37 -0
  107. package/dist/components/ui/profile/index.d.ts +4 -0
  108. package/dist/components/ui/progress/Progress.vue.d.ts +36 -0
  109. package/dist/components/ui/progress/index.d.ts +2 -0
  110. package/dist/components/ui/prompt-input/PromptInput.vue.d.ts +150 -0
  111. package/dist/components/ui/prompt-input/index.d.ts +2 -0
  112. package/dist/components/ui/prompt-input/types.d.ts +61 -0
  113. package/dist/components/ui/radio-group/RadioGroup.vue.d.ts +30 -0
  114. package/dist/components/ui/radio-group/RadioGroupItem.vue.d.ts +12 -0
  115. package/dist/components/ui/radio-group/RadioGroupOption.vue.d.ts +28 -0
  116. package/dist/components/ui/radio-group/index.d.ts +3 -0
  117. package/dist/components/ui/reasoning/Reasoning.vue.d.ts +40 -0
  118. package/dist/components/ui/reasoning/index.d.ts +2 -0
  119. package/dist/components/ui/reasoning/types.d.ts +26 -0
  120. package/dist/components/ui/select/Select.vue.d.ts +28 -0
  121. package/dist/components/ui/select/SelectContent.vue.d.ts +46 -0
  122. package/dist/components/ui/select/SelectGroup.vue.d.ts +18 -0
  123. package/dist/components/ui/select/SelectItem.vue.d.ts +26 -0
  124. package/dist/components/ui/select/SelectItemText.vue.d.ts +18 -0
  125. package/dist/components/ui/select/SelectLabel.vue.d.ts +22 -0
  126. package/dist/components/ui/select/SelectScrollDownButton.vue.d.ts +22 -0
  127. package/dist/components/ui/select/SelectScrollUpButton.vue.d.ts +22 -0
  128. package/dist/components/ui/select/SelectSeparator.vue.d.ts +7 -0
  129. package/dist/components/ui/select/SelectTrigger.vue.d.ts +25 -0
  130. package/dist/components/ui/select/SelectValue.vue.d.ts +18 -0
  131. package/dist/components/ui/select/index.d.ts +11 -0
  132. package/dist/components/ui/select/search.d.ts +18 -0
  133. package/dist/components/ui/separator/Separator.vue.d.ts +6 -0
  134. package/dist/components/ui/separator/index.d.ts +2 -0
  135. package/dist/components/ui/separator/types.d.ts +7 -0
  136. package/dist/components/ui/shimmer/Shimmer.vue.d.ts +37 -0
  137. package/dist/components/ui/shimmer/index.d.ts +2 -0
  138. package/dist/components/ui/sidebar/Sidebar.vue.d.ts +24 -0
  139. package/dist/components/ui/sidebar/SidebarContent.vue.d.ts +21 -0
  140. package/dist/components/ui/sidebar/SidebarFooter.vue.d.ts +21 -0
  141. package/dist/components/ui/sidebar/SidebarGroup.vue.d.ts +21 -0
  142. package/dist/components/ui/sidebar/SidebarGroupAction.vue.d.ts +24 -0
  143. package/dist/components/ui/sidebar/SidebarGroupContent.vue.d.ts +21 -0
  144. package/dist/components/ui/sidebar/SidebarGroupLabel.vue.d.ts +24 -0
  145. package/dist/components/ui/sidebar/SidebarHeader.vue.d.ts +21 -0
  146. package/dist/components/ui/sidebar/SidebarInput.vue.d.ts +6 -0
  147. package/dist/components/ui/sidebar/SidebarInset.vue.d.ts +21 -0
  148. package/dist/components/ui/sidebar/SidebarMenu.vue.d.ts +21 -0
  149. package/dist/components/ui/sidebar/SidebarMenuAction.vue.d.ts +25 -0
  150. package/dist/components/ui/sidebar/SidebarMenuBadge.vue.d.ts +21 -0
  151. package/dist/components/ui/sidebar/SidebarMenuButton.vue.d.ts +25 -0
  152. package/dist/components/ui/sidebar/SidebarMenuButtonChild.vue.d.ts +30 -0
  153. package/dist/components/ui/sidebar/SidebarMenuItem.vue.d.ts +21 -0
  154. package/dist/components/ui/sidebar/SidebarMenuSub.vue.d.ts +21 -0
  155. package/dist/components/ui/sidebar/SidebarMenuSubButton.vue.d.ts +27 -0
  156. package/dist/components/ui/sidebar/SidebarMenuSubItem.vue.d.ts +21 -0
  157. package/dist/components/ui/sidebar/SidebarProvider.vue.d.ts +36 -0
  158. package/dist/components/ui/sidebar/SidebarRail.vue.d.ts +21 -0
  159. package/dist/components/ui/sidebar/SidebarSeparator.vue.d.ts +6 -0
  160. package/dist/components/ui/sidebar/SidebarTrigger.vue.d.ts +6 -0
  161. package/dist/components/ui/sidebar/context.d.ts +19 -0
  162. package/dist/components/ui/sidebar/index.d.ts +26 -0
  163. package/dist/components/ui/sidebar/types.d.ts +11 -0
  164. package/dist/components/ui/sidebar/variants.d.ts +6 -0
  165. package/dist/components/ui/skeleton/Skeleton.vue.d.ts +18 -0
  166. package/dist/components/ui/skeleton/index.d.ts +2 -0
  167. package/dist/components/ui/sonner/Sonner.vue.d.ts +5 -0
  168. package/dist/components/ui/sonner/index.d.ts +2 -0
  169. package/dist/components/ui/spinner/Spinner.vue.d.ts +11 -0
  170. package/dist/components/ui/spinner/index.d.ts +1 -0
  171. package/dist/components/ui/stepper/Stepper.vue.d.ts +38 -0
  172. package/dist/components/ui/stepper/StepperDescription.vue.d.ts +22 -0
  173. package/dist/components/ui/stepper/StepperIndicator.vue.d.ts +30 -0
  174. package/dist/components/ui/stepper/StepperItem.vue.d.ts +24 -0
  175. package/dist/components/ui/stepper/StepperSeparator.vue.d.ts +7 -0
  176. package/dist/components/ui/stepper/StepperTitle.vue.d.ts +22 -0
  177. package/dist/components/ui/stepper/StepperTrigger.vue.d.ts +22 -0
  178. package/dist/components/ui/stepper/index.d.ts +7 -0
  179. package/dist/components/ui/switch/Switch.vue.d.ts +12 -0
  180. package/dist/components/ui/switch/index.d.ts +1 -0
  181. package/dist/components/ui/table/Table.vue.d.ts +22 -0
  182. package/dist/components/ui/table/TableBody.vue.d.ts +21 -0
  183. package/dist/components/ui/table/TableCaption.vue.d.ts +21 -0
  184. package/dist/components/ui/table/TableCell.vue.d.ts +22 -0
  185. package/dist/components/ui/table/TableEmpty.vue.d.ts +24 -0
  186. package/dist/components/ui/table/TableFooter.vue.d.ts +21 -0
  187. package/dist/components/ui/table/TableHead.vue.d.ts +21 -0
  188. package/dist/components/ui/table/TableHeader.vue.d.ts +21 -0
  189. package/dist/components/ui/table/TableRow.vue.d.ts +21 -0
  190. package/dist/components/ui/table/index.d.ts +9 -0
  191. package/dist/components/ui/tabs/Tabs.vue.d.ts +28 -0
  192. package/dist/components/ui/tabs/TabsContent.vue.d.ts +22 -0
  193. package/dist/components/ui/tabs/TabsList.vue.d.ts +22 -0
  194. package/dist/components/ui/tabs/TabsTrigger.vue.d.ts +22 -0
  195. package/dist/components/ui/tabs/index.d.ts +4 -0
  196. package/dist/components/ui/tag/Tag.vue.d.ts +35 -0
  197. package/dist/components/ui/tag/index.d.ts +2 -0
  198. package/dist/components/ui/tag/variants.d.ts +6 -0
  199. package/dist/components/ui/textarea/Textarea.vue.d.ts +14 -0
  200. package/dist/components/ui/textarea/TextareaControl.vue.d.ts +14 -0
  201. package/dist/components/ui/textarea/TextareaFieldGroup.vue.d.ts +21 -0
  202. package/dist/components/ui/textarea/index.d.ts +4 -0
  203. package/dist/components/ui/textarea/types.d.ts +32 -0
  204. package/dist/components/ui/tool/Tool.vue.d.ts +61 -0
  205. package/dist/components/ui/tool/index.d.ts +2 -0
  206. package/dist/components/ui/tooltip/Tooltip.vue.d.ts +24 -0
  207. package/dist/components/ui/tooltip/TooltipContent.vue.d.ts +32 -0
  208. package/dist/components/ui/tooltip/TooltipProvider.vue.d.ts +20 -0
  209. package/dist/components/ui/tooltip/TooltipTrigger.vue.d.ts +18 -0
  210. package/dist/components/ui/tooltip/index.d.ts +4 -0
  211. package/dist/docs/component-docs.d.ts +18 -0
  212. package/dist/docs/markdown.d.ts +27 -0
  213. package/dist/index.d.ts +45 -0
  214. package/dist/lib/code-highlight.d.ts +11 -0
  215. package/dist/lib/utils.d.ts +2 -0
  216. package/dist/styles.css +3 -0
  217. package/package.json +76 -0
  218. package/public/r/accordion.json +52 -0
  219. package/public/r/alert.json +51 -0
  220. package/public/r/button-group.json +31 -0
  221. package/public/r/button.json +39 -0
  222. package/public/r/card.json +61 -0
  223. package/public/r/chart.json +31 -0
  224. package/public/r/chat.json +186 -0
  225. package/public/r/checkbox.json +34 -0
  226. package/public/r/commit.json +32 -0
  227. package/public/r/contribution-graph.json +63 -0
  228. package/public/r/data-table.json +197 -0
  229. package/public/r/date-picker.json +33 -0
  230. package/public/r/dialog.json +88 -0
  231. package/public/r/diff.json +71 -0
  232. package/public/r/dropdown-menu.json +112 -0
  233. package/public/r/gauge.json +31 -0
  234. package/public/r/git-graph.json +32 -0
  235. package/public/r/incident-timeline.json +64 -0
  236. package/public/r/input.json +49 -0
  237. package/public/r/kpi-card.json +32 -0
  238. package/public/r/kpi-line-card.json +32 -0
  239. package/public/r/model-selector.json +148 -0
  240. package/public/r/network-graph.json +33 -0
  241. package/public/r/pagination.json +95 -0
  242. package/public/r/profile.json +37 -0
  243. package/public/r/progress.json +31 -0
  244. package/public/r/prompt-input.json +293 -0
  245. package/public/r/radio-group.json +45 -0
  246. package/public/r/reasoning.json +38 -0
  247. package/public/r/registry.json +2512 -0
  248. package/public/r/select.json +100 -0
  249. package/public/r/separator.json +37 -0
  250. package/public/r/shimmer.json +31 -0
  251. package/public/r/sidebar.json +221 -0
  252. package/public/r/skeleton.json +31 -0
  253. package/public/r/sonner.json +33 -0
  254. package/public/r/spinner.json +31 -0
  255. package/public/r/stepper.json +70 -0
  256. package/public/r/switch.json +33 -0
  257. package/public/r/table.json +79 -0
  258. package/public/r/tabs.json +51 -0
  259. package/public/r/tag.json +39 -0
  260. package/public/r/textarea.json +49 -0
  261. package/public/r/tool.json +32 -0
  262. package/public/r/tooltip.json +51 -0
  263. package/registry.json +2512 -0
  264. package/src/components/docs/MarkdownContent.vue +106 -0
  265. package/src/components/ui/accordion/Accordion.vue +24 -0
  266. package/src/components/ui/accordion/AccordionContent.vue +62 -0
  267. package/src/components/ui/accordion/AccordionItem.vue +23 -0
  268. package/src/components/ui/accordion/AccordionTrigger.vue +38 -0
  269. package/src/components/ui/accordion/index.ts +4 -0
  270. package/src/components/ui/alert/Alert.vue +40 -0
  271. package/src/components/ui/alert/AlertDescription.vue +24 -0
  272. package/src/components/ui/alert/AlertTitle.vue +24 -0
  273. package/src/components/ui/alert/index.ts +4 -0
  274. package/src/components/ui/alert/variants.ts +19 -0
  275. package/src/components/ui/button/Button.vue +27 -0
  276. package/src/components/ui/button/index.ts +2 -0
  277. package/src/components/ui/button/variants.ts +32 -0
  278. package/src/components/ui/button-group/ButtonGroup.vue +31 -0
  279. package/src/components/ui/button-group/index.ts +2 -0
  280. package/src/components/ui/card/Card.vue +17 -0
  281. package/src/components/ui/card/CardContent.vue +14 -0
  282. package/src/components/ui/card/CardDescription.vue +14 -0
  283. package/src/components/ui/card/CardFooter.vue +14 -0
  284. package/src/components/ui/card/CardHeader.vue +17 -0
  285. package/src/components/ui/card/CardTitle.vue +14 -0
  286. package/src/components/ui/card/index.ts +6 -0
  287. package/src/components/ui/chart/Chart.vue +1042 -0
  288. package/src/components/ui/chart/index.ts +13 -0
  289. package/src/components/ui/chat/Chat.vue +1297 -0
  290. package/src/components/ui/chat/ChatAttachments.vue +278 -0
  291. package/src/components/ui/chat/ChatCodeBlock.vue +283 -0
  292. package/src/components/ui/chat/code-block.ts +30 -0
  293. package/src/components/ui/chat/index.ts +24 -0
  294. package/src/components/ui/chat/types.ts +23 -0
  295. package/src/components/ui/checkbox/Checkbox.vue +38 -0
  296. package/src/components/ui/checkbox/index.ts +1 -0
  297. package/src/components/ui/commit/Commit.vue +423 -0
  298. package/src/components/ui/commit/index.ts +9 -0
  299. package/src/components/ui/contribution-graph/ContributionGraph.vue +719 -0
  300. package/src/components/ui/contribution-graph/index.ts +9 -0
  301. package/src/components/ui/data-table/DataTable.vue +534 -0
  302. package/src/components/ui/data-table/index.ts +9 -0
  303. package/src/components/ui/date-picker/DatePicker.vue +649 -0
  304. package/src/components/ui/date-picker/index.ts +7 -0
  305. package/src/components/ui/dialog/Dialog.vue +19 -0
  306. package/src/components/ui/dialog/DialogClose.vue +17 -0
  307. package/src/components/ui/dialog/DialogContent.vue +60 -0
  308. package/src/components/ui/dialog/DialogDescription.vue +23 -0
  309. package/src/components/ui/dialog/DialogFooter.vue +17 -0
  310. package/src/components/ui/dialog/DialogHeader.vue +17 -0
  311. package/src/components/ui/dialog/DialogOverlay.vue +23 -0
  312. package/src/components/ui/dialog/DialogScrollContent.vue +69 -0
  313. package/src/components/ui/dialog/DialogTitle.vue +23 -0
  314. package/src/components/ui/dialog/DialogTrigger.vue +17 -0
  315. package/src/components/ui/dialog/index.ts +10 -0
  316. package/src/components/ui/diff/DiffTool.vue +513 -0
  317. package/src/components/ui/diff/diff-parser.ts +423 -0
  318. package/src/components/ui/diff/diff-tool.ts +39 -0
  319. package/src/components/ui/diff/index.ts +5 -0
  320. package/src/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
  321. package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +39 -0
  322. package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -0
  323. package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +15 -0
  324. package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +31 -0
  325. package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +23 -0
  326. package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
  327. package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -0
  328. package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
  329. package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -0
  330. package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -0
  331. package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +27 -0
  332. package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -0
  333. package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
  334. package/src/components/ui/dropdown-menu/index.ts +16 -0
  335. package/src/components/ui/gauge/Gauge.vue +725 -0
  336. package/src/components/ui/gauge/index.ts +9 -0
  337. package/src/components/ui/git-graph/GitGraph.vue +715 -0
  338. package/src/components/ui/git-graph/index.ts +9 -0
  339. package/src/components/ui/incident-timeline/IncidentTimeline.vue +360 -0
  340. package/src/components/ui/incident-timeline/index.ts +7 -0
  341. package/src/components/ui/input/Input.vue +159 -0
  342. package/src/components/ui/input/InputControl.vue +135 -0
  343. package/src/components/ui/input/InputFieldGroup.vue +14 -0
  344. package/src/components/ui/input/index.ts +9 -0
  345. package/src/components/ui/input/types.ts +34 -0
  346. package/src/components/ui/kpi-card/KpiCard.vue +268 -0
  347. package/src/components/ui/kpi-card/index.ts +9 -0
  348. package/src/components/ui/kpi-line-card/KpiLineCard.vue +622 -0
  349. package/src/components/ui/kpi-line-card/index.ts +11 -0
  350. package/src/components/ui/model-selector/ModelSelector.vue +328 -0
  351. package/src/components/ui/model-selector/index.ts +6 -0
  352. package/src/components/ui/model-selector/types.ts +15 -0
  353. package/src/components/ui/network-graph/NetworkGraph.vue +902 -0
  354. package/src/components/ui/network-graph/index.ts +7 -0
  355. package/src/components/ui/pagination/Pagination.vue +26 -0
  356. package/src/components/ui/pagination/PaginationContent.vue +24 -0
  357. package/src/components/ui/pagination/PaginationEllipsis.vue +27 -0
  358. package/src/components/ui/pagination/PaginationFirst.vue +33 -0
  359. package/src/components/ui/pagination/PaginationItem.vue +39 -0
  360. package/src/components/ui/pagination/PaginationLast.vue +33 -0
  361. package/src/components/ui/pagination/PaginationNext.vue +33 -0
  362. package/src/components/ui/pagination/PaginationPrevious.vue +33 -0
  363. package/src/components/ui/pagination/index.ts +8 -0
  364. package/src/components/ui/profile/Profile.vue +226 -0
  365. package/src/components/ui/profile/ProfileGroup.vue +96 -0
  366. package/src/components/ui/profile/index.ts +8 -0
  367. package/src/components/ui/progress/Progress.vue +271 -0
  368. package/src/components/ui/progress/index.ts +9 -0
  369. package/src/components/ui/prompt-input/PromptInput.vue +1094 -0
  370. package/src/components/ui/prompt-input/index.ts +14 -0
  371. package/src/components/ui/prompt-input/types.ts +78 -0
  372. package/src/components/ui/radio-group/RadioGroup.vue +36 -0
  373. package/src/components/ui/radio-group/RadioGroupItem.vue +45 -0
  374. package/src/components/ui/radio-group/RadioGroupOption.vue +80 -0
  375. package/src/components/ui/radio-group/index.ts +3 -0
  376. package/src/components/ui/reasoning/Reasoning.vue +278 -0
  377. package/src/components/ui/reasoning/index.ts +8 -0
  378. package/src/components/ui/reasoning/types.ts +29 -0
  379. package/src/components/ui/select/Select.vue +19 -0
  380. package/src/components/ui/select/SelectContent.vue +166 -0
  381. package/src/components/ui/select/SelectGroup.vue +23 -0
  382. package/src/components/ui/select/SelectItem.vue +97 -0
  383. package/src/components/ui/select/SelectItemText.vue +15 -0
  384. package/src/components/ui/select/SelectLabel.vue +17 -0
  385. package/src/components/ui/select/SelectScrollDownButton.vue +26 -0
  386. package/src/components/ui/select/SelectScrollUpButton.vue +26 -0
  387. package/src/components/ui/select/SelectSeparator.vue +19 -0
  388. package/src/components/ui/select/SelectTrigger.vue +33 -0
  389. package/src/components/ui/select/SelectValue.vue +15 -0
  390. package/src/components/ui/select/index.ts +11 -0
  391. package/src/components/ui/select/search.ts +26 -0
  392. package/src/components/ui/separator/Separator.vue +30 -0
  393. package/src/components/ui/separator/index.ts +5 -0
  394. package/src/components/ui/separator/types.ts +9 -0
  395. package/src/components/ui/shimmer/Shimmer.vue +110 -0
  396. package/src/components/ui/shimmer/index.ts +5 -0
  397. package/src/components/ui/sidebar/Sidebar.vue +142 -0
  398. package/src/components/ui/sidebar/SidebarContent.vue +18 -0
  399. package/src/components/ui/sidebar/SidebarFooter.vue +18 -0
  400. package/src/components/ui/sidebar/SidebarGroup.vue +18 -0
  401. package/src/components/ui/sidebar/SidebarGroupAction.vue +31 -0
  402. package/src/components/ui/sidebar/SidebarGroupContent.vue +18 -0
  403. package/src/components/ui/sidebar/SidebarGroupLabel.vue +30 -0
  404. package/src/components/ui/sidebar/SidebarHeader.vue +18 -0
  405. package/src/components/ui/sidebar/SidebarInput.vue +26 -0
  406. package/src/components/ui/sidebar/SidebarInset.vue +23 -0
  407. package/src/components/ui/sidebar/SidebarMenu.vue +18 -0
  408. package/src/components/ui/sidebar/SidebarMenuAction.vue +34 -0
  409. package/src/components/ui/sidebar/SidebarMenuBadge.vue +25 -0
  410. package/src/components/ui/sidebar/SidebarMenuButton.vue +37 -0
  411. package/src/components/ui/sidebar/SidebarMenuButtonChild.vue +38 -0
  412. package/src/components/ui/sidebar/SidebarMenuItem.vue +18 -0
  413. package/src/components/ui/sidebar/SidebarMenuSub.vue +18 -0
  414. package/src/components/ui/sidebar/SidebarMenuSubButton.vue +36 -0
  415. package/src/components/ui/sidebar/SidebarMenuSubItem.vue +18 -0
  416. package/src/components/ui/sidebar/SidebarProvider.vue +119 -0
  417. package/src/components/ui/sidebar/SidebarRail.vue +35 -0
  418. package/src/components/ui/sidebar/SidebarSeparator.vue +18 -0
  419. package/src/components/ui/sidebar/SidebarTrigger.vue +28 -0
  420. package/src/components/ui/sidebar/context.ts +39 -0
  421. package/src/components/ui/sidebar/index.ts +43 -0
  422. package/src/components/ui/sidebar/types.ts +13 -0
  423. package/src/components/ui/sidebar/variants.ts +25 -0
  424. package/src/components/ui/skeleton/Skeleton.vue +53 -0
  425. package/src/components/ui/skeleton/index.ts +5 -0
  426. package/src/components/ui/sonner/Sonner.vue +69 -0
  427. package/src/components/ui/sonner/index.ts +12 -0
  428. package/src/components/ui/spinner/Spinner.vue +33 -0
  429. package/src/components/ui/spinner/index.ts +1 -0
  430. package/src/components/ui/stepper/Stepper.vue +29 -0
  431. package/src/components/ui/stepper/StepperDescription.vue +30 -0
  432. package/src/components/ui/stepper/StepperIndicator.vue +50 -0
  433. package/src/components/ui/stepper/StepperItem.vue +28 -0
  434. package/src/components/ui/stepper/StepperSeparator.vue +25 -0
  435. package/src/components/ui/stepper/StepperTitle.vue +27 -0
  436. package/src/components/ui/stepper/StepperTrigger.vue +27 -0
  437. package/src/components/ui/stepper/index.ts +7 -0
  438. package/src/components/ui/switch/Switch.vue +41 -0
  439. package/src/components/ui/switch/index.ts +1 -0
  440. package/src/components/ui/table/Table.vue +23 -0
  441. package/src/components/ui/table/TableBody.vue +17 -0
  442. package/src/components/ui/table/TableCaption.vue +17 -0
  443. package/src/components/ui/table/TableCell.vue +24 -0
  444. package/src/components/ui/table/TableEmpty.vue +31 -0
  445. package/src/components/ui/table/TableFooter.vue +17 -0
  446. package/src/components/ui/table/TableHead.vue +22 -0
  447. package/src/components/ui/table/TableHeader.vue +17 -0
  448. package/src/components/ui/table/TableRow.vue +22 -0
  449. package/src/components/ui/table/index.ts +9 -0
  450. package/src/components/ui/tabs/Tabs.vue +24 -0
  451. package/src/components/ui/tabs/TabsContent.vue +22 -0
  452. package/src/components/ui/tabs/TabsList.vue +27 -0
  453. package/src/components/ui/tabs/TabsTrigger.vue +27 -0
  454. package/src/components/ui/tabs/index.ts +4 -0
  455. package/src/components/ui/tag/Tag.vue +55 -0
  456. package/src/components/ui/tag/index.ts +2 -0
  457. package/src/components/ui/tag/variants.ts +29 -0
  458. package/src/components/ui/textarea/Textarea.vue +159 -0
  459. package/src/components/ui/textarea/TextareaControl.vue +120 -0
  460. package/src/components/ui/textarea/TextareaFieldGroup.vue +14 -0
  461. package/src/components/ui/textarea/index.ts +10 -0
  462. package/src/components/ui/textarea/types.ts +35 -0
  463. package/src/components/ui/tool/Tool.vue +304 -0
  464. package/src/components/ui/tool/index.ts +7 -0
  465. package/src/components/ui/tooltip/Tooltip.vue +19 -0
  466. package/src/components/ui/tooltip/TooltipContent.vue +44 -0
  467. package/src/components/ui/tooltip/TooltipProvider.vue +14 -0
  468. package/src/components/ui/tooltip/TooltipTrigger.vue +15 -0
  469. package/src/components/ui/tooltip/index.ts +4 -0
  470. package/src/lib/code-highlight.ts +220 -0
  471. package/src/lib/utils.ts +6 -0
  472. package/src/styles.css +684 -0
@@ -0,0 +1,61 @@
1
+ {
2
+ "$schema": "https://shadcn-vue.com/schema/registry-item.json",
3
+ "name": "card",
4
+ "title": "Card",
5
+ "description": "Composable card sections for structured UI surfaces.",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "src/components/ui/card/Card.vue",
13
+ "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from \"vue\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<{\n class?: HTMLAttributes[\"class\"]\n}>()\n</script>\n\n<template>\n <div\n data-slot=\"card\"\n :class=\"cn('flex flex-col gap-6 rounded-lg border bg-card py-6 text-card-foreground', props.class)\"\n >\n <slot />\n </div>\n</template>\n",
14
+ "type": "registry:ui",
15
+ "target": "components/ui/card/Card.vue"
16
+ },
17
+ {
18
+ "path": "src/components/ui/card/CardHeader.vue",
19
+ "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from \"vue\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<{\n class?: HTMLAttributes[\"class\"]\n}>()\n</script>\n\n<template>\n <div\n data-slot=\"card-header\"\n :class=\"cn('grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6', props.class)\"\n >\n <slot />\n </div>\n</template>\n",
20
+ "type": "registry:ui",
21
+ "target": "components/ui/card/CardHeader.vue"
22
+ },
23
+ {
24
+ "path": "src/components/ui/card/CardTitle.vue",
25
+ "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from \"vue\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<{\n class?: HTMLAttributes[\"class\"]\n}>()\n</script>\n\n<template>\n <div data-slot=\"card-title\" :class=\"cn('text-base font-semibold leading-none', props.class)\">\n <slot />\n </div>\n</template>\n",
26
+ "type": "registry:ui",
27
+ "target": "components/ui/card/CardTitle.vue"
28
+ },
29
+ {
30
+ "path": "src/components/ui/card/CardDescription.vue",
31
+ "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from \"vue\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<{\n class?: HTMLAttributes[\"class\"]\n}>()\n</script>\n\n<template>\n <div data-slot=\"card-description\" :class=\"cn('text-sm text-muted-foreground', props.class)\">\n <slot />\n </div>\n</template>\n",
32
+ "type": "registry:ui",
33
+ "target": "components/ui/card/CardDescription.vue"
34
+ },
35
+ {
36
+ "path": "src/components/ui/card/CardContent.vue",
37
+ "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from \"vue\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<{\n class?: HTMLAttributes[\"class\"]\n}>()\n</script>\n\n<template>\n <div data-slot=\"card-content\" :class=\"cn('px-6', props.class)\">\n <slot />\n </div>\n</template>\n",
38
+ "type": "registry:ui",
39
+ "target": "components/ui/card/CardContent.vue"
40
+ },
41
+ {
42
+ "path": "src/components/ui/card/CardFooter.vue",
43
+ "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from \"vue\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<{\n class?: HTMLAttributes[\"class\"]\n}>()\n</script>\n\n<template>\n <div data-slot=\"card-footer\" :class=\"cn('flex items-center gap-2 px-6', props.class)\">\n <slot />\n </div>\n</template>\n",
44
+ "type": "registry:ui",
45
+ "target": "components/ui/card/CardFooter.vue"
46
+ },
47
+ {
48
+ "path": "src/components/ui/card/index.ts",
49
+ "content": "export { default as Card } from \"./Card.vue\"\nexport { default as CardContent } from \"./CardContent.vue\"\nexport { default as CardDescription } from \"./CardDescription.vue\"\nexport { default as CardFooter } from \"./CardFooter.vue\"\nexport { default as CardHeader } from \"./CardHeader.vue\"\nexport { default as CardTitle } from \"./CardTitle.vue\"\n",
50
+ "type": "registry:ui",
51
+ "target": "components/ui/card/index.ts"
52
+ },
53
+ {
54
+ "path": "src/lib/utils.ts",
55
+ "content": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
56
+ "type": "registry:lib",
57
+ "target": "lib/utils.ts"
58
+ }
59
+ ],
60
+ "type": "registry:ui"
61
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "https://shadcn-vue.com/schema/registry-item.json",
3
+ "name": "chart",
4
+ "title": "Chart",
5
+ "description": "A responsive SVG chart for grouped bars, line series, translucent area fills, legends, axis labels, and selectable data points.",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "src/components/ui/chart/Chart.vue",
13
+ "content": "<script setup lang=\"ts\">\nimport { computed } from \"vue\"\nimport type { CSSProperties, HTMLAttributes } from \"vue\"\nimport { cn } from \"@/lib/utils\"\n\nexport type ChartType = \"bar\" | \"line\"\nexport type ChartSize = \"sm\" | \"default\" | \"lg\"\nexport type ChartValue = number | string | null | undefined\nexport type ChartLineCurve = \"linear\" | \"smooth\"\nexport type ChartLegendPosition = \"bottom\" | \"top\"\n\nexport interface ChartDataPoint extends Record<string, unknown> {\n id?: number | string\n label?: number | string\n value?: ChartValue\n values?: Record<string, ChartValue>\n}\n\nexport interface ChartSeries {\n areaOpacity?: number\n color?: string\n curve?: ChartLineCurve\n key: string\n label?: string\n showArea?: boolean\n showPoints?: boolean\n type?: ChartType\n}\n\nexport interface ChartValueFormatterContext {\n data: ChartDataPoint\n index: number\n key: string\n label: string\n series: ChartSeries\n}\n\nexport type ChartValueFormatter = (\n value: number,\n context?: ChartValueFormatterContext,\n) => number | string\n\nexport interface ChartSelectPayload extends ChartValueFormatterContext {\n value: number\n}\n\ninterface ChartProps {\n areaOpacity?: number\n class?: HTMLAttributes[\"class\"]\n data?: ChartDataPoint[]\n description?: string\n emptyLabel?: string\n gridLineCount?: number\n height?: number\n includeZero?: boolean\n legendLabel?: string\n legendPosition?: ChartLegendPosition\n lineCurve?: ChartLineCurve\n maxXLabels?: number\n modelValue?: string\n selectable?: boolean\n series?: ChartSeries[]\n showArea?: boolean\n showGrid?: boolean\n showLegend?: boolean\n showPoints?: boolean\n showXAxis?: boolean\n showYAxis?: boolean\n size?: ChartSize\n summary?: string\n title?: string\n type?: ChartType\n valueFormatter?: ChartValueFormatter\n xKey?: string\n yMax?: number\n yMin?: number\n}\n\ninterface NormalizedChartPoint {\n data: ChartDataPoint\n id: string\n index: number\n label: string\n}\n\ninterface NormalizedChartSeries extends ChartSeries {\n areaOpacity: number\n color: string\n curve: ChartLineCurve\n label: string\n showArea: boolean\n showPoints: boolean\n type: ChartType\n}\n\ninterface ChartSeriesValue {\n data: ChartDataPoint\n id: string\n index: number\n key: string\n label: string\n series: NormalizedChartSeries\n value: number | null\n}\n\ninterface ChartNode extends ChartSelectPayload {\n color: string\n id: string\n x: number\n y: number\n}\n\ninterface BarNode extends ChartNode {\n height: number\n width: number\n}\n\ninterface LinePath {\n color: string\n d: string\n key: string\n label: string\n}\n\ninterface LineAreaPath extends LinePath {\n opacity: number\n}\n\nconst props = withDefaults(defineProps<ChartProps>(), {\n areaOpacity: 0.16,\n data: () => [],\n emptyLabel: \"No chart data.\",\n gridLineCount: undefined,\n includeZero: true,\n legendLabel: \"Chart series\",\n legendPosition: \"bottom\",\n lineCurve: \"linear\",\n maxXLabels: undefined,\n selectable: true,\n showArea: false,\n showGrid: true,\n showLegend: true,\n showPoints: true,\n showXAxis: true,\n showYAxis: true,\n size: \"default\",\n type: \"bar\",\n xKey: \"label\",\n})\n\nconst emit = defineEmits<{\n \"select\": [payload: ChartSelectPayload]\n \"update:modelValue\": [value: string]\n}>()\n\nconst chartWidth = 720\nconst defaultColors = [\n \"oklch(0.58 0.19 248)\",\n \"oklch(0.6 0.16 153)\",\n \"oklch(0.67 0.18 69)\",\n \"oklch(0.59 0.21 338)\",\n \"oklch(0.62 0.18 204)\",\n \"oklch(0.58 0.2 27)\",\n]\n\nconst numberFormatter = computed(() =>\n new Intl.NumberFormat(undefined, {\n maximumFractionDigits: 2,\n }),\n)\n\nconst sizeClasses = computed(() => {\n if (props.size === \"sm\") {\n return {\n axis: \"text-[10px]\",\n body: \"p-3\",\n content: \"text-xs\",\n description: \"text-xs\",\n header: \"gap-2 px-3 py-2.5\",\n height: 190,\n legend: \"gap-2 text-[11px]\",\n lineWidth: 2,\n pointRadius: 3,\n tickCount: 4,\n title: \"text-xs\",\n xLabels: 6,\n }\n }\n\n if (props.size === \"lg\") {\n return {\n axis: \"text-xs\",\n body: \"p-5\",\n content: \"text-sm\",\n description: \"text-sm\",\n header: \"gap-3 px-5 py-4\",\n height: 340,\n legend: \"gap-3 text-xs\",\n lineWidth: 3,\n pointRadius: 4.5,\n tickCount: 6,\n title: \"text-sm\",\n xLabels: 10,\n }\n }\n\n return {\n axis: \"text-[11px]\",\n body: \"p-4\",\n content: \"text-[13px]\",\n description: \"text-sm\",\n header: \"gap-2.5 px-4 py-3\",\n height: 260,\n legend: \"gap-2.5 text-xs\",\n lineWidth: 2.5,\n pointRadius: 3.75,\n tickCount: 5,\n title: \"text-[13px]\",\n xLabels: 8,\n }\n})\n\nconst chartHeight = computed(() =>\n Math.max(160, Math.trunc(props.height ?? sizeClasses.value.height)),\n)\n\nconst geometry = computed(() => {\n const bottom = props.showXAxis ? 40 : 16\n const left = props.showYAxis ? yAxisWidth.value : 16\n const right = 18\n const top = 16\n\n return {\n bottom,\n left,\n plotHeight: Math.max(1, chartHeight.value - top - bottom),\n plotWidth: Math.max(1, chartWidth - left - right),\n right,\n top,\n }\n})\n\nconst normalizedSeries = computed<NormalizedChartSeries[]>(() => {\n const source = props.series?.length\n ? props.series\n : [{ key: \"value\", label: \"Value\" }]\n\n return source\n .filter((series) => series.key.trim().length > 0)\n .map((series, index) => ({\n ...series,\n areaOpacity: clampOpacity(series.areaOpacity ?? props.areaOpacity),\n color: series.color ?? defaultColors[index % defaultColors.length] ?? defaultColors[0],\n curve: series.curve ?? props.lineCurve,\n label: series.label ?? formatSeriesLabel(series.key),\n showArea: series.showArea ?? props.showArea,\n showPoints: series.showPoints ?? props.showPoints,\n type: series.type ?? props.type,\n }))\n})\n\nconst normalizedData = computed<NormalizedChartPoint[]>(() =>\n props.data.map((data, index) => {\n const rawLabel = getDataValue(data, props.xKey)\n const label = rawLabel === undefined || rawLabel === null || rawLabel === \"\"\n ? `Item ${index + 1}`\n : String(rawLabel)\n const id = data.id === undefined || data.id === null\n ? `${index}-${label}`\n : String(data.id)\n\n return {\n data,\n id,\n index,\n label,\n }\n }),\n)\n\nconst seriesValues = computed(() =>\n normalizedSeries.value.map((series) => ({\n series,\n values: normalizedData.value.map((point): ChartSeriesValue => ({\n data: point.data,\n id: `${point.id}:${series.key}`,\n index: point.index,\n key: series.key,\n label: point.label,\n series,\n value: getSeriesValue(point.data, series.key),\n })),\n })),\n)\n\nconst numericValues = computed(() =>\n seriesValues.value.flatMap((group) =>\n group.values\n .map((point) => point.value)\n .filter((value): value is number => value !== null),\n ),\n)\n\nconst rawDomain = computed(() => {\n const values = numericValues.value\n\n if (!values.length) {\n return {\n max: 1,\n min: 0,\n }\n }\n\n let min = props.yMin ?? Math.min(...values)\n let max = props.yMax ?? Math.max(...values)\n\n if (props.includeZero) {\n min = Math.min(min, 0)\n max = Math.max(max, 0)\n }\n\n if (min > max) {\n const nextMin = max\n max = min\n min = nextMin\n }\n\n if (min === max) {\n const delta = min === 0 ? 1 : Math.abs(min * 0.1)\n min -= delta\n max += delta\n }\n\n return {\n max,\n min,\n }\n})\n\nconst yTicks = computed(() =>\n getNiceTicks(rawDomain.value.min, rawDomain.value.max, props.gridLineCount ?? sizeClasses.value.tickCount),\n)\n\nconst yAxisWidth = computed(() => {\n const longestLabel = yTicks.value.reduce((maxLength, value) =>\n Math.max(maxLength, formatValue(value).length), 0)\n\n return Math.min(132, Math.max(58, (longestLabel * 7) + 16))\n})\n\nconst yDomain = computed(() => {\n const ticks = yTicks.value\n\n if (ticks.length >= 2) {\n return {\n max: ticks[ticks.length - 1] ?? rawDomain.value.max,\n min: ticks[0] ?? rawDomain.value.min,\n }\n }\n\n return rawDomain.value\n})\n\nconst gridTicks = computed(() =>\n yTicks.value.map((value) => ({\n label: formatValue(value),\n value,\n y: yScale(value),\n })),\n)\n\nconst xLabels = computed(() => {\n const points = normalizedData.value\n const maxLabels = props.maxXLabels ?? sizeClasses.value.xLabels\n const visibleIndexes = getVisibleLabelIndexes(points.length, maxLabels)\n\n return points\n .filter((_, index) => visibleIndexes.has(index))\n .map((point) => ({\n ...point,\n x: categoryCenterX(point.index),\n }))\n})\n\nconst barSeriesGroups = computed(() =>\n seriesValues.value.filter((group) => group.series.type === \"bar\"),\n)\n\nconst lineSeriesGroups = computed(() =>\n seriesValues.value.filter((group) => group.series.type === \"line\"),\n)\n\nconst barNodes = computed<BarNode[]>(() => {\n const groupCount = Math.max(1, barSeriesGroups.value.length)\n const categoryStep = getCategoryStep()\n const categoryPadding = Math.min(32, Math.max(10, categoryStep * 0.18))\n const gap = groupCount > 1 ? Math.min(8, Math.max(3, categoryStep * 0.04)) : 0\n const availableWidth = Math.max(2, categoryStep - (categoryPadding * 2) - (gap * (groupCount - 1)))\n const width = Math.max(2, availableWidth / groupCount)\n const baseline = yScale(0)\n const nodes: BarNode[] = []\n\n for (const [seriesIndex, group] of barSeriesGroups.value.entries()) {\n for (const point of group.values) {\n if (point.value === null) continue\n\n const valueY = yScale(point.value)\n const x = geometry.value.left +\n (point.index * categoryStep) +\n categoryPadding +\n (seriesIndex * (width + gap))\n const height = Math.max(1, Math.abs(baseline - valueY))\n const y = point.value >= 0 ? valueY : baseline\n\n nodes.push({\n color: point.series.color,\n data: point.data,\n height,\n id: point.id,\n index: point.index,\n key: point.key,\n label: point.label,\n series: point.series,\n value: point.value,\n width,\n x,\n y,\n })\n }\n }\n\n return nodes\n})\n\nconst lineNodesBySeries = computed(() =>\n lineSeriesGroups.value.map((group) => ({\n series: group.series,\n nodes: group.values\n .filter((point): point is ChartSeriesValue & { value: number } => point.value !== null)\n .map((point): ChartNode => ({\n color: point.series.color,\n data: point.data,\n id: point.id,\n index: point.index,\n key: point.key,\n label: point.label,\n series: point.series,\n value: point.value,\n x: categoryCenterX(point.index),\n y: yScale(point.value),\n })),\n })),\n)\n\nconst linePaths = computed<LinePath[]>(() =>\n lineNodesBySeries.value\n .map((group) => ({\n color: group.series.color,\n d: buildLinePath(group.nodes, group.series.curve),\n key: group.series.key,\n label: group.series.label,\n }))\n .filter((path) => path.d.length > 0),\n)\n\nconst lineAreaPaths = computed<LineAreaPath[]>(() =>\n lineNodesBySeries.value\n .filter((group) => group.series.showArea)\n .map((group) => ({\n color: group.series.color,\n d: buildLineAreaPath(group.nodes, group.series.curve),\n key: group.series.key,\n label: group.series.label,\n opacity: group.series.areaOpacity,\n }))\n .filter((path) => path.d.length > 0),\n)\n\nconst lineNodes = computed(() =>\n lineNodesBySeries.value.flatMap((group) => group.nodes),\n)\n\nconst visibleLineNodes = computed(() =>\n lineNodesBySeries.value.flatMap((group) =>\n group.series.showPoints ? group.nodes : [],\n ),\n)\n\nconst lineHitNodes = computed(() =>\n lineNodesBySeries.value.flatMap((group) =>\n props.selectable && !group.series.showPoints ? group.nodes : [],\n ),\n)\n\nconst hasRenderableData = computed(() =>\n barNodes.value.length > 0 || lineNodes.value.length > 0,\n)\n\nconst hasHeader = computed(() =>\n Boolean(props.title || props.description || props.summary),\n)\n\nconst baselineY = computed(() => yScale(0))\n\nfunction getDataValue(data: ChartDataPoint, key: string) {\n return (data as Record<string, unknown>)[key]\n}\n\nfunction getSeriesValue(data: ChartDataPoint, key: string) {\n const nested = data.values\n const rawValue = nested && Object.prototype.hasOwnProperty.call(nested, key)\n ? nested[key]\n : getDataValue(data, key)\n\n return toFiniteNumber(rawValue)\n}\n\nfunction toFiniteNumber(value: unknown) {\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null\n }\n\n if (typeof value === \"string\" && value.trim().length > 0) {\n const parsed = Number(value)\n\n return Number.isFinite(parsed) ? parsed : null\n }\n\n return null\n}\n\nfunction yScale(value: number) {\n const domain = yDomain.value\n const span = domain.max - domain.min || 1\n\n return geometry.value.top + ((domain.max - value) / span) * geometry.value.plotHeight\n}\n\nfunction getCategoryStep() {\n const count = Math.max(1, normalizedData.value.length)\n\n return geometry.value.plotWidth / count\n}\n\nfunction categoryCenterX(index: number) {\n return geometry.value.left + (getCategoryStep() * index) + (getCategoryStep() / 2)\n}\n\nfunction buildLinePath(nodes: ChartNode[], curve: ChartLineCurve) {\n if (curve === \"smooth\") return buildSmoothLinePath(nodes)\n\n return buildLinearLinePath(nodes)\n}\n\nfunction buildLineAreaPath(nodes: ChartNode[], curve: ChartLineCurve) {\n if (nodes.length < 2) return \"\"\n\n const linePath = buildLinePath(nodes, curve)\n const first = nodes[0]\n const last = nodes[nodes.length - 1]\n const bottomY = geometry.value.top + geometry.value.plotHeight\n\n if (!first || !last || !linePath) return \"\"\n\n return [\n linePath,\n \"L\",\n roundCoordinate(last.x),\n roundCoordinate(bottomY),\n \"L\",\n roundCoordinate(first.x),\n roundCoordinate(bottomY),\n \"Z\",\n ].join(\" \")\n}\n\nfunction buildLinearLinePath(nodes: ChartNode[]) {\n return nodes\n .map((node, index) => `${index === 0 ? \"M\" : \"L\"} ${roundCoordinate(node.x)} ${roundCoordinate(node.y)}`)\n .join(\" \")\n}\n\nfunction buildSmoothLinePath(nodes: ChartNode[]) {\n if (nodes.length < 3) return buildLinearLinePath(nodes)\n\n const commands = [`M ${roundCoordinate(nodes[0].x)} ${roundCoordinate(nodes[0].y)}`]\n\n for (let index = 0; index < nodes.length - 1; index += 1) {\n const previous = nodes[index - 1] ?? nodes[index]\n const current = nodes[index]\n const next = nodes[index + 1]\n const following = nodes[index + 2] ?? next\n\n if (!previous || !current || !next || !following) continue\n\n const controlPoint1X = current.x + ((next.x - previous.x) / 6)\n const controlPoint1Y = current.y + ((next.y - previous.y) / 6)\n const controlPoint2X = next.x - ((following.x - current.x) / 6)\n const controlPoint2Y = next.y - ((following.y - current.y) / 6)\n\n commands.push([\n \"C\",\n roundCoordinate(controlPoint1X),\n roundCoordinate(controlPoint1Y),\n roundCoordinate(controlPoint2X),\n roundCoordinate(controlPoint2Y),\n roundCoordinate(next.x),\n roundCoordinate(next.y),\n ].join(\" \"))\n }\n\n return commands.join(\" \")\n}\n\nfunction getVisibleLabelIndexes(length: number, maxLabels: number) {\n const count = Math.max(0, length)\n const limit = Math.max(1, Math.trunc(maxLabels))\n const indexes = new Set<number>()\n\n if (count === 0) return indexes\n\n if (count <= limit) {\n for (let index = 0; index < count; index += 1) {\n indexes.add(index)\n }\n\n return indexes\n }\n\n const slots = Math.max(1, limit - 1)\n\n for (let index = 0; index < limit; index += 1) {\n indexes.add(Math.round((index / slots) * (count - 1)))\n }\n\n indexes.add(count - 1)\n\n return indexes\n}\n\nfunction formatSeriesLabel(key: string) {\n return key\n .replace(/[-_]/g, \" \")\n .replace(/\\b\\w/g, (letter) => letter.toUpperCase())\n}\n\nfunction formatValue(value: number, context?: ChartValueFormatterContext) {\n const formatted = props.valueFormatter?.(value, context)\n\n if (formatted !== undefined) return String(formatted)\n\n return numberFormatter.value.format(value)\n}\n\nfunction formatNodeLabel(node: ChartNode) {\n return `${node.label}, ${node.series.label}: ${formatValue(node.value, node)}`\n}\n\nfunction getNiceTicks(min: number, max: number, count: number) {\n const tickCount = Math.max(2, Math.min(8, Math.trunc(count)))\n const span = max - min\n\n if (span <= 0 || !Number.isFinite(span)) return [min, max]\n\n const step = niceNumber(span / (tickCount - 1), true)\n const niceMin = Math.floor(min / step) * step\n const niceMax = Math.ceil(max / step) * step\n const precision = decimalPrecision(step)\n const ticks: number[] = []\n\n for (let value = niceMin; value <= niceMax + (step / 2); value += step) {\n ticks.push(roundTo(value, precision))\n }\n\n return ticks\n}\n\nfunction niceNumber(value: number, round: boolean) {\n const exponent = Math.floor(Math.log10(value))\n const fraction = value / (10 ** exponent)\n let niceFraction: number\n\n if (round) {\n if (fraction < 1.5) niceFraction = 1\n else if (fraction < 3) niceFraction = 2\n else if (fraction < 7) niceFraction = 5\n else niceFraction = 10\n } else if (fraction <= 1) niceFraction = 1\n else if (fraction <= 2) niceFraction = 2\n else if (fraction <= 5) niceFraction = 5\n else niceFraction = 10\n\n return niceFraction * (10 ** exponent)\n}\n\nfunction decimalPrecision(value: number) {\n if (!Number.isFinite(value)) return 0\n\n const exponent = Math.floor(Math.log10(Math.abs(value)))\n\n return Math.max(0, -exponent + 1)\n}\n\nfunction roundTo(value: number, precision: number) {\n const multiplier = 10 ** precision\n\n return Math.round(value * multiplier) / multiplier\n}\n\nfunction roundCoordinate(value: number) {\n return Math.round(value * 100) / 100\n}\n\nfunction clampOpacity(value: number) {\n if (!Number.isFinite(value)) return 0.16\n\n return Math.min(1, Math.max(0, value))\n}\n\nfunction isSelectedNode(node: ChartNode) {\n return props.modelValue === node.id\n}\n\nfunction swatchStyle(color: string): CSSProperties {\n return {\n backgroundColor: color,\n }\n}\n\nfunction isLineSeries(series: NormalizedChartSeries) {\n return series.type === \"line\"\n}\n\nfunction handleSelect(node: ChartNode) {\n if (!props.selectable) return\n\n emit(\"update:modelValue\", node.id)\n emit(\"select\", {\n data: node.data,\n index: node.index,\n key: node.key,\n label: node.label,\n series: node.series,\n value: node.value,\n })\n}\n\nfunction handleNodeKeydown(event: KeyboardEvent, node: ChartNode) {\n if (event.key !== \"Enter\" && event.key !== \" \") return\n\n event.preventDefault()\n handleSelect(node)\n}\n</script>\n\n<template>\n <article\n data-slot=\"chart\"\n :data-size=\"props.size\"\n :data-type=\"props.type\"\n :class=\"cn('overflow-hidden rounded-xl border bg-card text-card-foreground shadow-xs', props.class)\"\n >\n <header\n v-if=\"hasHeader\"\n data-slot=\"chart-header\"\n :class=\"cn('flex flex-wrap items-start justify-between border-b', sizeClasses.header)\"\n >\n <div v-if=\"props.title || props.description\" class=\"flex min-w-0 flex-col gap-1\">\n <h3\n v-if=\"props.title\"\n data-slot=\"chart-title\"\n :class=\"cn('truncate font-semibold leading-tight tracking-normal', sizeClasses.title)\"\n >\n {{ props.title }}\n </h3>\n <p\n v-if=\"props.description\"\n data-slot=\"chart-description\"\n :class=\"cn('text-muted-foreground', sizeClasses.description)\"\n >\n {{ props.description }}\n </p>\n </div>\n\n <p\n v-if=\"props.summary\"\n data-slot=\"chart-summary\"\n class=\"shrink-0 text-xs font-medium text-muted-foreground\"\n >\n {{ props.summary }}\n </p>\n </header>\n\n <div\n v-if=\"hasRenderableData\"\n data-slot=\"chart-body\"\n :class=\"cn('flex min-w-0 flex-col gap-3', sizeClasses.body)\"\n >\n <div\n v-if=\"props.showLegend && normalizedSeries.length && props.legendPosition === 'top'\"\n data-slot=\"chart-legend\"\n data-position=\"top\"\n :class=\"cn('flex flex-wrap items-center justify-center text-muted-foreground', sizeClasses.legend)\"\n :aria-label=\"props.legendLabel\"\n >\n <span\n v-for=\"seriesItem in normalizedSeries\"\n :key=\"seriesItem.key\"\n data-slot=\"chart-legend-item\"\n class=\"inline-flex min-w-0 items-center gap-1.5\"\n >\n <span\n data-slot=\"chart-legend-swatch\"\n aria-hidden=\"true\"\n :class=\"cn('shrink-0', isLineSeries(seriesItem) ? 'h-0.5 w-5 rounded-full' : 'size-2.5 rounded-full')\"\n :style=\"swatchStyle(seriesItem.color)\"\n />\n <span class=\"truncate\">{{ seriesItem.label }}</span>\n </span>\n </div>\n\n <div data-slot=\"chart-viewport\" class=\"min-w-0 overflow-x-auto pb-1\">\n <svg\n data-slot=\"chart-svg\"\n role=\"img\"\n :aria-label=\"props.title ?? 'Chart'\"\n class=\"block h-auto min-w-[34rem] max-w-none overflow-visible sm:min-w-0 sm:w-full\"\n :height=\"chartHeight\"\n :viewBox=\"`0 0 ${chartWidth} ${chartHeight}`\"\n :width=\"chartWidth\"\n >\n <g\n v-if=\"props.showGrid\"\n data-slot=\"chart-grid\"\n aria-hidden=\"true\"\n >\n <line\n v-for=\"tick in gridTicks\"\n :key=\"`grid-${tick.value}`\"\n class=\"text-border\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-dasharray=\"4 6\"\n stroke-linecap=\"round\"\n :stroke-width=\"tick.value === 0 ? 1.25 : 1\"\n :x1=\"geometry.left\"\n :x2=\"geometry.left + geometry.plotWidth\"\n :y1=\"tick.y\"\n :y2=\"tick.y\"\n />\n </g>\n\n <line\n v-if=\"yDomain.min < 0 && yDomain.max > 0\"\n data-slot=\"chart-zero-line\"\n aria-hidden=\"true\"\n class=\"text-foreground/60\"\n fill=\"none\"\n stroke=\"currentColor\"\n :stroke-width=\"1.25\"\n :x1=\"geometry.left\"\n :x2=\"geometry.left + geometry.plotWidth\"\n :y1=\"baselineY\"\n :y2=\"baselineY\"\n />\n\n <g\n v-if=\"props.showYAxis\"\n data-slot=\"chart-y-axis\"\n >\n <text\n v-for=\"tick in gridTicks\"\n :key=\"`y-${tick.value}`\"\n :class=\"cn('text-muted-foreground', sizeClasses.axis)\"\n dominant-baseline=\"middle\"\n fill=\"currentColor\"\n text-anchor=\"end\"\n :x=\"geometry.left - 12\"\n :y=\"tick.y\"\n >\n {{ tick.label }}\n </text>\n </g>\n\n <g\n v-if=\"props.showXAxis\"\n data-slot=\"chart-x-axis\"\n >\n <line\n class=\"text-border\"\n fill=\"none\"\n stroke=\"currentColor\"\n :stroke-width=\"1\"\n :x1=\"geometry.left\"\n :x2=\"geometry.left + geometry.plotWidth\"\n :y1=\"geometry.top + geometry.plotHeight\"\n :y2=\"geometry.top + geometry.plotHeight\"\n />\n <text\n v-for=\"label in xLabels\"\n :key=\"`x-${label.id}`\"\n :class=\"cn('text-muted-foreground', sizeClasses.axis)\"\n dominant-baseline=\"hanging\"\n fill=\"currentColor\"\n text-anchor=\"middle\"\n :x=\"label.x\"\n :y=\"geometry.top + geometry.plotHeight + 12\"\n >\n {{ label.label }}\n </text>\n </g>\n\n <g data-slot=\"chart-bars\">\n <rect\n v-for=\"node in barNodes\"\n :key=\"node.id\"\n data-slot=\"chart-bar\"\n :aria-label=\"formatNodeLabel(node)\"\n :aria-selected=\"props.selectable ? isSelectedNode(node) : undefined\"\n :fill=\"node.color\"\n role=\"button\"\n :rx=\"Math.min(4, node.width / 2)\"\n stroke=\"transparent\"\n :stroke-width=\"0\"\n :tabindex=\"props.selectable ? 0 : undefined\"\n :width=\"node.width\"\n :height=\"node.height\"\n :x=\"node.x\"\n :y=\"node.y\"\n class=\"outline-none transition-opacity hover:opacity-80 focus-visible:opacity-80\"\n @click=\"handleSelect(node)\"\n @keydown=\"handleNodeKeydown($event, node)\"\n >\n <title>{{ formatNodeLabel(node) }}</title>\n </rect>\n </g>\n\n <g data-slot=\"chart-lines\">\n <path\n v-for=\"path in lineAreaPaths\"\n :key=\"`${path.key}:area`\"\n data-slot=\"chart-line-area\"\n aria-hidden=\"true\"\n :d=\"path.d\"\n :fill=\"path.color\"\n :fill-opacity=\"path.opacity\"\n stroke=\"none\"\n />\n\n <path\n v-for=\"path in linePaths\"\n :key=\"path.key\"\n data-slot=\"chart-line\"\n fill=\"none\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n :aria-label=\"path.label\"\n :d=\"path.d\"\n :stroke=\"path.color\"\n :stroke-width=\"sizeClasses.lineWidth\"\n />\n\n <template v-if=\"visibleLineNodes.length\">\n <circle\n v-for=\"node in visibleLineNodes\"\n :key=\"node.id\"\n data-slot=\"chart-point\"\n :aria-label=\"formatNodeLabel(node)\"\n :aria-selected=\"props.selectable ? isSelectedNode(node) : undefined\"\n :cx=\"node.x\"\n :cy=\"node.y\"\n :fill=\"node.color\"\n role=\"button\"\n :r=\"isSelectedNode(node) ? sizeClasses.pointRadius + 1 : sizeClasses.pointRadius\"\n stroke=\"var(--card)\"\n :stroke-width=\"1.5\"\n :tabindex=\"props.selectable ? 0 : undefined\"\n class=\"outline-none transition-opacity hover:opacity-80 focus-visible:opacity-80\"\n @click=\"handleSelect(node)\"\n @keydown=\"handleNodeKeydown($event, node)\"\n >\n <title>{{ formatNodeLabel(node) }}</title>\n </circle>\n </template>\n\n <circle\n v-for=\"node in lineHitNodes\"\n :key=\"`${node.id}:hit`\"\n data-slot=\"chart-line-hit-area\"\n :aria-label=\"formatNodeLabel(node)\"\n :aria-selected=\"props.selectable ? isSelectedNode(node) : undefined\"\n :cx=\"node.x\"\n :cy=\"node.y\"\n fill=\"transparent\"\n role=\"button\"\n :r=\"Math.max(8, sizeClasses.pointRadius + 4)\"\n stroke=\"transparent\"\n :stroke-width=\"0\"\n :tabindex=\"props.selectable ? 0 : undefined\"\n class=\"outline-none\"\n @click=\"handleSelect(node)\"\n @keydown=\"handleNodeKeydown($event, node)\"\n >\n <title>{{ formatNodeLabel(node) }}</title>\n </circle>\n </g>\n </svg>\n </div>\n\n <div\n v-if=\"props.showLegend && normalizedSeries.length && props.legendPosition === 'bottom'\"\n data-slot=\"chart-legend\"\n data-position=\"bottom\"\n :class=\"cn('flex flex-wrap items-center justify-end text-muted-foreground', sizeClasses.legend)\"\n :aria-label=\"props.legendLabel\"\n >\n <span\n v-for=\"seriesItem in normalizedSeries\"\n :key=\"seriesItem.key\"\n data-slot=\"chart-legend-item\"\n class=\"inline-flex min-w-0 items-center gap-1.5\"\n >\n <span\n data-slot=\"chart-legend-swatch\"\n aria-hidden=\"true\"\n :class=\"cn('shrink-0', isLineSeries(seriesItem) ? 'h-0.5 w-5 rounded-full' : 'size-2.5 rounded-full')\"\n :style=\"swatchStyle(seriesItem.color)\"\n />\n <span class=\"truncate\">{{ seriesItem.label }}</span>\n </span>\n </div>\n </div>\n\n <div\n v-else\n data-slot=\"chart-empty\"\n :class=\"cn('text-muted-foreground', sizeClasses.body, sizeClasses.content)\"\n >\n {{ props.emptyLabel }}\n </div>\n </article>\n</template>\n",
14
+ "type": "registry:ui",
15
+ "target": "components/ui/chart/Chart.vue"
16
+ },
17
+ {
18
+ "path": "src/components/ui/chart/index.ts",
19
+ "content": "export { default as Chart } from \"./Chart.vue\"\nexport type {\n ChartDataPoint,\n ChartLegendPosition,\n ChartLineCurve,\n ChartSelectPayload,\n ChartSeries,\n ChartSize,\n ChartType,\n ChartValue,\n ChartValueFormatter,\n ChartValueFormatterContext,\n} from \"./Chart.vue\"\n",
20
+ "type": "registry:ui",
21
+ "target": "components/ui/chart/index.ts"
22
+ },
23
+ {
24
+ "path": "src/lib/utils.ts",
25
+ "content": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
26
+ "type": "registry:lib",
27
+ "target": "lib/utils.ts"
28
+ }
29
+ ],
30
+ "type": "registry:ui"
31
+ }
@@ -0,0 +1,186 @@
1
+ {
2
+ "$schema": "https://shadcn-vue.com/schema/registry-item.json",
3
+ "name": "chat",
4
+ "title": "Chat",
5
+ "description": "A left and right chat message display component with attachments, streaming status, custom rendering slots, and a unified action toolbar.",
6
+ "dependencies": [
7
+ "@lucide/vue",
8
+ "@vueuse/core",
9
+ "class-variance-authority",
10
+ "clsx",
11
+ "highlight.js",
12
+ "reka-ui",
13
+ "tailwind-merge"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "src/components/ui/button/Button.vue",
18
+ "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from \"vue\"\nimport { Primitive, type PrimitiveProps } from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\nimport { buttonVariants, type ButtonVariants } from \"./variants\"\n\ninterface ButtonProps extends PrimitiveProps {\n variant?: ButtonVariants[\"variant\"]\n size?: ButtonVariants[\"size\"]\n class?: HTMLAttributes[\"class\"]\n}\n\nconst props = withDefaults(defineProps<ButtonProps>(), {\n as: \"button\",\n})\n</script>\n\n<template>\n <Primitive\n data-slot=\"button\"\n :as=\"props.as\"\n :as-child=\"props.asChild\"\n :class=\"cn(buttonVariants({ variant: props.variant, size: props.size }), props.class)\"\n >\n <slot />\n </Primitive>\n</template>\n",
19
+ "type": "registry:ui",
20
+ "target": "components/ui/button/Button.vue"
21
+ },
22
+ {
23
+ "path": "src/components/ui/button/variants.ts",
24
+ "content": "import { cva, type VariantProps } from \"class-variance-authority\"\n\nexport const buttonVariants = cva(\n \"inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-[background-color,border-color,color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/30\",\n outline:\n \"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground\",\n secondary: \"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80\",\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-9 px-4 py-2\",\n sm: \"h-8 rounded-md px-3 text-xs\",\n lg: \"h-10 rounded-md px-6\",\n icon: \"size-9\",\n \"icon-sm\": \"size-8\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n)\n\nexport type ButtonVariants = VariantProps<typeof buttonVariants>\n",
25
+ "type": "registry:ui",
26
+ "target": "components/ui/button/variants.ts"
27
+ },
28
+ {
29
+ "path": "src/components/ui/button/index.ts",
30
+ "content": "export { default as Button } from \"./Button.vue\"\nexport { buttonVariants, type ButtonVariants } from \"./variants\"\n",
31
+ "type": "registry:ui",
32
+ "target": "components/ui/button/index.ts"
33
+ },
34
+ {
35
+ "path": "src/components/ui/dropdown-menu/DropdownMenu.vue",
36
+ "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuRootEmits, DropdownMenuRootProps } from \"reka-ui\"\nimport { DropdownMenuRoot, useForwardPropsEmits } from \"reka-ui\"\n\nconst props = defineProps<DropdownMenuRootProps>()\nconst emits = defineEmits<DropdownMenuRootEmits>()\n\nconst forwarded = useForwardPropsEmits(props, emits)\n</script>\n\n<template>\n <DropdownMenuRoot\n v-slot=\"slotProps\"\n data-slot=\"dropdown-menu\"\n v-bind=\"forwarded\"\n >\n <slot v-bind=\"slotProps\" />\n </DropdownMenuRoot>\n</template>\n",
37
+ "type": "registry:ui",
38
+ "target": "components/ui/dropdown-menu/DropdownMenu.vue"
39
+ },
40
+ {
41
+ "path": "src/components/ui/dropdown-menu/DropdownMenuTrigger.vue",
42
+ "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuTriggerProps } from \"reka-ui\"\nimport { DropdownMenuTrigger, useForwardProps } from \"reka-ui\"\n\nconst props = defineProps<DropdownMenuTriggerProps>()\n\nconst forwardedProps = useForwardProps(props)\n</script>\n\n<template>\n <DropdownMenuTrigger\n data-slot=\"dropdown-menu-trigger\"\n v-bind=\"forwardedProps\"\n >\n <slot />\n </DropdownMenuTrigger>\n</template>\n",
43
+ "type": "registry:ui",
44
+ "target": "components/ui/dropdown-menu/DropdownMenuTrigger.vue"
45
+ },
46
+ {
47
+ "path": "src/components/ui/dropdown-menu/DropdownMenuContent.vue",
48
+ "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuContentEmits, DropdownMenuContentProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport {\n DropdownMenuContent,\n DropdownMenuPortal,\n useForwardPropsEmits,\n} from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nconst props = withDefaults(\n defineProps<DropdownMenuContentProps & { class?: HTMLAttributes[\"class\"] }>(),\n {\n sideOffset: 4,\n },\n)\nconst emits = defineEmits<DropdownMenuContentEmits>()\n\nconst delegatedProps = reactiveOmit(props, \"class\")\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits)\n</script>\n\n<template>\n <DropdownMenuPortal>\n <DropdownMenuContent\n data-slot=\"dropdown-menu-content\"\n v-bind=\"{ ...$attrs, ...forwarded }\"\n :class=\"cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md', props.class)\"\n >\n <slot />\n </DropdownMenuContent>\n </DropdownMenuPortal>\n</template>\n",
49
+ "type": "registry:ui",
50
+ "target": "components/ui/dropdown-menu/DropdownMenuContent.vue"
51
+ },
52
+ {
53
+ "path": "src/components/ui/dropdown-menu/DropdownMenuGroup.vue",
54
+ "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuGroupProps } from \"reka-ui\"\nimport { DropdownMenuGroup } from \"reka-ui\"\n\nconst props = defineProps<DropdownMenuGroupProps>()\n</script>\n\n<template>\n <DropdownMenuGroup\n data-slot=\"dropdown-menu-group\"\n v-bind=\"props\"\n >\n <slot />\n </DropdownMenuGroup>\n</template>\n",
55
+ "type": "registry:ui",
56
+ "target": "components/ui/dropdown-menu/DropdownMenuGroup.vue"
57
+ },
58
+ {
59
+ "path": "src/components/ui/dropdown-menu/DropdownMenuItem.vue",
60
+ "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuItemProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport { DropdownMenuItem, useForwardProps } from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = withDefaults(defineProps<DropdownMenuItemProps & {\n class?: HTMLAttributes[\"class\"]\n inset?: boolean\n variant?: \"default\" | \"destructive\"\n}>(), {\n variant: \"default\",\n})\n\nconst delegatedProps = reactiveOmit(props, \"inset\", \"variant\", \"class\")\n\nconst forwardedProps = useForwardProps(delegatedProps)\n</script>\n\n<template>\n <DropdownMenuItem\n data-slot=\"dropdown-menu-item\"\n :data-inset=\"inset ? '' : undefined\"\n :data-variant=\"variant\"\n v-bind=\"forwardedProps\"\n :class=\"cn('focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=text-])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4', props.class)\"\n >\n <slot />\n </DropdownMenuItem>\n</template>\n",
61
+ "type": "registry:ui",
62
+ "target": "components/ui/dropdown-menu/DropdownMenuItem.vue"
63
+ },
64
+ {
65
+ "path": "src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue",
66
+ "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { Check } from \"@lucide/vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport {\n DropdownMenuCheckboxItem,\n DropdownMenuItemIndicator,\n useForwardPropsEmits,\n} from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes[\"class\"] }>()\nconst emits = defineEmits<DropdownMenuCheckboxItemEmits>()\n\nconst delegatedProps = reactiveOmit(props, \"class\")\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits)\n</script>\n\n<template>\n <DropdownMenuCheckboxItem\n data-slot=\"dropdown-menu-checkbox-item\"\n v-bind=\"forwarded\"\n :class=\"cn(\n 'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',\n props.class,\n )\"\n >\n <span class=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <DropdownMenuItemIndicator>\n <slot name=\"indicator-icon\">\n <Check class=\"size-4\" />\n </slot>\n </DropdownMenuItemIndicator>\n </span>\n <slot />\n </DropdownMenuCheckboxItem>\n</template>\n",
67
+ "type": "registry:ui",
68
+ "target": "components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue"
69
+ },
70
+ {
71
+ "path": "src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue",
72
+ "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from \"reka-ui\"\nimport {\n DropdownMenuRadioGroup,\n useForwardPropsEmits,\n} from \"reka-ui\"\n\nconst props = defineProps<DropdownMenuRadioGroupProps>()\nconst emits = defineEmits<DropdownMenuRadioGroupEmits>()\n\nconst forwarded = useForwardPropsEmits(props, emits)\n</script>\n\n<template>\n <DropdownMenuRadioGroup\n data-slot=\"dropdown-menu-radio-group\"\n v-bind=\"forwarded\"\n >\n <slot />\n </DropdownMenuRadioGroup>\n</template>\n",
73
+ "type": "registry:ui",
74
+ "target": "components/ui/dropdown-menu/DropdownMenuRadioGroup.vue"
75
+ },
76
+ {
77
+ "path": "src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue",
78
+ "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { Circle } from \"@lucide/vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport {\n DropdownMenuItemIndicator,\n DropdownMenuRadioItem,\n useForwardPropsEmits,\n} from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes[\"class\"] }>()\n\nconst emits = defineEmits<DropdownMenuRadioItemEmits>()\n\nconst delegatedProps = reactiveOmit(props, \"class\")\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits)\n</script>\n\n<template>\n <DropdownMenuRadioItem\n data-slot=\"dropdown-menu-radio-item\"\n v-bind=\"forwarded\"\n :class=\"cn(\n 'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',\n props.class,\n )\"\n >\n <span class=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <DropdownMenuItemIndicator>\n <slot name=\"indicator-icon\">\n <Circle class=\"size-2 fill-current\" />\n </slot>\n </DropdownMenuItemIndicator>\n </span>\n <slot />\n </DropdownMenuRadioItem>\n</template>\n",
79
+ "type": "registry:ui",
80
+ "target": "components/ui/dropdown-menu/DropdownMenuRadioItem.vue"
81
+ },
82
+ {
83
+ "path": "src/components/ui/dropdown-menu/DropdownMenuLabel.vue",
84
+ "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuLabelProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport { DropdownMenuLabel, useForwardProps } from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes[\"class\"], inset?: boolean }>()\n\nconst delegatedProps = reactiveOmit(props, \"class\", \"inset\")\nconst forwardedProps = useForwardProps(delegatedProps)\n</script>\n\n<template>\n <DropdownMenuLabel\n data-slot=\"dropdown-menu-label\"\n :data-inset=\"inset ? '' : undefined\"\n v-bind=\"forwardedProps\"\n :class=\"cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', props.class)\"\n >\n <slot />\n </DropdownMenuLabel>\n</template>\n",
85
+ "type": "registry:ui",
86
+ "target": "components/ui/dropdown-menu/DropdownMenuLabel.vue"
87
+ },
88
+ {
89
+ "path": "src/components/ui/dropdown-menu/DropdownMenuSeparator.vue",
90
+ "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuSeparatorProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport {\n DropdownMenuSeparator,\n} from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<DropdownMenuSeparatorProps & {\n class?: HTMLAttributes[\"class\"]\n}>()\n\nconst delegatedProps = reactiveOmit(props, \"class\")\n</script>\n\n<template>\n <DropdownMenuSeparator\n data-slot=\"dropdown-menu-separator\"\n v-bind=\"delegatedProps\"\n :class=\"cn('bg-border -mx-1 my-1 h-px', props.class)\"\n />\n</template>\n",
91
+ "type": "registry:ui",
92
+ "target": "components/ui/dropdown-menu/DropdownMenuSeparator.vue"
93
+ },
94
+ {
95
+ "path": "src/components/ui/dropdown-menu/DropdownMenuShortcut.vue",
96
+ "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from \"vue\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<{\n class?: HTMLAttributes[\"class\"]\n}>()\n</script>\n\n<template>\n <span\n data-slot=\"dropdown-menu-shortcut\"\n :class=\"cn('text-muted-foreground ml-auto text-xs tracking-widest', props.class)\"\n >\n <slot />\n </span>\n</template>\n",
97
+ "type": "registry:ui",
98
+ "target": "components/ui/dropdown-menu/DropdownMenuShortcut.vue"
99
+ },
100
+ {
101
+ "path": "src/components/ui/dropdown-menu/DropdownMenuSub.vue",
102
+ "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuSubEmits, DropdownMenuSubProps } from \"reka-ui\"\nimport {\n DropdownMenuSub,\n useForwardPropsEmits,\n} from \"reka-ui\"\n\nconst props = defineProps<DropdownMenuSubProps>()\nconst emits = defineEmits<DropdownMenuSubEmits>()\n\nconst forwarded = useForwardPropsEmits(props, emits)\n</script>\n\n<template>\n <DropdownMenuSub data-slot=\"dropdown-menu-sub\" v-bind=\"forwarded\">\n <slot />\n </DropdownMenuSub>\n</template>\n",
103
+ "type": "registry:ui",
104
+ "target": "components/ui/dropdown-menu/DropdownMenuSub.vue"
105
+ },
106
+ {
107
+ "path": "src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue",
108
+ "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuSubTriggerProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { ChevronRight } from \"@lucide/vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport {\n DropdownMenuSubTrigger,\n useForwardProps,\n} from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes[\"class\"], inset?: boolean }>()\n\nconst delegatedProps = reactiveOmit(props, \"class\", \"inset\")\nconst forwardedProps = useForwardProps(delegatedProps)\n</script>\n\n<template>\n <DropdownMenuSubTrigger\n data-slot=\"dropdown-menu-sub-trigger\"\n v-bind=\"forwardedProps\"\n :data-inset=\"inset ? '' : undefined\"\n :class=\"cn(\n 'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4 data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*=text-])]:text-muted-foreground',\n props.class,\n )\"\n >\n <slot />\n <ChevronRight class=\"ml-auto size-4\" />\n </DropdownMenuSubTrigger>\n</template>\n",
109
+ "type": "registry:ui",
110
+ "target": "components/ui/dropdown-menu/DropdownMenuSubTrigger.vue"
111
+ },
112
+ {
113
+ "path": "src/components/ui/dropdown-menu/DropdownMenuSubContent.vue",
114
+ "content": "<script setup lang=\"ts\">\nimport type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport {\n DropdownMenuSubContent,\n useForwardPropsEmits,\n} from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes[\"class\"] }>()\nconst emits = defineEmits<DropdownMenuSubContentEmits>()\n\nconst delegatedProps = reactiveOmit(props, \"class\")\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits)\n</script>\n\n<template>\n <DropdownMenuSubContent\n data-slot=\"dropdown-menu-sub-content\"\n v-bind=\"forwarded\"\n :class=\"cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)\"\n >\n <slot />\n </DropdownMenuSubContent>\n</template>\n",
115
+ "type": "registry:ui",
116
+ "target": "components/ui/dropdown-menu/DropdownMenuSubContent.vue"
117
+ },
118
+ {
119
+ "path": "src/components/ui/dropdown-menu/index.ts",
120
+ "content": "export { default as DropdownMenu } from \"./DropdownMenu.vue\"\n\nexport { default as DropdownMenuCheckboxItem } from \"./DropdownMenuCheckboxItem.vue\"\nexport { default as DropdownMenuContent } from \"./DropdownMenuContent.vue\"\nexport { default as DropdownMenuGroup } from \"./DropdownMenuGroup.vue\"\nexport { default as DropdownMenuItem } from \"./DropdownMenuItem.vue\"\nexport { default as DropdownMenuLabel } from \"./DropdownMenuLabel.vue\"\nexport { default as DropdownMenuRadioGroup } from \"./DropdownMenuRadioGroup.vue\"\nexport { default as DropdownMenuRadioItem } from \"./DropdownMenuRadioItem.vue\"\nexport { default as DropdownMenuSeparator } from \"./DropdownMenuSeparator.vue\"\nexport { default as DropdownMenuShortcut } from \"./DropdownMenuShortcut.vue\"\nexport { default as DropdownMenuSub } from \"./DropdownMenuSub.vue\"\nexport { default as DropdownMenuSubContent } from \"./DropdownMenuSubContent.vue\"\nexport { default as DropdownMenuSubTrigger } from \"./DropdownMenuSubTrigger.vue\"\nexport { default as DropdownMenuTrigger } from \"./DropdownMenuTrigger.vue\"\nexport { DropdownMenuPortal } from \"reka-ui\"\n",
121
+ "type": "registry:ui",
122
+ "target": "components/ui/dropdown-menu/index.ts"
123
+ },
124
+ {
125
+ "path": "src/components/ui/chat/ChatAttachments.vue",
126
+ "content": "<script setup lang=\"ts\">\nimport type { Component, HTMLAttributes } from \"vue\"\nimport {\n AudioLinesIcon,\n FileArchiveIcon,\n FileCodeIcon,\n FileIcon,\n FileImageIcon,\n FileSpreadsheetIcon,\n FileTextIcon,\n FileVideoCameraIcon,\n} from \"@lucide/vue\"\nimport { cn } from \"@/lib/utils\"\nimport type { ChatAttachment, ChatSide } from \"./types\"\n\ntype ChatFileKind =\n | \"archive\"\n | \"audio\"\n | \"code\"\n | \"document\"\n | \"image\"\n | \"spreadsheet\"\n | \"unknown\"\n | \"video\"\n\ninterface ChatAttachmentsProps {\n attachments: ChatAttachment[]\n class?: HTMLAttributes[\"class\"]\n side?: ChatSide\n}\n\nconst props = withDefaults(defineProps<ChatAttachmentsProps>(), {\n side: \"left\",\n})\n\nconst codeExtensions = new Set([\n \"astro\",\n \"c\",\n \"cpp\",\n \"css\",\n \"go\",\n \"html\",\n \"java\",\n \"js\",\n \"jsx\",\n \"json\",\n \"kt\",\n \"mdx\",\n \"php\",\n \"py\",\n \"rb\",\n \"rs\",\n \"scss\",\n \"sh\",\n \"swift\",\n \"ts\",\n \"tsx\",\n \"vue\",\n \"xml\",\n \"yaml\",\n \"yml\",\n])\nconst documentExtensions = new Set([\n \"doc\",\n \"docx\",\n \"md\",\n \"pdf\",\n \"rtf\",\n \"txt\",\n])\nconst imageExtensions = new Set([\n \"avif\",\n \"gif\",\n \"heic\",\n \"jpeg\",\n \"jpg\",\n \"png\",\n \"svg\",\n \"webp\",\n])\nconst spreadsheetExtensions = new Set([\n \"csv\",\n \"numbers\",\n \"ods\",\n \"tsv\",\n \"xls\",\n \"xlsx\",\n])\nconst archiveExtensions = new Set([\n \"7z\",\n \"gz\",\n \"rar\",\n \"tar\",\n \"tgz\",\n \"zip\",\n])\nconst audioExtensions = new Set([\n \"aac\",\n \"flac\",\n \"m4a\",\n \"mp3\",\n \"ogg\",\n \"wav\",\n])\nconst videoExtensions = new Set([\n \"avi\",\n \"m4v\",\n \"mkv\",\n \"mov\",\n \"mp4\",\n \"webm\",\n])\n\nfunction attachmentKey(attachment: ChatAttachment, index: number) {\n return attachment.id ?? `${attachment.name}-${index}`\n}\n\nfunction normalizeExtension(extension?: string) {\n return extension?.replace(/^\\./, \"\").trim().toLowerCase() || undefined\n}\n\nfunction extensionFromName(name: string) {\n const cleanName = name.split(/[?#]/)[0] ?? name\n const lastDotIndex = cleanName.lastIndexOf(\".\")\n\n if (lastDotIndex <= 0 || lastDotIndex === cleanName.length - 1) {\n return undefined\n }\n\n return normalizeExtension(cleanName.slice(lastDotIndex + 1))\n}\n\nfunction fileExtension(attachment: ChatAttachment) {\n return normalizeExtension(attachment.extension) ??\n extensionFromName(attachment.name) ??\n extensionFromName(attachment.url ?? \"\")\n}\n\nfunction fileKind(attachment: ChatAttachment): ChatFileKind {\n const extension = fileExtension(attachment)\n\n if (extension) {\n if (codeExtensions.has(extension)) return \"code\"\n if (documentExtensions.has(extension)) return \"document\"\n if (imageExtensions.has(extension)) return \"image\"\n if (spreadsheetExtensions.has(extension)) return \"spreadsheet\"\n if (archiveExtensions.has(extension)) return \"archive\"\n if (audioExtensions.has(extension)) return \"audio\"\n if (videoExtensions.has(extension)) return \"video\"\n }\n\n const type = attachment.type?.toLowerCase()\n\n if (!type) return \"unknown\"\n if (type.includes(\"image\")) return \"image\"\n if (type.includes(\"video\")) return \"video\"\n if (type.includes(\"audio\")) return \"audio\"\n if (type.includes(\"zip\") || type.includes(\"archive\")) return \"archive\"\n if (type.includes(\"csv\") || type.includes(\"sheet\") || type.includes(\"excel\")) {\n return \"spreadsheet\"\n }\n if (\n type.includes(\"code\") ||\n type.includes(\"javascript\") ||\n type.includes(\"json\") ||\n type.includes(\"typescript\")\n ) {\n return \"code\"\n }\n if (type.includes(\"pdf\") || type.includes(\"text\") || type.includes(\"document\")) {\n return \"document\"\n }\n\n return \"unknown\"\n}\n\nfunction attachmentIcon(attachment: ChatAttachment): Component {\n const iconByKind: Record<ChatFileKind, Component> = {\n archive: FileArchiveIcon,\n audio: AudioLinesIcon,\n code: FileCodeIcon,\n document: FileTextIcon,\n image: FileImageIcon,\n spreadsheet: FileSpreadsheetIcon,\n unknown: FileIcon,\n video: FileVideoCameraIcon,\n }\n\n return iconByKind[fileKind(attachment)]\n}\n\nfunction attachmentTypeLabel(attachment: ChatAttachment) {\n return attachment.type ?? fileExtension(attachment)?.toUpperCase()\n}\n\nfunction attachmentMeta(attachment: ChatAttachment) {\n return [attachmentTypeLabel(attachment), attachment.size]\n .filter(Boolean)\n .join(\" / \")\n}\n\nfunction attachmentLinkAttrs(attachment: ChatAttachment) {\n if (!attachment.url) return {}\n\n return {\n href: attachment.url,\n rel: \"noreferrer\",\n target: \"_blank\",\n }\n}\n</script>\n\n<template>\n <div\n v-if=\"props.attachments.length > 0\"\n data-slot=\"chat-attachments\"\n :data-side=\"props.side\"\n :class=\"\n cn(\n 'flex max-w-full flex-col gap-2',\n props.side === 'right' ? 'items-end' : 'items-start',\n props.class,\n )\n \"\n >\n <component\n :is=\"attachment.url ? 'a' : 'div'\"\n v-for=\"(attachment, index) in props.attachments\"\n :key=\"attachmentKey(attachment, index)\"\n v-bind=\"attachmentLinkAttrs(attachment)\"\n data-slot=\"chat-attachment\"\n :data-file-kind=\"fileKind(attachment)\"\n :data-file-extension=\"fileExtension(attachment)\"\n :title=\"attachment.name\"\n class=\"flex w-64 max-w-full min-w-0 items-center gap-3 rounded-lg bg-muted px-3 py-2 text-left text-xs text-foreground no-underline transition-colors hover:bg-muted/80 [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n >\n <span\n v-if=\"attachment.thumbnailUrl\"\n data-slot=\"chat-attachment-preview\"\n class=\"flex size-9 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground\"\n >\n <img\n :alt=\"attachment.alt ?? attachment.name\"\n class=\"size-full rounded-md object-cover\"\n :src=\"attachment.thumbnailUrl\"\n />\n </span>\n <span\n v-else\n data-slot=\"chat-attachment-icon\"\n class=\"flex size-9 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground\"\n >\n <component\n :is=\"attachmentIcon(attachment)\"\n aria-hidden=\"true\"\n />\n </span>\n <span\n data-slot=\"chat-attachment-body\"\n class=\"flex min-w-0 flex-col\"\n >\n <span\n data-slot=\"chat-attachment-name\"\n class=\"truncate font-medium\"\n >\n {{ attachment.name }}\n </span>\n <span\n v-if=\"attachmentMeta(attachment)\"\n data-slot=\"chat-attachment-meta\"\n class=\"truncate text-current/65\"\n >\n {{ attachmentMeta(attachment) }}\n </span>\n </span>\n </component>\n </div>\n</template>\n",
127
+ "type": "registry:ui",
128
+ "target": "components/ui/chat/ChatAttachments.vue"
129
+ },
130
+ {
131
+ "path": "src/components/ui/chat/ChatCodeBlock.vue",
132
+ "content": "<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from \"vue\"\nimport { CheckIcon, CopyIcon, Maximize2Icon, Minimize2Icon } from \"@lucide/vue\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\nimport {\n chatCodeBlockDefaults,\n type ChatCodeBlockEmits,\n type ChatCodeBlockProps,\n} from \"./code-block\"\nimport { codeLineNumberWidth, createCodeLines } from \"@/lib/code-highlight\"\n\nconst props = withDefaults(\n defineProps<ChatCodeBlockProps>(),\n chatCodeBlockDefaults,\n)\n\nconst emit = defineEmits<ChatCodeBlockEmits>()\n\nconst copied = ref(false)\nconst expanded = ref(false)\nlet copiedTimer: ReturnType<typeof setTimeout> | undefined\nlet previousBodyOverflow: string | undefined\n\nconst codeLines = computed(() => createCodeLines(\n props.code,\n props.language,\n props.highlight,\n))\nconst visibleLineCount = computed(() =>\n Math.max(1, Math.trunc(props.maxVisibleLines)),\n)\nconst shouldScroll = computed(() => codeLines.value.length > visibleLineCount.value)\nconst languageLabel = computed(() => props.language || props.label)\nconst codeLineStyle = computed(() => ({\n gridTemplateColumns: props.lineNumbers\n ? `${codeLineNumberWidth(codeLines.value.length)} minmax(0, 1fr)`\n : \"minmax(0, 1fr)\",\n}))\nconst collapsedScrollStyle = computed(() => shouldScroll.value\n ? {\n maxHeight: `${visibleLineCount.value * 1.5 + 2}rem`,\n }\n : undefined)\n\nasync function copyCode() {\n try {\n if (typeof navigator !== \"undefined\") {\n await navigator.clipboard?.writeText(props.code)\n }\n } catch {\n // Clipboard permissions vary by browser; the component still reports intent.\n }\n\n emit(\"copy\", props.code)\n copied.value = true\n\n if (copiedTimer) clearTimeout(copiedTimer)\n copiedTimer = setTimeout(() => {\n copied.value = false\n }, 1200)\n}\n\nfunction openExpanded() {\n expanded.value = true\n}\n\nfunction closeExpanded() {\n expanded.value = false\n}\n\nfunction handleKeydown(event: KeyboardEvent) {\n if (event.key === \"Escape\") closeExpanded()\n}\n\nwatch(expanded, (isExpanded) => {\n if (typeof document === \"undefined\" || typeof window === \"undefined\") return\n\n if (isExpanded) {\n previousBodyOverflow = document.body.style.overflow\n document.body.style.overflow = \"hidden\"\n window.addEventListener(\"keydown\", handleKeydown)\n return\n }\n\n document.body.style.overflow = previousBodyOverflow ?? \"\"\n previousBodyOverflow = undefined\n window.removeEventListener(\"keydown\", handleKeydown)\n})\n\nonBeforeUnmount(() => {\n if (copiedTimer) clearTimeout(copiedTimer)\n\n if (typeof document !== \"undefined\") {\n document.body.style.overflow = previousBodyOverflow ?? \"\"\n }\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"keydown\", handleKeydown)\n }\n})\n</script>\n\n<template>\n <div\n data-slot=\"chat-code-block\"\n :data-language=\"props.language\"\n :data-scrollable=\"shouldScroll ? '' : undefined\"\n :class=\"\n cn(\n 'relative w-full min-w-0 max-w-full overflow-hidden rounded-xl border font-mono text-[13px] leading-6 shadow-sm',\n props.class,\n )\n \"\n >\n <div\n data-slot=\"chat-code-toolbar\"\n class=\"absolute top-2 right-2 z-10 flex max-w-[calc(100%-1rem)] items-center gap-1 rounded-md border px-1 py-0.5\"\n >\n <span\n data-slot=\"chat-code-language\"\n class=\"max-w-20 truncate px-1 text-xs\"\n >\n {{ languageLabel }}\n </span>\n <Button\n data-slot=\"chat-code-copy\"\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n class=\"size-7 rounded-md\"\n :aria-label=\"copied ? props.copiedLabel : props.copyLabel\"\n :title=\"copied ? props.copiedLabel : props.copyLabel\"\n @click=\"copyCode\"\n >\n <CheckIcon v-if=\"copied\" />\n <CopyIcon v-else />\n </Button>\n <Button\n data-slot=\"chat-code-expand-trigger\"\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n class=\"size-7 rounded-md\"\n :aria-label=\"props.expandLabel\"\n :title=\"props.expandLabel\"\n @click=\"openExpanded\"\n >\n <Maximize2Icon />\n </Button>\n </div>\n\n <pre\n data-slot=\"chat-code-scroll\"\n :data-scrollable=\"shouldScroll ? '' : undefined\"\n :class=\"\n cn(\n 'm-0 overflow-x-auto text-[13px] leading-6 [tab-size:2]',\n shouldScroll ? 'overflow-y-auto' : 'overflow-y-hidden',\n )\n \"\n :style=\"collapsedScrollStyle\"\n ><code\n data-slot=\"chat-code\"\n :data-language=\"props.language\"\n class=\"block min-w-max font-mono whitespace-pre\"\n ><span\n v-for=\"line in codeLines\"\n :key=\"line.number\"\n data-slot=\"chat-code-line\"\n :data-diff=\"line.diffType\"\n :data-line=\"line.number\"\n class=\"grid min-w-full w-max\"\n :style=\"codeLineStyle\"\n ><span\n v-if=\"props.lineNumbers\"\n data-slot=\"chat-code-line-number\"\n aria-hidden=\"true\"\n class=\"sticky left-0 w-full select-none border-r pl-2 pr-3 text-right tabular-nums\"\n >{{ line.number }}</span><span\n data-slot=\"chat-code-line-content\"\n class=\"block min-h-[1lh] pl-3 pr-4\"\n ><span\n v-if=\"line.isEmpty\"\n aria-hidden=\"true\"\n >&nbsp;</span><span\n v-else\n data-slot=\"chat-code-token\"\n v-html=\"line.html\"\n /></span></span></code></pre>\n </div>\n\n <Teleport to=\"body\">\n <div\n v-if=\"expanded\"\n data-slot=\"chat-code-dialog-overlay\"\n class=\"fixed inset-0 z-50 flex items-center justify-center p-4\"\n @click.self=\"closeExpanded\"\n >\n <section\n data-slot=\"chat-code-dialog\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"props.expandLabel\"\n class=\"grid w-full max-w-6xl grid-rows-[auto_minmax(0,1fr)] overflow-hidden rounded-2xl border shadow-2xl\"\n >\n <h2 class=\"sr-only\">\n {{ props.expandLabel }}\n </h2>\n <div\n data-slot=\"chat-code-dialog-header\"\n class=\"flex h-11 items-center justify-between gap-3 border-b px-4\"\n >\n <span\n data-slot=\"chat-code-language\"\n class=\"min-w-0 truncate text-xs\"\n >\n {{ languageLabel }}\n </span>\n <div class=\"flex shrink-0 items-center gap-1\">\n <Button\n data-slot=\"chat-code-copy\"\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n class=\"size-8 rounded-md\"\n :aria-label=\"copied ? props.copiedLabel : props.copyLabel\"\n :title=\"copied ? props.copiedLabel : props.copyLabel\"\n @click=\"copyCode\"\n >\n <CheckIcon v-if=\"copied\" />\n <CopyIcon v-else />\n </Button>\n <Button\n data-slot=\"chat-code-collapse-trigger\"\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n class=\"size-8 rounded-md\"\n :aria-label=\"props.closeLabel\"\n :title=\"props.closeLabel\"\n @click=\"closeExpanded\"\n >\n <Minimize2Icon />\n </Button>\n </div>\n </div>\n\n <pre\n data-slot=\"chat-code-scroll\"\n data-expanded\n class=\"m-0 max-h-[calc(90vh-4rem)] overflow-auto text-[13px] leading-6 [tab-size:2]\"\n ><code\n data-slot=\"chat-code\"\n :data-language=\"props.language\"\n class=\"block min-w-max font-mono whitespace-pre\"\n ><span\n v-for=\"line in codeLines\"\n :key=\"line.number\"\n data-slot=\"chat-code-line\"\n :data-diff=\"line.diffType\"\n :data-line=\"line.number\"\n class=\"grid min-w-full w-max\"\n :style=\"codeLineStyle\"\n ><span\n v-if=\"props.lineNumbers\"\n data-slot=\"chat-code-line-number\"\n aria-hidden=\"true\"\n class=\"sticky left-0 w-full select-none border-r pl-2 pr-3 text-right tabular-nums\"\n >{{ line.number }}</span><span\n data-slot=\"chat-code-line-content\"\n class=\"block min-h-[1lh] pl-3 pr-4\"\n ><span\n v-if=\"line.isEmpty\"\n aria-hidden=\"true\"\n >&nbsp;</span><span\n v-else\n data-slot=\"chat-code-token\"\n v-html=\"line.html\"\n /></span></span></code></pre>\n </section>\n </div>\n </Teleport>\n</template>\n",
133
+ "type": "registry:ui",
134
+ "target": "components/ui/chat/ChatCodeBlock.vue"
135
+ },
136
+ {
137
+ "path": "src/components/ui/chat/code-block.ts",
138
+ "content": "import type { HTMLAttributes } from \"vue\"\n\nexport interface ChatCodeBlockProps {\n class?: HTMLAttributes[\"class\"]\n closeLabel?: string\n code: string\n copiedLabel?: string\n copyLabel?: string\n expandLabel?: string\n highlight?: boolean\n label?: string\n language?: string\n lineNumbers?: boolean\n maxVisibleLines?: number\n}\n\nexport type ChatCodeBlockEmits = {\n copy: [code: string]\n}\n\nexport const chatCodeBlockDefaults = {\n closeLabel: \"닫기\",\n copiedLabel: \"복사됨\",\n copyLabel: \"코드 복사\",\n expandLabel: \"크게 보기\",\n highlight: true,\n label: \"Code\",\n lineNumbers: true,\n maxVisibleLines: 20,\n} satisfies Partial<ChatCodeBlockProps>\n",
139
+ "type": "registry:ui",
140
+ "target": "components/ui/chat/code-block.ts"
141
+ },
142
+ {
143
+ "path": "src/lib/code-highlight.ts",
144
+ "content": "import type { LanguageFn } from \"highlight.js\"\nimport hljs from \"highlight.js/lib/core\"\nimport bash from \"highlight.js/lib/languages/bash\"\nimport css from \"highlight.js/lib/languages/css\"\nimport diff from \"highlight.js/lib/languages/diff\"\nimport javascript from \"highlight.js/lib/languages/javascript\"\nimport json from \"highlight.js/lib/languages/json\"\nimport markdown from \"highlight.js/lib/languages/markdown\"\nimport python from \"highlight.js/lib/languages/python\"\nimport ruby from \"highlight.js/lib/languages/ruby\"\nimport scss from \"highlight.js/lib/languages/scss\"\nimport shell from \"highlight.js/lib/languages/shell\"\nimport typescript from \"highlight.js/lib/languages/typescript\"\nimport xml from \"highlight.js/lib/languages/xml\"\nimport yaml from \"highlight.js/lib/languages/yaml\"\n\nexport type CodeLine = {\n diffType?: \"addition\" | \"deletion\" | \"metadata\"\n html: string\n isEmpty: boolean\n number: number\n}\n\nconst highlighter = hljs.newInstance()\nconst registeredLanguages = [\n \"bash\",\n \"css\",\n \"diff\",\n \"javascript\",\n \"json\",\n \"markdown\",\n \"python\",\n \"ruby\",\n \"scss\",\n \"shell\",\n \"typescript\",\n \"xml\",\n \"yaml\",\n]\nconst languageAliases: Record<string, string> = {\n bash: \"bash\",\n cjs: \"javascript\",\n diff: \"diff\",\n patch: \"diff\",\n htm: \"xml\",\n html: \"xml\",\n js: \"javascript\",\n jsonc: \"json\",\n jsx: \"javascript\",\n md: \"markdown\",\n mjs: \"javascript\",\n py: \"python\",\n rb: \"ruby\",\n sh: \"bash\",\n shell: \"shell\",\n ts: \"typescript\",\n tsx: \"typescript\",\n vue: \"xml\",\n xml: \"xml\",\n yml: \"yaml\",\n}\n\nregisterLanguage(\"bash\", bash)\nregisterLanguage(\"css\", css)\nregisterLanguage(\"diff\", diff)\nregisterLanguage(\"javascript\", javascript)\nregisterLanguage(\"json\", json)\nregisterLanguage(\"markdown\", markdown)\nregisterLanguage(\"python\", python)\nregisterLanguage(\"ruby\", ruby)\nregisterLanguage(\"scss\", scss)\nregisterLanguage(\"shell\", shell)\nregisterLanguage(\"typescript\", typescript)\nregisterLanguage(\"xml\", xml)\nregisterLanguage(\"yaml\", yaml)\n\nhighlighter.registerAliases([\"htm\", \"html\", \"vue\"], { languageName: \"xml\" })\nhighlighter.registerAliases([\"patch\"], { languageName: \"diff\" })\nhighlighter.registerAliases([\"js\", \"jsx\", \"mjs\", \"cjs\"], { languageName: \"javascript\" })\nhighlighter.registerAliases([\"ts\", \"tsx\"], { languageName: \"typescript\" })\nhighlighter.registerAliases([\"md\"], { languageName: \"markdown\" })\nhighlighter.registerAliases([\"py\"], { languageName: \"python\" })\nhighlighter.registerAliases([\"rb\"], { languageName: \"ruby\" })\nhighlighter.registerAliases([\"sh\"], { languageName: \"bash\" })\nhighlighter.registerAliases([\"yml\"], { languageName: \"yaml\" })\n\nexport function createCodeLines(\n code: string,\n language?: string,\n highlight = true,\n): CodeLine[] {\n const normalizedCode = normalizeCode(code)\n const normalizedLanguage = normalizeCodeLanguage(language)\n const sourceLines = splitTextLines(normalizedCode)\n const highlightedLines = splitHighlightedHtmlLines(\n highlight ? highlightCode(normalizedCode, normalizedLanguage) : escapeHtml(normalizedCode),\n )\n\n return sourceLines.map((line, index) => ({\n diffType: normalizedLanguage === \"diff\" ? diffLineType(line) : undefined,\n html: highlightedLines[index] ?? \"\",\n isEmpty: line.length === 0,\n number: index + 1,\n }))\n}\n\nexport function codeLineNumberWidth(lineCount: number) {\n const digitCount = Math.max(2, String(Math.max(1, lineCount)).length)\n\n return `calc(${digitCount}ch + 1.5rem)`\n}\n\nfunction registerLanguage(language: string, defineLanguage: LanguageFn) {\n highlighter.registerLanguage(language, defineLanguage)\n}\n\nfunction highlightCode(code: string, language?: string) {\n const normalizedLanguage = normalizeCodeLanguage(language)\n\n try {\n if (normalizedLanguage && highlighter.getLanguage(normalizedLanguage)) {\n return highlighter.highlight(code, {\n ignoreIllegals: true,\n language: normalizedLanguage,\n }).value\n }\n\n return highlighter.highlightAuto(code, registeredLanguages).value\n } catch {\n return escapeHtml(code)\n }\n}\n\nexport function normalizeCodeLanguage(language?: string) {\n const normalized = language?.trim().toLowerCase()\n\n if (!normalized) return undefined\n\n return languageAliases[normalized] ?? normalized\n}\n\nfunction diffLineType(line: string): CodeLine[\"diffType\"] {\n if (line.startsWith(\"+\") && !line.startsWith(\"+++\")) return \"addition\"\n if (line.startsWith(\"-\") && !line.startsWith(\"---\")) return \"deletion\"\n if (\n line.startsWith(\"@@\") ||\n line.startsWith(\"diff \") ||\n line.startsWith(\"index \") ||\n line.startsWith(\"---\") ||\n line.startsWith(\"+++\")\n ) {\n return \"metadata\"\n }\n\n return undefined\n}\n\nexport function normalizeCode(code: string) {\n return code.replace(/\\r\\n?/g, \"\\n\")\n}\n\nexport function splitTextLines(text: string) {\n return text.length > 0 ? text.split(\"\\n\") : [\"\"]\n}\n\nfunction splitHighlightedHtmlLines(html: string) {\n const lines = [\"\"]\n const openTags: string[] = []\n const spanTagPattern = /<\\/?span\\b[^>]*>/gi\n let index = 0\n\n for (const match of html.matchAll(spanTagPattern)) {\n appendHtmlText(lines, openTags, html.slice(index, match.index))\n appendSpanTag(lines, openTags, match[0])\n index = match.index + match[0].length\n }\n\n appendHtmlText(lines, openTags, html.slice(index))\n\n return lines\n}\n\nfunction appendHtmlText(lines: string[], openTags: string[], text: string) {\n const parts = text.split(\"\\n\")\n\n for (const [index, part] of parts.entries()) {\n if (index > 0) {\n closeOpenTags(lines, openTags)\n lines.push(openTags.join(\"\"))\n }\n\n lines[lines.length - 1] += part\n }\n}\n\nfunction appendSpanTag(lines: string[], openTags: string[], tag: string) {\n lines[lines.length - 1] += tag\n\n if (tag.startsWith(\"</\")) {\n openTags.pop()\n return\n }\n\n openTags.push(tag)\n}\n\nfunction closeOpenTags(lines: string[], openTags: string[]) {\n for (let index = openTags.length - 1; index >= 0; index -= 1) {\n lines[lines.length - 1] += \"</span>\"\n }\n}\n\nfunction escapeHtml(text: string) {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#x27;\")\n}\n",
145
+ "type": "registry:lib",
146
+ "target": "lib/code-highlight.ts"
147
+ },
148
+ {
149
+ "path": "src/components/ui/chat/Chat.vue",
150
+ "content": "<script setup lang=\"ts\">\nimport {\n computed,\n nextTick,\n onBeforeUnmount,\n onMounted,\n ref,\n watch,\n} from \"vue\"\nimport type {\n Component,\n ComponentPublicInstance,\n HTMLAttributes,\n StyleValue,\n} from \"vue\"\nimport {\n BookmarkIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n CopyIcon,\n DownloadIcon,\n GitBranchIcon,\n MoreHorizontalIcon,\n PencilIcon,\n RotateCcwIcon,\n} from \"@lucide/vue\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { cn } from \"@/lib/utils\"\nimport ChatAttachments from \"./ChatAttachments.vue\"\nimport ChatCodeBlock from \"./ChatCodeBlock.vue\"\nimport type {\n ChatActionKey,\n ChatActionVisibility,\n ChatAttachment,\n ChatSide,\n ChatStatus,\n ChatVariant,\n} from \"./types\"\n\nexport type ChatProfile =\n | string\n | {\n alt?: string\n label?: string\n src?: string\n }\n\nexport type ChatMessage =\n | string\n | {\n actionsVisibility?: ChatActionVisibility\n attachments?: ChatAttachment[]\n content: string\n id?: number | string\n name?: string\n profile?: ChatProfile\n regenerate?: boolean\n regenerating?: boolean\n status?: ChatStatus\n version?: number\n versions?: string[]\n }\n\nexport interface ChatResolvedMessage {\n actionsVisibility: ChatActionVisibility\n attachments: ChatAttachment[]\n content: string\n id: number | string\n key: string\n name?: string\n profile?: ChatProfile\n regenerate: boolean\n regenerating: boolean\n side: ChatSide\n status: ChatStatus\n version: number\n versionCount: number\n versions: string[]\n variant: ChatVariant\n}\n\nexport interface ChatActionSlotProps {\n message: ChatResolvedMessage\n side: ChatSide\n status: ChatStatus\n}\n\nexport interface ChatAttachmentSlotProps extends ChatActionSlotProps {\n attachments: ChatAttachment[]\n}\n\nexport interface ChatContentSlotProps extends ChatActionSlotProps {\n attachments: ChatAttachment[]\n content: string\n isStreaming: boolean\n}\n\nexport interface ChatMessageSlotProps extends ChatContentSlotProps {\n canCollapse: boolean\n isCollapsed: boolean\n toggleExpanded: () => void\n}\n\ntype ChatActionSlotName = \"left-actions\" | \"message-actions\" | \"right-actions\"\ntype ChatContentPart =\n | {\n content: string\n key: string\n type: \"text\"\n }\n | {\n content: string\n key: string\n language?: string\n type: \"code\"\n }\ntype ChatSingleActionKey = Exclude<ChatActionKey, \"pagination\">\n\nconst actionIcons: Record<ChatSingleActionKey, Component> = {\n bookmark: BookmarkIcon,\n branch: GitBranchIcon,\n copy: CopyIcon,\n download: DownloadIcon,\n edit: PencilIcon,\n regenerate: RotateCcwIcon,\n}\n\nconst props = withDefaults(\n defineProps<{\n actionItems?: ChatActionKey[]\n actionMenuThreshold?: number\n actionsLabel?: string\n ariaLive?: \"assertive\" | \"off\" | \"polite\"\n ariaLabel?: string\n autoScroll?: boolean\n autoScrollBehavior?: ScrollBehavior\n bookmarkAction?: boolean\n bookmarkLabel?: string\n branchAction?: boolean\n branchLabel?: string\n class?: HTMLAttributes[\"class\"]\n collapseLabel?: string\n collapsible?: boolean\n codeBlockCopiedLabel?: string\n codeBlockCloseLabel?: string\n codeBlockCopyLabel?: string\n codeBlockExpandLabel?: string\n codeBlockHighlight?: boolean\n codeBlockLabel?: string\n codeBlockLineNumbers?: boolean\n codeBlockMaxVisibleLines?: number\n codeBlocks?: boolean\n copyAction?: boolean\n copyLabel?: string\n downloadAction?: boolean\n downloadLabel?: string\n editAction?: boolean\n editLabel?: string\n expandLabel?: string\n left?: ChatMessage | ChatMessage[]\n leftActionItems?: ChatActionKey[]\n leftName?: string\n leftProfile?: ChatProfile\n leftVariant?: ChatVariant\n moreActionsLabel?: string\n nextVersionLabel?: string\n paginationAction?: boolean\n previousVersionLabel?: string\n regenerateAction?: boolean\n regenerateLabel?: string\n regeneratingLabel?: string\n right?: ChatMessage | ChatMessage[]\n rightActionItems?: ChatActionKey[]\n rightName?: string\n rightProfile?: ChatProfile\n rightVariant?: ChatVariant\n }>(),\n {\n actionItems: () => [\"pagination\", \"regenerate\"],\n actionMenuThreshold: 3,\n actionsLabel: \"Message actions\",\n ariaLive: \"polite\",\n ariaLabel: \"Chat messages\",\n autoScroll: false,\n autoScrollBehavior: \"smooth\",\n bookmarkLabel: \"북마크\",\n branchLabel: \"브랜치\",\n collapseLabel: \"간단히\",\n collapsible: true,\n codeBlockCopiedLabel: \"복사됨\",\n codeBlockCloseLabel: \"닫기\",\n codeBlockCopyLabel: \"코드 복사\",\n codeBlockExpandLabel: \"크게 보기\",\n codeBlockHighlight: true,\n codeBlockLabel: \"Code\",\n codeBlockLineNumbers: true,\n codeBlockMaxVisibleLines: 20,\n codeBlocks: true,\n copyLabel: \"복사\",\n downloadLabel: \"다운로드\",\n editLabel: \"수정\",\n expandLabel: \"더보기\",\n leftVariant: \"bubble\",\n moreActionsLabel: \"더 많은 작업\",\n nextVersionLabel: \"다음 응답\",\n previousVersionLabel: \"이전 응답\",\n regenerateLabel: \"재생성\",\n regeneratingLabel: \"재생성 중\",\n rightVariant: \"bubble\",\n },\n)\n\nconst emit = defineEmits<{\n action: [action: ChatSingleActionKey, message: ChatResolvedMessage]\n bookmark: [message: ChatResolvedMessage]\n branch: [message: ChatResolvedMessage]\n codeCopy: [message: ChatResolvedMessage, code: string, language?: string]\n copy: [message: ChatResolvedMessage]\n download: [message: ChatResolvedMessage]\n edit: [message: ChatResolvedMessage]\n paginate: [message: ChatResolvedMessage, version: number]\n regenerate: [message: ChatResolvedMessage]\n}>()\n\nconst rootRef = ref<HTMLElement | null>(null)\nconst slots = defineSlots<{\n attachments?: (props: ChatAttachmentSlotProps) => unknown\n content?: (props: ChatContentSlotProps) => unknown\n \"left-actions\"?: (props: ChatActionSlotProps) => unknown\n message?: (props: ChatMessageSlotProps) => unknown\n \"message-actions\"?: (props: ChatActionSlotProps) => unknown\n \"right-actions\"?: (props: ChatActionSlotProps) => unknown\n}>()\nconst collapsedHeightByKey = ref<Record<string, number>>({})\nconst overflowByKey = ref<Record<string, boolean>>({})\nconst expandedKeys = ref(new Set<string>())\nconst expandedHeightByKey = ref<Record<string, number>>({})\nconst contentRefs = new Map<string, HTMLElement>()\nconst observedContentElements = new Set<HTMLElement>()\nlet resizeObserver: ResizeObserver | undefined\nlet pendingMeasure = false\nlet pendingAutoScroll = false\n\nfunction toMessageList(value?: ChatMessage | ChatMessage[]) {\n if (!value) return []\n return Array.isArray(value) ? value : [value]\n}\n\nfunction normalizeMessage(\n value: ChatMessage,\n side: ChatSide,\n index: number,\n): ChatResolvedMessage {\n const sideName = side === \"left\" ? props.leftName : props.rightName\n const sideProfile = side === \"left\" ? props.leftProfile : props.rightProfile\n const sideVariant = side === \"left\" ? props.leftVariant : props.rightVariant\n\n if (typeof value === \"string\") {\n return {\n actionsVisibility: \"always\",\n attachments: [],\n content: value,\n id: `${side}-${index}`,\n key: `${side}-${index}`,\n name: sideName,\n profile: sideProfile,\n regenerate: false,\n regenerating: false,\n side,\n status: \"complete\",\n version: 0,\n versionCount: 1,\n versions: [value],\n variant: sideVariant,\n }\n }\n\n const id = value.id ?? `${side}-${index}`\n const versions = normalizeVersions(value.content, value.versions)\n const version = normalizeVersion(value.version, versions.length)\n\n return {\n actionsVisibility: value.actionsVisibility ?? \"always\",\n attachments: value.attachments ?? [],\n content: versions[version] ?? value.content,\n id,\n key: `${side}-${id}`,\n name: value.name ?? sideName,\n profile: value.profile ?? sideProfile,\n regenerate: value.regenerate ?? false,\n regenerating: value.regenerating ?? false,\n side,\n status: normalizeStatus(value.status),\n version,\n versionCount: versions.length,\n versions,\n variant: sideVariant,\n }\n}\n\nfunction normalizeVersions(content: string, versions?: string[]) {\n if (!versions || versions.length === 0) return [content]\n\n return versions\n}\n\nfunction normalizeStatus(status: ChatStatus | undefined): ChatStatus {\n return status ?? \"complete\"\n}\n\nfunction normalizeVersion(version: number | undefined, versionCount: number) {\n const fallbackVersion = versionCount - 1\n const nextVersion = version ?? fallbackVersion\n\n if (!Number.isFinite(nextVersion)) return fallbackVersion\n\n return Math.min(Math.max(Math.trunc(nextVersion), 0), versionCount - 1)\n}\n\nconst messages = computed(() => {\n const leftMessages = toMessageList(props.left).map((message, index) =>\n normalizeMessage(message, \"left\", index),\n )\n const rightMessages = toMessageList(props.right).map((message, index) =>\n normalizeMessage(message, \"right\", index),\n )\n const rows: ChatResolvedMessage[] = []\n const maxLength = Math.max(leftMessages.length, rightMessages.length)\n\n for (let index = 0; index < maxLength; index += 1) {\n if (leftMessages[index]) rows.push(leftMessages[index])\n if (rightMessages[index]) rows.push(rightMessages[index])\n }\n\n return rows\n})\n\nfunction profileSrc(profile?: ChatProfile) {\n if (!profile || typeof profile === \"string\") return undefined\n return profile.src\n}\n\nfunction profileLabel(profile?: ChatProfile) {\n if (!profile) return undefined\n if (typeof profile === \"string\") return profile\n return profile.label\n}\n\nfunction profileAlt(message: ChatResolvedMessage) {\n if (!message.profile || typeof message.profile === \"string\") {\n return message.name ?? \"Profile\"\n }\n\n return message.profile.alt ?? message.name ?? \"Profile\"\n}\n\nfunction hasProfile(message: ChatResolvedMessage) {\n return Boolean(message.profile)\n}\n\nfunction canCollapse(message: ChatResolvedMessage) {\n return !isStreaming(message) &&\n props.collapsible &&\n !hasCodeBlock(message) &&\n Boolean(overflowByKey.value[message.key])\n}\n\nfunction isMessageCollapsed(message: ChatResolvedMessage) {\n return canCollapse(message) && !expandedKeys.value.has(message.key)\n}\n\nfunction toggleExpanded(key: string) {\n const nextExpandedKeys = new Set(expandedKeys.value)\n\n if (nextExpandedKeys.has(key)) {\n nextExpandedKeys.delete(key)\n } else {\n nextExpandedKeys.add(key)\n }\n\n expandedKeys.value = nextExpandedKeys\n}\n\nfunction setContentRef(\n key: string,\n element: ComponentPublicInstance | Element | null,\n) {\n const currentElement = contentRefs.get(key)\n\n if (!element) {\n if (currentElement) unobserveContentElement(currentElement)\n contentRefs.delete(key)\n return\n }\n\n if (element instanceof HTMLElement) {\n if (currentElement && currentElement !== element) {\n unobserveContentElement(currentElement)\n }\n\n contentRefs.set(key, element)\n observeContentElement(element)\n measureOverflowSoon()\n return\n }\n\n if (currentElement) unobserveContentElement(currentElement)\n contentRefs.delete(key)\n}\n\nfunction observeContentElement(element: HTMLElement) {\n if (!resizeObserver || observedContentElements.has(element)) return\n\n resizeObserver.observe(element)\n observedContentElements.add(element)\n}\n\nfunction unobserveContentElement(element: HTMLElement) {\n resizeObserver?.unobserve(element)\n observedContentElements.delete(element)\n}\n\nfunction getLineHeight(element: HTMLElement) {\n const styles = window.getComputedStyle(element)\n const lineHeight = Number.parseFloat(styles.lineHeight)\n\n if (Number.isFinite(lineHeight)) return lineHeight\n\n const fontSize = Number.parseFloat(styles.fontSize)\n\n return Number.isFinite(fontSize) ? fontSize * 1.5 : 21\n}\n\nfunction measureOverflow() {\n if (!props.collapsible) {\n if (Object.keys(overflowByKey.value).length > 0) {\n overflowByKey.value = {}\n }\n if (Object.keys(collapsedHeightByKey.value).length > 0) {\n collapsedHeightByKey.value = {}\n }\n if (Object.keys(expandedHeightByKey.value).length > 0) {\n expandedHeightByKey.value = {}\n }\n return\n }\n\n const nextCollapsedHeightByKey: Record<string, number> = {}\n const nextExpandedHeightByKey: Record<string, number> = {}\n const nextOverflowByKey: Record<string, boolean> = {}\n const activeKeys = new Set(messages.value.map((message) => message.key))\n\n for (const [key, element] of contentRefs) {\n if (!activeKeys.has(key)) {\n unobserveContentElement(element)\n contentRefs.delete(key)\n continue\n }\n\n const maxHeight = getLineHeight(element) * 4\n nextCollapsedHeightByKey[key] = maxHeight\n nextExpandedHeightByKey[key] = element.scrollHeight\n nextOverflowByKey[key] = element.scrollHeight > maxHeight + 1\n }\n\n if (!isSameNumberMap(collapsedHeightByKey.value, nextCollapsedHeightByKey)) {\n collapsedHeightByKey.value = nextCollapsedHeightByKey\n }\n if (!isSameNumberMap(expandedHeightByKey.value, nextExpandedHeightByKey)) {\n expandedHeightByKey.value = nextExpandedHeightByKey\n }\n if (!isSameOverflowMap(overflowByKey.value, nextOverflowByKey)) {\n overflowByKey.value = nextOverflowByKey\n }\n}\n\nfunction isSameNumberMap(\n current: Record<string, number>,\n next: Record<string, number>,\n) {\n const currentKeys = Object.keys(current)\n const nextKeys = Object.keys(next)\n\n if (currentKeys.length !== nextKeys.length) return false\n\n return nextKeys.every((key) => current[key] === next[key])\n}\n\nfunction isSameOverflowMap(\n current: Record<string, boolean>,\n next: Record<string, boolean>,\n) {\n const currentKeys = Object.keys(current)\n const nextKeys = Object.keys(next)\n\n if (currentKeys.length !== nextKeys.length) return false\n\n return nextKeys.every((key) => current[key] === next[key])\n}\n\nfunction measureOverflowSoon() {\n if (pendingMeasure) return\n\n pendingMeasure = true\n\n void nextTick(() => {\n pendingMeasure = false\n measureOverflow()\n })\n}\n\nfunction autoScrollSoon() {\n if (!props.autoScroll || !messages.value.some(isStreaming)) return\n if (pendingAutoScroll) return\n\n pendingAutoScroll = true\n\n const scroll = () => {\n pendingAutoScroll = false\n const targetElement = rootRef.value?.lastElementChild\n\n if (targetElement instanceof HTMLElement) {\n targetElement.scrollIntoView({\n block: \"end\",\n behavior: props.autoScrollBehavior,\n })\n }\n }\n\n if (typeof requestAnimationFrame === \"function\") {\n requestAnimationFrame(scroll)\n } else {\n window.setTimeout(scroll, 0)\n }\n}\n\nfunction messageRowClass(message: ChatResolvedMessage) {\n return cn(\n \"flex w-full items-start gap-2\",\n message.side === \"right\" &&\n message.variant === \"bubble\" &&\n \"justify-end\",\n message.actionsVisibility === \"hover\" && \"group/chat-actions\",\n )\n}\n\nfunction messageBodyClass(message: ChatResolvedMessage) {\n return cn(\n \"relative flex min-w-0 flex-col gap-1\",\n message.variant === \"plain\" || hasCodeBlock(message)\n ? \"w-full flex-1\"\n : \"max-w-[78%]\",\n message.side === \"right\" ? \"order-1 items-end\" : \"items-start\",\n )\n}\n\nfunction bubbleClass(message: ChatResolvedMessage) {\n return cn(\n \"relative min-w-0 max-w-full text-sm leading-relaxed break-words\",\n message.variant === \"plain\"\n ? \"w-full text-foreground\"\n : \"rounded-2xl bg-secondary px-4 py-2 text-secondary-foreground\",\n message.variant === \"bubble\" &&\n message.side === \"right\" &&\n \"bg-primary text-primary-foreground\",\n )\n}\n\nfunction fadeClass(message: ChatResolvedMessage) {\n return cn(\n \"absolute inset-x-0 bottom-0 flex h-16 items-end justify-center pb-2 transition-opacity duration-200 ease-out\",\n message.variant === \"plain\"\n ? \"bg-gradient-to-b from-transparent via-white/90 to-white\"\n : message.side === \"right\"\n ? \"bg-gradient-to-b from-transparent via-primary/90 to-primary\"\n : \"bg-gradient-to-b from-transparent via-secondary/90 to-secondary\",\n )\n}\n\nfunction contentClass(message: ChatResolvedMessage) {\n return cn(\n \"overflow-hidden transition-[max-height] duration-300 ease-in-out motion-reduce:transition-none\",\n !canCollapse(message) && \"overflow-visible\",\n )\n}\n\nfunction contentStyle(message: ChatResolvedMessage): StyleValue | undefined {\n if (!canCollapse(message)) return undefined\n\n const height = isMessageCollapsed(message)\n ? collapsedHeightByKey.value[message.key]\n : expandedHeightByKey.value[message.key]\n\n if (!height) return undefined\n\n return {\n maxHeight: `${height}px`,\n }\n}\n\nfunction toggleButtonLabel(message: ChatResolvedMessage) {\n return isMessageCollapsed(message) ? props.expandLabel : props.collapseLabel\n}\n\nfunction toggleArrow(message: ChatResolvedMessage) {\n return isMessageCollapsed(message) ? \"↓\" : \"↑\"\n}\n\nfunction toggleButtonClass(message: ChatResolvedMessage) {\n return cn(\n \"inline-flex items-center gap-1 border-0 bg-transparent p-0 text-xs font-medium shadow-none transition-colors hover:underline focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\",\n message.variant === \"plain\"\n ? \"text-muted-foreground hover:text-foreground\"\n : message.side === \"right\"\n ? \"text-primary-foreground/85 hover:text-primary-foreground\"\n : \"text-secondary-foreground/70 hover:text-secondary-foreground\",\n !isMessageCollapsed(message) && \"mt-2\",\n )\n}\n\nfunction isStreaming(message: ChatResolvedMessage) {\n return message.status === \"streaming\"\n}\n\nfunction streamingIndicatorClass(message: ChatResolvedMessage) {\n return cn(\n \"mt-2 inline-flex items-center text-current/70\",\n message.variant === \"plain\" && \"text-muted-foreground\",\n )\n}\n\nfunction actionSlotProps(message: ChatResolvedMessage): ChatActionSlotProps {\n return {\n message,\n side: message.side,\n status: message.status,\n }\n}\n\nfunction attachmentSlotProps(message: ChatResolvedMessage): ChatAttachmentSlotProps {\n return {\n ...actionSlotProps(message),\n attachments: message.attachments,\n }\n}\n\nfunction contentSlotProps(message: ChatResolvedMessage): ChatContentSlotProps {\n return {\n ...actionSlotProps(message),\n attachments: message.attachments,\n content: message.content,\n isStreaming: isStreaming(message),\n }\n}\n\nfunction messageSlotProps(message: ChatResolvedMessage): ChatMessageSlotProps {\n return {\n ...contentSlotProps(message),\n canCollapse: canCollapse(message),\n isCollapsed: isMessageCollapsed(message),\n toggleExpanded: () => toggleExpanded(message.key),\n }\n}\n\nfunction shouldRenderCodeBlocks(message: ChatResolvedMessage) {\n return props.codeBlocks && message.side === \"left\"\n}\n\nfunction hasCodeBlock(message: ChatResolvedMessage) {\n return shouldRenderCodeBlocks(message) &&\n /```([^\\n\\r`]*)\\r?\\n?([\\s\\S]*?)```/.test(message.content)\n}\n\nfunction contentParts(message: ChatResolvedMessage): ChatContentPart[] {\n if (!shouldRenderCodeBlocks(message)) {\n return [\n {\n content: message.content,\n key: `${message.key}-text`,\n type: \"text\",\n },\n ]\n }\n\n const parts: ChatContentPart[] = []\n const fencePattern = /```([^\\n\\r`]*)\\r?\\n?([\\s\\S]*?)```/g\n let lastIndex = 0\n let index = 0\n let match: RegExpExecArray | null\n\n const pushText = (content: string) => {\n const normalizedContent = content.replace(/^(?:\\r?\\n)+|(?:\\r?\\n)+$/g, \"\")\n\n if (!normalizedContent.trim()) return\n\n parts.push({\n content: normalizedContent,\n key: `${message.key}-text-${index}`,\n type: \"text\",\n })\n index += 1\n }\n\n while ((match = fencePattern.exec(message.content)) !== null) {\n pushText(message.content.slice(lastIndex, match.index))\n\n const language = normalizeCodeLanguage(match[1])\n const code = (match[2] ?? \"\").replace(/^(?:\\r?\\n)+|(?:\\r?\\n)+$/g, \"\")\n\n parts.push({\n content: code,\n key: `${message.key}-code-${index}`,\n language,\n type: \"code\",\n })\n index += 1\n lastIndex = fencePattern.lastIndex\n }\n\n pushText(message.content.slice(lastIndex))\n\n if (parts.length === 0) {\n return [\n {\n content: message.content,\n key: `${message.key}-text`,\n type: \"text\",\n },\n ]\n }\n\n return parts\n}\n\nfunction normalizeCodeLanguage(info?: string) {\n return info?.trim().split(/\\s+/)[0] || undefined\n}\n\nfunction handleCodeCopy(message: ChatResolvedMessage, part: ChatContentPart) {\n if (part.type !== \"code\") return\n\n emit(\"codeCopy\", message, part.content, part.language)\n}\n\nfunction sideActionSlotName(message: ChatResolvedMessage): ChatActionSlotName {\n return message.side === \"left\" ? \"left-actions\" : \"right-actions\"\n}\n\nfunction hasActions(message: ChatResolvedMessage) {\n return Boolean(\n messageActionItems(message).length > 0 ||\n slots[sideActionSlotName(message)] ||\n slots[\"message-actions\"],\n )\n}\n\nfunction hasAttachments(message: ChatResolvedMessage) {\n return message.attachments.length > 0\n}\n\nfunction actionClass(message: ChatResolvedMessage) {\n return cn(\n \"flex w-full items-center px-1 pt-1\",\n message.side === \"right\" ? \"justify-end\" : \"justify-start\",\n message.actionsVisibility === \"hover\" &&\n \"absolute inset-x-0 top-full pointer-events-none pt-2 opacity-0 transition-opacity duration-150 ease-out group-hover/chat-actions:pointer-events-auto group-hover/chat-actions:opacity-100 group-focus-within/chat-actions:pointer-events-auto group-focus-within/chat-actions:opacity-100\",\n )\n}\n\nfunction canRegenerate(message: ChatResolvedMessage) {\n return isActionEnabled(message, \"regenerate\") &&\n (message.regenerate || message.regenerating)\n}\n\nfunction hasResponseControls(message: ChatResolvedMessage) {\n return canPaginate(message) || canRegenerate(message)\n}\n\nfunction canPaginate(message: ChatResolvedMessage) {\n return isActionEnabled(message, \"pagination\") && message.versionCount > 1\n}\n\nfunction paginationStatus(message: ChatResolvedMessage) {\n return `${message.version + 1} / ${message.versionCount}`\n}\n\nfunction handlePaginate(message: ChatResolvedMessage, version: number) {\n if (message.regenerating) return\n if (version < 0 || version >= message.versionCount) return\n if (version === message.version) return\n\n emit(\"paginate\", message, version)\n}\n\nfunction configuredActionItems(message: ChatResolvedMessage) {\n const items = message.side === \"left\"\n ? props.leftActionItems ?? props.actionItems\n : props.rightActionItems ?? props.actionItems\n\n return withActionFlags(items)\n}\n\nfunction isActionEnabled(message: ChatResolvedMessage, action: ChatActionKey) {\n return configuredActionItems(message).includes(action)\n}\n\nfunction withActionFlags(items: ChatActionKey[]) {\n const nextItems = [...items]\n\n applyActionFlag(nextItems, \"pagination\", props.paginationAction)\n applyActionFlag(nextItems, \"regenerate\", props.regenerateAction)\n applyActionFlag(nextItems, \"copy\", props.copyAction)\n applyActionFlag(nextItems, \"bookmark\", props.bookmarkAction)\n applyActionFlag(nextItems, \"branch\", props.branchAction)\n applyActionFlag(nextItems, \"download\", props.downloadAction)\n applyActionFlag(nextItems, \"edit\", props.editAction)\n\n return nextItems\n}\n\nfunction applyActionFlag(\n items: ChatActionKey[],\n action: ChatActionKey,\n enabled: boolean | undefined,\n) {\n const index = items.indexOf(action)\n\n if (enabled === true && index === -1) {\n items.push(action)\n }\n}\n\nfunction messageActionItems(message: ChatResolvedMessage): ChatActionKey[] {\n return configuredActionItems(message).filter((action) => {\n if (action === \"pagination\") return message.versionCount > 1\n if (action === \"regenerate\") return message.regenerate || message.regenerating\n\n return true\n })\n}\n\nfunction singleActionItems(message: ChatResolvedMessage): ChatSingleActionKey[] {\n return messageActionItems(message).filter(\n (action): action is ChatSingleActionKey => action !== \"pagination\",\n )\n}\n\nfunction shouldUseActionMenu(message: ChatResolvedMessage) {\n const threshold = Math.max(1, Math.trunc(props.actionMenuThreshold))\n\n return messageActionItems(message).length >= threshold\n}\n\nfunction isPrimaryInlineAction(action: ChatSingleActionKey) {\n return action === \"regenerate\" || action === \"copy\"\n}\n\nfunction inlineSingleActionItems(message: ChatResolvedMessage) {\n const actions = singleActionItems(message)\n\n if (!shouldUseActionMenu(message)) return actions\n\n return actions.filter(isPrimaryInlineAction)\n}\n\nfunction menuActionItems(message: ChatResolvedMessage) {\n if (!shouldUseActionMenu(message)) return []\n\n return singleActionItems(message).filter(\n (action) => !isPrimaryInlineAction(action),\n )\n}\n\nfunction hasActionMenu(message: ChatResolvedMessage) {\n return menuActionItems(message).length > 0\n}\n\nfunction actionLabel(action: ChatSingleActionKey, message: ChatResolvedMessage) {\n if (action === \"regenerate\") return regenerateButtonLabel(message)\n if (action === \"copy\") return props.copyLabel\n if (action === \"bookmark\") return props.bookmarkLabel\n if (action === \"branch\") return props.branchLabel\n if (action === \"download\") return props.downloadLabel\n\n return props.editLabel\n}\n\nfunction actionSlot(action: ChatSingleActionKey) {\n return `chat-${action}`\n}\n\nfunction isSingleActionDisabled(action: ChatSingleActionKey, message: ChatResolvedMessage) {\n return action === \"regenerate\" && message.regenerating\n}\n\nasync function copyMessage(message: ChatResolvedMessage) {\n try {\n if (typeof navigator !== \"undefined\") {\n await navigator.clipboard?.writeText(message.content)\n }\n } catch {\n // Clipboard support depends on browser permissions; the action event still fires.\n }\n\n emit(\"copy\", message)\n emit(\"action\", \"copy\", message)\n}\n\nfunction handleSingleAction(action: ChatSingleActionKey, message: ChatResolvedMessage) {\n if (isSingleActionDisabled(action, message)) return\n\n if (action === \"regenerate\") {\n handleRegenerate(message)\n return\n }\n\n if (action === \"copy\") {\n void copyMessage(message)\n return\n }\n\n if (action === \"bookmark\") emit(\"bookmark\", message)\n if (action === \"branch\") emit(\"branch\", message)\n if (action === \"download\") emit(\"download\", message)\n if (action === \"edit\") emit(\"edit\", message)\n\n emit(\"action\", action, message)\n}\n\nfunction regenerateButtonLabel(message: ChatResolvedMessage) {\n return message.regenerating ? props.regeneratingLabel : props.regenerateLabel\n}\n\nfunction actionToolbarLabel(message: ChatResolvedMessage) {\n const details = messageActionItems(message)\n .map((action) =>\n action === \"pagination\"\n ? paginationStatus(message)\n : actionLabel(action, message),\n )\n .join(\", \")\n\n return details ? `${props.actionsLabel}, ${details}` : props.actionsLabel\n}\n\nfunction handleRegenerate(message: ChatResolvedMessage) {\n if (message.regenerating) return\n\n emit(\"regenerate\", message)\n emit(\"action\", \"regenerate\", message)\n}\n\nfunction actionToolbarClass(message: ChatResolvedMessage) {\n return cn(\n \"inline-flex max-w-full shrink-0 items-center rounded-md text-muted-foreground\",\n \"[&>button]:relative [&>button]:rounded-none [&>button]:shadow-none\",\n \"[&>button:first-child]:rounded-l-md [&>button:last-child]:rounded-r-md\",\n \"[&>button:not(:first-child)]:-ml-px [&>button:focus-visible]:z-10\",\n \"[&>[data-slot=button]]:relative [&>[data-slot=button]]:rounded-none [&>[data-slot=button]]:shadow-none\",\n \"[&>[data-slot=button]:first-child]:rounded-l-md [&>[data-slot=button]:last-child]:rounded-r-md\",\n \"[&>[data-slot=button]:not(:first-child)]:-ml-px [&>[data-slot=button]:focus-visible]:z-10\",\n \"[&>[data-slot=chat-pagination-status]]:relative [&>[data-slot=chat-pagination-status]]:rounded-none\",\n \"[&>[data-slot=chat-pagination-status]:first-child]:rounded-l-md [&>[data-slot=chat-pagination-status]:last-child]:rounded-r-md\",\n \"[&>[data-slot=chat-pagination-status]:not(:first-child)]:-ml-px\",\n message.side === \"right\" && \"order-2\",\n )\n}\n\nfunction actionButtonClass() {\n return \"text-muted-foreground hover:text-foreground\"\n}\n\nfunction actionStatusClass() {\n return cn(\n \"inline-flex h-8 min-w-12 items-center justify-center whitespace-nowrap border border-input bg-background px-2 text-xs font-medium text-muted-foreground shadow-none\",\n )\n}\n\nwatch(messages, () => {\n measureOverflowSoon()\n autoScrollSoon()\n}, { flush: \"post\" })\nwatch(() => props.collapsible, () => measureOverflowSoon(), { flush: \"post\" })\n\nonMounted(() => {\n measureOverflowSoon()\n autoScrollSoon()\n\n if (typeof ResizeObserver !== \"undefined\" && rootRef.value) {\n resizeObserver = new ResizeObserver(() => measureOverflowSoon())\n resizeObserver.observe(rootRef.value)\n contentRefs.forEach((element) => observeContentElement(element))\n }\n})\n\nonBeforeUnmount(() => {\n resizeObserver?.disconnect()\n})\n</script>\n\n<template>\n <section\n ref=\"rootRef\"\n data-slot=\"chat\"\n :aria-live=\"props.ariaLive\"\n :aria-label=\"props.ariaLabel\"\n :class=\"cn('flex w-full flex-col gap-4 rounded-lg bg-background p-4 text-foreground', props.class)\"\n role=\"log\"\n >\n <div\n v-for=\"message in messages\"\n :key=\"message.key\"\n data-slot=\"chat-message\"\n :data-side=\"message.side\"\n :data-status=\"message.status\"\n :data-variant=\"message.variant\"\n :class=\"messageRowClass(message)\"\n >\n <slot\n v-if=\"slots.message\"\n name=\"message\"\n v-bind=\"messageSlotProps(message)\"\n />\n\n <template v-else>\n <div\n v-if=\"hasProfile(message)\"\n data-slot=\"chat-profile\"\n :class=\"\n cn(\n 'flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium text-muted-foreground',\n message.side === 'right' && 'order-2 bg-primary text-primary-foreground',\n )\n \"\n >\n <img\n v-if=\"profileSrc(message.profile)\"\n :alt=\"profileAlt(message)\"\n class=\"size-full rounded-full object-cover\"\n :src=\"profileSrc(message.profile)\"\n />\n <span v-else>{{ profileLabel(message.profile) }}</span>\n </div>\n\n <div\n :class=\"messageBodyClass(message)\"\n >\n <span\n v-if=\"message.name\"\n data-slot=\"chat-name\"\n class=\"px-1 text-xs font-medium text-muted-foreground\"\n >\n {{ message.name }}\n </span>\n <slot\n v-if=\"hasAttachments(message) || slots.attachments\"\n name=\"attachments\"\n v-bind=\"attachmentSlotProps(message)\"\n >\n <ChatAttachments\n v-if=\"hasAttachments(message)\"\n :attachments=\"message.attachments\"\n :side=\"message.side\"\n />\n </slot>\n <div\n data-slot=\"chat-bubble\"\n :data-collapsed=\"isMessageCollapsed(message) ? '' : undefined\"\n :data-status=\"message.status\"\n :aria-busy=\"isStreaming(message) ? 'true' : undefined\"\n :class=\"bubbleClass(message)\"\n >\n <div\n data-slot=\"chat-content-frame\"\n class=\"relative\"\n >\n <div\n :ref=\"(element) => setContentRef(message.key, element)\"\n data-slot=\"chat-content\"\n :data-status=\"message.status\"\n :class=\"contentClass(message)\"\n :style=\"contentStyle(message)\"\n >\n <slot\n name=\"content\"\n v-bind=\"contentSlotProps(message)\"\n >\n <div\n data-slot=\"chat-default-content\"\n class=\"flex min-w-0 flex-col gap-2\"\n >\n <template\n v-for=\"part in contentParts(message)\"\n :key=\"part.key\"\n >\n <p\n v-if=\"part.type === 'text'\"\n data-slot=\"chat-text\"\n class=\"m-0 whitespace-pre-wrap\"\n >\n {{ part.content }}\n </p>\n <ChatCodeBlock\n v-else\n :code=\"part.content\"\n :close-label=\"props.codeBlockCloseLabel\"\n :copied-label=\"props.codeBlockCopiedLabel\"\n :copy-label=\"props.codeBlockCopyLabel\"\n :expand-label=\"props.codeBlockExpandLabel\"\n :highlight=\"props.codeBlockHighlight\"\n :label=\"props.codeBlockLabel\"\n :language=\"part.language\"\n :line-numbers=\"props.codeBlockLineNumbers\"\n :max-visible-lines=\"props.codeBlockMaxVisibleLines\"\n @copy=\"handleCodeCopy(message, part)\"\n />\n </template>\n </div>\n </slot>\n <span\n v-if=\"isStreaming(message)\"\n data-slot=\"chat-streaming-indicator\"\n :class=\"streamingIndicatorClass(message)\"\n >\n <Spinner decorative />\n </span>\n </div>\n <div\n v-if=\"isMessageCollapsed(message)\"\n data-slot=\"chat-fade\"\n :class=\"fadeClass(message)\"\n >\n <button\n data-slot=\"chat-expand\"\n type=\"button\"\n :aria-expanded=\"false\"\n :aria-label=\"props.expandLabel\"\n :class=\"toggleButtonClass(message)\"\n @click=\"toggleExpanded(message.key)\"\n >\n <span aria-hidden=\"true\">{{ toggleArrow(message) }}</span>\n <span>{{ props.expandLabel }}</span>\n </button>\n </div>\n </div>\n <div\n v-if=\"canCollapse(message) && !isMessageCollapsed(message)\"\n data-slot=\"chat-collapse\"\n class=\"flex justify-center\"\n >\n <button\n data-slot=\"chat-expand\"\n type=\"button\"\n :aria-expanded=\"true\"\n :aria-label=\"props.collapseLabel\"\n :class=\"toggleButtonClass(message)\"\n @click=\"toggleExpanded(message.key)\"\n >\n <span aria-hidden=\"true\">{{ toggleArrow(message) }}</span>\n <span>{{ toggleButtonLabel(message) }}</span>\n </button>\n </div>\n </div>\n <div\n v-if=\"hasActions(message)\"\n data-slot=\"chat-actions\"\n :data-actions-visibility=\"message.actionsVisibility\"\n :class=\"actionClass(message)\"\n >\n <div\n data-slot=\"chat-action-toolbar\"\n :data-response-controls=\"hasResponseControls(message) ? '' : undefined\"\n :aria-label=\"actionToolbarLabel(message)\"\n :class=\"actionToolbarClass(message)\"\n role=\"toolbar\"\n >\n <template v-if=\"canPaginate(message)\">\n <Button\n data-slot=\"chat-pagination-prev\"\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n :class=\"actionButtonClass()\"\n :aria-label=\"props.previousVersionLabel\"\n :title=\"props.previousVersionLabel\"\n :disabled=\"message.regenerating || message.version <= 0\"\n @click=\"handlePaginate(message, message.version - 1)\"\n >\n <ChevronLeftIcon />\n </Button>\n <span\n data-slot=\"chat-pagination-status\"\n data-action=\"pagination-status\"\n :class=\"actionStatusClass()\"\n >\n {{ paginationStatus(message) }}\n </span>\n <Button\n data-slot=\"chat-pagination-next\"\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n :class=\"actionButtonClass()\"\n :aria-label=\"props.nextVersionLabel\"\n :title=\"props.nextVersionLabel\"\n :disabled=\"message.regenerating || message.version >= message.versionCount - 1\"\n @click=\"handlePaginate(message, message.version + 1)\"\n >\n <ChevronRightIcon />\n </Button>\n </template>\n <Button\n v-for=\"action in inlineSingleActionItems(message)\"\n :key=\"action\"\n :data-slot=\"actionSlot(action)\"\n :data-action=\"action\"\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n :class=\"actionButtonClass()\"\n :aria-label=\"actionLabel(action, message)\"\n :title=\"actionLabel(action, message)\"\n :disabled=\"isSingleActionDisabled(action, message)\"\n @click=\"handleSingleAction(action, message)\"\n >\n <Spinner\n v-if=\"action === 'regenerate' && message.regenerating\"\n decorative\n />\n <component\n :is=\"actionIcons[action]\"\n v-else\n />\n <span class=\"sr-only\">{{ actionLabel(action, message) }}</span>\n </Button>\n <template v-if=\"hasActionMenu(message)\">\n <DropdownMenu>\n <DropdownMenuTrigger as-child>\n <Button\n data-slot=\"chat-action-menu-trigger\"\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n :class=\"actionButtonClass()\"\n :aria-label=\"props.moreActionsLabel\"\n :title=\"props.moreActionsLabel\"\n >\n <MoreHorizontalIcon />\n <span class=\"sr-only\">{{ props.moreActionsLabel }}</span>\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n :align=\"message.side === 'right' ? 'end' : 'start'\"\n class=\"min-w-44\"\n >\n <DropdownMenuGroup>\n <DropdownMenuItem\n v-for=\"action in menuActionItems(message)\"\n :key=\"action\"\n :data-action=\"action\"\n :disabled=\"isSingleActionDisabled(action, message)\"\n @click=\"handleSingleAction(action, message)\"\n >\n <component\n :is=\"actionIcons[action]\"\n />\n {{ actionLabel(action, message) }}\n </DropdownMenuItem>\n </DropdownMenuGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n </template>\n <slot\n v-if=\"slots[sideActionSlotName(message)]\"\n :name=\"sideActionSlotName(message)\"\n v-bind=\"actionSlotProps(message)\"\n />\n <slot\n v-else\n name=\"message-actions\"\n v-bind=\"actionSlotProps(message)\"\n />\n </div>\n </div>\n </div>\n </template>\n </div>\n </section>\n</template>\n",
151
+ "type": "registry:ui",
152
+ "target": "components/ui/chat/Chat.vue"
153
+ },
154
+ {
155
+ "path": "src/components/ui/chat/index.ts",
156
+ "content": "export { default as Chat } from \"./Chat.vue\"\nexport { default as ChatAttachments } from \"./ChatAttachments.vue\"\nexport { default as ChatCodeBlock } from \"./ChatCodeBlock.vue\"\nexport type {\n ChatCodeBlockEmits,\n ChatCodeBlockProps,\n} from \"./code-block\"\nexport type {\n ChatActionSlotProps,\n ChatAttachmentSlotProps,\n ChatContentSlotProps,\n ChatMessage,\n ChatMessageSlotProps,\n ChatProfile,\n ChatResolvedMessage,\n} from \"./Chat.vue\"\nexport type {\n ChatActionKey,\n ChatActionVisibility,\n ChatAttachment,\n ChatSide,\n ChatStatus,\n ChatVariant,\n} from \"./types\"\n",
157
+ "type": "registry:ui",
158
+ "target": "components/ui/chat/index.ts"
159
+ },
160
+ {
161
+ "path": "src/components/ui/chat/types.ts",
162
+ "content": "export type ChatAttachment = {\n alt?: string\n extension?: string\n id?: number | string\n name: string\n size?: string\n thumbnailUrl?: string\n type?: string\n url?: string\n}\n\nexport type ChatActionVisibility = \"always\" | \"hover\"\nexport type ChatActionKey =\n | \"bookmark\"\n | \"branch\"\n | \"copy\"\n | \"download\"\n | \"edit\"\n | \"pagination\"\n | \"regenerate\"\nexport type ChatSide = \"left\" | \"right\"\nexport type ChatStatus = \"complete\" | \"error\" | \"streaming\"\nexport type ChatVariant = \"bubble\" | \"plain\"\n",
163
+ "type": "registry:ui",
164
+ "target": "components/ui/chat/types.ts"
165
+ },
166
+ {
167
+ "path": "src/components/ui/spinner/Spinner.vue",
168
+ "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from \"vue\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = withDefaults(\n defineProps<{\n class?: HTMLAttributes[\"class\"]\n label?: string\n decorative?: boolean\n }>(),\n {\n label: \"Loading\",\n decorative: false,\n },\n)\n</script>\n\n<template>\n <span\n data-slot=\"spinner\"\n :aria-hidden=\"props.decorative ? 'true' : undefined\"\n :aria-label=\"props.decorative ? undefined : props.label\"\n :class=\"\n cn(\n 'inline-block size-4 shrink-0 animate-spin rounded-full border-2 border-current border-r-transparent align-[-0.125em]',\n props.class,\n )\n \"\n :role=\"props.decorative ? undefined : 'status'\"\n >\n <span v-if=\"!props.decorative\" class=\"sr-only\">{{ props.label }}</span>\n </span>\n</template>\n",
169
+ "type": "registry:ui",
170
+ "target": "components/ui/spinner/Spinner.vue"
171
+ },
172
+ {
173
+ "path": "src/components/ui/spinner/index.ts",
174
+ "content": "export { default as Spinner } from \"./Spinner.vue\"\n",
175
+ "type": "registry:ui",
176
+ "target": "components/ui/spinner/index.ts"
177
+ },
178
+ {
179
+ "path": "src/lib/utils.ts",
180
+ "content": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
181
+ "type": "registry:lib",
182
+ "target": "lib/utils.ts"
183
+ }
184
+ ],
185
+ "type": "registry:ui"
186
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://shadcn-vue.com/schema/registry-item.json",
3
+ "name": "checkbox",
4
+ "title": "Checkbox",
5
+ "description": "A checkbox control for boolean and indeterminate selection states.",
6
+ "dependencies": [
7
+ "@lucide/vue",
8
+ "@vueuse/core",
9
+ "clsx",
10
+ "reka-ui",
11
+ "tailwind-merge"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "src/components/ui/checkbox/Checkbox.vue",
16
+ "content": "<script setup lang=\"ts\">\nimport type { CheckboxRootEmits, CheckboxRootProps } from \"reka-ui\"\nimport type { HTMLAttributes } from \"vue\"\nimport { Check } from \"@lucide/vue\"\nimport { reactiveOmit } from \"@vueuse/core\"\nimport { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from \"reka-ui\"\nimport { cn } from \"@/lib/utils\"\n\nconst props = defineProps<CheckboxRootProps & { class?: HTMLAttributes[\"class\"] }>()\nconst emits = defineEmits<CheckboxRootEmits>()\n\nconst delegatedProps = reactiveOmit(props, \"class\")\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits)\n</script>\n\n<template>\n <CheckboxRoot\n v-slot=\"slotProps\"\n data-slot=\"checkbox\"\n v-bind=\"forwarded\"\n :class=\"\n cn(\n 'peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',\n props.class,\n )\n \"\n >\n <CheckboxIndicator\n data-slot=\"checkbox-indicator\"\n class=\"grid place-content-center text-current transition-none\"\n >\n <slot v-bind=\"slotProps\">\n <Check class=\"size-3.5\" />\n </slot>\n </CheckboxIndicator>\n </CheckboxRoot>\n</template>\n",
17
+ "type": "registry:ui",
18
+ "target": "components/ui/checkbox/Checkbox.vue"
19
+ },
20
+ {
21
+ "path": "src/components/ui/checkbox/index.ts",
22
+ "content": "export { default as Checkbox } from \"./Checkbox.vue\"\n",
23
+ "type": "registry:ui",
24
+ "target": "components/ui/checkbox/index.ts"
25
+ },
26
+ {
27
+ "path": "src/lib/utils.ts",
28
+ "content": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
29
+ "type": "registry:lib",
30
+ "target": "lib/utils.ts"
31
+ }
32
+ ],
33
+ "type": "registry:ui"
34
+ }