@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,113 +1,523 @@
1
1
  "use client";
2
2
 
3
- import React, { useEffect, useRef, useState } from "react";
4
- import { db } from "@pylonsync/react";
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { db, useRoom, useSyncStatus } from "@pylonsync/react";
5
5
  import { EnsureGuest, useAuth } from "@pylonsync/client";
6
- import { Button } from "@/components/ui/button";
6
+ import { Menu, X } from "lucide-react";
7
+ import { cn } from "@/lib/utils";
8
+ import {
9
+ guestHue,
10
+ guestName,
11
+ sortMessages,
12
+ typingLabel,
13
+ unreadCount,
14
+ type ChatMember,
15
+ type ChatMessage,
16
+ } from "@/lib/chat";
17
+ import { Avatar } from "@/components/chat/avatar";
18
+ import { Composer } from "@/components/chat/composer";
19
+ import { MessageList } from "@/components/chat/message-list";
20
+ import { Sidebar, type SidebarChannel } from "@/components/chat/sidebar";
7
21
 
8
- export interface Message {
22
+ interface Channel {
9
23
  id: string;
10
- authorId: string;
11
- text: string;
12
- createdAt: string;
24
+ slug: string;
25
+ name: string;
26
+ topic: string;
27
+ position: number;
13
28
  }
14
29
 
15
- // `<EnsureGuest>` mints a guest session so anyone can chat with no login.
16
- // Inside it, `useAuth()` exposes the guest's `userId` so we can right-align
17
- // your own messages.
18
- export function ChatRoom() {
30
+ /** Presence data each tab publishes to the room. Other clients control it, so it only feeds comparisons. */
31
+ interface Presence {
32
+ channelId?: unknown;
33
+ typingAt?: unknown;
34
+ }
35
+
36
+ const WORKSPACE = "__APP_NAME__";
37
+ const PRESENCE_ROOM = "chat:lobby";
38
+ /** A typing flag older than this is ignored, so a closed tab does not type forever. */
39
+ const TYPING_TTL_MS = 6000;
40
+ /** Minimum gap between "still typing" presence updates. */
41
+ const TYPING_REFRESH_MS = 2000;
42
+
43
+ // `<EnsureGuest>` mints a guest session so anyone can chat with no sign-up.
44
+ // The server renders the skeleton; the live workspace mounts in the browser.
45
+ export function ChatRoom({ initialChannel }: { initialChannel?: string }) {
19
46
  return (
20
- <EnsureGuest fallback={<div className="flex-1" />}>
21
- <Room />
47
+ <EnsureGuest fallback={<ChatSkeleton />}>
48
+ <Workspace initialChannel={initialChannel} />
22
49
  </EnsureGuest>
23
50
  );
24
51
  }
25
52
 
26
- function Room() {
53
+ /** How long to wait for a guest session before offering a retry. */
54
+ const SESSION_TIMEOUT_MS = 10_000;
55
+
56
+ function Workspace({ initialChannel }: { initialChannel?: string }) {
27
57
  const { userId } = useAuth();
28
- const [text, setText] = useState("");
29
- const { data: messages } = db.useQuery<Message>("Message");
30
- const bottomRef = useRef<HTMLDivElement>(null);
58
+ const [stalled, setStalled] = useState(false);
59
+ useEffect(() => {
60
+ if (userId) return;
61
+ const t = setTimeout(() => setStalled(true), SESSION_TIMEOUT_MS);
62
+ return () => clearTimeout(t);
63
+ }, [userId]);
64
+ // The session resolves a moment after the guest token lands.
65
+ if (!userId) {
66
+ return (
67
+ <ChatSkeleton
68
+ message={stalled ? "Could not start a session. Check your connection." : null}
69
+ onRetry={stalled ? () => window.location.reload() : undefined}
70
+ />
71
+ );
72
+ }
73
+ return <Chat selfId={userId} initialChannel={initialChannel} />;
74
+ }
75
+
76
+ function Chat({ selfId, initialChannel }: { selfId: string; initialChannel?: string }) {
77
+ const { data: channelRows } = db.useQuery<Channel>("Channel");
78
+ const { data: memberRows } = db.useQuery<ChatMember>("Member");
79
+ const { data: messageRows } = db.useQuery<ChatMessage>("Message");
80
+ const status = useSyncStatus();
81
+
82
+ const [activeSlug, setActiveSlug] = useState(initialChannel || "general");
83
+ const [drawerOpen, setDrawerOpen] = useState(false);
84
+ const [joinError, setJoinError] = useState<string | null>(null);
85
+ const [sendError, setSendError] = useState<string | null>(null);
86
+ const hidden = usePageHidden();
87
+ const isDesktop = useMediaQuery("(min-width: 768px)");
31
88
 
32
- // Oldest → newest, like a chat transcript.
33
- const ordered = [...messages].sort((a, b) =>
34
- a.createdAt.localeCompare(b.createdAt),
89
+ useEffect(() => {
90
+ if (!drawerOpen) return;
91
+ const onKey = (e: KeyboardEvent) => {
92
+ if (e.key === "Escape") setDrawerOpen(false);
93
+ };
94
+ window.addEventListener("keydown", onKey);
95
+ return () => window.removeEventListener("keydown", onKey);
96
+ }, [drawerOpen]);
97
+
98
+ // Seeds the demo workspace on first boot and creates this guest's Member row.
99
+ const [joinedAt, setJoinedAt] = useState<string | null>(null);
100
+ useEffect(() => {
101
+ db.fn<{ now: string }>("joinChat", {})
102
+ .then((r) => setJoinedAt(r.now))
103
+ .catch((e: unknown) => {
104
+ setJoinError(e instanceof Error ? e.message : "Could not join the chat.");
105
+ });
106
+ }, [selfId]);
107
+
108
+ const channels = useMemo(
109
+ () => [...channelRows].sort((a, b) => a.position - b.position),
110
+ [channelRows],
111
+ );
112
+ const active = channels.find((c) => c.slug === activeSlug) ?? channels[0];
113
+
114
+ const members = useMemo(() => {
115
+ const map = new Map<string, ChatMember>();
116
+ for (const m of memberRows) map.set(m.userId, m);
117
+ return map;
118
+ }, [memberRows]);
119
+ const self = members.get(selfId);
120
+
121
+ const channelMessages = useMemo(
122
+ () => (active ? sortMessages(messageRows.filter((m) => m.channelId === active.id)) : []),
123
+ [messageRows, active],
35
124
  );
36
125
 
37
- // db.useQuery is live, so this fires whenever a message arrives (here or in
38
- // another tab) — keep the newest message in view.
126
+ // ---- presence: who is online, who is typing ----
127
+ const room = useRoom(PRESENCE_ROOM, selfId, {
128
+ initialPresence: { channelId: active?.id ?? null, typingAt: 0 },
129
+ });
130
+ const { setPresence } = room;
131
+ const lastTypingSentRef = useRef(0);
132
+
39
133
  useEffect(() => {
40
- bottomRef.current?.scrollIntoView({ behavior: "smooth" });
41
- }, [ordered.length]);
42
-
43
- async function send(e: React.FormEvent) {
44
- e.preventDefault();
45
- const value = text.trim();
46
- if (!value) return;
47
- setText("");
48
- // No authorId sent — field.owner() stamps it from the session.
49
- await db.insert("Message", { text: value });
134
+ if (!active) return;
135
+ lastTypingSentRef.current = 0;
136
+ setPresence({ channelId: active.id, typingAt: 0 });
137
+ }, [active?.id, setPresence]); // eslint-disable-line react-hooks/exhaustive-deps
138
+
139
+ const onTyping = useCallback(() => {
140
+ if (!active) return;
141
+ const now = Date.now();
142
+ if (now - lastTypingSentRef.current < TYPING_REFRESH_MS) return;
143
+ lastTypingSentRef.current = now;
144
+ setPresence({ channelId: active.id, typingAt: now });
145
+ }, [active, setPresence]);
146
+
147
+ const onStopTyping = useCallback(() => {
148
+ if (!active || lastTypingSentRef.current === 0) return;
149
+ lastTypingSentRef.current = 0;
150
+ setPresence({ channelId: active.id, typingAt: 0 });
151
+ }, [active, setPresence]);
152
+
153
+ const memberFor = useCallback(
154
+ (userId: string): ChatMember =>
155
+ members.get(userId) ?? { id: userId, userId, name: guestName(userId), hue: guestHue(userId) },
156
+ [members],
157
+ );
158
+
159
+ const online = useMemo(() => {
160
+ const seen = new Set<string>([selfId]);
161
+ const list: ChatMember[] = [memberFor(selfId)];
162
+ for (const p of room.peers) {
163
+ if (seen.has(p.user_id)) continue;
164
+ seen.add(p.user_id);
165
+ list.push(memberFor(p.user_id));
166
+ }
167
+ return list;
168
+ }, [room.peers, selfId, memberFor]);
169
+
170
+ // Typing flags expire on this browser's clock: each new `typingAt` value a
171
+ // peer publishes is stamped with the time it arrived here. A peer's own
172
+ // clock (skewed, or set to a future time) never keeps a flag alive.
173
+ const [typingReceived, setTypingReceived] = useState<Record<string, number>>({});
174
+ useEffect(() => {
175
+ setTypingReceived((prev) => {
176
+ const next: Record<string, number> = {};
177
+ for (const p of room.peers) {
178
+ const data = (p.data ?? {}) as Presence;
179
+ if (p.user_id === selfId || data.channelId !== active?.id) continue;
180
+ if (typeof data.typingAt !== "number" || data.typingAt <= 0) continue;
181
+ const key = `${p.user_id}\u0000${data.typingAt}`;
182
+ next[key] = prev[key] ?? Date.now();
183
+ }
184
+ return next;
185
+ });
186
+ }, [room.peers, selfId, active?.id]);
187
+
188
+ // Re-render once a second while a flag is live, so it disappears on time.
189
+ const [now, setNow] = useState(() => Date.now());
190
+ const hasTyping = Object.keys(typingReceived).length > 0;
191
+ useEffect(() => {
192
+ if (!hasTyping) return;
193
+ const t = setInterval(() => setNow(Date.now()), 1000);
194
+ return () => clearInterval(t);
195
+ }, [hasTyping]);
196
+
197
+ const typingIds = new Set<string>();
198
+ for (const [key, receivedAt] of Object.entries(typingReceived)) {
199
+ if (Math.max(now, receivedAt) - receivedAt < TYPING_TTL_MS) typingIds.add(key.split("\u0000")[0]);
200
+ }
201
+ const typingNames = [...typingIds].map((id) => memberFor(id).name);
202
+
203
+ // ---- unread marks, kept per browser in localStorage ----
204
+ const lastSeen = useLastSeen(selfId, channels, joinedAt);
205
+ const { mark } = lastSeen;
206
+ const newestInActive = channelMessages[channelMessages.length - 1]?.createdAt;
207
+ useEffect(() => {
208
+ if (!active || !newestInActive) return;
209
+ const markIfVisible = () => {
210
+ if (document.visibilityState === "visible") mark(active.id, newestInActive);
211
+ };
212
+ markIfVisible();
213
+ document.addEventListener("visibilitychange", markIfVisible);
214
+ return () => document.removeEventListener("visibilitychange", markIfVisible);
215
+ }, [active, newestInActive, mark]);
216
+
217
+ const sidebarChannels: SidebarChannel[] = channels.map((c) => ({
218
+ id: c.id,
219
+ slug: c.slug,
220
+ name: c.name,
221
+ unread: unreadCount(messageRows, c.id, lastSeen.map[c.id], selfId),
222
+ }));
223
+ // The open channel counts too while the tab is in the background, since its
224
+ // mark only advances while the tab is visible.
225
+ const totalUnread = sidebarChannels.reduce(
226
+ (n, c) => n + (c.id === active?.id && !hidden ? 0 : c.unread),
227
+ 0,
228
+ );
229
+ useEffect(() => {
230
+ document.title = totalUnread > 0 ? `(${totalUnread}) ${WORKSPACE}` : WORKSPACE;
231
+ }, [totalUnread]);
232
+
233
+ function selectChannel(slug: string) {
234
+ setActiveSlug(slug);
235
+ setDrawerOpen(false);
236
+ const url = slug === "general" ? window.location.pathname : `${window.location.pathname}?c=${encodeURIComponent(slug)}`;
237
+ window.history.replaceState(window.history.state, "", url);
50
238
  }
51
239
 
240
+ function send(text: string) {
241
+ if (!active) return;
242
+ setSendError(null);
243
+ // authorId is the caller's own id so the optimistic row shows the right
244
+ // author at once; field.owner() on the server rejects any other value.
245
+ // createdAt is left out: the server stamps it.
246
+ db.insert("Message", { channelId: active.id, authorId: selfId, text }).catch(() => {
247
+ setSendError("Message not sent. Check your connection and try again.");
248
+ });
249
+ }
250
+
251
+ async function rename(name: string) {
252
+ await db.fn("renameMember", { name });
253
+ }
254
+
255
+ function remove(id: string) {
256
+ if (window.confirm("Delete this message? Everyone in the channel will stop seeing it.")) {
257
+ void db.delete("Message", id);
258
+ }
259
+ }
260
+
261
+ if (!active) return <ChatSkeleton message={joinError} />;
262
+
52
263
  return (
53
- <div className="flex min-h-0 flex-1 flex-col">
54
- <div className="min-h-0 flex-1 space-y-2 overflow-y-auto py-2">
55
- {ordered.length === 0 ? (
56
- <p className="text-sm text-muted-foreground">
57
- No messages yet — say hi. Open a second tab to watch it arrive live.
58
- </p>
59
- ) : (
60
- ordered.map((m) => {
61
- const mine = m.authorId === userId;
62
- return (
63
- <div
64
- key={m.id}
65
- className={mine ? "flex justify-end" : "flex justify-start"}
66
- >
67
- <div
68
- className={
69
- "max-w-[80%] rounded-2xl px-3 py-1.5 text-sm " +
70
- (mine
71
- ? "bg-primary text-primary-foreground"
72
- : "bg-muted text-foreground")
73
- }
74
- >
75
- {!mine && (
76
- <div className="mb-0.5 font-mono text-[10px] text-muted-foreground">
77
- {shortId(m.authorId)}
78
- </div>
79
- )}
80
- <span className="whitespace-pre-wrap break-words">
81
- {m.text}
82
- </span>
83
- </div>
84
- </div>
85
- );
86
- })
264
+ <div className="flex h-dvh overflow-hidden bg-background">
265
+ {/* Sidebar: a column on desktop, a drawer on phones. */}
266
+ <div
267
+ className={cn(
268
+ "fixed inset-0 z-30 bg-black/40 transition-opacity duration-300 ease-out-soft md:hidden",
269
+ drawerOpen ? "opacity-100" : "pointer-events-none opacity-0",
270
+ )}
271
+ onClick={() => setDrawerOpen(false)}
272
+ aria-hidden
273
+ />
274
+ <aside
275
+ // Closed on a phone, the drawer is off screen: keep it out of the tab
276
+ // order and the accessibility tree.
277
+ inert={!isDesktop && !drawerOpen}
278
+ className={cn(
279
+ "fixed inset-y-0 left-0 z-40 w-[284px] transition-transform duration-300 ease-out-soft md:static md:z-auto md:w-[264px] md:translate-x-0 md:transition-none",
280
+ drawerOpen ? "translate-x-0 shadow-2xl" : "-translate-x-full",
87
281
  )}
88
- <div ref={bottomRef} />
89
- </div>
90
-
91
- <form
92
- onSubmit={send}
93
- className="shrink-0 flex items-center gap-2 border-t py-3"
94
282
  >
95
- <input
96
- value={text}
97
- onChange={(e) => setText(e.target.value)}
98
- placeholder="Message…"
99
- aria-label="Message"
100
- autoComplete="off"
101
- className="flex h-10 w-full rounded-full border border-input bg-transparent px-4 py-1 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
283
+ <Sidebar
284
+ workspace={WORKSPACE}
285
+ channels={sidebarChannels}
286
+ activeSlug={active.slug}
287
+ onSelect={selectChannel}
288
+ online={online}
289
+ self={self}
290
+ onRename={rename}
291
+ />
292
+ <button
293
+ type="button"
294
+ onClick={() => setDrawerOpen(false)}
295
+ aria-label="Close channels"
296
+ className={cn(
297
+ "absolute top-3 -right-12 inline-flex size-9 items-center justify-center rounded-full bg-card text-foreground shadow-float md:hidden",
298
+ drawerOpen ? "opacity-100" : "pointer-events-none opacity-0",
299
+ )}
300
+ >
301
+ <X className="size-4" strokeWidth={1.75} aria-hidden />
302
+ </button>
303
+ </aside>
304
+
305
+ <main className="flex min-w-0 flex-1 flex-col">
306
+ <header className="flex h-14 shrink-0 items-center gap-2 border-b border-border/70 bg-background/85 px-2 backdrop-blur-md sm:px-4">
307
+ <button
308
+ type="button"
309
+ onClick={() => setDrawerOpen(true)}
310
+ aria-label="Open channels"
311
+ className="relative inline-flex size-9 items-center justify-center rounded-lg text-foreground hover:bg-hover md:hidden"
312
+ >
313
+ <Menu className="size-5" strokeWidth={1.75} aria-hidden />
314
+ {totalUnread > 0 ? (
315
+ <span aria-hidden className="absolute top-1.5 right-1.5 size-2 rounded-full bg-signal ring-2 ring-background" />
316
+ ) : null}
317
+ </button>
318
+ <div className="min-w-0 flex-1 pl-1">
319
+ <h1 className="flex items-center gap-1 text-[16px] font-semibold tracking-tight">
320
+ <span className="text-muted-foreground">#</span>
321
+ <span className="truncate">{active.name}</span>
322
+ </h1>
323
+ <p className="hidden truncate text-[12.5px] text-muted-foreground sm:block">{active.topic}</p>
324
+ </div>
325
+ {status === "reconnecting" || status === "offline" ? (
326
+ <span className="shrink-0 rounded-full bg-amber-100 px-2.5 py-1 text-[12px] font-medium text-amber-900">
327
+ Reconnecting
328
+ </span>
329
+ ) : null}
330
+ <OnlineStack online={online} />
331
+ </header>
332
+
333
+ <MessageList
334
+ key={active.id}
335
+ channelName={active.name}
336
+ channelTopic={active.topic}
337
+ messages={channelMessages}
338
+ members={members}
339
+ selfId={selfId}
340
+ onDelete={remove}
102
341
  />
103
- <Button type="submit" disabled={!text.trim()}>
104
- Send
105
- </Button>
106
- </form>
342
+
343
+ <div className="shrink-0 px-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] sm:px-5">
344
+ <div className="max-w-4xl">
345
+ <TypingIndicator names={typingNames} />
346
+ <Composer
347
+ channelName={active.name}
348
+ channelKey={active.id}
349
+ onSend={send}
350
+ onTyping={onTyping}
351
+ onStopTyping={onStopTyping}
352
+ />
353
+ <p className="h-5 px-1 pt-1 text-[11.5px] text-muted-foreground" aria-live="polite">
354
+ {sendError ?? joinError ?? (
355
+ <span className="hidden sm:inline">
356
+ <kbd className="font-sans font-medium">Enter</kbd> to send,{" "}
357
+ <kbd className="font-sans font-medium">Shift + Enter</kbd> for a new line
358
+ </span>
359
+ )}
360
+ </p>
361
+ </div>
362
+ </div>
363
+ </main>
364
+ </div>
365
+ );
366
+ }
367
+
368
+ function OnlineStack({ online }: { online: ChatMember[] }) {
369
+ const shown = online.slice(0, 4);
370
+ return (
371
+ <div className="flex shrink-0 items-center gap-2 pl-2" aria-label={`${online.length} online`}>
372
+ <div className="flex -space-x-1 [--avatar-ring:var(--background)]">
373
+ {shown.map((m) => (
374
+ <Avatar key={m.userId} name={m.name} hue={m.hue} size="sm" className="rounded-full ring-2 ring-background" />
375
+ ))}
376
+ </div>
377
+ <span className="flex items-center gap-1.5 text-[13px] text-muted-foreground tabular-nums">
378
+ <span aria-hidden className="size-1.5 rounded-full bg-online" />
379
+ {online.length} online
380
+ </span>
381
+ </div>
382
+ );
383
+ }
384
+
385
+ function TypingIndicator({ names }: { names: string[] }) {
386
+ const label = typingLabel(names);
387
+ return (
388
+ <div className="flex h-6 items-center gap-2 px-1 text-[12.5px] text-muted-foreground" aria-live="polite">
389
+ {label ? (
390
+ <>
391
+ <span aria-hidden className="typing-dots inline-flex items-center gap-[3px]">
392
+ <span />
393
+ <span />
394
+ <span />
395
+ </span>
396
+ <span className="truncate">{label}</span>
397
+ </>
398
+ ) : null}
107
399
  </div>
108
400
  );
109
401
  }
110
402
 
111
- function shortId(id: string) {
112
- return "@" + id.replace(/^guest_/, "").slice(0, 6);
403
+ /**
404
+ * Per-channel "last message seen" marks for unread badges. `startAt` is the
405
+ * server time from joinChat: a channel without a mark starts there, so a first
406
+ * visit shows no badges and later messages count.
407
+ */
408
+ function useLastSeen(selfId: string, channels: Channel[], startAt: string | null) {
409
+ const key = `chat:lastSeen:${selfId}`;
410
+ const [map, setMap] = useState<Record<string, string>>(() => readMarks(key));
411
+
412
+ useEffect(() => {
413
+ if (!startAt) return;
414
+ const missing = channels.filter((c) => !map[c.id]);
415
+ if (missing.length === 0) return;
416
+ setMap((prev) => {
417
+ const next = { ...prev };
418
+ for (const c of missing) next[c.id] ??= startAt;
419
+ writeMarks(key, next);
420
+ return next;
421
+ });
422
+ }, [channels, map, key, startAt]);
423
+
424
+ const mark = useCallback(
425
+ (channelId: string, iso: string) => {
426
+ setMap((prev) => {
427
+ if (prev[channelId] && prev[channelId] >= iso) return prev;
428
+ const next = { ...prev, [channelId]: iso };
429
+ writeMarks(key, next);
430
+ return next;
431
+ });
432
+ },
433
+ [key],
434
+ );
435
+
436
+ return { map, mark };
437
+ }
438
+
439
+ function readMarks(key: string): Record<string, string> {
440
+ try {
441
+ const raw = window.localStorage.getItem(key);
442
+ const parsed: unknown = raw ? JSON.parse(raw) : {};
443
+ return parsed && typeof parsed === "object" ? (parsed as Record<string, string>) : {};
444
+ } catch {
445
+ return {};
446
+ }
447
+ }
448
+
449
+ function writeMarks(key: string, marks: Record<string, string>) {
450
+ try {
451
+ window.localStorage.setItem(key, JSON.stringify(marks));
452
+ } catch {
453
+ // Storage can be full or blocked (private mode). Unread badges then reset per visit.
454
+ }
455
+ }
456
+
457
+ function usePageHidden(): boolean {
458
+ const [hidden, setHidden] = useState(() => document.visibilityState === "hidden");
459
+ useEffect(() => {
460
+ const update = () => setHidden(document.visibilityState === "hidden");
461
+ document.addEventListener("visibilitychange", update);
462
+ return () => document.removeEventListener("visibilitychange", update);
463
+ }, []);
464
+ return hidden;
465
+ }
466
+
467
+ function useMediaQuery(query: string): boolean {
468
+ const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
469
+ useEffect(() => {
470
+ const list = window.matchMedia(query);
471
+ const update = () => setMatches(list.matches);
472
+ update();
473
+ list.addEventListener("change", update);
474
+ return () => list.removeEventListener("change", update);
475
+ }, [query]);
476
+ return matches;
477
+ }
478
+
479
+ /** Layout-matched placeholder while the session and replica load. */
480
+ function ChatSkeleton({ message, onRetry }: { message?: string | null; onRetry?: () => void }) {
481
+ return (
482
+ <div className="flex h-dvh overflow-hidden bg-background" aria-busy="true">
483
+ <div className="hidden w-[264px] shrink-0 flex-col gap-3 bg-sidebar p-4 md:flex">
484
+ <div className="mb-4 h-7 w-32 rounded-lg bg-sidebar-accent" />
485
+ <div className="h-4 w-20 rounded bg-sidebar-accent/70" />
486
+ <div className="h-4 w-28 rounded bg-sidebar-accent/70" />
487
+ <div className="h-4 w-24 rounded bg-sidebar-accent/70" />
488
+ </div>
489
+ <div className="flex min-w-0 flex-1 flex-col">
490
+ <div className="h-14 shrink-0 border-b border-border/70" />
491
+ <div className="flex w-full max-w-4xl flex-1 flex-col justify-end gap-5 px-6 pb-6">
492
+ {message ? (
493
+ <div className="flex flex-col items-center gap-3 text-center text-[14px] text-muted-foreground">
494
+ <p>{message}</p>
495
+ {onRetry ? (
496
+ <button
497
+ type="button"
498
+ onClick={onRetry}
499
+ className="rounded-full bg-foreground px-4 py-1.5 text-[13px] font-medium text-background"
500
+ >
501
+ Try again
502
+ </button>
503
+ ) : null}
504
+ </div>
505
+ ) : (
506
+ [72, 48, 64].map((w, i) => (
507
+ <div key={i} className="flex animate-pulse gap-3">
508
+ <div className="size-9 rounded-full bg-muted" />
509
+ <div className="flex-1 space-y-2 pt-1">
510
+ <div className="h-3 w-28 rounded bg-muted" />
511
+ <div className="h-3 rounded bg-muted" style={{ width: `${w}%` }} />
512
+ </div>
513
+ </div>
514
+ ))
515
+ )}
516
+ </div>
517
+ <div className="w-full max-w-4xl px-3 pb-8 sm:px-5">
518
+ <div className="h-12 rounded-2xl bg-card shadow-composer" />
519
+ </div>
520
+ </div>
521
+ </div>
522
+ );
113
523
  }
@@ -11,7 +11,7 @@ import { Button } from "@/components/ui/button";
11
11
  export default function Error({ error, reset }: ErrorBoundaryProps) {
12
12
  const [tries, setTries] = React.useState(0);
13
13
  return (
14
- <div className="space-y-6">
14
+ <div className="mx-auto flex min-h-dvh max-w-md flex-col justify-center gap-6 px-6">
15
15
  <section>
16
16
  <h1 className="text-2xl font-semibold tracking-tight">
17
17
  Something went wrong