@locusai/web 0.1.7 → 0.2.2

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 (346) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/next.config.js +15 -2
  3. package/package.json +26 -3
  4. package/src/app/(auth)/invite/page.tsx +109 -0
  5. package/src/app/(auth)/layout.tsx +19 -0
  6. package/src/app/(auth)/login/page.tsx +65 -0
  7. package/src/app/(auth)/onboarding/workspace/page.tsx +46 -0
  8. package/src/app/(auth)/register/page.tsx +165 -0
  9. package/src/app/(dashboard)/activity/page.tsx +7 -0
  10. package/src/app/(dashboard)/backlog/page.tsx +195 -0
  11. package/src/app/(dashboard)/board/page.tsx +141 -0
  12. package/src/app/(dashboard)/layout.tsx +32 -0
  13. package/src/app/(dashboard)/page.tsx +14 -0
  14. package/src/app/(dashboard)/settings/page.tsx +161 -0
  15. package/src/app/(dashboard)/settings/team/page.tsx +75 -0
  16. package/src/app/globals.css +259 -0
  17. package/src/app/layout.tsx +10 -20
  18. package/src/app/providers.tsx +26 -3
  19. package/src/components/AuthLayoutUI.tsx +53 -0
  20. package/src/components/BoardFilter.tsx +75 -74
  21. package/src/components/CreateModal/CreateModal.tsx +142 -0
  22. package/src/components/CreateModal/index.ts +1 -0
  23. package/src/components/Editor.tsx +279 -0
  24. package/src/components/Header.tsx +99 -12
  25. package/src/components/PageLayout.tsx +69 -0
  26. package/src/components/PropertyItem.tsx +55 -9
  27. package/src/components/Sidebar.tsx +280 -36
  28. package/src/components/SprintCreateModal.tsx +84 -0
  29. package/src/components/TaskCard.tsx +196 -78
  30. package/src/components/TaskCreateModal.tsx +181 -178
  31. package/src/components/TaskPanel.tsx +140 -692
  32. package/src/components/WorkspaceCreateModal.tsx +97 -0
  33. package/src/components/WorkspaceProtected.tsx +91 -0
  34. package/src/components/auth/InviteSteps.tsx +220 -0
  35. package/src/components/auth/LoginSteps.tsx +86 -0
  36. package/src/components/auth/RegisterSteps.tsx +371 -0
  37. package/src/components/auth/index.ts +3 -0
  38. package/src/components/backlog/BacklogList.tsx +92 -0
  39. package/src/components/backlog/BacklogSection.tsx +137 -0
  40. package/src/components/backlog/CompletedSprintItem.tsx +95 -0
  41. package/src/components/backlog/CompletedSprintsSection.tsx +77 -0
  42. package/src/components/backlog/SprintSection.tsx +155 -0
  43. package/src/components/backlog/constants.ts +26 -0
  44. package/src/components/board/BoardColumn.tsx +97 -0
  45. package/src/components/board/BoardContent.tsx +84 -0
  46. package/src/components/board/BoardEmptyState.tsx +82 -0
  47. package/src/components/board/BoardHeader.tsx +47 -0
  48. package/src/components/board/SprintMindmap.tsx +65 -0
  49. package/src/components/board/constants.ts +40 -0
  50. package/src/components/board/index.ts +5 -0
  51. package/src/components/common/ErrorState.tsx +124 -0
  52. package/src/components/common/LoadingState.tsx +83 -0
  53. package/src/components/common/index.ts +40 -0
  54. package/src/components/dashboard/ActivityFeed.tsx +77 -0
  55. package/src/components/dashboard/ActivityItem.tsx +207 -0
  56. package/src/components/dashboard/QuickActions.tsx +50 -0
  57. package/src/components/dashboard/StatCard.tsx +79 -0
  58. package/src/components/dnd/index.tsx +51 -0
  59. package/src/components/docs/DocsEditorArea.tsx +87 -0
  60. package/src/components/docs/DocsHeaderActions.tsx +121 -0
  61. package/src/components/docs/DocsSidebar.tsx +351 -0
  62. package/src/components/index.ts +7 -0
  63. package/src/components/onboarding/StepProgress.tsx +21 -0
  64. package/src/components/onboarding/index.ts +1 -0
  65. package/src/components/settings/ApiKeyConfirmationModal.tsx +81 -0
  66. package/src/components/settings/ApiKeyCreatedModal.tsx +96 -0
  67. package/src/components/settings/ApiKeysList.tsx +143 -0
  68. package/src/components/settings/ApiKeysSettings.tsx +144 -0
  69. package/src/components/settings/InviteMemberModal.tsx +106 -0
  70. package/src/components/settings/ProjectSetupGuide.tsx +147 -0
  71. package/src/components/settings/SettingItem.tsx +32 -0
  72. package/src/components/settings/SettingSection.tsx +50 -0
  73. package/src/components/settings/TeamInvitationsList.tsx +90 -0
  74. package/src/components/settings/TeamMembersList.tsx +95 -0
  75. package/src/components/settings/index.ts +8 -0
  76. package/src/components/task-panel/TaskActivity.tsx +127 -0
  77. package/src/components/task-panel/TaskChecklist.tsx +142 -0
  78. package/src/components/task-panel/TaskDescription.tsx +201 -0
  79. package/src/components/task-panel/TaskDocs.tsx +137 -0
  80. package/src/components/task-panel/TaskHeader.tsx +125 -0
  81. package/src/components/task-panel/TaskProperties.tsx +111 -0
  82. package/src/components/task-panel/index.ts +12 -0
  83. package/src/components/typography/EmptyStateText.tsx +59 -0
  84. package/src/components/typography/MetadataText.tsx +65 -0
  85. package/src/components/typography/SecondaryText.tsx +60 -0
  86. package/src/components/typography/SectionLabel.tsx +60 -0
  87. package/src/components/typography/index.ts +14 -0
  88. package/src/components/typography-scales.tsx +218 -0
  89. package/src/components/ui/Avatar.tsx +123 -0
  90. package/src/components/ui/Badge.tsx +69 -2
  91. package/src/components/ui/Button.tsx +71 -30
  92. package/src/components/ui/Checkbox.tsx +34 -0
  93. package/src/components/ui/Dropdown.tsx +67 -1
  94. package/src/components/ui/EmptyState.tsx +129 -0
  95. package/src/components/ui/Input.tsx +53 -6
  96. package/src/components/ui/Modal.tsx +45 -12
  97. package/src/components/ui/OtpInput.tsx +148 -0
  98. package/src/components/ui/Skeleton.tsx +36 -0
  99. package/src/components/ui/Spinner.tsx +112 -0
  100. package/src/components/ui/Textarea.tsx +28 -3
  101. package/src/components/ui/Toast.tsx +99 -0
  102. package/src/components/ui/Toggle.tsx +63 -0
  103. package/src/components/ui/constants.ts +108 -0
  104. package/src/components/ui/index.ts +7 -0
  105. package/src/context/AuthContext.tsx +140 -0
  106. package/src/context/index.ts +1 -0
  107. package/src/hooks/backlog/index.ts +13 -0
  108. package/src/hooks/backlog/useBacklogActions.ts +144 -0
  109. package/src/hooks/backlog/useBacklogComposite.ts +73 -0
  110. package/src/hooks/backlog/useBacklogData.ts +74 -0
  111. package/src/hooks/backlog/useBacklogDragDrop.ts +118 -0
  112. package/src/hooks/backlog/useBacklogUI.ts +74 -0
  113. package/src/hooks/index.ts +22 -0
  114. package/src/hooks/task-panel/index.ts +13 -0
  115. package/src/hooks/task-panel/useTaskActions.ts +200 -0
  116. package/src/hooks/task-panel/useTaskComputedValues.ts +30 -0
  117. package/src/hooks/task-panel/useTaskData.ts +78 -0
  118. package/src/hooks/task-panel/useTaskPanelComposite.ts +161 -0
  119. package/src/hooks/task-panel/useTaskUIState.ts +80 -0
  120. package/src/hooks/useAuthLayoutLogic.ts +43 -0
  121. package/src/hooks/useAuthenticatedUser.ts +36 -0
  122. package/src/hooks/useAuthenticatedUserWithOrg.ts +41 -0
  123. package/src/hooks/useBacklog.ts +303 -0
  124. package/src/hooks/useBoard.ts +230 -0
  125. package/src/hooks/useDashboardLayout.ts +49 -0
  126. package/src/hooks/useDocs.ts +279 -0
  127. package/src/hooks/useDocsQuery.ts +99 -0
  128. package/src/hooks/useDocsSidebarState.ts +104 -0
  129. package/src/hooks/useFormState.ts +40 -0
  130. package/src/hooks/useGlobalKeydowns.ts +52 -0
  131. package/src/hooks/useInviteForm.ts +122 -0
  132. package/src/hooks/useLoginForm.ts +84 -0
  133. package/src/hooks/useMutationWithToast.ts +56 -0
  134. package/src/hooks/useOrganizationQuery.ts +55 -0
  135. package/src/hooks/useRegisterForm.ts +216 -0
  136. package/src/hooks/useSprintsQuery.ts +38 -0
  137. package/src/hooks/useTaskDescription.ts +102 -0
  138. package/src/hooks/useTaskPanel.ts +341 -0
  139. package/src/hooks/useTasksQuery.ts +39 -0
  140. package/src/hooks/useTeamManagement.ts +92 -0
  141. package/src/hooks/useWorkspaceCreateForm.ts +70 -0
  142. package/src/hooks/useWorkspaceId.ts +29 -0
  143. package/src/lib/api-client.ts +40 -23
  144. package/src/lib/config.ts +25 -0
  145. package/src/lib/constants.ts +83 -0
  146. package/src/lib/options.ts +96 -0
  147. package/src/lib/query-keys.ts +91 -0
  148. package/src/lib/typography.ts +103 -0
  149. package/src/lib/utils.ts +4 -0
  150. package/src/lib/validation.ts +192 -0
  151. package/src/services/index.ts +7 -3
  152. package/src/services/notifications.ts +80 -0
  153. package/src/utils/env.utils.ts +15 -0
  154. package/src/views/ActivityView.tsx +123 -0
  155. package/src/views/Dashboard.tsx +126 -0
  156. package/src/views/Docs.tsx +98 -612
  157. package/tsconfig.tsbuildinfo +1 -1
  158. package/.next/BUILD_ID +0 -1
  159. package/.next/app-build-manifest.json +0 -55
  160. package/.next/app-path-routes-manifest.json +0 -8
  161. package/.next/build-manifest.json +0 -33
  162. package/.next/cache/.previewinfo +0 -1
  163. package/.next/cache/.rscinfo +0 -1
  164. package/.next/cache/.tsbuildinfo +0 -1
  165. package/.next/cache/config.json +0 -7
  166. package/.next/cache/webpack/client-production/0.pack +0 -0
  167. package/.next/cache/webpack/client-production/index.pack +0 -0
  168. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  169. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  170. package/.next/cache/webpack/server-production/0.pack +0 -0
  171. package/.next/cache/webpack/server-production/index.pack +0 -0
  172. package/.next/diagnostics/build-diagnostics.json +0 -6
  173. package/.next/diagnostics/framework.json +0 -1
  174. package/.next/export-detail.json +0 -5
  175. package/.next/export-marker.json +0 -6
  176. package/.next/images-manifest.json +0 -57
  177. package/.next/next-minimal-server.js.nft.json +0 -1
  178. package/.next/next-server.js.nft.json +0 -1
  179. package/.next/package.json +0 -1
  180. package/.next/prerender-manifest.json +0 -137
  181. package/.next/react-loadable-manifest.json +0 -32
  182. package/.next/required-server-files.json +0 -324
  183. package/.next/routes-manifest.json +0 -77
  184. package/.next/server/app/_not-found/page.js +0 -2
  185. package/.next/server/app/_not-found/page.js.nft.json +0 -1
  186. package/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
  187. package/.next/server/app/_not-found.html +0 -1
  188. package/.next/server/app/_not-found.meta +0 -8
  189. package/.next/server/app/_not-found.rsc +0 -21
  190. package/.next/server/app/backlog/page.js +0 -2
  191. package/.next/server/app/backlog/page.js.nft.json +0 -1
  192. package/.next/server/app/backlog/page_client-reference-manifest.js +0 -1
  193. package/.next/server/app/backlog.html +0 -1
  194. package/.next/server/app/backlog.meta +0 -7
  195. package/.next/server/app/backlog.rsc +0 -25
  196. package/.next/server/app/docs/page.js +0 -97
  197. package/.next/server/app/docs/page.js.nft.json +0 -1
  198. package/.next/server/app/docs/page_client-reference-manifest.js +0 -1
  199. package/.next/server/app/docs.html +0 -1
  200. package/.next/server/app/docs.meta +0 -7
  201. package/.next/server/app/docs.rsc +0 -26
  202. package/.next/server/app/favicon.ico/route.js +0 -1
  203. package/.next/server/app/favicon.ico/route.js.nft.json +0 -1
  204. package/.next/server/app/favicon.ico.body +0 -0
  205. package/.next/server/app/favicon.ico.meta +0 -1
  206. package/.next/server/app/index.html +0 -1
  207. package/.next/server/app/index.meta +0 -7
  208. package/.next/server/app/index.rsc +0 -25
  209. package/.next/server/app/page.js +0 -2
  210. package/.next/server/app/page.js.nft.json +0 -1
  211. package/.next/server/app/page_client-reference-manifest.js +0 -1
  212. package/.next/server/app/settings/page.js +0 -2
  213. package/.next/server/app/settings/page.js.nft.json +0 -1
  214. package/.next/server/app/settings/page_client-reference-manifest.js +0 -1
  215. package/.next/server/app/settings.html +0 -1
  216. package/.next/server/app/settings.meta +0 -7
  217. package/.next/server/app/settings.rsc +0 -25
  218. package/.next/server/app-paths-manifest.json +0 -8
  219. package/.next/server/chunks/496.js +0 -6
  220. package/.next/server/chunks/585.js +0 -1
  221. package/.next/server/chunks/665.js +0 -1
  222. package/.next/server/chunks/699.js +0 -22
  223. package/.next/server/chunks/852.js +0 -7
  224. package/.next/server/functions-config-manifest.json +0 -4
  225. package/.next/server/interception-route-rewrite-manifest.js +0 -1
  226. package/.next/server/middleware-build-manifest.js +0 -1
  227. package/.next/server/middleware-manifest.json +0 -6
  228. package/.next/server/middleware-react-loadable-manifest.js +0 -1
  229. package/.next/server/next-font-manifest.js +0 -1
  230. package/.next/server/next-font-manifest.json +0 -1
  231. package/.next/server/pages/404.html +0 -1
  232. package/.next/server/pages/500.html +0 -1
  233. package/.next/server/pages/_app.js +0 -1
  234. package/.next/server/pages/_app.js.nft.json +0 -1
  235. package/.next/server/pages/_document.js +0 -1
  236. package/.next/server/pages/_document.js.nft.json +0 -1
  237. package/.next/server/pages/_error.js +0 -19
  238. package/.next/server/pages/_error.js.nft.json +0 -1
  239. package/.next/server/pages-manifest.json +0 -6
  240. package/.next/server/server-reference-manifest.js +0 -1
  241. package/.next/server/server-reference-manifest.json +0 -1
  242. package/.next/server/webpack-runtime.js +0 -1
  243. package/.next/static/D0NXe04ZCLNDckV_quc8g/_buildManifest.js +0 -1
  244. package/.next/static/D0NXe04ZCLNDckV_quc8g/_ssgManifest.js +0 -1
  245. package/.next/static/chunks/138.b98511c56423f8bb.js +0 -1
  246. package/.next/static/chunks/146-34259952c594a3b0.js +0 -1
  247. package/.next/static/chunks/337-d3bb75304d130513.js +0 -1
  248. package/.next/static/chunks/477.1a6ecfe53375bd9c.js +0 -1
  249. package/.next/static/chunks/487-1808785ba665f784.js +0 -1
  250. package/.next/static/chunks/544.a9569941cc886e9d.js +0 -1
  251. package/.next/static/chunks/87c73c54-1f4741035a95c140.js +0 -1
  252. package/.next/static/chunks/902-d6926825a9fe8784.js +0 -1
  253. package/.next/static/chunks/955-c8f8f6235ae8f8c6.js +0 -1
  254. package/.next/static/chunks/996.e0a334e6ae90900e.js +0 -1
  255. package/.next/static/chunks/app/_not-found/page-44b1804abb44a34d.js +0 -1
  256. package/.next/static/chunks/app/backlog/page-dce1450769bfae8f.js +0 -1
  257. package/.next/static/chunks/app/docs/page-1efee819f25492cb.js +0 -1
  258. package/.next/static/chunks/app/layout-05f504c042b9f7ee.js +0 -1
  259. package/.next/static/chunks/app/page-3fd91aaaa4776ced.js +0 -1
  260. package/.next/static/chunks/app/settings/page-84e16c9638d657e4.js +0 -1
  261. package/.next/static/chunks/framework-152a1bc8c81c7458.js +0 -1
  262. package/.next/static/chunks/main-843ab130fc1be309.js +0 -1
  263. package/.next/static/chunks/main-app-123e879c5a937a00.js +0 -1
  264. package/.next/static/chunks/pages/_app-a050a8e6e4fb04cf.js +0 -1
  265. package/.next/static/chunks/pages/_error-3e422ffd891594de.js +0 -1
  266. package/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  267. package/.next/static/chunks/webpack-99a10a055b5bb9c4.js +0 -1
  268. package/.next/static/css/13e8617b72f9d3aa.css +0 -1
  269. package/.next/static/css/8aea088cdc4338f0.css +0 -1
  270. package/.next/static/css/b301ab0424111664.css +0 -1
  271. package/.next/static/media/24c15609eaa28576-s.woff2 +0 -0
  272. package/.next/static/media/2c07349e02a7b712-s.woff2 +0 -0
  273. package/.next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
  274. package/.next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
  275. package/.next/static/media/4f77bef990aad698-s.woff2 +0 -0
  276. package/.next/static/media/627d916fd739a539-s.woff2 +0 -0
  277. package/.next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
  278. package/.next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
  279. package/.next/static/media/84602850c8fd81c3-s.woff2 +0 -0
  280. package/.next/trace +0 -46
  281. package/.next/types/app/backlog/page.ts +0 -84
  282. package/.next/types/app/docs/page.ts +0 -84
  283. package/.next/types/app/layout.ts +0 -84
  284. package/.next/types/app/page.ts +0 -84
  285. package/.next/types/app/settings/page.ts +0 -84
  286. package/.next/types/cache-life.d.ts +0 -141
  287. package/.next/types/package.json +0 -1
  288. package/next-env.d.ts +0 -5
  289. package/out/404.html +0 -1
  290. package/out/_next/static/D0NXe04ZCLNDckV_quc8g/_buildManifest.js +0 -1
  291. package/out/_next/static/D0NXe04ZCLNDckV_quc8g/_ssgManifest.js +0 -1
  292. package/out/_next/static/chunks/138.b98511c56423f8bb.js +0 -1
  293. package/out/_next/static/chunks/146-34259952c594a3b0.js +0 -1
  294. package/out/_next/static/chunks/337-d3bb75304d130513.js +0 -1
  295. package/out/_next/static/chunks/477.1a6ecfe53375bd9c.js +0 -1
  296. package/out/_next/static/chunks/487-1808785ba665f784.js +0 -1
  297. package/out/_next/static/chunks/544.a9569941cc886e9d.js +0 -1
  298. package/out/_next/static/chunks/87c73c54-1f4741035a95c140.js +0 -1
  299. package/out/_next/static/chunks/902-d6926825a9fe8784.js +0 -1
  300. package/out/_next/static/chunks/955-c8f8f6235ae8f8c6.js +0 -1
  301. package/out/_next/static/chunks/996.e0a334e6ae90900e.js +0 -1
  302. package/out/_next/static/chunks/app/_not-found/page-44b1804abb44a34d.js +0 -1
  303. package/out/_next/static/chunks/app/backlog/page-dce1450769bfae8f.js +0 -1
  304. package/out/_next/static/chunks/app/docs/page-1efee819f25492cb.js +0 -1
  305. package/out/_next/static/chunks/app/layout-05f504c042b9f7ee.js +0 -1
  306. package/out/_next/static/chunks/app/page-3fd91aaaa4776ced.js +0 -1
  307. package/out/_next/static/chunks/app/settings/page-84e16c9638d657e4.js +0 -1
  308. package/out/_next/static/chunks/framework-152a1bc8c81c7458.js +0 -1
  309. package/out/_next/static/chunks/main-843ab130fc1be309.js +0 -1
  310. package/out/_next/static/chunks/main-app-123e879c5a937a00.js +0 -1
  311. package/out/_next/static/chunks/pages/_app-a050a8e6e4fb04cf.js +0 -1
  312. package/out/_next/static/chunks/pages/_error-3e422ffd891594de.js +0 -1
  313. package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  314. package/out/_next/static/chunks/webpack-99a10a055b5bb9c4.js +0 -1
  315. package/out/_next/static/css/13e8617b72f9d3aa.css +0 -1
  316. package/out/_next/static/css/8aea088cdc4338f0.css +0 -1
  317. package/out/_next/static/css/b301ab0424111664.css +0 -1
  318. package/out/_next/static/media/24c15609eaa28576-s.woff2 +0 -0
  319. package/out/_next/static/media/2c07349e02a7b712-s.woff2 +0 -0
  320. package/out/_next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
  321. package/out/_next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
  322. package/out/_next/static/media/4f77bef990aad698-s.woff2 +0 -0
  323. package/out/_next/static/media/627d916fd739a539-s.woff2 +0 -0
  324. package/out/_next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
  325. package/out/_next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
  326. package/out/_next/static/media/84602850c8fd81c3-s.woff2 +0 -0
  327. package/out/backlog.html +0 -1
  328. package/out/backlog.txt +0 -25
  329. package/out/docs.html +0 -1
  330. package/out/docs.txt +0 -26
  331. package/out/favicon.ico +0 -0
  332. package/out/index.html +0 -1
  333. package/out/index.txt +0 -25
  334. package/out/logo.png +0 -0
  335. package/out/settings.html +0 -1
  336. package/out/settings.txt +0 -25
  337. package/src/app/backlog/page.tsx +0 -19
  338. package/src/app/page.tsx +0 -16
  339. package/src/app/settings/page.tsx +0 -194
  340. package/src/hooks/useTasks.ts +0 -119
  341. package/src/services/doc.service.ts +0 -27
  342. package/src/services/sprint.service.ts +0 -24
  343. package/src/services/task.service.ts +0 -75
  344. package/src/views/Backlog.tsx +0 -691
  345. package/src/views/Board.tsx +0 -306
  346. /package/src/app/{docs → (dashboard)/docs}/page.tsx +0 -0
@@ -1,28 +1,57 @@
1
1
  import { forwardRef, type InputHTMLAttributes, type ReactNode } from "react";
2
+ import { cn } from "@/lib/utils";
2
3
 
4
+ /**
5
+ * Input component props
6
+ *
7
+ * @property icon - Optional left icon element
8
+ * @property rightElement - Optional right element (e.g., clear button)
9
+ * @property error - Show error state
10
+ */
3
11
  interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
12
+ /** Icon displayed on the left side of input */
4
13
  icon?: ReactNode;
14
+ /** Element displayed on the right side of input */
5
15
  rightElement?: ReactNode;
16
+ /** Show error state */
17
+ error?: boolean;
6
18
  }
7
19
 
8
20
  const Input = forwardRef<HTMLInputElement, InputProps>(
9
- ({ className, icon, rightElement, ...props }, ref) => {
21
+ ({ className, icon, rightElement, error, ...props }, ref) => {
10
22
  return (
11
23
  <div className="relative w-full group">
12
24
  {icon && (
13
- <div className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted group-focus-within:text-accent transition-colors">
25
+ <div
26
+ className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground group-focus-within:text-primary transition-colors"
27
+ aria-hidden="true"
28
+ >
14
29
  {icon}
15
30
  </div>
16
31
  )}
17
32
  <input
18
33
  ref={ref}
19
- className={`flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 ${
20
- icon ? "pl-10" : "pl-3"
21
- } ${rightElement ? "pr-10" : "pr-3"} ${className || ""}`}
34
+ className={cn(
35
+ "flex h-9 w-full rounded-lg border bg-secondary/30 px-3 py-1 text-sm transition-all",
36
+ "placeholder:text-muted-foreground/50",
37
+ "hover:border-border hover:bg-secondary/50",
38
+ "focus:outline-none focus:border-primary/50 focus:bg-background focus:ring-2 focus:ring-primary/10",
39
+ "disabled:cursor-not-allowed disabled:opacity-50",
40
+ "file:border-0 file:bg-transparent file:text-sm file:font-medium",
41
+ error
42
+ ? "border-red-500/50 ring-2 ring-red-500/10"
43
+ : "border-border/60",
44
+ icon && "pl-10",
45
+ rightElement && "pr-10",
46
+ className
47
+ )}
22
48
  {...props}
23
49
  />
24
50
  {rightElement && (
25
- <div className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground">
51
+ <div
52
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground"
53
+ aria-hidden="true"
54
+ >
26
55
  {rightElement}
27
56
  </div>
28
57
  )}
@@ -31,6 +60,24 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
31
60
  }
32
61
  );
33
62
 
63
+ /**
64
+ * Input component
65
+ *
66
+ * A flexible input component with support for left/right icons and error states.
67
+ * Automatically adjusts padding when icons are present.
68
+ *
69
+ * @example
70
+ * // Basic input
71
+ * <Input placeholder="Enter text..." />
72
+ *
73
+ * @example
74
+ * // With left icon
75
+ * <Input icon={<Search size={16} />} placeholder="Search..." />
76
+ *
77
+ * @example
78
+ * // With error state
79
+ * <Input error value={value} onChange={handleChange} />
80
+ */
34
81
  Input.displayName = "Input";
35
82
 
36
83
  export { Input };
@@ -3,13 +3,27 @@
3
3
  import { X } from "lucide-react";
4
4
  import { type ReactNode, useEffect, useRef } from "react";
5
5
  import { createPortal } from "react-dom";
6
+ import { MODAL_SIZES, Z_INDEXES } from "./constants";
6
7
 
8
+ /**
9
+ * Modal component props
10
+ *
11
+ * @property isOpen - Control modal visibility
12
+ * @property onClose - Callback when modal should close
13
+ * @property title - Optional modal header title
14
+ * @property size - Modal width size (default: "md")
15
+ */
7
16
  interface ModalProps {
17
+ /** Whether modal is visible */
8
18
  isOpen: boolean;
19
+ /** Called when user attempts to close modal */
9
20
  onClose: () => void;
21
+ /** Modal content */
10
22
  children: ReactNode;
23
+ /** Optional modal header title */
11
24
  title?: string;
12
- size?: "sm" | "md" | "lg";
25
+ /** Modal size */
26
+ size?: keyof typeof MODAL_SIZES;
13
27
  }
14
28
 
15
29
  export function Modal({
@@ -39,35 +53,37 @@ export function Modal({
39
53
 
40
54
  if (!isOpen) return null;
41
55
 
42
- const widths = {
43
- sm: "w-[400px]",
44
- md: "w-[520px]",
45
- lg: "w-[680px]",
46
- };
47
-
48
56
  return createPortal(
49
57
  <div
50
58
  ref={overlayRef}
51
- className="fixed inset-0 bg-black/40 backdrop-blur-md flex items-center justify-center z-1000 animate-in fade-in duration-300"
59
+ className={`fixed inset-0 bg-black/40 backdrop-blur-md flex items-center justify-center ${Z_INDEXES.modalOverlay} animate-in fade-in duration-300`}
52
60
  onClick={(e) => {
53
61
  if (e.target === overlayRef.current) onClose();
54
62
  }}
63
+ role="presentation"
55
64
  >
56
65
  <div
57
66
  className={`bg-background border border-border rounded-xl shadow-lg animate-in zoom-in-95 duration-200 max-h-[90vh] overflow-hidden flex flex-col ${
58
- widths[size]
59
- } max-w-[90vw]`}
67
+ MODAL_SIZES[size]
68
+ } max-w-[90vw] ${Z_INDEXES.modal}`}
69
+ role="dialog"
70
+ aria-modal="true"
71
+ aria-labelledby={title ? "modal-title" : undefined}
60
72
  >
61
73
  {title && (
62
74
  <div className="flex justify-between items-center p-5 border-b border-border">
63
- <h3 className="text-lg font-semibold m-0 text-foreground">
75
+ <h3
76
+ id="modal-title"
77
+ className="text-lg font-semibold m-0 text-foreground"
78
+ >
64
79
  {title}
65
80
  </h3>
66
81
  <button
67
82
  className="bg-transparent border-none text-muted-foreground hover:text-foreground cursor-pointer p-0 transition-colors duration-200"
68
83
  onClick={onClose}
84
+ aria-label="Close modal"
69
85
  >
70
- <X size={20} />
86
+ <X size={20} aria-hidden="true" />
71
87
  </button>
72
88
  </div>
73
89
  )}
@@ -77,3 +93,20 @@ export function Modal({
77
93
  document.body
78
94
  );
79
95
  }
96
+
97
+ /**
98
+ * Modal component
99
+ *
100
+ * A accessible, dismissable modal dialog component.
101
+ * - Closes on Escape key
102
+ * - Closes on backdrop click
103
+ * - Prevents body scroll when open
104
+ * - Includes proper ARIA attributes
105
+ *
106
+ * @example
107
+ * // Basic modal
108
+ * <Modal isOpen={isOpen} onClose={handleClose} title="Confirm Action">
109
+ * <p>Are you sure?</p>
110
+ * <Button onClick={handleConfirm}>Confirm</Button>
111
+ * </Modal>
112
+ */
@@ -0,0 +1,148 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ /**
7
+ * OTP input component props
8
+ *
9
+ * @property length - Number of input fields (default: 6)
10
+ * @property value - Current OTP value (e.g., "123456")
11
+ * @property onChange - Callback when OTP value changes
12
+ * @property disabled - Disable the inputs
13
+ */
14
+ interface OtpInputProps {
15
+ /** Number of OTP digits (default: 6) */
16
+ length?: number;
17
+ /** Current OTP value as string */
18
+ value: string;
19
+ /** Callback when OTP changes */
20
+ onChange: (value: string) => void;
21
+ /** Disable OTP inputs */
22
+ disabled?: boolean;
23
+ }
24
+
25
+ /**
26
+ * OTP input component
27
+ *
28
+ * A specialized input component for one-time passwords.
29
+ * - Auto-focuses next field on digit entry
30
+ * - Supports backspace and arrow navigation
31
+ * - Supports paste functionality
32
+ * - Numeric input only
33
+ *
34
+ * @example
35
+ * const [otp, setOtp] = useState("");
36
+ * <OtpInput
37
+ * length={6}
38
+ * value={otp}
39
+ * onChange={setOtp}
40
+ * />
41
+ */
42
+ export function OtpInput({
43
+ length = 6,
44
+ value,
45
+ onChange,
46
+ disabled = false,
47
+ }: OtpInputProps) {
48
+ const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
49
+ const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
50
+
51
+ // Initialize refs array
52
+ useEffect(() => {
53
+ inputRefs.current = inputRefs.current.slice(0, length);
54
+ }, [length]);
55
+
56
+ const handleChange = (
57
+ e: React.ChangeEvent<HTMLInputElement>,
58
+ index: number
59
+ ) => {
60
+ const char = e.target.value.slice(-1); // Only take last character
61
+
62
+ // Only allow numbers
63
+ if (char && !/^\d$/.test(char)) return;
64
+
65
+ const newValue = value.split("");
66
+ newValue[index] = char;
67
+ const finalValue = newValue.join("");
68
+ onChange(finalValue);
69
+
70
+ // Focus next input
71
+ if (char && index < length - 1) {
72
+ inputRefs.current[index + 1]?.focus();
73
+ }
74
+ };
75
+
76
+ const handleKeyDown = (
77
+ e: React.KeyboardEvent<HTMLInputElement>,
78
+ index: number
79
+ ) => {
80
+ if (e.key === "Backspace") {
81
+ if (!value[index] && index > 0) {
82
+ // If current is empty, move back and clear previous
83
+ const newValue = value.split("");
84
+ newValue[index - 1] = "";
85
+ onChange(newValue.join(""));
86
+ inputRefs.current[index - 1]?.focus();
87
+ } else {
88
+ // Just clear current
89
+ const newValue = value.split("");
90
+ newValue[index] = "";
91
+ onChange(newValue.join(""));
92
+ }
93
+ } else if (e.key === "ArrowLeft" && index > 0) {
94
+ inputRefs.current[index - 1]?.focus();
95
+ } else if (e.key === "ArrowRight" && index < length - 1) {
96
+ inputRefs.current[index + 1]?.focus();
97
+ }
98
+ };
99
+
100
+ const handlePaste = (e: React.ClipboardEvent) => {
101
+ e.preventDefault();
102
+ const pastedData = e.clipboardData
103
+ .getData("text")
104
+ .slice(0, length)
105
+ .replace(/\D/g, "");
106
+ if (pastedData) {
107
+ onChange(pastedData);
108
+ // Focus the next available slot or the last one
109
+ const nextIndex = Math.min(pastedData.length, length - 1);
110
+ inputRefs.current[nextIndex]?.focus();
111
+ }
112
+ };
113
+
114
+ return (
115
+ <fieldset
116
+ className="flex justify-between gap-2 sm:gap-3"
117
+ onPaste={handlePaste}
118
+ >
119
+ <legend className="sr-only">One-time password input</legend>
120
+ {Array.from({ length }).map((_, i) => (
121
+ <input
122
+ key={i}
123
+ ref={(el) => {
124
+ inputRefs.current[i] = el;
125
+ }}
126
+ type="text"
127
+ inputMode="numeric"
128
+ autoComplete="one-time-code"
129
+ value={value[i] || ""}
130
+ onChange={(e) => handleChange(e, i)}
131
+ onKeyDown={(e) => handleKeyDown(e, i)}
132
+ onFocus={() => setFocusedIndex(i)}
133
+ onBlur={() => setFocusedIndex(null)}
134
+ disabled={disabled}
135
+ className={cn(
136
+ "w-full h-12 sm:h-14 text-center text-xl font-bold rounded-xl border transition-all duration-200 outline-none",
137
+ "bg-secondary/20 border-border/40 text-foreground",
138
+ focusedIndex === i
139
+ ? "border-primary bg-background ring-4 ring-primary/10"
140
+ : "hover:border-border/80",
141
+ disabled && "opacity-50 cursor-not-allowed"
142
+ )}
143
+ aria-label={`Digit ${i + 1}`}
144
+ />
145
+ ))}
146
+ </fieldset>
147
+ );
148
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ /**
6
+ * Skeleton component props
7
+ *
8
+ * @property className - Additional CSS classes
9
+ */
10
+ interface SkeletonProps {
11
+ /** Additional CSS classes */
12
+ className?: string;
13
+ }
14
+
15
+ /**
16
+ * Skeleton component
17
+ *
18
+ * A placeholder element with pulsing animation for loading states.
19
+ * Use to create skeleton screens that match your content layout.
20
+ *
21
+ * @example
22
+ * // Skeleton for avatar
23
+ * <Skeleton className="h-10 w-10 rounded-full" />
24
+ *
25
+ * @example
26
+ * // Skeleton for text
27
+ * <Skeleton className="h-4 w-full rounded" />
28
+ */
29
+ export function Skeleton({ className }: SkeletonProps) {
30
+ return (
31
+ <div
32
+ className={cn("animate-pulse rounded-md bg-muted/50", className)}
33
+ aria-busy="true"
34
+ />
35
+ );
36
+ }
@@ -0,0 +1,112 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import { SPINNER_SIZES } from "./constants";
5
+
6
+ /**
7
+ * Spinner component props
8
+ *
9
+ * @property size - Spinner size (default: "md")
10
+ * @property className - Additional CSS classes
11
+ */
12
+ interface SpinnerProps {
13
+ /** Additional CSS classes */
14
+ className?: string;
15
+ /** Spinner size */
16
+ size?: keyof typeof SPINNER_SIZES;
17
+ }
18
+
19
+ /**
20
+ * Spinner component
21
+ *
22
+ * A circular loading indicator with smooth animation.
23
+ * Used to indicate data fetching or processing.
24
+ *
25
+ * @example
26
+ * // Default spinner
27
+ * <Spinner />
28
+ *
29
+ * @example
30
+ * // Large spinner
31
+ * <Spinner size="lg" />
32
+ */
33
+ export function Spinner({ className, size = "md" }: SpinnerProps) {
34
+ const sizeClasses = {
35
+ sm: "h-4 w-4 border-2",
36
+ md: "h-8 w-8 border-3",
37
+ lg: "h-12 w-12 border-4",
38
+ xl: "h-16 w-16 border-4",
39
+ };
40
+
41
+ return (
42
+ <div
43
+ className={cn(
44
+ "animate-spin rounded-full border-primary border-t-transparent",
45
+ sizeClasses[size],
46
+ className
47
+ )}
48
+ />
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Full-page loading state component
54
+ *
55
+ * Displays a centered spinner for full-page loading scenarios.
56
+ * Use for page transitions and initial data loading.
57
+ *
58
+ * @example
59
+ * <LoadingPage />
60
+ */
61
+ export function LoadingPage() {
62
+ return (
63
+ <div className="flex items-center justify-center w-full h-full min-h-screen">
64
+ <Spinner size="lg" />
65
+ </div>
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Full-screen loading skeleton component
71
+ *
72
+ * Displays a full-screen loading state with sidebar placeholder
73
+ * to prevent layout shift during initial hydration.
74
+ * Use during app initialization in dashboard layout.
75
+ *
76
+ * @example
77
+ * <LoadingSkeleton />
78
+ */
79
+ export function LoadingSkeleton() {
80
+ return (
81
+ <div className="flex h-screen overflow-hidden bg-background">
82
+ {/* Sidebar Skeleton */}
83
+ <aside className="w-56 border-r border-border bg-background flex flex-col">
84
+ <div className="flex-1" />
85
+ </aside>
86
+
87
+ {/* Main Content Skeleton */}
88
+ <main className="flex-1 overflow-auto bg-background p-6">
89
+ <div className="flex items-center justify-center h-full">
90
+ <Spinner size="lg" />
91
+ </div>
92
+ </main>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Overlay loading state component
99
+ *
100
+ * Displays a semi-transparent overlay with centered spinner.
101
+ * Use for asynchronous operations on pages that should remain interactive.
102
+ *
103
+ * @example
104
+ * {isLoading && <LoadingOverlay />}
105
+ */
106
+ export function LoadingOverlay() {
107
+ return (
108
+ <div className="absolute inset-0 bg-background/50 backdrop-blur-sm z-50 flex items-center justify-center">
109
+ <Spinner size="lg" />
110
+ </div>
111
+ );
112
+ }
@@ -1,15 +1,40 @@
1
1
  import { forwardRef, type TextareaHTMLAttributes } from "react";
2
+ import { cn } from "@/lib/utils";
2
3
 
4
+ /**
5
+ * Textarea component props
6
+ *
7
+ * Extends standard HTML textarea attributes
8
+ */
3
9
  interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {}
4
10
 
11
+ /**
12
+ * Textarea component
13
+ *
14
+ * A multi-line text input field with consistent styling.
15
+ * Automatically prevents resizing via CSS (use JavaScript if needed).
16
+ *
17
+ * @example
18
+ * // Basic textarea
19
+ * <Textarea placeholder="Enter description..." />
20
+ *
21
+ * @example
22
+ * // With value binding
23
+ * <Textarea
24
+ * value={description}
25
+ * onChange={(e) => setDescription(e.target.value)}
26
+ * disabled={isLoading}
27
+ * />
28
+ */
5
29
  const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
6
30
  ({ className, ...props }, ref) => {
7
31
  return (
8
32
  <textarea
9
33
  ref={ref}
10
- className={`flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 resize-none ${
11
- className || ""
12
- }`}
34
+ className={cn(
35
+ "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 resize-none",
36
+ className
37
+ )}
13
38
  {...props}
14
39
  />
15
40
  );
@@ -0,0 +1,99 @@
1
+ "use client";
2
+
3
+ import { Toaster as SonnerToaster, toast } from "sonner";
4
+
5
+ // Re-export toast for convenience
6
+ export { toast };
7
+
8
+ /**
9
+ * Toaster component
10
+ *
11
+ * Renders the toast notification container.
12
+ * Place at root level in your app (typically in layout).
13
+ * Handles all notification positioning and styling.
14
+ *
15
+ * @example
16
+ * // In layout.tsx
17
+ * export default function Layout() {
18
+ * return (
19
+ * <>
20
+ * {children}
21
+ * <Toaster />
22
+ * </>
23
+ * );
24
+ * }
25
+ */
26
+ export function Toaster() {
27
+ return (
28
+ <SonnerToaster
29
+ position="top-center"
30
+ expand={false}
31
+ richColors
32
+ theme="dark"
33
+ toastOptions={{
34
+ style: {
35
+ background: "rgba(23, 23, 23, 0.95)",
36
+ border: "1px solid rgba(255, 255, 255, 0.1)",
37
+ backdropFilter: "blur(12px)",
38
+ },
39
+ classNames: {
40
+ toast: "rounded-xl shadow-xl",
41
+ title: "font-semibold text-sm",
42
+ description: "text-muted-foreground text-xs",
43
+ success: "!bg-emerald-950/90 !border-emerald-500/30",
44
+ error: "!bg-rose-950/90 !border-rose-500/30",
45
+ warning: "!bg-amber-950/90 !border-amber-500/30",
46
+ info: "!bg-sky-950/90 !border-sky-500/30",
47
+ },
48
+ }}
49
+ />
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Toast notification convenience wrapper
55
+ *
56
+ * Provides typed toast methods with consistent options.
57
+ *
58
+ * @example
59
+ * showToast.success("Saved!", "Your changes have been saved");
60
+ * showToast.error("Error", "Something went wrong");
61
+ * showToast.promise(apiCall(), {
62
+ * loading: "Saving...",
63
+ * success: "Saved!",
64
+ * error: "Error saving",
65
+ * });
66
+ */
67
+ export const showToast = {
68
+ /** Success notification */
69
+ success: (title: string, description?: string) => {
70
+ toast.success(title, { description, dismissible: true });
71
+ },
72
+ /** Error notification */
73
+ error: (title: string, description?: string) => {
74
+ toast.error(title, { description, dismissible: true });
75
+ },
76
+ /** Warning notification */
77
+ warning: (title: string, description?: string) => {
78
+ toast.warning(title, { description, dismissible: true });
79
+ },
80
+ /** Info notification */
81
+ info: (title: string, description?: string) => {
82
+ toast.info(title, { description, dismissible: true });
83
+ },
84
+ /** Loading notification (returns ID for updating) */
85
+ loading: (title: string, description?: string) => {
86
+ return toast.loading(title, { description, dismissible: true });
87
+ },
88
+ /** Promise-based notification for async operations */
89
+ promise: <T,>(
90
+ promise: Promise<T>,
91
+ messages: {
92
+ loading: string;
93
+ success: string;
94
+ error: string;
95
+ }
96
+ ) => {
97
+ return toast.promise(promise, messages);
98
+ },
99
+ };
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+
5
+ /**
6
+ * Toggle component props
7
+ *
8
+ * @property checked - Whether toggle is on
9
+ * @property onChange - Callback when toggle state changes
10
+ * @property disabled - Disable toggle interaction
11
+ */
12
+ interface ToggleProps {
13
+ /** Whether toggle is currently on/checked */
14
+ checked: boolean;
15
+ /** Callback when toggle state changes */
16
+ onChange: (checked: boolean) => void;
17
+ /** Disable toggle */
18
+ disabled?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Toggle component
23
+ *
24
+ * An accessible, animated toggle switch component.
25
+ * Uses Framer Motion for smooth animations.
26
+ *
27
+ * @example
28
+ * const [isEnabled, setIsEnabled] = useState(false);
29
+ * <Toggle
30
+ * checked={isEnabled}
31
+ * onChange={setIsEnabled}
32
+ * />
33
+ *
34
+ * @example
35
+ * // Disabled toggle
36
+ * <Toggle
37
+ * checked={true}
38
+ * onChange={handleChange}
39
+ * disabled={isLoading}
40
+ * />
41
+ */
42
+ export function Toggle({ checked, onChange, disabled }: ToggleProps) {
43
+ return (
44
+ <button
45
+ type="button"
46
+ role="switch"
47
+ aria-checked={checked}
48
+ aria-label={checked ? "Enabled" : "Disabled"}
49
+ disabled={disabled}
50
+ onClick={() => onChange(!checked)}
51
+ className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 ${
52
+ checked ? "bg-primary" : "bg-secondary"
53
+ } ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
54
+ >
55
+ <motion.span
56
+ animate={{ x: checked ? 20 : 0 }}
57
+ transition={{ type: "spring", stiffness: 500, damping: 30 }}
58
+ className="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0"
59
+ aria-hidden="true"
60
+ />
61
+ </button>
62
+ );
63
+ }