@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
@@ -0,0 +1,279 @@
1
+ import { type Doc } from "@locusai/shared";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { toast } from "sonner";
4
+ import {
5
+ useCreateDocGroupMutation,
6
+ useCreateDocMutation,
7
+ useDeleteDocMutation,
8
+ useDocGroupsQuery,
9
+ useDocsQuery,
10
+ useUpdateDocMutation,
11
+ } from "./useDocsQuery";
12
+
13
+ export const DOC_TEMPLATES = [
14
+ { id: "blank", label: "Blank Document", content: "# Untitled\n\n" },
15
+ {
16
+ id: "prd",
17
+ label: "Product Spec (PRD)",
18
+ category: "product",
19
+ content: `# Product Requirements Document
20
+
21
+ ## Overview
22
+ Brief description of the feature/product.
23
+
24
+ ## Goals
25
+ - Goal 1
26
+ - Goal 2
27
+
28
+ ## User Stories
29
+ As a [user type], I want [action] so that [benefit].
30
+
31
+ ## Requirements
32
+ ### Functional Requirements
33
+ 1.
34
+
35
+ ### Non-Functional Requirements
36
+ 1.
37
+
38
+ ## Success Metrics
39
+ -
40
+
41
+ ## Timeline
42
+ | Phase | Description | Date |
43
+ |-------|-------------|------|
44
+ | | | |
45
+ `,
46
+ },
47
+ {
48
+ id: "technical",
49
+ label: "Technical Design",
50
+ category: "engineering",
51
+ content: `# Technical Design Document
52
+
53
+ ## Summary
54
+ Brief technical overview.
55
+
56
+ ## Architecture
57
+ Describe the system architecture.
58
+
59
+ ## API Design
60
+ \`\`\`typescript
61
+ // API endpoints
62
+ \`\`\`
63
+
64
+ ## Database Schema
65
+ \`\`\`sql
66
+ -- Schema changes
67
+ \`\`\`
68
+
69
+ ## Implementation Plan
70
+ 1.
71
+
72
+ ## Testing Strategy
73
+ - Unit tests
74
+ - Integration tests
75
+
76
+ ## Rollout Plan
77
+ -
78
+ `,
79
+ },
80
+ {
81
+ id: "api",
82
+ label: "API Documentation",
83
+ category: "engineering",
84
+ content: `# API Documentation
85
+
86
+ ## Endpoints
87
+
88
+ ### GET /api/resource
89
+ Description of the endpoint.
90
+
91
+ **Parameters:**
92
+ | Name | Type | Required | Description |
93
+ |------|------|----------|-------------|
94
+ | | | | |
95
+
96
+ **Response:**
97
+ \`\`\`json
98
+ {
99
+ "data": []
100
+ }
101
+ \`\`\`
102
+
103
+ ### POST /api/resource
104
+ `,
105
+ },
106
+ ];
107
+
108
+ export function useDocs() {
109
+ // Queries
110
+ const { data: docs = [], isLoading: docsLoading } = useDocsQuery();
111
+ const { data: groups = [], isLoading: groupsLoading } = useDocGroupsQuery();
112
+
113
+ // Mutations
114
+ const createDocMutation = useCreateDocMutation();
115
+ const updateDocMutation = useUpdateDocMutation();
116
+ const deleteDocMutation = useDeleteDocMutation();
117
+ const createGroupMutation = useCreateDocGroupMutation();
118
+
119
+ // Local UI State
120
+ const [selectedId, setSelectedId] = useState<string | null>(null);
121
+ const [content, setContent] = useState("");
122
+ const [originalContent, setOriginalContent] = useState("");
123
+ const [isCreating, setIsCreating] = useState(false);
124
+ const [newTitle, setNewFileName] = useState("");
125
+ const [selectedTemplate, setSelectedTemplate] = useState("blank");
126
+ const [contentMode, setContentMode] = useState<"edit" | "preview">("edit");
127
+ const [searchQuery, setSearchQuery] = useState("");
128
+ const [activeCategory, setActiveCategory] = useState("all");
129
+ const [selectedGroupId, setSelectedGroupId] = useState<string | null>(null);
130
+
131
+ const selectedDoc = useMemo(
132
+ () => docs.find((d) => d.id === selectedId) || null,
133
+ [docs, selectedId]
134
+ );
135
+
136
+ const hasUnsavedChanges = content !== originalContent;
137
+
138
+ useEffect(() => {
139
+ if (selectedDoc) {
140
+ setContent(selectedDoc.content || "");
141
+ setOriginalContent(selectedDoc.content || "");
142
+ } else {
143
+ setContent("");
144
+ setOriginalContent("");
145
+ }
146
+ }, [selectedDoc]);
147
+
148
+ const handleSave = async () => {
149
+ if (!selectedId) return;
150
+ try {
151
+ await updateDocMutation.mutateAsync({
152
+ id: selectedId,
153
+ updates: { content },
154
+ });
155
+ toast.success("Document saved");
156
+ } catch {
157
+ toast.error("Failed to save document");
158
+ }
159
+ };
160
+
161
+ const handleCreateFile = async () => {
162
+ if (!newTitle.trim()) return;
163
+
164
+ const template = DOC_TEMPLATES.find((t) => t.id === selectedTemplate);
165
+ const initialContent = template?.content || `# ${newTitle}\n\n`;
166
+
167
+ try {
168
+ const newDoc = await createDocMutation.mutateAsync({
169
+ title: newTitle,
170
+ content: initialContent,
171
+ groupId: selectedGroupId || undefined,
172
+ });
173
+ setIsCreating(false);
174
+ setNewFileName("");
175
+ setSelectedTemplate("blank");
176
+ setSelectedId(newDoc.id);
177
+ toast.success("Document created");
178
+ } catch {
179
+ toast.error("Failed to create document");
180
+ }
181
+ };
182
+
183
+ const handleDelete = async (id: string) => {
184
+ if (!confirm("Are you sure you want to delete this document?")) return;
185
+ try {
186
+ await deleteDocMutation.mutateAsync(id);
187
+ if (selectedId === id) {
188
+ setSelectedId(null);
189
+ }
190
+ toast.success("Document deleted");
191
+ } catch {
192
+ toast.error("Failed to delete document");
193
+ }
194
+ };
195
+
196
+ const handleCreateGroup = async (name: string) => {
197
+ try {
198
+ await createGroupMutation.mutateAsync({ name });
199
+ toast.success("Group created");
200
+ } catch {
201
+ toast.error("Failed to create group");
202
+ }
203
+ };
204
+
205
+ const filteredDocs = useMemo(() => {
206
+ return docs.filter((doc) => {
207
+ const matchesSearch = doc.title
208
+ .toLowerCase()
209
+ .includes(searchQuery.toLowerCase());
210
+ return matchesSearch;
211
+ });
212
+ }, [docs, searchQuery]);
213
+
214
+ // Group docs by their groupId
215
+ const docsByGroup = useMemo(() => {
216
+ const grouped: Record<string, Doc[]> = {
217
+ ungrouped: [],
218
+ };
219
+
220
+ groups.forEach((g) => {
221
+ grouped[g.id] = [];
222
+ });
223
+
224
+ filteredDocs.forEach((doc) => {
225
+ if (doc.groupId && grouped[doc.groupId]) {
226
+ grouped[doc.groupId].push(doc);
227
+ } else {
228
+ grouped.ungrouped.push(doc);
229
+ }
230
+ });
231
+
232
+ return grouped;
233
+ }, [filteredDocs, groups]);
234
+
235
+ const handleGroupChange = async (docId: string, groupId: string | null) => {
236
+ try {
237
+ await updateDocMutation.mutateAsync({
238
+ id: docId,
239
+ updates: { groupId },
240
+ });
241
+ toast.success("Document moved");
242
+ } catch {
243
+ toast.error("Failed to move document");
244
+ }
245
+ };
246
+
247
+ return {
248
+ docs: filteredDocs,
249
+ groups,
250
+ docsByGroup,
251
+ selectedId,
252
+ setSelectedId,
253
+ selectedDoc,
254
+ content,
255
+ setContent,
256
+ originalContent,
257
+ hasUnsavedChanges,
258
+ isLoading: docsLoading || groupsLoading,
259
+ isCreating,
260
+ setIsCreating,
261
+ newFileName: newTitle,
262
+ setNewFileName,
263
+ selectedTemplate,
264
+ setSelectedTemplate,
265
+ contentMode,
266
+ setContentMode,
267
+ searchQuery,
268
+ setSearchQuery,
269
+ activeCategory,
270
+ setActiveCategory,
271
+ selectedGroupId,
272
+ setSelectedGroupId,
273
+ handleSave,
274
+ handleCreateFile,
275
+ handleDelete,
276
+ handleCreateGroup,
277
+ handleGroupChange,
278
+ };
279
+ }
@@ -0,0 +1,99 @@
1
+ "use client";
2
+
3
+ import {
4
+ type CreateDoc,
5
+ type CreateDocGroup,
6
+ type Doc,
7
+ type DocGroup,
8
+ type UpdateDoc,
9
+ } from "@locusai/shared";
10
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
11
+ import { locusClient } from "@/lib/api-client";
12
+ import { queryKeys } from "@/lib/query-keys";
13
+ import { useWorkspaceId } from "./useWorkspaceId";
14
+
15
+ export function useDocsQuery() {
16
+ const workspaceId = useWorkspaceId();
17
+
18
+ return useQuery<Doc[]>({
19
+ queryKey: queryKeys.docs.list(workspaceId),
20
+ queryFn: () => locusClient.docs.list(workspaceId),
21
+ enabled: !!workspaceId,
22
+ });
23
+ }
24
+
25
+ export function useDocGroupsQuery() {
26
+ const workspaceId = useWorkspaceId();
27
+
28
+ return useQuery<DocGroup[]>({
29
+ queryKey: queryKeys.docGroups.list(workspaceId),
30
+ queryFn: () => locusClient.docs.listGroups(workspaceId),
31
+ enabled: !!workspaceId,
32
+ });
33
+ }
34
+
35
+ export function useCreateDocMutation() {
36
+ const workspaceId = useWorkspaceId();
37
+ const queryClient = useQueryClient();
38
+
39
+ return useMutation({
40
+ mutationFn: (data: CreateDoc) => locusClient.docs.create(workspaceId, data),
41
+ onSuccess: (newDoc) => {
42
+ queryClient.invalidateQueries({
43
+ queryKey: queryKeys.docs.list(workspaceId),
44
+ });
45
+ queryClient.setQueryData(
46
+ queryKeys.docs.detail(newDoc.id, workspaceId),
47
+ newDoc
48
+ );
49
+ },
50
+ });
51
+ }
52
+
53
+ export function useUpdateDocMutation() {
54
+ const workspaceId = useWorkspaceId();
55
+ const queryClient = useQueryClient();
56
+
57
+ return useMutation({
58
+ mutationFn: ({ id, updates }: { id: string; updates: UpdateDoc }) =>
59
+ locusClient.docs.update(id, workspaceId, updates),
60
+ onSuccess: (updatedDoc) => {
61
+ queryClient.invalidateQueries({
62
+ queryKey: queryKeys.docs.list(workspaceId),
63
+ });
64
+ queryClient.setQueryData(
65
+ queryKeys.docs.detail(updatedDoc.id, workspaceId),
66
+ updatedDoc
67
+ );
68
+ },
69
+ });
70
+ }
71
+
72
+ export function useDeleteDocMutation() {
73
+ const workspaceId = useWorkspaceId();
74
+ const queryClient = useQueryClient();
75
+
76
+ return useMutation({
77
+ mutationFn: (id: string) => locusClient.docs.delete(id, workspaceId),
78
+ onSuccess: () => {
79
+ queryClient.invalidateQueries({
80
+ queryKey: queryKeys.docs.list(workspaceId),
81
+ });
82
+ },
83
+ });
84
+ }
85
+
86
+ export function useCreateDocGroupMutation() {
87
+ const workspaceId = useWorkspaceId();
88
+ const queryClient = useQueryClient();
89
+
90
+ return useMutation({
91
+ mutationFn: (data: CreateDocGroup) =>
92
+ locusClient.docs.createGroup(workspaceId, data),
93
+ onSuccess: () => {
94
+ queryClient.invalidateQueries({
95
+ queryKey: queryKeys.docGroups.list(workspaceId),
96
+ });
97
+ },
98
+ });
99
+ }
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ /**
6
+ * DocsSidebar state management hook.
7
+ *
8
+ * Encapsulates all local UI state for the docs sidebar:
9
+ * - Search and filtering
10
+ * - Creation mode for docs and groups
11
+ * - Group expansion
12
+ * - Selection and form inputs
13
+ *
14
+ * This significantly reduces prop drilling and makes the component
15
+ * easier to maintain and extend.
16
+ */
17
+ export function useDocsSidebarState() {
18
+ // Search/Filter
19
+ const [searchQuery, setSearchQuery] = useState("");
20
+
21
+ // Creation/Editing
22
+ const [isCreating, setIsCreating] = useState(false);
23
+ const [isCreatingGroup, setIsCreatingGroup] = useState(false);
24
+ const [newFileName, setNewFileName] = useState("");
25
+ const [newGroupName, setNewGroupName] = useState("");
26
+
27
+ // Selection
28
+ const [selectedTemplate, setSelectedTemplate] = useState("readme");
29
+ const [selectedGroupId, setSelectedGroupId] = useState<string | null>(null);
30
+
31
+ // Expansion
32
+ const [expandedGroups, setExpandedGroups] = useState<Set<string>>(
33
+ new Set(["ungrouped"])
34
+ );
35
+
36
+ // Document selection
37
+ const [selectedId, setSelectedId] = useState<string | null>(null);
38
+
39
+ // Group toggling
40
+ const toggleGroup = (groupId: string) => {
41
+ const next = new Set(expandedGroups);
42
+ if (next.has(groupId)) {
43
+ next.delete(groupId);
44
+ } else {
45
+ next.add(groupId);
46
+ }
47
+ setExpandedGroups(next);
48
+ };
49
+
50
+ // Reset group form
51
+ const resetGroupForm = () => {
52
+ setNewGroupName("");
53
+ setIsCreatingGroup(false);
54
+ };
55
+
56
+ // Reset file form
57
+ const resetFileForm = () => {
58
+ setNewFileName("");
59
+ setSelectedTemplate("readme");
60
+ setSelectedGroupId(null);
61
+ setIsCreating(false);
62
+ };
63
+
64
+ // Close all forms
65
+ const closeAllForms = () => {
66
+ resetGroupForm();
67
+ resetFileForm();
68
+ };
69
+
70
+ return {
71
+ // Search
72
+ searchQuery,
73
+ setSearchQuery,
74
+
75
+ // Creation/Editing
76
+ isCreating,
77
+ setIsCreating,
78
+ isCreatingGroup,
79
+ setIsCreatingGroup,
80
+ newFileName,
81
+ setNewFileName,
82
+ newGroupName,
83
+ setNewGroupName,
84
+
85
+ // Selection
86
+ selectedTemplate,
87
+ setSelectedTemplate,
88
+ selectedGroupId,
89
+ setSelectedGroupId,
90
+
91
+ // Expansion
92
+ expandedGroups,
93
+ toggleGroup,
94
+
95
+ // Selection
96
+ selectedId,
97
+ setSelectedId,
98
+
99
+ // Helpers
100
+ resetGroupForm,
101
+ resetFileForm,
102
+ closeAllForms,
103
+ };
104
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { $FixMe } from "@locusai/shared";
4
+ import { useState } from "react";
5
+
6
+ /**
7
+ * Generic form state management hook.
8
+ *
9
+ * Simplifies managing multiple form fields with a single state object
10
+ * and provides utilities for updating and resetting form state.
11
+ *
12
+ * @example
13
+ * const form = useFormState({ email: "", name: "" });
14
+ * <Input value={form.email} onChange={e => form.setField("email", e.target.value)} />
15
+ * <Button onClick={() => form.reset()}>Clear</Button>
16
+ */
17
+ export function useFormState<T extends Record<string, $FixMe>>(
18
+ initialState: T
19
+ ) {
20
+ const [state, setState] = useState(initialState);
21
+
22
+ const setField = (name: keyof T, value: T[keyof T]) => {
23
+ setState((prev) => ({ ...prev, [name]: value }));
24
+ };
25
+
26
+ const reset = () => setState(initialState);
27
+
28
+ const getFieldProps = (name: keyof T) => ({
29
+ value: state[name],
30
+ onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
31
+ setField(name, e.target.value as T[keyof T]),
32
+ });
33
+
34
+ return {
35
+ ...state,
36
+ setField,
37
+ reset,
38
+ getFieldProps,
39
+ };
40
+ }
@@ -0,0 +1,52 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+
5
+ type UseGlobalKeydownsProps = {
6
+ onOpenCreateTask?: () => void;
7
+ onOpenCreateSprint?: () => void;
8
+ onCloseCreateTask?: () => void;
9
+ };
10
+
11
+ export const useGlobalKeydowns = ({
12
+ onOpenCreateTask = () => {
13
+ /* no-op */
14
+ },
15
+ onOpenCreateSprint = () => {
16
+ /* no-op */
17
+ },
18
+ onCloseCreateTask = () => {
19
+ /* no-op */
20
+ },
21
+ }: UseGlobalKeydownsProps) => {
22
+ useEffect(() => {
23
+ const handleKeyDown = (e: KeyboardEvent) => {
24
+ const isInput =
25
+ e.target instanceof HTMLInputElement ||
26
+ e.target instanceof HTMLTextAreaElement ||
27
+ e.target instanceof HTMLSelectElement ||
28
+ (e.target instanceof HTMLElement && e.target.isContentEditable);
29
+
30
+ if (isInput) {
31
+ return;
32
+ }
33
+
34
+ if (e.altKey && e.code === "KeyN") {
35
+ e.preventDefault();
36
+ onOpenCreateTask();
37
+ }
38
+
39
+ if (e.altKey && e.code === "KeyS") {
40
+ e.preventDefault();
41
+ onOpenCreateSprint();
42
+ }
43
+
44
+ if (e.key === "Escape") {
45
+ onCloseCreateTask();
46
+ }
47
+ };
48
+
49
+ window.addEventListener("keydown", handleKeyDown);
50
+ return () => window.removeEventListener("keydown", handleKeyDown);
51
+ }, [onOpenCreateTask, onOpenCreateSprint, onCloseCreateTask]);
52
+ };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Hook for invitation acceptance form management
3
+ * Handles token verification and invitation acceptance
4
+ */
5
+ "use client";
6
+
7
+ import { type Invitation } from "@locusai/shared";
8
+ import { useQueryClient } from "@tanstack/react-query";
9
+ import { useCallback, useEffect, useState } from "react";
10
+ import { toast } from "sonner";
11
+ import { useAuth } from "@/context/AuthContext";
12
+ import { locusClient } from "@/lib/api-client";
13
+ import { queryKeys } from "@/lib/query-keys";
14
+
15
+ interface UseInviteFormReturn {
16
+ invitation: Invitation | null;
17
+ userExists: boolean;
18
+ loading: boolean;
19
+ error: string | null;
20
+ name: string;
21
+ isSubmitting: boolean;
22
+ isLoggingInAsInvitedUser: boolean;
23
+ setName: (name: string) => void;
24
+ handleJoin: (e: React.FormEvent) => Promise<void>;
25
+ }
26
+
27
+ /**
28
+ * Custom hook for managing invitation acceptance
29
+ * Handles token verification and acceptance flow
30
+ */
31
+ export function useInviteForm(token: string | null): UseInviteFormReturn {
32
+ const queryClient = useQueryClient();
33
+ const { user: currentUser, isAuthenticated } = useAuth();
34
+
35
+ const [invitation, setInvitation] = useState<Invitation | null>(null);
36
+ const [userExists, setUserExists] = useState(false);
37
+ const [loading, setLoading] = useState(true);
38
+ const [error, setError] = useState<string | null>(null);
39
+ const [name, setName] = useState("");
40
+ const [isSubmitting, setIsSubmitting] = useState(false);
41
+
42
+ const verifyToken = useCallback(async () => {
43
+ if (!token) {
44
+ setError("No invitation token provided");
45
+ setLoading(false);
46
+ return;
47
+ }
48
+
49
+ try {
50
+ const response = await locusClient.invitations.verify(token);
51
+ setInvitation(response.invitation);
52
+ setUserExists(!!response.userExists);
53
+ } catch (err: unknown) {
54
+ const message =
55
+ err instanceof Error ? err.message : "Invalid or expired invitation";
56
+ setError(message);
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ }, [token]);
61
+
62
+ useEffect(() => {
63
+ verifyToken();
64
+ }, [verifyToken]);
65
+
66
+ const isLoggingInAsInvitedUser =
67
+ isAuthenticated && currentUser?.email === invitation?.email;
68
+
69
+ const handleJoin = async (e: React.FormEvent) => {
70
+ e.preventDefault();
71
+ if (!token || !invitation) return;
72
+
73
+ setIsSubmitting(true);
74
+ try {
75
+ await locusClient.invitations.accept({
76
+ token,
77
+ name: userExists ? undefined : name,
78
+ });
79
+
80
+ toast.success(
81
+ userExists
82
+ ? "Successfully joined organization!"
83
+ : "Account created and organization joined!"
84
+ );
85
+
86
+ // Invalidate caches after successful join
87
+ if (isAuthenticated && currentUser?.orgId) {
88
+ await queryClient.invalidateQueries({
89
+ queryKey: queryKeys.invitations.list(currentUser.orgId),
90
+ });
91
+ // Also invalidate members list to show the new member
92
+ await queryClient.invalidateQueries({
93
+ queryKey: queryKeys.organizations.members(currentUser.orgId),
94
+ });
95
+ }
96
+
97
+ return {
98
+ success: true,
99
+ isAuthenticated,
100
+ invitationEmail: invitation.email,
101
+ };
102
+ } catch (err: unknown) {
103
+ const message = err instanceof Error ? err.message : "Failed to join";
104
+ toast.error(message);
105
+ return { success: false };
106
+ } finally {
107
+ setIsSubmitting(false);
108
+ }
109
+ };
110
+
111
+ return {
112
+ invitation,
113
+ userExists,
114
+ loading,
115
+ error,
116
+ name,
117
+ isSubmitting,
118
+ isLoggingInAsInvitedUser,
119
+ setName,
120
+ handleJoin: handleJoin as (e: React.FormEvent) => Promise<void>,
121
+ };
122
+ }