@pylonsync/create-pylon 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (675) hide show
  1. package/bin/create-pylon.js +30 -7
  2. package/package.json +1 -1
  3. package/templates/agency/.env.example +7 -0
  4. package/templates/agency/README.md +28 -3
  5. package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
  6. package/templates/agency/app/(marketing)/page.tsx +3 -3
  7. package/templates/agency/app/(marketing)/work/page.tsx +3 -3
  8. package/templates/agency/app/auth-form.tsx +4 -4
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
  10. package/templates/agency/app/dashboard/page.tsx +14 -16
  11. package/templates/agency/app/icon.svg +3 -0
  12. package/templates/agency/app/login/page.tsx +12 -14
  13. package/templates/agency/components/marketing.tsx +12 -5
  14. package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
  15. package/templates/agency/lib/agency.ts +22 -0
  16. package/templates/agency/lib/demo.ts +118 -0
  17. package/templates/agency/lib/site.config.ts +19 -13
  18. package/templates/agency/public/favicon.ico +0 -0
  19. package/templates/agency/tests/agency.test.ts +57 -0
  20. package/templates/ai-chat/.env.example +13 -27
  21. package/templates/ai-chat/README.md +53 -60
  22. package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
  23. package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
  24. package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
  25. package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
  26. package/templates/ai-chat/app/(chat)/page.tsx +7 -11
  27. package/templates/ai-chat/app/auth-form.tsx +45 -38
  28. package/templates/ai-chat/app/error.tsx +9 -12
  29. package/templates/ai-chat/app/globals.css +464 -102
  30. package/templates/ai-chat/app/layout.tsx +11 -19
  31. package/templates/ai-chat/app/login/page.tsx +39 -24
  32. package/templates/ai-chat/app.ts +118 -43
  33. package/templates/ai-chat/components/chat/composer.tsx +193 -0
  34. package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
  35. package/templates/ai-chat/components/chat/icons.tsx +198 -0
  36. package/templates/ai-chat/components/chat/markdown.tsx +164 -0
  37. package/templates/ai-chat/components/chat/message.tsx +183 -0
  38. package/templates/ai-chat/components/chat/setup.tsx +71 -0
  39. package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
  40. package/templates/ai-chat/functions/_beginTurn.ts +145 -0
  41. package/templates/ai-chat/functions/_finishTurn.ts +55 -0
  42. package/templates/ai-chat/functions/assistantStatus.ts +13 -0
  43. package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
  44. package/templates/ai-chat/functions/createConversation.ts +28 -0
  45. package/templates/ai-chat/functions/deleteConversation.ts +15 -20
  46. package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
  47. package/templates/ai-chat/functions/renameConversation.ts +19 -0
  48. package/templates/ai-chat/functions/respond.ts +88 -0
  49. package/templates/ai-chat/functions/seedExamples.ts +46 -0
  50. package/templates/ai-chat/functions/stopResponse.ts +30 -0
  51. package/templates/ai-chat/lib/brand-icon.ts +7 -0
  52. package/templates/ai-chat/lib/chat.ts +277 -0
  53. package/templates/ai-chat/lib/claim.ts +15 -0
  54. package/templates/ai-chat/lib/examples.ts +179 -0
  55. package/templates/ai-chat/lib/highlight.ts +107 -0
  56. package/templates/ai-chat/lib/markdown.ts +317 -0
  57. package/templates/ai-chat/lib/provider.ts +38 -0
  58. package/templates/ai-chat/lib/site.config.ts +55 -39
  59. package/templates/ai-chat/tests/chat.test.ts +174 -0
  60. package/templates/ai-chat/tests/components.test.tsx +57 -0
  61. package/templates/ai-chat/tests/markdown.test.ts +88 -0
  62. package/templates/ai-chat/tests/provider.test.ts +32 -0
  63. package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
  64. package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
  65. package/templates/backend/consumer/apps/api/app.ts +166 -0
  66. package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
  67. package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
  68. package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
  69. package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
  70. package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
  71. package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
  72. package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
  73. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
  74. package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
  75. package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
  76. package/templates/backend/consumer/apps/api/package.json +8 -2
  77. package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
  78. package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
  79. package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
  80. package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
  81. package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
  82. package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
  83. package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
  84. package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
  85. package/templates/chat/README.md +43 -21
  86. package/templates/chat/app/chat-client.tsx +495 -85
  87. package/templates/chat/app/error.tsx +1 -1
  88. package/templates/chat/app/globals.css +170 -54
  89. package/templates/chat/app/layout.tsx +12 -9
  90. package/templates/chat/app/not-found.tsx +4 -2
  91. package/templates/chat/app/page.tsx +9 -19
  92. package/templates/chat/app.ts +96 -17
  93. package/templates/chat/components/chat/avatar.tsx +45 -0
  94. package/templates/chat/components/chat/composer.tsx +110 -0
  95. package/templates/chat/components/chat/message-list.tsx +285 -0
  96. package/templates/chat/components/chat/sidebar.tsx +190 -0
  97. package/templates/chat/functions/joinChat.ts +83 -0
  98. package/templates/chat/functions/renameMember.ts +25 -0
  99. package/templates/chat/lib/chat.ts +188 -0
  100. package/templates/chat/lib/seed.ts +78 -0
  101. package/templates/chat/tests/chat.test.ts +159 -0
  102. package/templates/chat/tests/composer.test.tsx +54 -0
  103. package/templates/consumer/README.md +3 -3
  104. package/templates/consumer/app/globals.css +10 -3
  105. package/templates/consumer/app/icon.svg +1 -0
  106. package/templates/consumer/components/social/avatar.tsx +25 -11
  107. package/templates/consumer/functions/seedFeed.ts +3 -3
  108. package/templates/consumer/lib/seed.ts +43 -85
  109. package/templates/consumer/public/favicon.ico +0 -0
  110. package/templates/consumer/public/images/posts/arch.jpg +0 -0
  111. package/templates/consumer/public/images/posts/coffee.jpg +0 -0
  112. package/templates/consumer/public/images/posts/dog.jpg +0 -0
  113. package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
  114. package/templates/consumer/public/images/posts/ramen.jpg +0 -0
  115. package/templates/consumer/public/images/posts/run.jpg +0 -0
  116. package/templates/consumer/public/images/posts/sunset.jpg +0 -0
  117. package/templates/consumer/tests/seed.test.ts +39 -0
  118. package/templates/creator/.env.example +7 -0
  119. package/templates/creator/README.md +24 -5
  120. package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
  121. package/templates/creator/app/auth-form.tsx +7 -7
  122. package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
  123. package/templates/creator/app/dashboard/page.tsx +18 -18
  124. package/templates/creator/app/icon.svg +2 -0
  125. package/templates/creator/app/login/page.tsx +14 -14
  126. package/templates/creator/app.ts +4 -4
  127. package/templates/creator/functions/seedDemo.ts +32 -0
  128. package/templates/creator/functions/subscribe.ts +4 -15
  129. package/templates/creator/lib/demo.ts +94 -0
  130. package/templates/creator/lib/site.config.ts +15 -10
  131. package/templates/creator/lib/stats.ts +35 -0
  132. package/templates/creator/public/favicon.ico +0 -0
  133. package/templates/creator/tests/counter.test.ts +92 -0
  134. package/templates/creator/tests/live-count.test.tsx +45 -0
  135. package/templates/crm/README.md +22 -1
  136. package/templates/crm/app/apple-icon.png +0 -0
  137. package/templates/crm/app/companies/companies-view.tsx +32 -14
  138. package/templates/crm/app/companies/page.tsx +2 -1
  139. package/templates/crm/app/contacts/contacts-view.tsx +34 -10
  140. package/templates/crm/app/contacts/page.tsx +2 -1
  141. package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
  142. package/templates/crm/app/deals/[id]/page.tsx +2 -1
  143. package/templates/crm/app/globals.css +29 -14
  144. package/templates/crm/app/icon.svg +1 -0
  145. package/templates/crm/app/layout.tsx +7 -5
  146. package/templates/crm/app/page.tsx +2 -1
  147. package/templates/crm/app/pipeline-view.tsx +5 -5
  148. package/templates/crm/app/workspace.tsx +29 -17
  149. package/templates/crm/app.ts +5 -5
  150. package/templates/crm/components/activity-timeline.tsx +14 -4
  151. package/templates/crm/components/app-shell.tsx +94 -0
  152. package/templates/crm/components/auth-form.tsx +35 -5
  153. package/templates/crm/components/avatar.tsx +5 -1
  154. package/templates/crm/components/board-skeleton.tsx +5 -5
  155. package/templates/crm/components/brand-mark.tsx +25 -0
  156. package/templates/crm/components/command-palette.tsx +17 -17
  157. package/templates/crm/components/data-table.tsx +97 -36
  158. package/templates/crm/components/deal-card.tsx +23 -27
  159. package/templates/crm/components/filter-tabs.tsx +101 -0
  160. package/templates/crm/components/metrics-bar.tsx +6 -6
  161. package/templates/crm/components/nav.tsx +15 -0
  162. package/templates/crm/components/page-header.tsx +43 -7
  163. package/templates/crm/components/pipeline-board.tsx +16 -10
  164. package/templates/crm/components/require-auth.tsx +24 -26
  165. package/templates/crm/components/sidebar.tsx +54 -49
  166. package/templates/crm/components/stage-badge.tsx +4 -2
  167. package/templates/crm/components/ui/button.tsx +5 -4
  168. package/templates/crm/components/ui/dialog.tsx +1 -1
  169. package/templates/crm/components/ui/input.tsx +1 -1
  170. package/templates/crm/components/ui/select.tsx +1 -1
  171. package/templates/crm/components/ui/textarea.tsx +1 -1
  172. package/templates/crm/functions/seedWorkspace.ts +32 -17
  173. package/templates/crm/lib/brand.ts +9 -0
  174. package/templates/crm/lib/demo-team.ts +51 -0
  175. package/templates/crm/lib/pipeline.ts +37 -0
  176. package/templates/crm/lib/search.ts +4 -3
  177. package/templates/crm/lib/seed.ts +53 -8
  178. package/templates/crm/public/favicon.ico +0 -0
  179. package/templates/crm/tests/components.test.tsx +5 -5
  180. package/templates/crm/tests/demo-team.test.ts +18 -0
  181. package/templates/crm/tests/pipeline.test.ts +59 -3
  182. package/templates/directory/.env.example +7 -0
  183. package/templates/directory/README.md +22 -3
  184. package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
  185. package/templates/directory/app/(marketing)/page.tsx +6 -4
  186. package/templates/directory/app/auth-form.tsx +4 -4
  187. package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
  188. package/templates/directory/app/dashboard/page.tsx +16 -14
  189. package/templates/directory/app/icon.svg +4 -0
  190. package/templates/directory/app/login/page.tsx +16 -15
  191. package/templates/directory/functions/seedDemo.ts +43 -0
  192. package/templates/directory/lib/demo.ts +145 -0
  193. package/templates/directory/lib/directory.ts +27 -0
  194. package/templates/directory/lib/site.config.ts +21 -16
  195. package/templates/directory/public/favicon.ico +0 -0
  196. package/templates/directory/tests/directory.test.ts +46 -0
  197. package/templates/expo/consumer/apps/expo/App.tsx +360 -288
  198. package/templates/expo/consumer/apps/expo/app.json +9 -1
  199. package/templates/expo/consumer/apps/expo/package.json +2 -1
  200. package/templates/helpdesk/README.md +21 -0
  201. package/templates/helpdesk/app/apple-icon.png +0 -0
  202. package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
  203. package/templates/helpdesk/app/customers/page.tsx +2 -1
  204. package/templates/helpdesk/app/globals.css +29 -14
  205. package/templates/helpdesk/app/icon.svg +1 -0
  206. package/templates/helpdesk/app/inbox-view.tsx +66 -47
  207. package/templates/helpdesk/app/layout.tsx +7 -5
  208. package/templates/helpdesk/app/page.tsx +4 -7
  209. package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
  210. package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
  211. package/templates/helpdesk/app/workspace.tsx +33 -22
  212. package/templates/helpdesk/app.ts +2 -2
  213. package/templates/helpdesk/components/app-shell.tsx +94 -0
  214. package/templates/helpdesk/components/auth-form.tsx +35 -5
  215. package/templates/helpdesk/components/avatar.tsx +5 -1
  216. package/templates/helpdesk/components/brand-mark.tsx +28 -0
  217. package/templates/helpdesk/components/command-palette.tsx +17 -17
  218. package/templates/helpdesk/components/data-table.tsx +97 -36
  219. package/templates/helpdesk/components/filter-tabs.tsx +101 -0
  220. package/templates/helpdesk/components/nav.tsx +14 -0
  221. package/templates/helpdesk/components/page-header.tsx +43 -7
  222. package/templates/helpdesk/components/priority-badge.tsx +49 -6
  223. package/templates/helpdesk/components/require-auth.tsx +24 -26
  224. package/templates/helpdesk/components/sidebar.tsx +54 -48
  225. package/templates/helpdesk/components/sla-indicator.tsx +5 -8
  226. package/templates/helpdesk/components/thread.tsx +7 -7
  227. package/templates/helpdesk/components/ticket-list.tsx +79 -19
  228. package/templates/helpdesk/components/ui/button.tsx +5 -4
  229. package/templates/helpdesk/components/ui/dialog.tsx +1 -1
  230. package/templates/helpdesk/components/ui/input.tsx +1 -1
  231. package/templates/helpdesk/components/ui/select.tsx +1 -1
  232. package/templates/helpdesk/components/ui/textarea.tsx +1 -1
  233. package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
  234. package/templates/helpdesk/lib/brand.ts +9 -0
  235. package/templates/helpdesk/lib/demo-team.ts +51 -0
  236. package/templates/helpdesk/lib/search.ts +4 -3
  237. package/templates/helpdesk/lib/seed.ts +43 -2
  238. package/templates/helpdesk/lib/tickets.ts +68 -20
  239. package/templates/helpdesk/public/favicon.ico +0 -0
  240. package/templates/helpdesk/tests/components.test.tsx +10 -0
  241. package/templates/helpdesk/tests/demo-team.test.ts +18 -0
  242. package/templates/helpdesk/tests/tickets.test.ts +70 -0
  243. package/templates/imessage/.env.example +49 -0
  244. package/templates/imessage/AGENTS.md +153 -0
  245. package/templates/imessage/CLAUDE.md +7 -0
  246. package/templates/imessage/README.md +187 -0
  247. package/templates/imessage/app/console.tsx +382 -0
  248. package/templates/imessage/app/contacts/page.tsx +13 -0
  249. package/templates/imessage/app/error.tsx +16 -0
  250. package/templates/imessage/app/gate.tsx +31 -0
  251. package/templates/imessage/app/globals.css +183 -0
  252. package/templates/imessage/app/layout.tsx +17 -0
  253. package/templates/imessage/app/login/login-form.tsx +115 -0
  254. package/templates/imessage/app/login/page.tsx +30 -0
  255. package/templates/imessage/app/not-found.tsx +13 -0
  256. package/templates/imessage/app/page.tsx +14 -0
  257. package/templates/imessage/app/robots.ts +6 -0
  258. package/templates/imessage/app/setup/page.tsx +13 -0
  259. package/templates/imessage/app/sign-out.tsx +20 -0
  260. package/templates/imessage/app.ts +299 -0
  261. package/templates/imessage/bunfig +4 -0
  262. package/templates/imessage/components/avatar.tsx +46 -0
  263. package/templates/imessage/components/contact-panel.tsx +263 -0
  264. package/templates/imessage/components/contacts-view.tsx +172 -0
  265. package/templates/imessage/components/conversation-list.tsx +129 -0
  266. package/templates/imessage/components/nav-rail.tsx +67 -0
  267. package/templates/imessage/components/setup-view.tsx +408 -0
  268. package/templates/imessage/components/thread.tsx +229 -0
  269. package/templates/imessage/components/ui/badge.tsx +37 -0
  270. package/templates/imessage/components/ui/button.tsx +56 -0
  271. package/templates/imessage/components/ui/card.tsx +90 -0
  272. package/templates/imessage/components/ui/dialog.tsx +110 -0
  273. package/templates/imessage/components/ui/input.tsx +30 -0
  274. package/templates/imessage/components/ui/label.tsx +21 -0
  275. package/templates/imessage/components/ui/select.tsx +39 -0
  276. package/templates/imessage/components/ui/table.tsx +124 -0
  277. package/templates/imessage/components/ui/textarea.tsx +24 -0
  278. package/templates/imessage/components.json +20 -0
  279. package/templates/imessage/functions/answerNow.ts +34 -0
  280. package/templates/imessage/functions/assistant.ts +96 -0
  281. package/templates/imessage/functions/beginTurn.ts +101 -0
  282. package/templates/imessage/functions/cancelReminder.ts +13 -0
  283. package/templates/imessage/functions/claimOutbound.ts +44 -0
  284. package/templates/imessage/functions/clearDemo.ts +18 -0
  285. package/templates/imessage/functions/deleteNote.ts +11 -0
  286. package/templates/imessage/functions/deliverOutbound.ts +55 -0
  287. package/templates/imessage/functions/finishTurn.ts +84 -0
  288. package/templates/imessage/functions/fireReminder.ts +42 -0
  289. package/templates/imessage/functions/ingestInbound.ts +22 -0
  290. package/templates/imessage/functions/ownerAccount.ts +13 -0
  291. package/templates/imessage/functions/processTurn.ts +74 -0
  292. package/templates/imessage/functions/relayApply.ts +102 -0
  293. package/templates/imessage/functions/relaySync.ts +63 -0
  294. package/templates/imessage/functions/releaseOutbound.ts +18 -0
  295. package/templates/imessage/functions/resolveTurn.ts +25 -0
  296. package/templates/imessage/functions/saveContact.ts +27 -0
  297. package/templates/imessage/functions/seedDemo.ts +70 -0
  298. package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
  299. package/templates/imessage/functions/sendblueWebhook.ts +49 -0
  300. package/templates/imessage/functions/setContactStatus.ts +17 -0
  301. package/templates/imessage/functions/settleOutbound.ts +35 -0
  302. package/templates/imessage/functions/setupStatus.ts +33 -0
  303. package/templates/imessage/functions/sweep.ts +58 -0
  304. package/templates/imessage/functions/toolCancelReminder.ts +17 -0
  305. package/templates/imessage/functions/toolForgetNote.ts +15 -0
  306. package/templates/imessage/functions/toolListNotes.ts +16 -0
  307. package/templates/imessage/functions/toolListReminders.ts +17 -0
  308. package/templates/imessage/functions/toolSaveNote.ts +22 -0
  309. package/templates/imessage/functions/toolSetReminder.ts +55 -0
  310. package/templates/imessage/functions/updateSettings.ts +48 -0
  311. package/templates/imessage/gitignore +11 -0
  312. package/templates/imessage/lib/agent-call.ts +251 -0
  313. package/templates/imessage/lib/assistant.ts +74 -0
  314. package/templates/imessage/lib/chunk.ts +135 -0
  315. package/templates/imessage/lib/demo.ts +68 -0
  316. package/templates/imessage/lib/format.ts +170 -0
  317. package/templates/imessage/lib/gate.ts +104 -0
  318. package/templates/imessage/lib/handles.ts +67 -0
  319. package/templates/imessage/lib/provider.ts +38 -0
  320. package/templates/imessage/lib/relay-protocol.ts +95 -0
  321. package/templates/imessage/lib/reminders.ts +41 -0
  322. package/templates/imessage/lib/secrets.ts +59 -0
  323. package/templates/imessage/lib/sendblue.ts +173 -0
  324. package/templates/imessage/lib/store.ts +407 -0
  325. package/templates/imessage/lib/transport.ts +135 -0
  326. package/templates/imessage/lib/utils.ts +10 -0
  327. package/templates/imessage/package.json +40 -0
  328. package/templates/imessage/relay/.env.example +14 -0
  329. package/templates/imessage/relay/applescript.ts +70 -0
  330. package/templates/imessage/relay/chatdb.ts +165 -0
  331. package/templates/imessage/relay/client.ts +41 -0
  332. package/templates/imessage/relay/main.ts +137 -0
  333. package/templates/imessage/relay/state.ts +35 -0
  334. package/templates/imessage/tests/agent-call.test.ts +112 -0
  335. package/templates/imessage/tests/applescript.test.ts +88 -0
  336. package/templates/imessage/tests/chatdb.test.ts +139 -0
  337. package/templates/imessage/tests/chunk.test.ts +61 -0
  338. package/templates/imessage/tests/fake-ctx.ts +88 -0
  339. package/templates/imessage/tests/ingest.test.ts +141 -0
  340. package/templates/imessage/tests/lib.test.ts +99 -0
  341. package/templates/imessage/tests/review-fixes.test.ts +138 -0
  342. package/templates/imessage/tests/secrets.test.ts +36 -0
  343. package/templates/imessage/tests/sendblue.test.ts +143 -0
  344. package/templates/imessage/tests/setup.ts +5 -0
  345. package/templates/imessage/tsconfig.json +18 -0
  346. package/templates/inventory/README.md +21 -0
  347. package/templates/inventory/app/apple-icon.png +0 -0
  348. package/templates/inventory/app/globals.css +29 -14
  349. package/templates/inventory/app/icon.svg +1 -0
  350. package/templates/inventory/app/layout.tsx +7 -5
  351. package/templates/inventory/app/movements/movements-view.tsx +32 -10
  352. package/templates/inventory/app/movements/page.tsx +2 -1
  353. package/templates/inventory/app/page.tsx +4 -6
  354. package/templates/inventory/app/products/[id]/page.tsx +2 -1
  355. package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
  356. package/templates/inventory/app/products-view.tsx +74 -33
  357. package/templates/inventory/app/workspace.tsx +34 -16
  358. package/templates/inventory/app.ts +10 -2
  359. package/templates/inventory/components/app-shell.tsx +94 -0
  360. package/templates/inventory/components/auth-form.tsx +35 -5
  361. package/templates/inventory/components/avatar.tsx +5 -1
  362. package/templates/inventory/components/brand-mark.tsx +25 -0
  363. package/templates/inventory/components/category-tile.tsx +49 -0
  364. package/templates/inventory/components/command-palette.tsx +17 -17
  365. package/templates/inventory/components/data-table.tsx +97 -36
  366. package/templates/inventory/components/filter-tabs.tsx +101 -0
  367. package/templates/inventory/components/nav.tsx +14 -0
  368. package/templates/inventory/components/page-header.tsx +43 -7
  369. package/templates/inventory/components/require-auth.tsx +24 -26
  370. package/templates/inventory/components/sidebar.tsx +54 -48
  371. package/templates/inventory/components/stock-level.tsx +30 -14
  372. package/templates/inventory/components/summary-bar.tsx +8 -15
  373. package/templates/inventory/components/ui/button.tsx +5 -4
  374. package/templates/inventory/components/ui/dialog.tsx +1 -1
  375. package/templates/inventory/components/ui/input.tsx +1 -1
  376. package/templates/inventory/components/ui/select.tsx +1 -1
  377. package/templates/inventory/components/ui/textarea.tsx +1 -1
  378. package/templates/inventory/functions/seedWorkspace.ts +18 -8
  379. package/templates/inventory/lib/brand.ts +9 -0
  380. package/templates/inventory/lib/demo-team.ts +51 -0
  381. package/templates/inventory/lib/search.ts +4 -3
  382. package/templates/inventory/lib/seed.ts +29 -2
  383. package/templates/inventory/lib/stock.ts +32 -0
  384. package/templates/inventory/public/favicon.ico +0 -0
  385. package/templates/inventory/tests/demo-team.test.ts +18 -0
  386. package/templates/inventory/tests/stock.test.ts +29 -0
  387. package/templates/invoices/README.md +16 -0
  388. package/templates/invoices/app/apple-icon.png +0 -0
  389. package/templates/invoices/app/clients/clients-view.tsx +18 -6
  390. package/templates/invoices/app/clients/page.tsx +2 -1
  391. package/templates/invoices/app/globals.css +29 -14
  392. package/templates/invoices/app/icon.svg +1 -0
  393. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
  394. package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
  395. package/templates/invoices/app/invoices-view.tsx +81 -32
  396. package/templates/invoices/app/layout.tsx +7 -5
  397. package/templates/invoices/app/page.tsx +4 -6
  398. package/templates/invoices/app/workspace.tsx +26 -16
  399. package/templates/invoices/app.ts +2 -2
  400. package/templates/invoices/components/app-shell.tsx +94 -0
  401. package/templates/invoices/components/auth-form.tsx +35 -5
  402. package/templates/invoices/components/avatar.tsx +5 -1
  403. package/templates/invoices/components/brand-mark.tsx +26 -0
  404. package/templates/invoices/components/command-palette.tsx +17 -17
  405. package/templates/invoices/components/data-table.tsx +97 -36
  406. package/templates/invoices/components/filter-tabs.tsx +101 -0
  407. package/templates/invoices/components/line-items.tsx +19 -14
  408. package/templates/invoices/components/nav.tsx +14 -0
  409. package/templates/invoices/components/page-header.tsx +43 -7
  410. package/templates/invoices/components/require-auth.tsx +24 -26
  411. package/templates/invoices/components/sidebar.tsx +54 -48
  412. package/templates/invoices/components/status-badge.tsx +7 -7
  413. package/templates/invoices/components/summary-bar.tsx +5 -5
  414. package/templates/invoices/components/ui/button.tsx +5 -4
  415. package/templates/invoices/components/ui/dialog.tsx +1 -1
  416. package/templates/invoices/components/ui/input.tsx +1 -1
  417. package/templates/invoices/components/ui/select.tsx +1 -1
  418. package/templates/invoices/components/ui/textarea.tsx +1 -1
  419. package/templates/invoices/functions/recordPayment.ts +3 -0
  420. package/templates/invoices/lib/billing.ts +40 -0
  421. package/templates/invoices/lib/brand.ts +9 -0
  422. package/templates/invoices/lib/search.ts +4 -3
  423. package/templates/invoices/public/favicon.ico +0 -0
  424. package/templates/invoices/tests/billing.test.ts +27 -0
  425. package/templates/ios/consumer/apps/ios/Package.swift +4 -3
  426. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
  427. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
  428. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
  429. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
  430. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
  431. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
  432. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
  433. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
  434. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
  435. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
  436. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
  437. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
  438. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
  439. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
  440. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
  441. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
  442. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
  443. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
  444. package/templates/ios/consumer/apps/ios/project.yml +13 -9
  445. package/templates/local-service/.env.example +7 -0
  446. package/templates/local-service/README.md +26 -2
  447. package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
  448. package/templates/local-service/app/(marketing)/page.tsx +66 -42
  449. package/templates/local-service/app/auth-form.tsx +8 -8
  450. package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
  451. package/templates/local-service/app/dashboard/page.tsx +14 -14
  452. package/templates/local-service/app/icon.svg +2 -0
  453. package/templates/local-service/app/login/page.tsx +15 -15
  454. package/templates/local-service/functions/seedDemo.ts +49 -0
  455. package/templates/local-service/lib/demo.ts +135 -0
  456. package/templates/local-service/lib/hours.ts +44 -0
  457. package/templates/local-service/lib/site.config.ts +8 -3
  458. package/templates/local-service/lib/slots.ts +3 -0
  459. package/templates/local-service/public/favicon.ico +0 -0
  460. package/templates/local-service/tests/booking.test.ts +104 -0
  461. package/templates/marketplace/README.md +23 -15
  462. package/templates/marketplace/app/globals.css +172 -114
  463. package/templates/marketplace/app/layout.tsx +75 -30
  464. package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
  465. package/templates/marketplace/app/me/page.tsx +4 -4
  466. package/templates/marketplace/app/page.tsx +221 -298
  467. package/templates/marketplace/app/sell/page.tsx +8 -11
  468. package/templates/marketplace/app.ts +32 -7
  469. package/templates/marketplace/client/ActivityFeed.tsx +311 -0
  470. package/templates/marketplace/client/AuthNav.tsx +24 -16
  471. package/templates/marketplace/client/ListingCard.tsx +118 -0
  472. package/templates/marketplace/client/LiveGrid.tsx +92 -0
  473. package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
  474. package/templates/marketplace/client/LoginCard.tsx +114 -136
  475. package/templates/marketplace/client/MarketProvider.tsx +3 -5
  476. package/templates/marketplace/client/MyMarket.tsx +371 -185
  477. package/templates/marketplace/client/OfferPanel.tsx +421 -287
  478. package/templates/marketplace/client/SellForm.tsx +271 -192
  479. package/templates/marketplace/client/ThemeToggle.tsx +1 -0
  480. package/templates/marketplace/client/WatchButton.tsx +16 -16
  481. package/templates/marketplace/client/market.ts +61 -14
  482. package/templates/marketplace/functions/seedMarket.ts +143 -36
  483. package/templates/marketplace/functions/seedOffers.ts +101 -0
  484. package/templates/marketplace/lib/catalog.ts +72 -1
  485. package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
  486. package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
  487. package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
  488. package/templates/marketplace/public/images/listings/duffel.webp +0 -0
  489. package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
  490. package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
  491. package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
  492. package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
  493. package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
  494. package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
  495. package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
  496. package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
  497. package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
  498. package/templates/marketplace/public/images/listings/turntable.webp +0 -0
  499. package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
  500. package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
  501. package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
  502. package/templates/marketplace/public/images/listings/watch.webp +0 -0
  503. package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
  504. package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
  505. package/templates/marketplace/tests/example.test.ts +64 -2
  506. package/templates/marketplace/tests/listing-card.test.tsx +61 -0
  507. package/templates/projects/README.md +24 -3
  508. package/templates/projects/app/apple-icon.png +0 -0
  509. package/templates/projects/app/clients/clients-view.tsx +20 -6
  510. package/templates/projects/app/clients/page.tsx +2 -1
  511. package/templates/projects/app/globals.css +29 -14
  512. package/templates/projects/app/icon.svg +1 -0
  513. package/templates/projects/app/layout.tsx +7 -5
  514. package/templates/projects/app/page.tsx +3 -5
  515. package/templates/projects/app/projects/[id]/page.tsx +2 -1
  516. package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
  517. package/templates/projects/app/projects-view.tsx +105 -52
  518. package/templates/projects/app/workspace.tsx +41 -20
  519. package/templates/projects/app.ts +2 -2
  520. package/templates/projects/components/app-shell.tsx +94 -0
  521. package/templates/projects/components/auth-form.tsx +35 -5
  522. package/templates/projects/components/avatar.tsx +5 -1
  523. package/templates/projects/components/board-skeleton.tsx +3 -3
  524. package/templates/projects/components/brand-mark.tsx +25 -0
  525. package/templates/projects/components/command-palette.tsx +17 -17
  526. package/templates/projects/components/data-table.tsx +97 -36
  527. package/templates/projects/components/filter-tabs.tsx +101 -0
  528. package/templates/projects/components/meter.tsx +48 -0
  529. package/templates/projects/components/nav.tsx +14 -0
  530. package/templates/projects/components/page-header.tsx +43 -7
  531. package/templates/projects/components/require-auth.tsx +24 -26
  532. package/templates/projects/components/sidebar.tsx +54 -48
  533. package/templates/projects/components/summary-bar.tsx +49 -0
  534. package/templates/projects/components/task-board.tsx +10 -9
  535. package/templates/projects/components/time-dialog.tsx +29 -5
  536. package/templates/projects/components/ui/button.tsx +5 -4
  537. package/templates/projects/components/ui/dialog.tsx +1 -1
  538. package/templates/projects/components/ui/input.tsx +1 -1
  539. package/templates/projects/components/ui/select.tsx +1 -1
  540. package/templates/projects/components/ui/textarea.tsx +1 -1
  541. package/templates/projects/functions/seedWorkspace.ts +26 -12
  542. package/templates/projects/lib/brand.ts +9 -0
  543. package/templates/projects/lib/demo-team.ts +51 -0
  544. package/templates/projects/lib/search.ts +4 -3
  545. package/templates/projects/lib/seed.ts +35 -1
  546. package/templates/projects/lib/work.ts +63 -0
  547. package/templates/projects/public/favicon.ico +0 -0
  548. package/templates/projects/tests/demo-team.test.ts +18 -0
  549. package/templates/projects/tests/work.test.ts +65 -0
  550. package/templates/restaurant/.env.example +7 -0
  551. package/templates/restaurant/README.md +29 -3
  552. package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
  553. package/templates/restaurant/app/(marketing)/page.tsx +47 -23
  554. package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
  555. package/templates/restaurant/app/auth-form.tsx +8 -8
  556. package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
  557. package/templates/restaurant/app/dashboard/page.tsx +14 -14
  558. package/templates/restaurant/app/icon.svg +4 -0
  559. package/templates/restaurant/app/login/page.tsx +15 -15
  560. package/templates/restaurant/functions/seedDemo.ts +44 -0
  561. package/templates/restaurant/lib/demo.ts +144 -0
  562. package/templates/restaurant/lib/hours.ts +45 -0
  563. package/templates/restaurant/lib/site.config.ts +3 -3
  564. package/templates/restaurant/public/favicon.ico +0 -0
  565. package/templates/restaurant/tests/reservations.test.ts +67 -0
  566. package/templates/saas/README.md +54 -35
  567. package/templates/saas/app/(auth)/auth-form.tsx +25 -23
  568. package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
  569. package/templates/saas/app/(auth)/layout.tsx +27 -33
  570. package/templates/saas/app/(auth)/login/page.tsx +1 -1
  571. package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
  572. package/templates/saas/app/(auth)/signup/page.tsx +1 -1
  573. package/templates/saas/app/(marketing)/layout.tsx +127 -253
  574. package/templates/saas/app/(marketing)/page.tsx +133 -372
  575. package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
  576. package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
  577. package/templates/saas/app/dashboard/board-client.tsx +416 -0
  578. package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
  579. package/templates/saas/app/dashboard/layout.tsx +27 -13
  580. package/templates/saas/app/dashboard/overview-client.tsx +332 -0
  581. package/templates/saas/app/dashboard/page.tsx +29 -35
  582. package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
  583. package/templates/saas/app/dashboard/projects/page.tsx +11 -7
  584. package/templates/saas/app/dashboard/projects-client.tsx +398 -0
  585. package/templates/saas/app/globals.css +5 -4
  586. package/templates/saas/app/icon.svg +1 -0
  587. package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
  588. package/templates/saas/app/onboarding/page.tsx +6 -2
  589. package/templates/saas/app.ts +68 -13
  590. package/templates/saas/components/dashboard-shell.tsx +212 -132
  591. package/templates/saas/components/marketing.tsx +137 -274
  592. package/templates/saas/components/pricing-table.tsx +12 -11
  593. package/templates/saas/components/setup-checklist.tsx +31 -29
  594. package/templates/saas/components/task-ui.tsx +199 -0
  595. package/templates/saas/functions/createProject.ts +7 -13
  596. package/templates/saas/functions/createTask.ts +67 -0
  597. package/templates/saas/functions/deleteProject.ts +17 -0
  598. package/templates/saas/functions/deleteTask.ts +14 -0
  599. package/templates/saas/functions/seedWorkspace.ts +46 -0
  600. package/templates/saas/functions/setProjectStatus.ts +41 -0
  601. package/templates/saas/functions/updateProfile.ts +13 -0
  602. package/templates/saas/functions/updateProject.ts +4 -5
  603. package/templates/saas/functions/updateTask.ts +88 -0
  604. package/templates/saas/lib/members.ts +53 -0
  605. package/templates/saas/lib/names.ts +53 -0
  606. package/templates/saas/lib/plans.ts +11 -0
  607. package/templates/saas/lib/sample-workspace.ts +117 -0
  608. package/templates/saas/lib/site.config.ts +200 -463
  609. package/templates/saas/lib/tasks.ts +101 -0
  610. package/templates/saas/lib/use-live.ts +25 -0
  611. package/templates/saas/public/favicon.ico +0 -0
  612. package/templates/saas/public/screenshots/board.png +0 -0
  613. package/templates/saas/public/screenshots/members.png +0 -0
  614. package/templates/saas/public/screenshots/overview.png +0 -0
  615. package/templates/saas/public/screenshots/projects.png +0 -0
  616. package/templates/saas/scripts/capture-screenshots.mjs +156 -0
  617. package/templates/saas/tests/names.test.ts +35 -0
  618. package/templates/saas/tests/plans.test.ts +12 -0
  619. package/templates/saas/tests/sample-workspace.test.ts +46 -0
  620. package/templates/saas/tests/tasks.test.ts +81 -0
  621. package/templates/shop/README.md +25 -13
  622. package/templates/shop/app/(marketing)/layout.tsx +94 -67
  623. package/templates/shop/app/(marketing)/not-found.tsx +13 -12
  624. package/templates/shop/app/(marketing)/page.tsx +120 -59
  625. package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
  626. package/templates/shop/app/(marketing)/success/page.tsx +75 -22
  627. package/templates/shop/app/globals.css +94 -1
  628. package/templates/shop/app/layout.tsx +4 -4
  629. package/templates/shop/app/login/page.tsx +4 -4
  630. package/templates/shop/app.ts +18 -6
  631. package/templates/shop/components/cart-button.tsx +33 -0
  632. package/templates/shop/components/marketing.tsx +17 -87
  633. package/templates/shop/components/shelf-count.tsx +36 -0
  634. package/templates/shop/functions/checkout.ts +17 -1
  635. package/templates/shop/lib/cart-store.ts +57 -0
  636. package/templates/shop/lib/shelf-store.ts +35 -0
  637. package/templates/shop/lib/shop.ts +27 -0
  638. package/templates/shop/lib/site.config.ts +108 -98
  639. package/templates/shop/public/images/hero.jpg +0 -0
  640. package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
  641. package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
  642. package/templates/shop/public/images/products/linen.jpg +0 -0
  643. package/templates/shop/public/images/products/sea-salt.jpg +0 -0
  644. package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
  645. package/templates/shop/tests/shop.test.ts +64 -0
  646. package/templates/waitlist/.env.example +7 -0
  647. package/templates/waitlist/README.md +29 -13
  648. package/templates/waitlist/app/(marketing)/page.tsx +9 -7
  649. package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
  650. package/templates/waitlist/app/auth-form.tsx +8 -8
  651. package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
  652. package/templates/waitlist/app/dashboard/page.tsx +17 -16
  653. package/templates/waitlist/app/icon.svg +2 -0
  654. package/templates/waitlist/app/login/page.tsx +14 -14
  655. package/templates/waitlist/app.ts +6 -6
  656. package/templates/waitlist/functions/joinWaitlist.ts +6 -18
  657. package/templates/waitlist/functions/seedDemo.ts +32 -0
  658. package/templates/waitlist/lib/demo.ts +94 -0
  659. package/templates/waitlist/lib/launch.ts +19 -0
  660. package/templates/waitlist/lib/site.config.ts +14 -9
  661. package/templates/waitlist/lib/stats.ts +35 -0
  662. package/templates/waitlist/public/favicon.ico +0 -0
  663. package/templates/waitlist/tests/counter.test.ts +107 -0
  664. package/templates/waitlist/tests/live-count.test.tsx +48 -0
  665. package/templates/ai-chat/tests/example.test.ts +0 -12
  666. package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
  667. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
  668. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
  669. package/templates/backend/consumer/apps/api/schema.ts +0 -143
  670. package/templates/chat/functions/_keep.ts +0 -13
  671. package/templates/chat/tests/example.test.ts +0 -12
  672. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
  673. package/templates/marketplace/client/LiveTicker.tsx +0 -116
  674. package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
  675. package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
3
  import React, { useState } from "react";
4
- import { callFn } from "@pylonsync/react";
5
- import { Check, X } from "lucide-react";
4
+ import { callFn, Link } from "@pylonsync/react";
5
+ import { Check, ChevronRight, X } from "lucide-react";
6
6
 
7
7
  export interface SetupState {
8
8
  orgId: string;
@@ -13,9 +13,9 @@ export interface SetupState {
13
13
  }
14
14
 
15
15
  /**
16
- * "Getting started" on the Overview. Items are derived from real data (a
16
+ * "Get started" steps on the Overview. Each step is derived from real data (a
17
17
  * project exists, a teammate or pending invite exists, Pro is active), so the
18
- * list ticks itself off; it disappears when all three are done or an
18
+ * list ticks itself off. It disappears when all three are done or an
19
19
  * owner/admin dismisses it for the workspace.
20
20
  */
21
21
  export function SetupChecklist({ state }: { state: SetupState }) {
@@ -23,10 +23,10 @@ export function SetupChecklist({ state }: { state: SetupState }) {
23
23
  const items = [
24
24
  { done: state.hasProject, label: "Create a project", href: "/dashboard/projects" },
25
25
  { done: state.hasTeammate, label: "Invite a teammate", href: "/dashboard/members" },
26
- { done: state.hasBilling, label: "Start your Pro trial", href: "/dashboard/billing" },
26
+ { done: state.hasBilling, label: "Start the Pro trial", href: "/dashboard/billing" },
27
27
  ];
28
- const remaining = items.filter((i) => !i.done).length;
29
- if (hidden || remaining === 0) return null;
28
+ const doneCount = items.filter((i) => i.done).length;
29
+ if (hidden || doneCount === items.length) return null;
30
30
 
31
31
  async function dismiss() {
32
32
  setHidden(true);
@@ -38,42 +38,44 @@ export function SetupChecklist({ state }: { state: SetupState }) {
38
38
  }
39
39
 
40
40
  return (
41
- <div className="rounded-xl border border-zinc-200 bg-white p-5">
42
- <div className="flex items-center justify-between">
43
- <div>
44
- <h2 className="text-sm font-semibold text-zinc-900">Getting started</h2>
45
- <p className="mt-0.5 text-xs text-zinc-500">
46
- {items.length - remaining} of {items.length} done
47
- </p>
41
+ <div className="rounded-xl border border-zinc-200/80 bg-white">
42
+ <div className="flex items-center justify-between gap-3 border-b border-zinc-100 px-4 py-3">
43
+ <div className="flex items-center gap-3">
44
+ <h2 className="text-[13px] font-semibold text-zinc-900">Get started</h2>
45
+ <span className="font-mono text-[11.5px] tabular-nums text-zinc-400">
46
+ {doneCount}/{items.length}
47
+ </span>
48
48
  </div>
49
49
  {state.canDismiss && (
50
50
  <button
51
51
  type="button"
52
52
  onClick={() => void dismiss()}
53
- aria-label="Dismiss checklist"
53
+ aria-label="Hide the setup steps"
54
54
  className="flex size-7 items-center justify-center rounded-md text-zinc-400 hover:bg-zinc-100 hover:text-zinc-700"
55
55
  >
56
56
  <X className="size-4" />
57
57
  </button>
58
58
  )}
59
59
  </div>
60
- <ul className="mt-3 divide-y divide-zinc-100">
60
+ <ul className="grid divide-y divide-zinc-100 sm:grid-cols-3 sm:divide-x sm:divide-y-0">
61
61
  {items.map((it) => (
62
- <li key={it.label} className="flex items-center gap-3 py-2.5">
63
- <span
64
- className={
65
- "flex size-5 shrink-0 items-center justify-center rounded-full border " +
66
- (it.done ? "border-emerald-500 bg-emerald-500 text-white" : "border-zinc-300")
67
- }
68
- >
69
- {it.done && <Check className="size-3" />}
70
- </span>
62
+ <li key={it.label}>
71
63
  {it.done ? (
72
- <span className="text-sm text-zinc-400 line-through">{it.label}</span>
64
+ <div className="flex items-center gap-2.5 px-4 py-3 text-[13px] text-zinc-400">
65
+ <span className="flex size-[18px] items-center justify-center rounded-full bg-brand text-white">
66
+ <Check className="size-3" strokeWidth={2.5} />
67
+ </span>
68
+ <span className="line-through">{it.label}</span>
69
+ </div>
73
70
  ) : (
74
- <a href={it.href} className="text-sm font-medium text-zinc-900 hover:underline">
75
- {it.label} →
76
- </a>
71
+ <Link
72
+ href={it.href}
73
+ className="group flex items-center gap-2.5 px-4 py-3 text-[13px] font-medium text-zinc-800 transition-colors hover:bg-zinc-50"
74
+ >
75
+ <span className="size-[18px] rounded-full border-[1.5px] border-dashed border-zinc-300" />
76
+ {it.label}
77
+ <ChevronRight className="ml-auto size-3.5 text-zinc-300 transition-transform group-hover:translate-x-0.5 group-hover:text-zinc-500" />
78
+ </Link>
77
79
  )}
78
80
  </li>
79
81
  ))}
@@ -0,0 +1,199 @@
1
+ import React from "react";
2
+ import { initials } from "@/lib/names";
3
+ import { daysUntil, type TaskStatus } from "@/lib/tasks";
4
+
5
+ // Small presentational pieces shared by the Overview, the project list, and
6
+ // the task board. No data access here: every component renders from props.
7
+
8
+ /** Status glyph: an outlined circle that fills as the task moves to done. */
9
+ export function StatusIcon({
10
+ status,
11
+ className = "size-3.5",
12
+ }: {
13
+ status: string;
14
+ className?: string;
15
+ }) {
16
+ const s = status as TaskStatus;
17
+ if (s === "done") {
18
+ return (
19
+ <svg viewBox="0 0 14 14" className={`${className} shrink-0 text-brand`} aria-hidden>
20
+ <circle cx="7" cy="7" r="6.25" fill="currentColor" stroke="currentColor" strokeWidth="1.5" />
21
+ <path d="M4.3 7.2 6.1 9l3.6-3.8" fill="none" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
22
+ </svg>
23
+ );
24
+ }
25
+ const tone =
26
+ s === "in_progress" ? "text-amber-500" : s === "in_review" ? "text-emerald-500" : "text-zinc-400";
27
+ // The share of the inner disc that is filled: 0 for to do, half for in
28
+ // progress, three quarters for in review.
29
+ const fill = s === "in_progress" ? 0.5 : s === "in_review" ? 0.75 : 0;
30
+ const r = 3.25;
31
+ const c = 2 * Math.PI * r;
32
+ return (
33
+ <svg viewBox="0 0 14 14" className={`${className} shrink-0 ${tone}`} aria-hidden>
34
+ <circle cx="7" cy="7" r="6.25" fill="none" stroke="currentColor" strokeWidth="1.5" />
35
+ {fill > 0 ? (
36
+ <circle
37
+ cx="7"
38
+ cy="7"
39
+ r={r}
40
+ fill="none"
41
+ stroke="currentColor"
42
+ strokeWidth={r * 2}
43
+ strokeDasharray={`${c * fill} ${c}`}
44
+ transform="rotate(-90 7 7)"
45
+ />
46
+ ) : null}
47
+ </svg>
48
+ );
49
+ }
50
+
51
+ /** Priority glyph: three bars for high/medium/low, a filled square for urgent. */
52
+ export function PriorityIcon({
53
+ priority,
54
+ className = "size-3.5",
55
+ }: {
56
+ priority?: string | null;
57
+ className?: string;
58
+ }) {
59
+ const p = priority ?? "none";
60
+ if (p === "urgent") {
61
+ return (
62
+ <svg viewBox="0 0 14 14" className={`${className} shrink-0 text-orange-500`} aria-hidden>
63
+ <rect x="1" y="1" width="12" height="12" rx="3" fill="currentColor" />
64
+ <path d="M7 3.8v4" stroke="white" strokeWidth="1.6" strokeLinecap="round" />
65
+ <circle cx="7" cy="10.1" r="0.95" fill="white" />
66
+ </svg>
67
+ );
68
+ }
69
+ if (p === "none") {
70
+ return (
71
+ <svg viewBox="0 0 14 14" className={`${className} shrink-0 text-zinc-300`} aria-hidden>
72
+ <path d="M2.5 7h2M6 7h2M9.5 7h2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
73
+ </svg>
74
+ );
75
+ }
76
+ const level = p === "high" ? 3 : p === "medium" ? 2 : 1;
77
+ return (
78
+ <svg viewBox="0 0 14 14" className={`${className} shrink-0 text-zinc-600`} aria-hidden>
79
+ {[0, 1, 2].map((i) => (
80
+ <rect
81
+ key={i}
82
+ x={2 + i * 3.75}
83
+ y={9.5 - i * 3}
84
+ width="2.5"
85
+ height={2.5 + i * 3}
86
+ rx="0.75"
87
+ fill="currentColor"
88
+ opacity={i < level ? 1 : 0.22}
89
+ />
90
+ ))}
91
+ </svg>
92
+ );
93
+ }
94
+
95
+ // Stable avatar color per person, hashed from their id.
96
+ const AVATAR_TONES = [
97
+ "bg-[#e0e7ff] text-[#3730a3]",
98
+ "bg-[#dcfce7] text-[#166534]",
99
+ "bg-[#fef3c7] text-[#92400e]",
100
+ "bg-[#fce7f3] text-[#9d174d]",
101
+ "bg-[#e0f2fe] text-[#075985]",
102
+ "bg-[#ede9fe] text-[#5b21b6]",
103
+ ];
104
+
105
+ export function toneFor(key: string): string {
106
+ let h = 0;
107
+ for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) >>> 0;
108
+ return AVATAR_TONES[h % AVATAR_TONES.length];
109
+ }
110
+
111
+ export function Avatar({
112
+ id,
113
+ name,
114
+ size = "sm",
115
+ }: {
116
+ id: string;
117
+ name: string;
118
+ size?: "xs" | "sm" | "md";
119
+ }) {
120
+ const dims =
121
+ size === "xs" ? "size-[18px] text-[8.5px]" : size === "sm" ? "size-6 text-[10px]" : "size-8 text-[11.5px]";
122
+ return (
123
+ <span
124
+ title={name}
125
+ className={`inline-flex shrink-0 items-center justify-center rounded-full font-semibold ${dims} ${toneFor(id)}`}
126
+ >
127
+ {initials(name)}
128
+ </span>
129
+ );
130
+ }
131
+
132
+ /** A dashed ring for tasks with nobody assigned. */
133
+ export function EmptyAvatar({ size = "sm" }: { size?: "xs" | "sm" }) {
134
+ const dims = size === "xs" ? "size-[18px]" : "size-6";
135
+ return (
136
+ <span
137
+ title="Unassigned"
138
+ className={`inline-flex shrink-0 rounded-full border border-dashed border-zinc-300 ${dims}`}
139
+ />
140
+ );
141
+ }
142
+
143
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
144
+
145
+ /** "Sep 30" from `YYYY-MM-DD`, read as a calendar day (no time zone shift). */
146
+ export function shortDay(day: string): string {
147
+ const [, m, d] = day.split("-").map(Number);
148
+ return `${MONTHS[(m ?? 1) - 1]} ${d}`;
149
+ }
150
+
151
+ /**
152
+ * Due date with its urgency: red when overdue, amber when due today or
153
+ * tomorrow. `today` comes from the server render so the SSR HTML and the
154
+ * hydrated client agree.
155
+ */
156
+ export function DueLabel({
157
+ due,
158
+ today,
159
+ done,
160
+ }: {
161
+ due?: string | null;
162
+ today: string;
163
+ done?: boolean;
164
+ }) {
165
+ if (!due) return null;
166
+ const days = daysUntil(due, today);
167
+ const tone = done
168
+ ? "text-zinc-400"
169
+ : days < 0
170
+ ? "text-red-600"
171
+ : days <= 1
172
+ ? "text-amber-600"
173
+ : "text-zinc-500";
174
+ const label = done ? shortDay(due) : days === 0 ? "Today" : days === 1 ? "Tomorrow" : shortDay(due);
175
+ return <span className={`whitespace-nowrap text-[12px] tabular-nums ${tone}`}>{label}</span>;
176
+ }
177
+
178
+ /** A thin progress bar with the percentage beside it. */
179
+ export function Progress({ percent, className = "" }: { percent: number; className?: string }) {
180
+ return (
181
+ <div className={`flex items-center gap-2.5 ${className}`}>
182
+ <div className="h-1.5 flex-1 overflow-hidden rounded-full bg-zinc-100">
183
+ <div className="h-full rounded-full bg-brand" style={{ width: `${percent}%` }} />
184
+ </div>
185
+ <span className="w-9 text-right font-mono text-[11.5px] tabular-nums text-zinc-500">{percent}%</span>
186
+ </div>
187
+ );
188
+ }
189
+
190
+ /** Project monogram tile, colored per project. */
191
+ export function ProjectMark({ id, name, className = "size-7 text-[11px]" }: { id: string; name: string; className?: string }) {
192
+ return (
193
+ <span
194
+ className={`inline-flex shrink-0 items-center justify-center rounded-md font-semibold ${className} ${toneFor(id)}`}
195
+ >
196
+ {initials(name)}
197
+ </span>
198
+ );
199
+ }
@@ -1,6 +1,6 @@
1
1
  import { mutation, v } from "@pylonsync/functions";
2
2
  import { normalizeProjectName } from "../lib/projects";
3
- import { canCreateProject, planFromSubscription } from "../lib/plans";
3
+ import { canCreateProject, planFromSubscriptions } from "../lib/plans";
4
4
 
5
5
  // createProject — the free-tier cap lives here, on the server. The Project
6
6
  // policy denies client inserts, so a browser cannot skip the paywall by
@@ -23,21 +23,15 @@ export default mutation<
23
23
  // Any member of the workspace may create a project. Fails closed.
24
24
  await ctx.requireMember(args.orgId);
25
25
 
26
- const subs = (await ctx.db.unsafe.list("StripeSubscription")) as Array<{
27
- referenceId: string;
28
- plan?: string;
29
- status?: string;
30
- }>;
31
- const sub = subs.find((s) => s.referenceId === args.orgId) ?? null;
32
- const plan = planFromSubscription(sub);
26
+ const subs = (await ctx.db.unsafe.query("StripeSubscription", {
27
+ referenceId: args.orgId,
28
+ })) as Array<{ plan?: string; status?: string }>;
29
+ const plan = planFromSubscriptions(subs);
33
30
 
34
- const projects = (await ctx.db.unsafe.list("Project")) as Array<{
35
- orgId: string;
31
+ const projects = (await ctx.db.unsafe.query("Project", { orgId: args.orgId })) as Array<{
36
32
  status?: string;
37
33
  }>;
38
- const active = projects.filter(
39
- (p) => p.orgId === args.orgId && (p.status ?? "active") !== "archived",
40
- ).length;
34
+ const active = projects.filter((p) => (p.status ?? "active") !== "archived").length;
41
35
  if (!canCreateProject(plan, active)) {
42
36
  throw ctx.error(
43
37
  "LIMIT_REACHED",
@@ -0,0 +1,67 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { memberName } from "../lib/members";
3
+ import {
4
+ isTaskPriority,
5
+ isTaskStatus,
6
+ normalizeDueDate,
7
+ normalizeTaskTitle,
8
+ } from "../lib/tasks";
9
+
10
+ // createTask — adds a task to a project. The workspace comes from the
11
+ // project row, never from the caller, and the caller must be a member of it.
12
+ // An assignee must be a member of the same workspace.
13
+ export default mutation<
14
+ {
15
+ projectId: string;
16
+ title: string;
17
+ status?: string;
18
+ priority?: string;
19
+ assigneeId?: string;
20
+ dueDate?: string;
21
+ },
22
+ { id: string }
23
+ >({
24
+ args: {
25
+ projectId: v.string(),
26
+ title: v.string(),
27
+ status: v.optional(v.string()),
28
+ priority: v.optional(v.string()),
29
+ assigneeId: v.optional(v.string()),
30
+ dueDate: v.optional(v.string()),
31
+ },
32
+ async handler(ctx, args) {
33
+ const title = normalizeTaskTitle(args.title);
34
+ if (!title) throw ctx.error("INVALID_ARGS", "Task title must be 1–200 characters.");
35
+ const status = args.status ?? "todo";
36
+ if (!isTaskStatus(status)) throw ctx.error("INVALID_ARGS", "Unknown task status.");
37
+ const priority = args.priority ?? "none";
38
+ if (!isTaskPriority(priority)) throw ctx.error("INVALID_ARGS", "Unknown task priority.");
39
+ const dueDate = args.dueDate ? normalizeDueDate(args.dueDate) : null;
40
+ if (args.dueDate && !dueDate) throw ctx.error("INVALID_ARGS", "Due date must be YYYY-MM-DD.");
41
+
42
+ const project = await ctx.db.unsafe.get("Project", args.projectId);
43
+ if (!project) throw ctx.error("NOT_FOUND", "Project not found.");
44
+ const orgId = String(project.orgId);
45
+ await ctx.requireMember(orgId);
46
+
47
+ let assigneeName: string | null = null;
48
+ if (args.assigneeId) {
49
+ assigneeName = await memberName(ctx.db, orgId, args.assigneeId);
50
+ if (assigneeName === null) {
51
+ throw ctx.error("INVALID_ARGS", "The assignee is not a member of this workspace.");
52
+ }
53
+ }
54
+
55
+ const id = (await ctx.db.unsafe.insert("Task", {
56
+ orgId,
57
+ projectId: args.projectId,
58
+ title,
59
+ status,
60
+ priority,
61
+ ...(args.assigneeId ? { assigneeId: args.assigneeId, assigneeName } : {}),
62
+ ...(dueDate ? { dueDate } : {}),
63
+ ...(status === "done" ? { completedAt: new Date().toISOString() } : {}),
64
+ })) as string;
65
+ return { id };
66
+ },
67
+ });
@@ -0,0 +1,17 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ // deleteProject — removes a project and every task in it. The caller must be
4
+ // a member of the project's own workspace.
5
+ export default mutation<{ projectId: string }, { ok: true; tasks: number }>({
6
+ args: { projectId: v.string() },
7
+ async handler(ctx, args) {
8
+ const project = await ctx.db.unsafe.get("Project", args.projectId);
9
+ if (!project) throw ctx.error("NOT_FOUND", "Project not found.");
10
+ await ctx.requireMember(String(project.orgId));
11
+
12
+ const tasks = await ctx.db.unsafe.query("Task", { projectId: args.projectId });
13
+ for (const t of tasks) await ctx.db.unsafe.delete("Task", String(t.id));
14
+ await ctx.db.unsafe.delete("Project", args.projectId);
15
+ return { ok: true, tasks: tasks.length };
16
+ },
17
+ });
@@ -0,0 +1,14 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ // deleteTask — removes one task. The caller must be a member of the task's
4
+ // own workspace.
5
+ export default mutation<{ taskId: string }, { ok: true }>({
6
+ args: { taskId: v.string() },
7
+ async handler(ctx, args) {
8
+ const task = await ctx.db.unsafe.get("Task", args.taskId);
9
+ if (!task) throw ctx.error("NOT_FOUND", "Task not found.");
10
+ await ctx.requireMember(String(task.orgId));
11
+ await ctx.db.unsafe.delete("Task", args.taskId);
12
+ return { ok: true };
13
+ },
14
+ });
@@ -0,0 +1,46 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { workspaceMembers } from "../lib/members";
3
+ import { planSampleWorkspace } from "../lib/sample-workspace";
4
+
5
+ // seedWorkspace — fills an empty workspace with the sample projects and
6
+ // tasks in lib/sample-workspace.ts. Owners and admins only. It does nothing
7
+ // when the workspace already has a project, so calling it twice is safe.
8
+ // Tasks are assigned to the workspace's current members.
9
+ export default mutation<{ orgId: string }, { seeded: boolean; projects: number; tasks: number }>({
10
+ args: { orgId: v.string() },
11
+ async handler(ctx, args) {
12
+ await ctx.requireMember(args.orgId, { role: ["owner", "admin"] });
13
+
14
+ const existing = await ctx.db.unsafe.query("Project", { orgId: args.orgId });
15
+ if (existing.length > 0) return { seeded: false, projects: 0, tasks: 0 };
16
+
17
+ const members = await workspaceMembers(ctx.db, args.orgId);
18
+ // The caller first, so the tasks marked for member 0 land on them.
19
+ members.sort((a, b) => (a.userId === ctx.auth.userId ? -1 : b.userId === ctx.auth.userId ? 1 : 0));
20
+ const plan = planSampleWorkspace(members, new Date());
21
+
22
+ let taskCount = 0;
23
+ for (const p of plan) {
24
+ const projectId = (await ctx.db.unsafe.insert("Project", {
25
+ orgId: args.orgId,
26
+ name: p.name,
27
+ description: p.description,
28
+ status: p.status,
29
+ })) as string;
30
+ for (const t of p.tasks) {
31
+ await ctx.db.unsafe.insert("Task", {
32
+ orgId: args.orgId,
33
+ projectId,
34
+ title: t.title,
35
+ status: t.status,
36
+ priority: t.priority,
37
+ dueDate: t.dueDate,
38
+ ...(t.completedAt ? { completedAt: t.completedAt } : {}),
39
+ ...(t.assigneeId ? { assigneeId: t.assigneeId, assigneeName: t.assigneeName } : {}),
40
+ });
41
+ taskCount++;
42
+ }
43
+ }
44
+ return { seeded: true, projects: plan.length, tasks: taskCount };
45
+ },
46
+ });
@@ -0,0 +1,41 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { canCreateProject, planFromSubscriptions } from "../lib/plans";
3
+
4
+ // setProjectStatus — archives or restores a project. Restoring counts
5
+ // against the free plan's active-project cap the same way createProject
6
+ // does, so archiving and restoring cannot get a free workspace past it.
7
+ export default mutation<{ projectId: string; status: string }, { id: string; status: string }>({
8
+ args: { projectId: v.string(), status: v.string() },
9
+ async handler(ctx, args) {
10
+ if (args.status !== "active" && args.status !== "archived") {
11
+ throw ctx.error("INVALID_ARGS", "Status must be active or archived.");
12
+ }
13
+ const project = await ctx.db.unsafe.get("Project", args.projectId);
14
+ if (!project) throw ctx.error("NOT_FOUND", "Project not found.");
15
+ const orgId = String(project.orgId);
16
+ await ctx.requireMember(orgId);
17
+
18
+ const current = (project.status as string | undefined) ?? "active";
19
+ if (current === args.status) return { id: args.projectId, status: current };
20
+
21
+ if (args.status === "active") {
22
+ const subs = (await ctx.db.unsafe.query("StripeSubscription", {
23
+ referenceId: orgId,
24
+ })) as Array<{ plan?: string; status?: string }>;
25
+ const plan = planFromSubscriptions(subs);
26
+ const projects = (await ctx.db.unsafe.query("Project", { orgId })) as Array<{
27
+ status?: string;
28
+ }>;
29
+ const active = projects.filter((p) => (p.status ?? "active") !== "archived").length;
30
+ if (!canCreateProject(plan, active)) {
31
+ throw ctx.error(
32
+ "LIMIT_REACHED",
33
+ "This workspace is at the free plan's project limit. Upgrade to Pro for unlimited projects.",
34
+ );
35
+ }
36
+ }
37
+
38
+ await ctx.db.unsafe.update("Project", args.projectId, { status: args.status });
39
+ return { id: args.projectId, status: args.status };
40
+ },
41
+ });
@@ -3,6 +3,9 @@ import { mutation, v } from "@pylonsync/functions";
3
3
  // The one User field a person may edit about themselves. Email changes
4
4
  // need re-verification and passwords have their own route, so the User
5
5
  // policy denies client writes and this function owns display-name edits.
6
+ // Tasks keep a copy of their assignee's name (see the Task entity in
7
+ // app.ts), so the person's assigned tasks in their current workspaces are
8
+ // updated with the new name.
6
9
  export default mutation<{ displayName: string }, { displayName: string }>({
7
10
  args: { displayName: v.string() },
8
11
  async handler(ctx, args) {
@@ -13,6 +16,16 @@ export default mutation<{ displayName: string }, { displayName: string }>({
13
16
  throw ctx.error("INVALID_ARGS", "Name must be 1–60 characters.");
14
17
  }
15
18
  await ctx.db.unsafe.update("User", userId, { displayName });
19
+ // Only in workspaces the person still belongs to: a former member must
20
+ // not be able to write into a workspace they have left.
21
+ const memberships = await ctx.db.unsafe.query("OrgMember", { userId });
22
+ const orgs = new Set(memberships.map((m) => String(m.orgId)));
23
+ const assigned = await ctx.db.unsafe.query("Task", { assigneeId: userId });
24
+ for (const t of assigned) {
25
+ if (orgs.has(String(t.orgId)) && t.assigneeName !== displayName) {
26
+ await ctx.db.unsafe.update("Task", String(t.id), { assigneeName: displayName });
27
+ }
28
+ }
16
29
  return { displayName };
17
30
  },
18
31
  });
@@ -3,8 +3,8 @@ import { normalizeProjectName } from "../lib/projects";
3
3
 
4
4
  // updateProject — the reference example for a first-party server function, and
5
5
  // the core authoring loop end to end: entity → policy → FUNCTION → call it from
6
- // the client. The Projects tab's edit form (app/dashboard/dashboard-client.tsx)
7
- // saves through this via `callFn("updateProject", …)`.
6
+ // the client. The project dialog (app/dashboard/projects-client.tsx) saves
7
+ // through this via `callFn("updateProject", …)`.
8
8
  //
9
9
  // Why a server function and not a direct client `db.update`? Two reasons the
10
10
  // pattern exists:
@@ -17,9 +17,8 @@ import { normalizeProjectName } from "../lib/projects";
17
17
  // FORBIDDEN otherwise). Pass `{ role: ["owner", "admin"] }` to make edits
18
18
  // admin-only.
19
19
  //
20
- // Quick archive/delete stay as direct client `db` writes — the Project row
21
- // policy (`auth.tenantId == data.orgId`) already covers "edit a row you own".
22
- // Reach for a function when you need more than that.
20
+ // Archive/restore (setProjectStatus) and delete (deleteProject) are server
21
+ // functions too: the Project policy denies every direct client write.
23
22
  export default mutation<
24
23
  { projectId: string; name: string; description?: string },
25
24
  { id: string; name: string }
@@ -0,0 +1,88 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { memberName } from "../lib/members";
3
+ import {
4
+ isTaskPriority,
5
+ isTaskStatus,
6
+ normalizeDueDate,
7
+ normalizeTaskTitle,
8
+ } from "../lib/tasks";
9
+
10
+ // updateTask — changes the fields a person edits on the board. Only the
11
+ // fields passed are written. An empty string clears `assigneeId` or
12
+ // `dueDate`. The caller must be a member of the task's own workspace, and the
13
+ // task can never move to another project or workspace.
14
+ export default mutation<
15
+ {
16
+ taskId: string;
17
+ title?: string;
18
+ status?: string;
19
+ priority?: string;
20
+ assigneeId?: string;
21
+ dueDate?: string;
22
+ },
23
+ { id: string }
24
+ >({
25
+ args: {
26
+ taskId: v.string(),
27
+ title: v.optional(v.string()),
28
+ status: v.optional(v.string()),
29
+ priority: v.optional(v.string()),
30
+ assigneeId: v.optional(v.string()),
31
+ dueDate: v.optional(v.string()),
32
+ },
33
+ async handler(ctx, args) {
34
+ const task = await ctx.db.unsafe.get("Task", args.taskId);
35
+ if (!task) throw ctx.error("NOT_FOUND", "Task not found.");
36
+ const orgId = String(task.orgId);
37
+ await ctx.requireMember(orgId);
38
+
39
+ const patch: Record<string, unknown> = {};
40
+ if (args.title !== undefined) {
41
+ const title = normalizeTaskTitle(args.title);
42
+ if (!title) throw ctx.error("INVALID_ARGS", "Task title must be 1–200 characters.");
43
+ patch.title = title;
44
+ }
45
+ if (args.status !== undefined) {
46
+ if (!isTaskStatus(args.status)) throw ctx.error("INVALID_ARGS", "Unknown task status.");
47
+ patch.status = args.status;
48
+ if (args.status === "done" && task.status !== "done") {
49
+ patch.completedAt = new Date().toISOString();
50
+ } else if (args.status !== "done") {
51
+ patch.completedAt = null;
52
+ }
53
+ }
54
+ if (args.priority !== undefined) {
55
+ if (!isTaskPriority(args.priority)) throw ctx.error("INVALID_ARGS", "Unknown task priority.");
56
+ patch.priority = args.priority;
57
+ }
58
+ if (args.assigneeId !== undefined) {
59
+ if (args.assigneeId === (task.assigneeId ?? "")) {
60
+ // Unchanged. The dialog sends every field, and a person who has since
61
+ // left the workspace stays assigned until someone picks a new owner.
62
+ } else if (args.assigneeId === "") {
63
+ patch.assigneeId = null;
64
+ patch.assigneeName = null;
65
+ } else {
66
+ const name = await memberName(ctx.db, orgId, args.assigneeId);
67
+ if (name === null) {
68
+ throw ctx.error("INVALID_ARGS", "The assignee is not a member of this workspace.");
69
+ }
70
+ patch.assigneeId = args.assigneeId;
71
+ patch.assigneeName = name;
72
+ }
73
+ }
74
+ if (args.dueDate !== undefined) {
75
+ if (args.dueDate === "") {
76
+ patch.dueDate = null;
77
+ } else {
78
+ const due = normalizeDueDate(args.dueDate);
79
+ if (!due) throw ctx.error("INVALID_ARGS", "Due date must be YYYY-MM-DD.");
80
+ patch.dueDate = due;
81
+ }
82
+ }
83
+ if (Object.keys(patch).length > 0) {
84
+ await ctx.db.unsafe.update("Task", args.taskId, patch);
85
+ }
86
+ return { id: args.taskId };
87
+ },
88
+ });