@devxcrew/react-ui 0.0.0-stage → 0.1.7

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 (326) hide show
  1. package/README.md +85 -2
  2. package/package.json +105 -4
  3. package/src/blocks/agent-chat-workspace/agent-chat-workspace.tsx +162 -0
  4. package/src/blocks/agent-chat-workspace/index.ts +2 -0
  5. package/src/blocks/agent-provider-settings/agent-provider-settings.tsx +183 -0
  6. package/src/blocks/agent-provider-settings/index.ts +2 -0
  7. package/src/blocks/agent-task-workspace/agent-task-workspace.tsx +48 -0
  8. package/src/blocks/agent-task-workspace/index.ts +2 -0
  9. package/src/blocks/analysis-context/analysis-context.tsx +6 -0
  10. package/src/blocks/analysis-context/index.ts +1 -0
  11. package/src/blocks/app-header/app-header.tsx +159 -0
  12. package/src/blocks/app-header/index.ts +2 -0
  13. package/src/blocks/archived-chat-workspace/archived-chat-workspace.tsx +29 -0
  14. package/src/blocks/archived-chat-workspace/index.ts +2 -0
  15. package/src/blocks/auth/README.md +32 -0
  16. package/src/blocks/auth/admin-login-page.tsx +15 -0
  17. package/src/blocks/auth/admin-password-forgot-page.tsx +5 -0
  18. package/src/blocks/auth/auth-page-layout.tsx +90 -0
  19. package/src/blocks/auth/client-login-page.tsx +20 -0
  20. package/src/blocks/auth/client-password-forgot-page.tsx +5 -0
  21. package/src/blocks/auth/identity-management-desk.tsx +530 -0
  22. package/src/blocks/auth/index.ts +12 -0
  23. package/src/blocks/auth/login-page.tsx +206 -0
  24. package/src/blocks/auth/password-forgot-page.tsx +60 -0
  25. package/src/blocks/auth/portal-pages.tsx +32 -0
  26. package/src/blocks/auth/privileged-desk.tsx +55 -0
  27. package/src/blocks/auth/register-page.tsx +117 -0
  28. package/src/blocks/auth/session-boundary.tsx +296 -0
  29. package/src/blocks/auth/super-admin-login-page.tsx +15 -0
  30. package/src/blocks/auth/super-admin-password-forgot-page.tsx +5 -0
  31. package/src/blocks/blog/blog-card.tsx +156 -0
  32. package/src/blocks/blog/blog-reader.tsx +175 -0
  33. package/src/blocks/blog/blog-types.ts +47 -0
  34. package/src/blocks/blog/index.ts +3 -0
  35. package/src/blocks/chat-composer/chat-composer.tsx +140 -0
  36. package/src/blocks/chat-composer/index.ts +1 -0
  37. package/src/blocks/chat-history/chat-history.tsx +59 -0
  38. package/src/blocks/chat-history/index.ts +1 -0
  39. package/src/blocks/chat-response-progress/chat-response-progress.tsx +34 -0
  40. package/src/blocks/chat-response-progress/index.ts +2 -0
  41. package/src/blocks/chat-runtime-controls/chat-runtime-controls.tsx +56 -0
  42. package/src/blocks/chat-runtime-controls/index.ts +2 -0
  43. package/src/blocks/chat-runtime-trace/chat-runtime-trace.tsx +71 -0
  44. package/src/blocks/chat-runtime-trace/index.ts +2 -0
  45. package/src/blocks/codex-connection-settings/codex-connection-settings.tsx +42 -0
  46. package/src/blocks/codex-connection-settings/index.ts +1 -0
  47. package/src/blocks/dropzone/dropzone-file-item.tsx +113 -0
  48. package/src/blocks/dropzone/dropzone-types.ts +32 -0
  49. package/src/blocks/dropzone/dropzone.tsx +216 -0
  50. package/src/blocks/dropzone/index.ts +3 -0
  51. package/src/blocks/ecommerce/cart/cart-drawer.tsx +228 -0
  52. package/src/blocks/ecommerce/cart/cart-types.ts +27 -0
  53. package/src/blocks/ecommerce/cart/index.ts +2 -0
  54. package/src/blocks/ecommerce/categories/categories-types.ts +23 -0
  55. package/src/blocks/ecommerce/categories/category-showcase.tsx +125 -0
  56. package/src/blocks/ecommerce/categories/index.ts +2 -0
  57. package/src/blocks/ecommerce/checkout/checkout-types.ts +48 -0
  58. package/src/blocks/ecommerce/checkout/checkout-wizard.tsx +468 -0
  59. package/src/blocks/ecommerce/checkout/index.ts +2 -0
  60. package/src/blocks/ecommerce/comparison/comparison-types.ts +28 -0
  61. package/src/blocks/ecommerce/comparison/index.ts +2 -0
  62. package/src/blocks/ecommerce/comparison/product-comparison.tsx +174 -0
  63. package/src/blocks/ecommerce/coupon-wallet/coupon-wallet-types.ts +21 -0
  64. package/src/blocks/ecommerce/coupon-wallet/coupon-wallet.tsx +154 -0
  65. package/src/blocks/ecommerce/coupon-wallet/index.ts +2 -0
  66. package/src/blocks/ecommerce/delivery-tracker/delivery-tracker-types.ts +20 -0
  67. package/src/blocks/ecommerce/delivery-tracker/delivery-tracker.tsx +169 -0
  68. package/src/blocks/ecommerce/delivery-tracker/index.ts +2 -0
  69. package/src/blocks/ecommerce/index.ts +12 -0
  70. package/src/blocks/ecommerce/payment-methods/index.ts +2 -0
  71. package/src/blocks/ecommerce/payment-methods/payment-method-selector.tsx +224 -0
  72. package/src/blocks/ecommerce/payment-methods/payment-methods-types.ts +21 -0
  73. package/src/blocks/ecommerce/price-history/index.ts +2 -0
  74. package/src/blocks/ecommerce/price-history/price-history-chart.tsx +217 -0
  75. package/src/blocks/ecommerce/price-history/price-history-types.ts +16 -0
  76. package/src/blocks/ecommerce/reviews/index.ts +2 -0
  77. package/src/blocks/ecommerce/reviews/reviews-section.tsx +309 -0
  78. package/src/blocks/ecommerce/reviews/reviews-types.ts +26 -0
  79. package/src/blocks/ecommerce/wishlist/index.ts +2 -0
  80. package/src/blocks/ecommerce/wishlist/wishlist-grid.tsx +149 -0
  81. package/src/blocks/ecommerce/wishlist/wishlist-types.ts +19 -0
  82. package/src/blocks/execution-status/README.md +25 -0
  83. package/src/blocks/execution-status/execution-status.tsx +135 -0
  84. package/src/blocks/execution-status/index.ts +2 -0
  85. package/src/blocks/file-tree/file-icons.tsx +84 -0
  86. package/src/blocks/file-tree/file-tree-node.tsx +158 -0
  87. package/src/blocks/file-tree/file-tree-types.ts +33 -0
  88. package/src/blocks/file-tree/file-tree.tsx +163 -0
  89. package/src/blocks/file-tree/index.ts +5 -0
  90. package/src/blocks/filter-builder/filter-builder.tsx +169 -0
  91. package/src/blocks/filter-builder/filter-row.tsx +164 -0
  92. package/src/blocks/filter-builder/filter-types.ts +61 -0
  93. package/src/blocks/filter-builder/index.ts +12 -0
  94. package/src/blocks/footer/footer-types.ts +35 -0
  95. package/src/blocks/footer/index.ts +2 -0
  96. package/src/blocks/footer/site-footer.tsx +212 -0
  97. package/src/blocks/form/form-block.tsx +113 -0
  98. package/src/blocks/form/form-lookup-field.tsx +60 -0
  99. package/src/blocks/form/index.ts +4 -0
  100. package/src/blocks/handover-stack/handover-stack.tsx +41 -0
  101. package/src/blocks/handover-stack/index.ts +1 -0
  102. package/src/blocks/idea-handover/idea-handover-readiness.ts +41 -0
  103. package/src/blocks/idea-handover/idea-handover-workspace.tsx +150 -0
  104. package/src/blocks/idea-handover/index.ts +4 -0
  105. package/src/blocks/kanban/index.ts +11 -0
  106. package/src/blocks/kanban/kanban-board.tsx +146 -0
  107. package/src/blocks/kanban/kanban-card.tsx +149 -0
  108. package/src/blocks/kanban/kanban-column.tsx +102 -0
  109. package/src/blocks/kanban/kanban-types.ts +38 -0
  110. package/src/blocks/loader/global-loader.tsx +90 -0
  111. package/src/blocks/loader/index.ts +1 -0
  112. package/src/blocks/mascot/README.md +21 -0
  113. package/src/blocks/mascot/index.ts +2 -0
  114. package/src/blocks/mascot/mascot.tsx +202 -0
  115. package/src/blocks/master-list/README.md +21 -0
  116. package/src/blocks/master-list/index.ts +12 -0
  117. package/src/blocks/master-list/master-form.tsx +157 -0
  118. package/src/blocks/master-list/master-list-desk.tsx +370 -0
  119. package/src/blocks/master-list/master-list.tsx +189 -0
  120. package/src/blocks/master-list/types.ts +18 -0
  121. package/src/blocks/mermaid-preview/index.ts +1 -0
  122. package/src/blocks/mermaid-preview/mermaid-preview.tsx +164 -0
  123. package/src/blocks/notifications/README.md +7 -0
  124. package/src/blocks/notifications/index.ts +1 -0
  125. package/src/blocks/notifications/notification-center-page.tsx +73 -0
  126. package/src/blocks/pricing/index.ts +7 -0
  127. package/src/blocks/pricing/pricing-table.tsx +173 -0
  128. package/src/blocks/pricing/pricing-types.ts +34 -0
  129. package/src/blocks/product-card/index.ts +2 -0
  130. package/src/blocks/product-card/product-card-types.ts +35 -0
  131. package/src/blocks/product-card/product-card.tsx +183 -0
  132. package/src/blocks/table/data-table-block.tsx +313 -0
  133. package/src/blocks/table/data-table-filter-menu.tsx +48 -0
  134. package/src/blocks/table/data-table-icon-menu-trigger.tsx +44 -0
  135. package/src/blocks/table/data-table-menu-content.tsx +32 -0
  136. package/src/blocks/table/data-table-pagination.tsx +149 -0
  137. package/src/blocks/table/data-table-row-actions.tsx +53 -0
  138. package/src/blocks/table/data-table-status.tsx +42 -0
  139. package/src/blocks/table/data-table-toolbar.tsx +124 -0
  140. package/src/blocks/table/data-table-totals.tsx +31 -0
  141. package/src/blocks/table/index.ts +13 -0
  142. package/src/blocks/workspace/index.ts +19 -0
  143. package/src/blocks/workspace/workspace-action-card.tsx +73 -0
  144. package/src/blocks/workspace/workspace-application-header.tsx +67 -0
  145. package/src/blocks/workspace/workspace-entity-card.tsx +48 -0
  146. package/src/blocks/workspace/workspace-health-summary.tsx +15 -0
  147. package/src/blocks/workspace/workspace-metric-card.tsx +97 -0
  148. package/src/blocks/workspace/workspace-page-header.tsx +43 -0
  149. package/src/blocks/workspace/workspace-publish-status.tsx +15 -0
  150. package/src/blocks/workspace/workspace-route-checklist.tsx +28 -0
  151. package/src/blocks/workspace/workspace-runtime-status.tsx +48 -0
  152. package/src/blocks/workspace/workspace-section-card.tsx +42 -0
  153. package/src/components/accordion.tsx +65 -0
  154. package/src/components/alert-dialog.tsx +160 -0
  155. package/src/components/alert.tsx +68 -0
  156. package/src/components/aspect-ratio.tsx +22 -0
  157. package/src/components/attachment.tsx +195 -0
  158. package/src/components/avatar.tsx +90 -0
  159. package/src/components/badge.tsx +55 -0
  160. package/src/components/breadcrumb.tsx +100 -0
  161. package/src/components/bubble.tsx +123 -0
  162. package/src/components/button-group.tsx +79 -0
  163. package/src/components/button.tsx +71 -0
  164. package/src/components/calendar.tsx +185 -0
  165. package/src/components/card.tsx +100 -0
  166. package/src/components/carousel.tsx +229 -0
  167. package/src/components/chart.tsx +338 -0
  168. package/src/components/checkbox.tsx +27 -0
  169. package/src/components/collapsible.tsx +15 -0
  170. package/src/components/combobox.tsx +269 -0
  171. package/src/components/command.tsx +181 -0
  172. package/src/components/compact-model-switcher.tsx +128 -0
  173. package/src/components/context-menu.tsx +244 -0
  174. package/src/components/dialog.tsx +138 -0
  175. package/src/components/direction.tsx +1 -0
  176. package/src/components/drawer.tsx +208 -0
  177. package/src/components/dropdown-menu.tsx +257 -0
  178. package/src/components/empty.tsx +93 -0
  179. package/src/components/field.tsx +232 -0
  180. package/src/components/hover-card.tsx +45 -0
  181. package/src/components/input-group.tsx +146 -0
  182. package/src/components/input-otp.tsx +83 -0
  183. package/src/components/input.tsx +19 -0
  184. package/src/components/item.tsx +188 -0
  185. package/src/components/kbd.tsx +26 -0
  186. package/src/components/label.tsx +17 -0
  187. package/src/components/markdown-content.tsx +113 -0
  188. package/src/components/marker.tsx +67 -0
  189. package/src/components/menubar.tsx +267 -0
  190. package/src/components/message-scroller.tsx +91 -0
  191. package/src/components/message.tsx +84 -0
  192. package/src/components/native-select.tsx +54 -0
  193. package/src/components/navigation-menu.tsx +160 -0
  194. package/src/components/pagination.tsx +118 -0
  195. package/src/components/popover.tsx +74 -0
  196. package/src/components/progress.tsx +82 -0
  197. package/src/components/questionnaire.tsx +150 -0
  198. package/src/components/radio-group.tsx +35 -0
  199. package/src/components/resizable.tsx +41 -0
  200. package/src/components/rich-text-editor.tsx +604 -0
  201. package/src/components/scroll-area.tsx +47 -0
  202. package/src/components/select.tsx +189 -0
  203. package/src/components/separator.tsx +18 -0
  204. package/src/components/sheet.tsx +125 -0
  205. package/src/components/sidebar.tsx +693 -0
  206. package/src/components/skeleton.tsx +13 -0
  207. package/src/components/slider.tsx +51 -0
  208. package/src/components/sonner.tsx +45 -0
  209. package/src/components/sparkline.tsx +231 -0
  210. package/src/components/spinner.tsx +20 -0
  211. package/src/components/status-badge.tsx +114 -0
  212. package/src/components/switch.tsx +31 -0
  213. package/src/components/table.tsx +91 -0
  214. package/src/components/tabs.tsx +72 -0
  215. package/src/components/textarea.tsx +17 -0
  216. package/src/components/toast.tsx +222 -0
  217. package/src/components/toggle-group.tsx +85 -0
  218. package/src/components/toggle.tsx +42 -0
  219. package/src/components/tooltip.tsx +53 -0
  220. package/src/design-system/README.md +55 -0
  221. package/src/design-system/action-intents.ts +21 -0
  222. package/src/design-system/agent-manifest.ts +83 -0
  223. package/src/design-system/block-registry.ts +272 -0
  224. package/src/design-system/component-registry.ts +142 -0
  225. package/src/design-system/contracts.ts +69 -0
  226. package/src/design-system/defaults.ts +3 -0
  227. package/src/design-system/index.ts +29 -0
  228. package/src/design-system/mcp-server.ts +156 -0
  229. package/src/design-system/page-registry.ts +74 -0
  230. package/src/design-system/selection.ts +67 -0
  231. package/src/design-system/template-registry.ts +33 -0
  232. package/src/features/interface-topology/index.ts +14 -0
  233. package/src/features/interface-topology/interface-topology.types.ts +40 -0
  234. package/src/features/interface-topology/topology-control.tsx +18 -0
  235. package/src/features/interface-topology/topology-inspector.tsx +228 -0
  236. package/src/features/interface-topology/topology-marker.tsx +47 -0
  237. package/src/features/interface-topology/topology-region.tsx +44 -0
  238. package/src/features/interface-topology/use-interface-topology.ts +178 -0
  239. package/src/hooks/use-mobile.ts +19 -0
  240. package/src/hooks/use-scroll-follow.ts +22 -0
  241. package/src/index.ts +11 -0
  242. package/src/layouts/README.md +65 -0
  243. package/src/layouts/agent-workspace/agent-activity-rail.tsx +101 -0
  244. package/src/layouts/agent-workspace/agent-workspace.tsx +27 -0
  245. package/src/layouts/agent-workspace/agent-workspace.types.ts +28 -0
  246. package/src/layouts/agent-workspace/index.ts +7 -0
  247. package/src/layouts/blog-header/blog-header-types.ts +25 -0
  248. package/src/layouts/blog-header/blog-header.tsx +172 -0
  249. package/src/layouts/blog-header/index.ts +2 -0
  250. package/src/layouts/documentation-workspace/README.md +8 -0
  251. package/src/layouts/documentation-workspace/documentation-workspace.tsx +34 -0
  252. package/src/layouts/documentation-workspace/index.ts +1 -0
  253. package/src/layouts/ecommerce-header/ecommerce-actions.tsx +89 -0
  254. package/src/layouts/ecommerce-header/ecommerce-announcement.tsx +43 -0
  255. package/src/layouts/ecommerce-header/ecommerce-drawer.tsx +97 -0
  256. package/src/layouts/ecommerce-header/ecommerce-header-types.ts +59 -0
  257. package/src/layouts/ecommerce-header/ecommerce-header.tsx +47 -0
  258. package/src/layouts/ecommerce-header/ecommerce-navigation.tsx +98 -0
  259. package/src/layouts/ecommerce-header/ecommerce-search.tsx +86 -0
  260. package/src/layouts/ecommerce-header/index.ts +2 -0
  261. package/src/layouts/main-workspace/index.ts +28 -0
  262. package/src/layouts/mdi/index.ts +1 -0
  263. package/src/layouts/mdi/mdi.tsx +20 -0
  264. package/src/layouts/mdi-main/index.ts +27 -0
  265. package/src/layouts/mdi-main/mdi-app-catalog.generated.ts +159 -0
  266. package/src/layouts/mdi-main/mdi-app-catalog.ts +76 -0
  267. package/src/layouts/mdi-main/mdi-app-switcher.tsx +107 -0
  268. package/src/layouts/mdi-main/mdi-feature-settings.tsx +135 -0
  269. package/src/layouts/mdi-main/mdi-global-search.tsx +192 -0
  270. package/src/layouts/mdi-main/mdi-main.tsx +241 -0
  271. package/src/layouts/mdi-main/mdi-notifications-menu.tsx +197 -0
  272. package/src/layouts/mdi-main/mdi-profile-menu.tsx +79 -0
  273. package/src/layouts/mdi-main/mdi-sidebar-navigation.tsx +226 -0
  274. package/src/layouts/mdi-main/mdi-sidebar.tsx +110 -0
  275. package/src/layouts/mdi-main/mdi-status-bar.tsx +26 -0
  276. package/src/layouts/mdi-main/mdi-top-menu-control.ts +4 -0
  277. package/src/layouts/mdi-main/mdi-top-menu.tsx +117 -0
  278. package/src/layouts/mdi-main/mdi-topology.tsx +297 -0
  279. package/src/layouts/mdi-main/mdi-tweak-panel.tsx +104 -0
  280. package/src/layouts/mdi-main/mdi-types.ts +131 -0
  281. package/src/layouts/mdi-main/use-mdi-features.ts +36 -0
  282. package/src/layouts/mdi-main/use-mdi-sidebar-state.ts +59 -0
  283. package/src/layouts/site-header/announcement-bar.tsx +55 -0
  284. package/src/layouts/site-header/category-strip.tsx +41 -0
  285. package/src/layouts/site-header/index.ts +16 -0
  286. package/src/layouts/site-header/main-navbar.tsx +179 -0
  287. package/src/layouts/site-header/mobile-drawer.tsx +140 -0
  288. package/src/layouts/site-header/site-header-types.ts +63 -0
  289. package/src/layouts/site-header/site-header.tsx +58 -0
  290. package/src/lib/random-id.ts +19 -0
  291. package/src/lib/utils.ts +6 -0
  292. package/src/providers/pdf-provider.ts +137 -0
  293. package/src/styles/globals.css +374 -0
  294. package/src/templates/README.md +12 -0
  295. package/src/templates/dashboard-01/components/app-sidebar.tsx +177 -0
  296. package/src/templates/dashboard-01/components/chart-area-interactive.tsx +276 -0
  297. package/src/templates/dashboard-01/components/data-table-cell-viewer.tsx +213 -0
  298. package/src/templates/dashboard-01/components/data-table.tsx +577 -0
  299. package/src/templates/dashboard-01/components/nav-documents.tsx +79 -0
  300. package/src/templates/dashboard-01/components/nav-main.tsx +55 -0
  301. package/src/templates/dashboard-01/components/nav-secondary.tsx +39 -0
  302. package/src/templates/dashboard-01/components/nav-user.tsx +97 -0
  303. package/src/templates/dashboard-01/components/section-cards.tsx +99 -0
  304. package/src/templates/dashboard-01/components/site-header.tsx +14 -0
  305. package/src/templates/dashboard-01/data.json +614 -0
  306. package/src/templates/dashboard-01/index.ts +7 -0
  307. package/src/templates/documentation-sidebar/app-sidebar.tsx +95 -0
  308. package/src/templates/documentation-sidebar/index.ts +2 -0
  309. package/src/templates/sidebar-07/components/app-sidebar.tsx +175 -0
  310. package/src/templates/sidebar-07/components/nav-main.tsx +64 -0
  311. package/src/templates/sidebar-07/components/nav-projects.tsx +79 -0
  312. package/src/templates/sidebar-07/components/nav-user.tsx +105 -0
  313. package/src/templates/sidebar-07/components/team-switcher.tsx +94 -0
  314. package/src/templates/sidebar-07/index.ts +1 -0
  315. package/src/templates/ui-page/index.ts +7 -0
  316. package/src/templates/ui-page/ui-browser-frame.tsx +27 -0
  317. package/src/templates/ui-page/ui-template-code.tsx +48 -0
  318. package/src/templates/ui-page/ui-template-header.tsx +87 -0
  319. package/src/templates/ui-page/ui-template-navigation.tsx +60 -0
  320. package/src/templates/ui-page/ui-template-page.tsx +97 -0
  321. package/src/theme/index.ts +12 -0
  322. package/src/theme/theme-config.ts +30 -0
  323. package/src/theme/theme-provider.tsx +142 -0
  324. package/src/theme/theme-selector.tsx +75 -0
  325. package/src/tokens/index.ts +15 -0
  326. package/src/vite-env.d.ts +3 -0
@@ -0,0 +1,47 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export type BlogPostAuthor = {
4
+ readonly avatar?: string
5
+ readonly bio?: string
6
+ readonly name: string
7
+ readonly role?: string
8
+ }
9
+
10
+ export type BlogPostSummary = {
11
+ readonly author: BlogPostAuthor
12
+ readonly category: string
13
+ readonly coverImage: string
14
+ readonly date: string
15
+ readonly excerpt: string
16
+ readonly href: string
17
+ readonly id: string
18
+ readonly isFeatured?: boolean
19
+ readonly readingTime: string
20
+ readonly tags?: readonly string[]
21
+ readonly title: string
22
+ }
23
+
24
+ export type BlogPostArticle = BlogPostSummary & {
25
+ readonly contentHtml?: string
26
+ readonly relatedPosts?: readonly BlogPostSummary[]
27
+ readonly tableOfContents?: readonly {
28
+ readonly href: string
29
+ readonly id: string
30
+ readonly level: number
31
+ readonly title: string
32
+ }[]
33
+ }
34
+
35
+ export type BlogCardProps = {
36
+ readonly className?: string
37
+ readonly onSelect?: (postId: string) => void
38
+ readonly post: BlogPostSummary
39
+ readonly variant?: 'compact' | 'horizontal' | 'standard'
40
+ }
41
+
42
+ export type BlogReaderProps = {
43
+ readonly article: BlogPostArticle
44
+ readonly children?: ReactNode
45
+ readonly className?: string
46
+ readonly onShare?: (platform: 'twitter' | 'linkedin' | 'copy') => void
47
+ }
@@ -0,0 +1,3 @@
1
+ export * from './blog-card'
2
+ export * from './blog-reader'
3
+ export * from './blog-types'
@@ -0,0 +1,140 @@
1
+ import { FileImageIcon, MicIcon, PaperclipIcon, SquareIcon, XIcon } from 'lucide-react'
2
+ import { useRef, useState, type ChangeEvent, type ClipboardEvent, type DragEvent, type KeyboardEvent, type RefObject } from 'react'
3
+
4
+ import { Button } from '../../components/button'
5
+ import { Textarea } from '../../components/textarea'
6
+ import { cn } from '../../lib/utils'
7
+
8
+ export type ChatComposerAttachment = {
9
+ file: File
10
+ id: string
11
+ name: string
12
+ previewUrl?: string
13
+ type: string
14
+ }
15
+
16
+ export type ChatComposerProps = {
17
+ attachments?: readonly ChatComposerAttachment[]
18
+ className?: string
19
+ disabled?: boolean
20
+ isRecording?: boolean
21
+ isWorking?: boolean
22
+ onAddFiles?: (files: File[]) => void
23
+ onLongTextPaste?: (file: File) => void
24
+ onRemoveAttachment?: (id: string) => void
25
+ onSteer?: () => void
26
+ onStop?: () => void
27
+ onSubmit: () => void
28
+ onValueChange: (value: string) => void
29
+ onVoiceToggle?: () => void
30
+ placeholder?: string
31
+ queuedSteerCount?: number
32
+ textareaRef?: RefObject<HTMLTextAreaElement | null>
33
+ value: string
34
+ }
35
+
36
+ export function ChatComposer({
37
+ attachments = [],
38
+ className,
39
+ disabled = false,
40
+ isRecording = false,
41
+ isWorking = false,
42
+ onAddFiles,
43
+ onLongTextPaste,
44
+ onRemoveAttachment,
45
+ onSteer,
46
+ onStop,
47
+ onSubmit,
48
+ onValueChange,
49
+ onVoiceToggle,
50
+ placeholder = 'Share an idea, question, or draft…',
51
+ queuedSteerCount = 0,
52
+ textareaRef,
53
+ value,
54
+ }: ChatComposerProps) {
55
+ const fileInputRef = useRef<HTMLInputElement>(null)
56
+ const [isDragging, setIsDragging] = useState(false)
57
+
58
+ function addFiles(files: FileList | null): void {
59
+ if (!files?.length) return
60
+ onAddFiles?.(Array.from(files))
61
+ }
62
+
63
+ function handleInputChange(event: ChangeEvent<HTMLInputElement>): void {
64
+ addFiles(event.target.files)
65
+ event.target.value = ''
66
+ }
67
+
68
+ function handleDrop(event: DragEvent<HTMLDivElement>): void {
69
+ event.preventDefault()
70
+ setIsDragging(false)
71
+ addFiles(event.dataTransfer.files)
72
+ }
73
+
74
+ function handleKeyDown(event: KeyboardEvent<HTMLTextAreaElement>): void {
75
+ if (event.key !== 'Enter' || event.shiftKey) return
76
+ event.preventDefault()
77
+ if (isWorking) onSteer?.()
78
+ else onSubmit()
79
+ }
80
+
81
+ function handlePaste(event: ClipboardEvent<HTMLTextAreaElement>): void {
82
+ if (event.clipboardData.files.length) {
83
+ addFiles(event.clipboardData.files)
84
+ return
85
+ }
86
+ const text = event.clipboardData.getData('text/plain')
87
+ if (text.length < 12_000 || !onLongTextPaste) return
88
+ event.preventDefault()
89
+ onLongTextPaste(new File([text], `zetro-paste-${Date.now()}.txt`, { type: 'text/plain' }))
90
+ }
91
+
92
+ return (
93
+ <div
94
+ className={cn('relative mx-auto w-4/5 rounded-md transition-colors', isDragging && 'ring-2 ring-primary/50', className)}
95
+ onDragEnter={() => setIsDragging(true)}
96
+ onDragLeave={(event) => {
97
+ if (event.currentTarget === event.target) setIsDragging(false)
98
+ }}
99
+ onDragOver={(event) => event.preventDefault()}
100
+ onDrop={handleDrop}
101
+ >
102
+ <input ref={fileInputRef} className="sr-only" multiple type="file" onChange={handleInputChange} />
103
+ {attachments.length ? (
104
+ <div className="absolute inset-x-3 top-3 z-10 flex flex-wrap gap-1.5">
105
+ {attachments.map((attachment) => (
106
+ <div key={attachment.id} className="flex h-8 max-w-48 items-center gap-1.5 rounded-sm border bg-background px-1.5 text-xs shadow-sm">
107
+ {attachment.previewUrl ? <img alt="" className="size-5 rounded-sm object-cover" src={attachment.previewUrl} /> : <FileImageIcon className="size-3.5 text-muted-foreground" />}
108
+ <span className="truncate">{attachment.name}</span>
109
+ <Button aria-label={`Remove ${attachment.name}`} className="size-5" size="icon-xs" variant="ghost" onClick={() => onRemoveAttachment?.(attachment.id)}><XIcon /></Button>
110
+ </div>
111
+ ))}
112
+ </div>
113
+ ) : null}
114
+ <Textarea
115
+ aria-label="Message"
116
+ className={cn(
117
+ 'h-[7.25rem] min-h-0 max-h-[7.25rem] w-full resize-none border-foreground/20 pb-10 pr-28 leading-5 [field-sizing:fixed] [scrollbar-color:var(--border)_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/70 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-1',
118
+ attachments.length && 'pt-12',
119
+ )}
120
+ disabled={disabled}
121
+ placeholder={isDragging ? 'Drop files to attach' : placeholder}
122
+ rows={5}
123
+ ref={textareaRef}
124
+ value={value}
125
+ onChange={(event) => onValueChange(event.target.value)}
126
+ onKeyDown={handleKeyDown}
127
+ onPaste={handlePaste}
128
+ />
129
+ <div className="absolute bottom-3 left-3 flex items-center gap-1">
130
+ <Button aria-label="Attach files" disabled={disabled} size="icon-xs" variant="ghost" onClick={() => fileInputRef.current?.click()}><PaperclipIcon /></Button>
131
+ <Button aria-label="Attach image" disabled={disabled} size="icon-xs" variant="ghost" onClick={() => fileInputRef.current?.click()}><FileImageIcon /></Button>
132
+ <Button aria-label={isRecording ? 'Stop voice input' : 'Start voice input'} className={isRecording ? 'bg-destructive/10 text-destructive hover:bg-destructive/15' : undefined} disabled={disabled} size="icon-xs" variant="ghost" onClick={onVoiceToggle}><MicIcon /></Button>
133
+ </div>
134
+ <div className="absolute right-3 bottom-3 flex items-center gap-1">
135
+ {isWorking ? <Button aria-label="Steer response" disabled={!value.trim()} size="sm" variant="ghost" onClick={onSteer}>Steer{queuedSteerCount ? ` (${queuedSteerCount})` : ''}</Button> : null}
136
+ <Button aria-label={isWorking ? 'Stop response' : 'Send message'} className={isWorking ? 'shimmer-surface shimmer-surface-orange' : undefined} disabled={!isWorking && !value.trim() && !attachments.length} size="icon-sm" onClick={isWorking ? onStop : onSubmit}>{isWorking ? <SquareIcon /> : <span aria-hidden="true">↑</span>}</Button>
137
+ </div>
138
+ </div>
139
+ )
140
+ }
@@ -0,0 +1 @@
1
+ export { ChatComposer, type ChatComposerAttachment, type ChatComposerProps } from './chat-composer'
@@ -0,0 +1,59 @@
1
+ import { ArchiveIcon, PencilIcon, PinIcon, PlusIcon, SearchIcon, Trash2Icon } from 'lucide-react'
2
+ import { useMemo, useState } from 'react'
3
+
4
+ import { Button } from '../../components/button'
5
+ import { Input } from '../../components/input'
6
+ import { cn } from '../../lib/utils'
7
+
8
+ export type ChatHistoryItem = {
9
+ id: string
10
+ pinned?: boolean
11
+ subtitle?: string
12
+ title: string
13
+ }
14
+
15
+ export type ChatHistoryProps = {
16
+ activeId?: string
17
+ className?: string
18
+ emptyLabel?: string
19
+ items: readonly ChatHistoryItem[]
20
+ onArchive?: (id: string) => void
21
+ onCreate: () => void
22
+ onDelete?: (id: string) => void
23
+ onPin?: (id: string) => void
24
+ onRename?: (id: string) => void
25
+ onSelect: (id: string) => void
26
+ }
27
+
28
+ export function ChatHistory({ activeId, className, emptyLabel = 'No conversations yet', items, onArchive, onCreate, onDelete, onPin, onRename, onSelect }: ChatHistoryProps) {
29
+ const [query, setQuery] = useState('')
30
+ const filteredItems = useMemo(() => items.filter((item) => item.title.toLocaleLowerCase().includes(query.toLocaleLowerCase())), [items, query])
31
+
32
+ return (
33
+ <div className={cn('flex min-h-0 flex-1 flex-col gap-3 px-2 py-3', className)}>
34
+ <Button className="w-full" onClick={onCreate}><PlusIcon />New conversation</Button>
35
+ <div className="relative">
36
+ <SearchIcon aria-hidden="true" className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground" />
37
+ <Input aria-label="Search conversation history" className="pl-8" placeholder="Search history" value={query} onChange={(event) => setQuery(event.target.value)} />
38
+ </div>
39
+ <div className="min-h-0 flex-1 overflow-y-auto">
40
+ <div className="flex flex-col gap-1">
41
+ {filteredItems.map((item) => (
42
+ <div key={item.id} className="group/history-item relative overflow-hidden">
43
+ <Button className="w-full justify-start truncate" size="sm" variant={item.id === activeId ? 'secondary' : 'ghost'} onClick={() => onSelect(item.id)}>{item.pinned ? <PinIcon className="size-3" /> : null}{item.title}</Button>
44
+ {onRename || onPin || onArchive || onDelete ? (
45
+ <div className="invisible absolute top-1/2 right-0 flex -translate-y-1/2 items-center rounded-sm bg-background/95 opacity-0 transition-opacity group-hover/history-item:visible group-hover/history-item:opacity-100 group-focus-within/history-item:visible group-focus-within/history-item:opacity-100">
46
+ {onRename ? <Button aria-label={`Rename ${item.title}`} size="icon-xs" variant="ghost" onClick={() => onRename(item.id)}><PencilIcon /></Button> : null}
47
+ {onPin ? <Button aria-label={`${item.pinned ? 'Unpin' : 'Pin'} ${item.title}`} size="icon-xs" variant="ghost" onClick={() => onPin(item.id)}><PinIcon /></Button> : null}
48
+ {onArchive ? <Button aria-label={`Archive ${item.title}`} size="icon-xs" variant="ghost" onClick={() => onArchive(item.id)}><ArchiveIcon /></Button> : null}
49
+ {onDelete ? <Button aria-label={`Delete ${item.title}`} size="icon-xs" variant="ghost" onClick={() => onDelete(item.id)}><Trash2Icon /></Button> : null}
50
+ </div>
51
+ ) : null}
52
+ </div>
53
+ ))}
54
+ {!filteredItems.length ? <p className="px-2 py-4 text-center text-xs text-muted-foreground">{emptyLabel}</p> : null}
55
+ </div>
56
+ </div>
57
+ </div>
58
+ )
59
+ }
@@ -0,0 +1 @@
1
+ export { ChatHistory, type ChatHistoryItem, type ChatHistoryProps } from './chat-history'
@@ -0,0 +1,34 @@
1
+ import { Badge } from '../../components/badge'
2
+ import { Separator } from '../../components/separator'
3
+
4
+ export type ChatResponseProgressProps = {
5
+ elapsedSeconds: number
6
+ label?: string
7
+ tone?: 'default' | 'orange'
8
+ }
9
+
10
+ export type ChatDateDividerProps = {
11
+ label: string
12
+ }
13
+
14
+ export function ChatDateDivider({ label }: ChatDateDividerProps) {
15
+ return (
16
+ <div className="flex items-center gap-3 py-2">
17
+ <Separator className="w-auto flex-1 basis-0 border-t border-muted-foreground/35 bg-transparent" />
18
+ <Badge className="rounded-sm px-2 text-muted-foreground" variant="outline">{label}</Badge>
19
+ <Separator className="w-auto flex-1 basis-0 border-t border-muted-foreground/35 bg-transparent" />
20
+ </div>
21
+ )
22
+ }
23
+
24
+ export function ChatResponseProgress({ elapsedSeconds, label = 'Working', tone = 'default' }: ChatResponseProgressProps) {
25
+ return (
26
+ <div aria-live="polite" className="py-0 text-left">
27
+ <span className={`shimmer text-xs font-medium ${tone === 'orange' ? 'shimmer-orange' : ''}`}>{label} for {elapsedSeconds}s</span>
28
+ </div>
29
+ )
30
+ }
31
+
32
+ export function ChatTurnDivider() {
33
+ return <Separator className="w-full border-t border-muted-foreground/20 bg-transparent" />
34
+ }
@@ -0,0 +1,2 @@
1
+ export { ChatDateDivider, ChatResponseProgress, ChatTurnDivider } from './chat-response-progress'
2
+ export type { ChatDateDividerProps, ChatResponseProgressProps } from './chat-response-progress'
@@ -0,0 +1,56 @@
1
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/select'
2
+ import { Separator } from '../../components/separator'
3
+ import { Button } from '../../components/button'
4
+ import { RefreshCwIcon } from 'lucide-react'
5
+
6
+ export type ChatRuntimeControlsProps = {
7
+ connected: boolean
8
+ message: string
9
+ model: string
10
+ models: readonly string[]
11
+ onModelChange: (value: string) => void
12
+ onProviderChange: (value: string) => void
13
+ onReasoningChange: (value: string) => void
14
+ onReconnect: () => void
15
+ provider: string
16
+ providers: readonly string[]
17
+ reasoning: string
18
+ reasoningLevels: readonly string[]
19
+ }
20
+
21
+ export function ChatRuntimeControls({
22
+ connected,
23
+ message,
24
+ model,
25
+ models,
26
+ onModelChange,
27
+ onProviderChange,
28
+ onReasoningChange,
29
+ onReconnect,
30
+ provider,
31
+ providers,
32
+ reasoning,
33
+ reasoningLevels,
34
+ }: ChatRuntimeControlsProps) {
35
+ return (
36
+ <div className="flex shrink-0 items-center gap-2">
37
+ <RuntimeSelect ariaLabel="Provider" disabled={!connected} items={providers} onValueChange={onProviderChange} value={provider} />
38
+ <Separator className="-mx-1 h-4 bg-muted-foreground/40" orientation="vertical" />
39
+ <RuntimeSelect ariaLabel="Model" disabled={!connected} items={models} onValueChange={onModelChange} value={model} />
40
+ <Separator className="-mx-1 h-4 bg-muted-foreground/40" orientation="vertical" />
41
+ <RuntimeSelect ariaLabel="Reasoning" disabled={!connected} items={reasoningLevels} onValueChange={onReasoningChange} value={reasoning} />
42
+ {connected ? <span aria-label="Connected to local Codex" className="size-2.5 rounded-full bg-green-500 shadow-[0_0_9px_2px_rgb(34_197_94_/_0.7)]" role="status" title={message} /> : <Button aria-label="Reconnect to local Codex" className="h-7 gap-1 rounded-sm px-2 text-xs" size="sm" variant="outline" onClick={onReconnect}><RefreshCwIcon className="size-3" />Reconnect</Button>}
43
+ </div>
44
+ )
45
+ }
46
+
47
+ function RuntimeSelect({ ariaLabel, disabled, items, onValueChange, value }: { ariaLabel: string; disabled: boolean; items: readonly string[]; onValueChange: (value: string) => void; value: string }) {
48
+ return (
49
+ <Select disabled={disabled} onValueChange={(nextValue) => { if (nextValue) onValueChange(nextValue) }} value={value}>
50
+ <SelectTrigger aria-label={ariaLabel} className="max-w-32 rounded-sm border-transparent bg-transparent px-2 hover:bg-accent" size="sm"><SelectValue /></SelectTrigger>
51
+ <SelectContent align="end" alignItemWithTrigger={false} className="rounded-sm" side="bottom" sideOffset={6}>
52
+ {items.map((item) => <SelectItem key={item} value={item}>{item}</SelectItem>)}
53
+ </SelectContent>
54
+ </Select>
55
+ )
56
+ }
@@ -0,0 +1,2 @@
1
+ export { ChatRuntimeControls } from './chat-runtime-controls'
2
+ export type { ChatRuntimeControlsProps } from './chat-runtime-controls'
@@ -0,0 +1,71 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { CircleAlertIcon, CircleCheckIcon, EyeIcon, FilePenLineIcon, LoaderCircleIcon, MessageSquareIcon, SearchIcon, TerminalIcon } from 'lucide-react'
3
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../components/collapsible'
4
+ import { cn } from '../../lib/utils'
5
+
6
+ export type ChatRuntimeTraceEvent = {
7
+ message: string
8
+ raw?: string
9
+ type: string
10
+ }
11
+
12
+ export type ChatRuntimeTraceProps = {
13
+ className?: string
14
+ elapsedSeconds: number
15
+ events: readonly ChatRuntimeTraceEvent[]
16
+ isWorking: boolean
17
+ }
18
+
19
+ export function ChatRuntimeTrace({ className, elapsedSeconds, events, isWorking }: ChatRuntimeTraceProps) {
20
+ const [open, setOpen] = useState(isWorking)
21
+ const latestEvent = events.at(-1)
22
+
23
+ useEffect(() => setOpen(isWorking), [isWorking])
24
+
25
+ if (!events.length) return null
26
+
27
+ return (
28
+ <Collapsible className={cn("w-full shrink-0 border-y border-border/50", className)} open={open} onOpenChange={setOpen}>
29
+ <CollapsibleTrigger className="group/runtime-trace flex w-full items-center gap-2 py-2 text-left text-xs text-muted-foreground hover:text-foreground">
30
+ <TerminalIcon className="size-3 shrink-0" />
31
+ <span className={isWorking ? 'shimmer shimmer-orange font-medium' : 'font-medium'}>{isWorking ? `${phaseLabel(latestEvent?.type)} · Working for ${elapsedSeconds}s` : `Completed · ${events.length} visible events`}</span>
32
+ <span className="ml-auto">{events.length} events</span>
33
+ </CollapsibleTrigger>
34
+ <CollapsibleContent className="pb-2">
35
+ <div className="max-h-44 space-y-1 overflow-y-auto pr-1 [scrollbar-color:var(--border)_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/70 [&::-webkit-scrollbar]:w-1">
36
+ {events.map((event, index) => <TraceEvent key={`${event.raw ?? event.message}-${index}`} event={event} />)}
37
+ </div>
38
+ </CollapsibleContent>
39
+ </Collapsible>
40
+ )
41
+ }
42
+
43
+ function phaseLabel(type?: string): string {
44
+ if (type === 'command') return 'Running read-only command'
45
+ if (type === 'request') return 'Checking sources'
46
+ if (type === 'review') return 'Reviewing context'
47
+ if (type === 'change') return 'Recording file activity'
48
+ return 'Compacting auto'
49
+ }
50
+
51
+ function TraceEvent({ event }: { event: ChatRuntimeTraceEvent }) {
52
+ const [open, setOpen] = useState(false)
53
+ const content = event.raw ?? event.message
54
+
55
+ return (
56
+ <Collapsible open={open} onOpenChange={setOpen}>
57
+ <CollapsibleTrigger className="group/runtime-event flex w-full items-center gap-2 rounded-sm px-1 py-1 text-left text-xs text-muted-foreground hover:bg-muted/50 hover:text-foreground">
58
+ <EventIcon type={event.type} />
59
+ <span className="truncate">{event.message}</span>
60
+ </CollapsibleTrigger>
61
+ <CollapsibleContent className="px-1 pb-1">
62
+ <pre className="max-h-32 overflow-auto rounded-sm bg-muted/50 p-2 font-mono text-[11px] leading-4 text-foreground [scrollbar-color:var(--border)_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/70 [&::-webkit-scrollbar]:h-1 [&::-webkit-scrollbar]:w-1">{content}</pre>
63
+ </CollapsibleContent>
64
+ </Collapsible>
65
+ )
66
+ }
67
+
68
+ function EventIcon({ type }: { type: string }) {
69
+ const Icon = type === 'command' ? TerminalIcon : type === 'response' ? MessageSquareIcon : type === 'request' ? SearchIcon : type === 'review' ? EyeIcon : type === 'change' ? FilePenLineIcon : type === 'complete' ? CircleCheckIcon : type === 'error' ? CircleAlertIcon : LoaderCircleIcon
70
+ return <Icon aria-label={type} className={cn('size-3 shrink-0', type === 'processing' && 'animate-spin')} />
71
+ }
@@ -0,0 +1,2 @@
1
+ export { ChatRuntimeTrace } from './chat-runtime-trace'
2
+ export type { ChatRuntimeTraceEvent, ChatRuntimeTraceProps } from './chat-runtime-trace'
@@ -0,0 +1,42 @@
1
+ import { type ReactNode, useState } from "react";
2
+ import { CopyIcon, ExternalLinkIcon, KeyRoundIcon, LaptopIcon, RefreshCwIcon, ShieldCheckIcon } from "lucide-react";
3
+ import { Alert, AlertDescription, AlertTitle } from "@devxcrew/react-ui/components/alert";
4
+ import { Button } from "@devxcrew/react-ui/components/button";
5
+ import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@devxcrew/react-ui/components/sheet";
6
+ import { Switch } from "@devxcrew/react-ui/components/switch";
7
+
8
+ export type CodexDeviceCode = { message: string; status: "idle" | "awaiting" | "connected" | "failed"; userCode?: string; verificationUrl?: string };
9
+ export type CodexConnectionSettingsProps = { connected: boolean; deviceCode: CodexDeviceCode; message: string; onConnectLocal: () => void; onCopyCode: () => void; onCopyUrl: () => void; onGenerateDeviceCode: () => void; onOpenBrowser: () => void; onOpenChange: (open: boolean) => void; open: boolean };
10
+
11
+ export function CodexConnectionSettings({ connected, deviceCode, message, onConnectLocal, onCopyCode, onCopyUrl, onGenerateDeviceCode, onOpenBrowser, onOpenChange, open }: CodexConnectionSettingsProps) {
12
+ const [useDeviceCode, setUseDeviceCode] = useState(false);
13
+ const [checkingLocal, setCheckingLocal] = useState(false);
14
+ const awaitingCode = deviceCode.status === "awaiting";
15
+
16
+ async function checkLocalCodex(): Promise<void> {
17
+ setCheckingLocal(true);
18
+ try {
19
+ await onConnectLocal();
20
+ } finally {
21
+ setCheckingLocal(false);
22
+ }
23
+ }
24
+
25
+ return <Sheet open={open} onOpenChange={onOpenChange}><SheetContent className="gap-0 p-0 sm:max-w-md" side="right">
26
+ <SheetHeader className="border-b pr-12"><SheetTitle>Codex connection</SheetTitle><SheetDescription>Choose how this Zetro workspace connects to Codex.</SheetDescription></SheetHeader>
27
+ <div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4">
28
+ <section className="flex items-center gap-3" aria-live="polite"><span className={connected ? "size-2 shrink-0 rounded-full bg-emerald-500 shadow-[0_0_0_4px_rgb(16_185_129_/_0.13),0_0_12px_rgb(16_185_129_/_0.8)]" : "size-2 shrink-0 rounded-full bg-muted-foreground/50"} /><div className="min-w-0"><p className="text-sm font-medium">{connected ? "Connected" : "Not connected"}</p><p className="mt-0.5 text-sm text-muted-foreground">{message}</p></div></section>
29
+ <ConnectionMethod title="Local Codex" description="Use the Codex CLI already installed on this computer." icon={<LaptopIcon className="size-4" />}><Button disabled={checkingLocal} variant="outline" onClick={checkLocalCodex}><RefreshCwIcon className={checkingLocal ? "animate-spin" : undefined} /> {checkingLocal ? "Checking local Codex" : "Check local Codex"}</Button></ConnectionMethod>
30
+ <ConnectionMethod title="Device code" description="Sign in using a short-lived code in your browser." icon={<KeyRoundIcon className="size-4" />} trailing={<Switch aria-label="Use device-code connection" checked={useDeviceCode} onCheckedChange={setUseDeviceCode} />}>
31
+ <Button disabled={!useDeviceCode} onClick={onGenerateDeviceCode}><KeyRoundIcon /> Generate device code</Button>
32
+ </ConnectionMethod>
33
+ {useDeviceCode && awaitingCode ? <div className="flex flex-col gap-3 rounded-md border border-border/70 p-3"><div className="flex items-center gap-2"><p className="flex-1 text-sm font-medium">One-time device code</p><Button aria-label="Copy device code" size="icon-xs" variant="ghost" onClick={onCopyCode}><CopyIcon /></Button></div><code className="w-fit rounded-sm border bg-muted px-3 py-2 text-base font-semibold tracking-[0.12em]">{deviceCode.userCode}</code><p className="text-sm text-muted-foreground">{deviceCode.message}</p><div className="flex flex-wrap gap-2"><Button variant="outline" onClick={onOpenBrowser}><ExternalLinkIcon /> Open browser</Button><Button variant="outline" onClick={onCopyUrl}><CopyIcon /> Copy URL</Button></div><p className="text-sm leading-6 text-muted-foreground">This code is held only in running memory and is not added to Zetro history, storage, or logs.</p></div> : null}
34
+ {deviceCode.status === "failed" ? <Alert variant="destructive"><ShieldCheckIcon /><AlertTitle>Device code unavailable</AlertTitle><AlertDescription>{deviceCode.message}</AlertDescription></Alert> : null}
35
+ </div>
36
+ <SheetFooter className="border-t"><Button className="w-full" disabled={checkingLocal} variant="outline" onClick={checkLocalCodex}><RefreshCwIcon className={checkingLocal ? "animate-spin" : undefined} /> {checkingLocal ? "Checking local Codex" : "Recheck local Codex"}</Button></SheetFooter>
37
+ </SheetContent></Sheet>;
38
+ }
39
+
40
+ function ConnectionMethod({ children, description, icon, title, trailing }: { children: ReactNode; description: string; icon: ReactNode; title: string; trailing?: ReactNode }) {
41
+ return <section className="flex flex-col gap-3 rounded-md border border-border/70 p-3"><div className="flex items-start gap-2"><span className="mt-0.5 text-muted-foreground">{icon}</span><div className="min-w-0 flex-1"><p className="text-sm font-medium">{title}</p><p className="mt-0.5 text-sm leading-5 text-muted-foreground">{description}</p></div>{trailing}</div>{children}</section>;
42
+ }
@@ -0,0 +1 @@
1
+ export { CodexConnectionSettings, type CodexConnectionSettingsProps, type CodexDeviceCode } from "./codex-connection-settings";
@@ -0,0 +1,113 @@
1
+ import { AlertCircle, CheckCircle2, File, RefreshCw, X } from 'lucide-react'
2
+ import { cn } from '../../lib/utils'
3
+ import type { DropzoneFile } from './dropzone-types'
4
+
5
+ export function formatBytes(bytes: number): string {
6
+ if (bytes === 0) return '0 B'
7
+ const k = 1024
8
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
9
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
10
+ return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
11
+ }
12
+
13
+ export function DropzoneFileItem({
14
+ file,
15
+ onRemove,
16
+ onRetry,
17
+ }: {
18
+ file: DropzoneFile
19
+ onRemove?: (id: string) => void
20
+ onRetry?: (id: string) => void
21
+ }) {
22
+ const isUploading = file.status === 'uploading'
23
+ const isComplete = file.status === 'complete'
24
+ const isError = file.status === 'error'
25
+
26
+ return (
27
+ <div
28
+ className={cn(
29
+ 'group flex flex-col gap-1.5 rounded-xl border border-border/80 bg-card p-3 shadow-2xs transition-colors',
30
+ isError && 'border-destructive/40 bg-destructive/5',
31
+ isComplete && 'border-emerald-500/30 bg-emerald-500/5',
32
+ )}
33
+ >
34
+ <div className="flex items-center justify-between gap-3">
35
+ <div className="flex min-w-0 items-center gap-2.5">
36
+ {file.previewUrl ? (
37
+ <img
38
+ alt={file.name}
39
+ className="size-8 rounded-lg object-cover ring-1 ring-border"
40
+ src={file.previewUrl}
41
+ />
42
+ ) : (
43
+ <div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
44
+ <File className="size-4" />
45
+ </div>
46
+ )}
47
+
48
+ <div className="min-w-0">
49
+ <p className="truncate text-xs font-medium text-foreground">{file.name}</p>
50
+ <p className="text-[11px] text-muted-foreground">{formatBytes(file.sizeBytes)}</p>
51
+ </div>
52
+ </div>
53
+
54
+ <div className="flex items-center gap-1">
55
+ {isComplete && (
56
+ <span title="Upload complete">
57
+ <CheckCircle2 className="size-4 text-emerald-500" />
58
+ </span>
59
+ )}
60
+
61
+ {isError && (
62
+ <>
63
+ <span title="Upload failed">
64
+ <AlertCircle className="size-4 text-destructive" />
65
+ </span>
66
+ {onRetry && (
67
+ <button
68
+ aria-label="Retry upload"
69
+ className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
70
+ type="button"
71
+ onClick={() => onRetry(file.id)}
72
+ >
73
+ <RefreshCw className="size-3.5" />
74
+ </button>
75
+ )}
76
+ </>
77
+ )}
78
+
79
+ {onRemove && (
80
+ <button
81
+ aria-label={`Remove ${file.name}`}
82
+ className="rounded p-1 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
83
+ type="button"
84
+ onClick={() => onRemove(file.id)}
85
+ >
86
+ <X className="size-3.5" />
87
+ </button>
88
+ )}
89
+ </div>
90
+ </div>
91
+
92
+ {isUploading && (
93
+ <div
94
+ aria-label={`Uploading ${file.name}`}
95
+ aria-valuemax={100}
96
+ aria-valuemin={0}
97
+ aria-valuenow={file.progress ?? 0}
98
+ className="relative h-1.5 w-full overflow-hidden rounded-full bg-muted"
99
+ role="progressbar"
100
+ >
101
+ <div
102
+ className="h-full rounded-full bg-primary transition-all duration-300"
103
+ style={{ width: `${Math.min(100, Math.max(0, file.progress ?? 0))}%` }}
104
+ />
105
+ </div>
106
+ )}
107
+
108
+ {isError && file.errorMessage && (
109
+ <p className="text-[11px] font-medium text-destructive">{file.errorMessage}</p>
110
+ )}
111
+ </div>
112
+ )
113
+ }
@@ -0,0 +1,32 @@
1
+ export type DropzoneFileStatus = 'idle' | 'uploading' | 'complete' | 'error'
2
+
3
+ export interface DropzoneFile {
4
+ errorMessage?: string
5
+ file?: File
6
+ id: string
7
+ name: string
8
+ previewUrl?: string
9
+ progress?: number
10
+ sizeBytes: number
11
+ status: DropzoneFileStatus
12
+ type: string
13
+ }
14
+
15
+ export interface DropzoneProps {
16
+ accept?: readonly string[]
17
+ allowMultiple?: boolean
18
+ ariaLabel?: string
19
+ className?: string
20
+ disabled?: boolean
21
+ emptyMessage?: string
22
+ files?: readonly DropzoneFile[]
23
+ hint?: string
24
+ maxFiles?: number
25
+ maxSizeBytes?: number
26
+ onClearFiles?: () => void
27
+ onFilesDrop?: (acceptedFiles: File[], rejectedFiles: { error: string; file: File }[]) => void
28
+ onRemoveFile?: (fileId: string) => void
29
+ onRetryFile?: (fileId: string) => void
30
+ showFileList?: boolean
31
+ title?: string
32
+ }