@pylonsync/create-pylon 0.19.1 → 0.21.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 (696) hide show
  1. package/bin/create-pylon.js +30 -7
  2. package/package.json +1 -1
  3. package/templates/_root/AGENTS.md +1 -1
  4. package/templates/agency/.env.example +7 -0
  5. package/templates/agency/AGENTS.md +1 -1
  6. package/templates/agency/README.md +28 -3
  7. package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
  8. package/templates/agency/app/(marketing)/page.tsx +3 -3
  9. package/templates/agency/app/(marketing)/work/page.tsx +3 -3
  10. package/templates/agency/app/auth-form.tsx +4 -4
  11. package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
  12. package/templates/agency/app/dashboard/page.tsx +14 -16
  13. package/templates/agency/app/icon.svg +3 -0
  14. package/templates/agency/app/login/page.tsx +12 -14
  15. package/templates/agency/components/marketing.tsx +12 -5
  16. package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
  17. package/templates/agency/lib/agency.ts +22 -0
  18. package/templates/agency/lib/demo.ts +118 -0
  19. package/templates/agency/lib/site.config.ts +19 -13
  20. package/templates/agency/public/favicon.ico +0 -0
  21. package/templates/agency/tests/agency.test.ts +57 -0
  22. package/templates/ai-chat/.env.example +13 -27
  23. package/templates/ai-chat/AGENTS.md +1 -1
  24. package/templates/ai-chat/README.md +53 -60
  25. package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
  26. package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
  27. package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
  28. package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
  29. package/templates/ai-chat/app/(chat)/page.tsx +7 -11
  30. package/templates/ai-chat/app/auth-form.tsx +45 -38
  31. package/templates/ai-chat/app/error.tsx +9 -12
  32. package/templates/ai-chat/app/globals.css +464 -102
  33. package/templates/ai-chat/app/layout.tsx +11 -19
  34. package/templates/ai-chat/app/login/page.tsx +39 -24
  35. package/templates/ai-chat/app.ts +118 -43
  36. package/templates/ai-chat/components/chat/composer.tsx +193 -0
  37. package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
  38. package/templates/ai-chat/components/chat/icons.tsx +198 -0
  39. package/templates/ai-chat/components/chat/markdown.tsx +164 -0
  40. package/templates/ai-chat/components/chat/message.tsx +183 -0
  41. package/templates/ai-chat/components/chat/setup.tsx +71 -0
  42. package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
  43. package/templates/ai-chat/functions/_beginTurn.ts +145 -0
  44. package/templates/ai-chat/functions/_finishTurn.ts +55 -0
  45. package/templates/ai-chat/functions/assistantStatus.ts +13 -0
  46. package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
  47. package/templates/ai-chat/functions/createConversation.ts +28 -0
  48. package/templates/ai-chat/functions/deleteConversation.ts +15 -20
  49. package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
  50. package/templates/ai-chat/functions/renameConversation.ts +19 -0
  51. package/templates/ai-chat/functions/respond.ts +88 -0
  52. package/templates/ai-chat/functions/seedExamples.ts +46 -0
  53. package/templates/ai-chat/functions/stopResponse.ts +30 -0
  54. package/templates/ai-chat/lib/brand-icon.ts +7 -0
  55. package/templates/ai-chat/lib/chat.ts +277 -0
  56. package/templates/ai-chat/lib/claim.ts +15 -0
  57. package/templates/ai-chat/lib/examples.ts +179 -0
  58. package/templates/ai-chat/lib/highlight.ts +107 -0
  59. package/templates/ai-chat/lib/markdown.ts +317 -0
  60. package/templates/ai-chat/lib/provider.ts +38 -0
  61. package/templates/ai-chat/lib/site.config.ts +55 -39
  62. package/templates/ai-chat/tests/chat.test.ts +174 -0
  63. package/templates/ai-chat/tests/components.test.tsx +57 -0
  64. package/templates/ai-chat/tests/markdown.test.ts +88 -0
  65. package/templates/ai-chat/tests/provider.test.ts +32 -0
  66. package/templates/ai-studio/AGENTS.md +1 -1
  67. package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
  68. package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
  69. package/templates/backend/consumer/apps/api/app.ts +166 -0
  70. package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
  71. package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
  72. package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
  73. package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
  74. package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
  75. package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
  76. package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
  77. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
  78. package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
  79. package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
  80. package/templates/backend/consumer/apps/api/package.json +8 -2
  81. package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
  82. package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
  83. package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
  84. package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
  85. package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
  86. package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
  87. package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
  88. package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
  89. package/templates/barebones/AGENTS.md +1 -1
  90. package/templates/chat/AGENTS.md +1 -1
  91. package/templates/chat/README.md +43 -21
  92. package/templates/chat/app/chat-client.tsx +495 -85
  93. package/templates/chat/app/error.tsx +1 -1
  94. package/templates/chat/app/globals.css +170 -54
  95. package/templates/chat/app/layout.tsx +12 -9
  96. package/templates/chat/app/not-found.tsx +4 -2
  97. package/templates/chat/app/page.tsx +9 -19
  98. package/templates/chat/app.ts +96 -17
  99. package/templates/chat/components/chat/avatar.tsx +45 -0
  100. package/templates/chat/components/chat/composer.tsx +110 -0
  101. package/templates/chat/components/chat/message-list.tsx +285 -0
  102. package/templates/chat/components/chat/sidebar.tsx +190 -0
  103. package/templates/chat/functions/joinChat.ts +83 -0
  104. package/templates/chat/functions/renameMember.ts +25 -0
  105. package/templates/chat/lib/chat.ts +188 -0
  106. package/templates/chat/lib/seed.ts +78 -0
  107. package/templates/chat/tests/chat.test.ts +159 -0
  108. package/templates/chat/tests/composer.test.tsx +54 -0
  109. package/templates/consumer/AGENTS.md +1 -1
  110. package/templates/consumer/README.md +3 -3
  111. package/templates/consumer/app/globals.css +10 -3
  112. package/templates/consumer/app/icon.svg +1 -0
  113. package/templates/consumer/components/social/avatar.tsx +25 -11
  114. package/templates/consumer/functions/seedFeed.ts +3 -3
  115. package/templates/consumer/lib/seed.ts +43 -85
  116. package/templates/consumer/public/favicon.ico +0 -0
  117. package/templates/consumer/public/images/posts/arch.jpg +0 -0
  118. package/templates/consumer/public/images/posts/coffee.jpg +0 -0
  119. package/templates/consumer/public/images/posts/dog.jpg +0 -0
  120. package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
  121. package/templates/consumer/public/images/posts/ramen.jpg +0 -0
  122. package/templates/consumer/public/images/posts/run.jpg +0 -0
  123. package/templates/consumer/public/images/posts/sunset.jpg +0 -0
  124. package/templates/consumer/tests/seed.test.ts +39 -0
  125. package/templates/creator/.env.example +7 -0
  126. package/templates/creator/AGENTS.md +1 -1
  127. package/templates/creator/README.md +24 -5
  128. package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
  129. package/templates/creator/app/auth-form.tsx +7 -7
  130. package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
  131. package/templates/creator/app/dashboard/page.tsx +18 -18
  132. package/templates/creator/app/icon.svg +2 -0
  133. package/templates/creator/app/login/page.tsx +14 -14
  134. package/templates/creator/app.ts +4 -4
  135. package/templates/creator/functions/seedDemo.ts +32 -0
  136. package/templates/creator/functions/subscribe.ts +4 -15
  137. package/templates/creator/lib/demo.ts +94 -0
  138. package/templates/creator/lib/site.config.ts +15 -10
  139. package/templates/creator/lib/stats.ts +35 -0
  140. package/templates/creator/public/favicon.ico +0 -0
  141. package/templates/creator/tests/counter.test.ts +92 -0
  142. package/templates/creator/tests/live-count.test.tsx +45 -0
  143. package/templates/crm/AGENTS.md +1 -1
  144. package/templates/crm/README.md +22 -1
  145. package/templates/crm/app/apple-icon.png +0 -0
  146. package/templates/crm/app/companies/companies-view.tsx +32 -14
  147. package/templates/crm/app/companies/page.tsx +2 -1
  148. package/templates/crm/app/contacts/contacts-view.tsx +34 -10
  149. package/templates/crm/app/contacts/page.tsx +2 -1
  150. package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
  151. package/templates/crm/app/deals/[id]/page.tsx +2 -1
  152. package/templates/crm/app/globals.css +29 -14
  153. package/templates/crm/app/icon.svg +1 -0
  154. package/templates/crm/app/layout.tsx +7 -5
  155. package/templates/crm/app/page.tsx +2 -1
  156. package/templates/crm/app/pipeline-view.tsx +5 -5
  157. package/templates/crm/app/workspace.tsx +29 -17
  158. package/templates/crm/app.ts +5 -5
  159. package/templates/crm/components/activity-timeline.tsx +14 -4
  160. package/templates/crm/components/app-shell.tsx +94 -0
  161. package/templates/crm/components/auth-form.tsx +35 -5
  162. package/templates/crm/components/avatar.tsx +5 -1
  163. package/templates/crm/components/board-skeleton.tsx +5 -5
  164. package/templates/crm/components/brand-mark.tsx +25 -0
  165. package/templates/crm/components/command-palette.tsx +17 -17
  166. package/templates/crm/components/data-table.tsx +97 -36
  167. package/templates/crm/components/deal-card.tsx +23 -27
  168. package/templates/crm/components/filter-tabs.tsx +101 -0
  169. package/templates/crm/components/metrics-bar.tsx +6 -6
  170. package/templates/crm/components/nav.tsx +15 -0
  171. package/templates/crm/components/page-header.tsx +43 -7
  172. package/templates/crm/components/pipeline-board.tsx +16 -10
  173. package/templates/crm/components/require-auth.tsx +24 -26
  174. package/templates/crm/components/sidebar.tsx +54 -49
  175. package/templates/crm/components/stage-badge.tsx +4 -2
  176. package/templates/crm/components/ui/button.tsx +5 -4
  177. package/templates/crm/components/ui/dialog.tsx +1 -1
  178. package/templates/crm/components/ui/input.tsx +1 -1
  179. package/templates/crm/components/ui/select.tsx +1 -1
  180. package/templates/crm/components/ui/textarea.tsx +1 -1
  181. package/templates/crm/functions/seedWorkspace.ts +32 -17
  182. package/templates/crm/lib/brand.ts +9 -0
  183. package/templates/crm/lib/demo-team.ts +51 -0
  184. package/templates/crm/lib/pipeline.ts +37 -0
  185. package/templates/crm/lib/search.ts +4 -3
  186. package/templates/crm/lib/seed.ts +53 -8
  187. package/templates/crm/public/favicon.ico +0 -0
  188. package/templates/crm/tests/components.test.tsx +5 -5
  189. package/templates/crm/tests/demo-team.test.ts +18 -0
  190. package/templates/crm/tests/pipeline.test.ts +59 -3
  191. package/templates/directory/.env.example +7 -0
  192. package/templates/directory/AGENTS.md +1 -1
  193. package/templates/directory/README.md +22 -3
  194. package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
  195. package/templates/directory/app/(marketing)/page.tsx +6 -4
  196. package/templates/directory/app/auth-form.tsx +4 -4
  197. package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
  198. package/templates/directory/app/dashboard/page.tsx +16 -14
  199. package/templates/directory/app/icon.svg +4 -0
  200. package/templates/directory/app/login/page.tsx +16 -15
  201. package/templates/directory/functions/seedDemo.ts +43 -0
  202. package/templates/directory/lib/demo.ts +145 -0
  203. package/templates/directory/lib/directory.ts +27 -0
  204. package/templates/directory/lib/site.config.ts +21 -16
  205. package/templates/directory/public/favicon.ico +0 -0
  206. package/templates/directory/tests/directory.test.ts +46 -0
  207. package/templates/expo/consumer/apps/expo/App.tsx +360 -288
  208. package/templates/expo/consumer/apps/expo/app.json +9 -1
  209. package/templates/expo/consumer/apps/expo/package.json +2 -1
  210. package/templates/helpdesk/AGENTS.md +1 -1
  211. package/templates/helpdesk/README.md +21 -0
  212. package/templates/helpdesk/app/apple-icon.png +0 -0
  213. package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
  214. package/templates/helpdesk/app/customers/page.tsx +2 -1
  215. package/templates/helpdesk/app/globals.css +29 -14
  216. package/templates/helpdesk/app/icon.svg +1 -0
  217. package/templates/helpdesk/app/inbox-view.tsx +66 -47
  218. package/templates/helpdesk/app/layout.tsx +7 -5
  219. package/templates/helpdesk/app/page.tsx +4 -7
  220. package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
  221. package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
  222. package/templates/helpdesk/app/workspace.tsx +33 -22
  223. package/templates/helpdesk/app.ts +2 -2
  224. package/templates/helpdesk/components/app-shell.tsx +94 -0
  225. package/templates/helpdesk/components/auth-form.tsx +35 -5
  226. package/templates/helpdesk/components/avatar.tsx +5 -1
  227. package/templates/helpdesk/components/brand-mark.tsx +28 -0
  228. package/templates/helpdesk/components/command-palette.tsx +17 -17
  229. package/templates/helpdesk/components/data-table.tsx +97 -36
  230. package/templates/helpdesk/components/filter-tabs.tsx +101 -0
  231. package/templates/helpdesk/components/nav.tsx +14 -0
  232. package/templates/helpdesk/components/page-header.tsx +43 -7
  233. package/templates/helpdesk/components/priority-badge.tsx +49 -6
  234. package/templates/helpdesk/components/require-auth.tsx +24 -26
  235. package/templates/helpdesk/components/sidebar.tsx +54 -48
  236. package/templates/helpdesk/components/sla-indicator.tsx +5 -8
  237. package/templates/helpdesk/components/thread.tsx +7 -7
  238. package/templates/helpdesk/components/ticket-list.tsx +79 -19
  239. package/templates/helpdesk/components/ui/button.tsx +5 -4
  240. package/templates/helpdesk/components/ui/dialog.tsx +1 -1
  241. package/templates/helpdesk/components/ui/input.tsx +1 -1
  242. package/templates/helpdesk/components/ui/select.tsx +1 -1
  243. package/templates/helpdesk/components/ui/textarea.tsx +1 -1
  244. package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
  245. package/templates/helpdesk/lib/brand.ts +9 -0
  246. package/templates/helpdesk/lib/demo-team.ts +51 -0
  247. package/templates/helpdesk/lib/search.ts +4 -3
  248. package/templates/helpdesk/lib/seed.ts +43 -2
  249. package/templates/helpdesk/lib/tickets.ts +68 -20
  250. package/templates/helpdesk/public/favicon.ico +0 -0
  251. package/templates/helpdesk/tests/components.test.tsx +10 -0
  252. package/templates/helpdesk/tests/demo-team.test.ts +18 -0
  253. package/templates/helpdesk/tests/tickets.test.ts +70 -0
  254. package/templates/imessage/.env.example +49 -0
  255. package/templates/imessage/AGENTS.md +153 -0
  256. package/templates/imessage/CLAUDE.md +7 -0
  257. package/templates/imessage/README.md +187 -0
  258. package/templates/imessage/app/console.tsx +382 -0
  259. package/templates/imessage/app/contacts/page.tsx +13 -0
  260. package/templates/imessage/app/error.tsx +16 -0
  261. package/templates/imessage/app/gate.tsx +31 -0
  262. package/templates/imessage/app/globals.css +183 -0
  263. package/templates/imessage/app/layout.tsx +17 -0
  264. package/templates/imessage/app/login/login-form.tsx +115 -0
  265. package/templates/imessage/app/login/page.tsx +30 -0
  266. package/templates/imessage/app/not-found.tsx +13 -0
  267. package/templates/imessage/app/page.tsx +14 -0
  268. package/templates/imessage/app/robots.ts +6 -0
  269. package/templates/imessage/app/setup/page.tsx +13 -0
  270. package/templates/imessage/app/sign-out.tsx +20 -0
  271. package/templates/imessage/app.ts +299 -0
  272. package/templates/imessage/bunfig +4 -0
  273. package/templates/imessage/components/avatar.tsx +46 -0
  274. package/templates/imessage/components/contact-panel.tsx +263 -0
  275. package/templates/imessage/components/contacts-view.tsx +172 -0
  276. package/templates/imessage/components/conversation-list.tsx +129 -0
  277. package/templates/imessage/components/nav-rail.tsx +67 -0
  278. package/templates/imessage/components/setup-view.tsx +408 -0
  279. package/templates/imessage/components/thread.tsx +229 -0
  280. package/templates/imessage/components/ui/badge.tsx +37 -0
  281. package/templates/imessage/components/ui/button.tsx +56 -0
  282. package/templates/imessage/components/ui/card.tsx +90 -0
  283. package/templates/imessage/components/ui/dialog.tsx +110 -0
  284. package/templates/imessage/components/ui/input.tsx +30 -0
  285. package/templates/imessage/components/ui/label.tsx +21 -0
  286. package/templates/imessage/components/ui/select.tsx +39 -0
  287. package/templates/imessage/components/ui/table.tsx +124 -0
  288. package/templates/imessage/components/ui/textarea.tsx +24 -0
  289. package/templates/imessage/components.json +20 -0
  290. package/templates/imessage/functions/answerNow.ts +34 -0
  291. package/templates/imessage/functions/assistant.ts +96 -0
  292. package/templates/imessage/functions/beginTurn.ts +101 -0
  293. package/templates/imessage/functions/cancelReminder.ts +13 -0
  294. package/templates/imessage/functions/claimOutbound.ts +44 -0
  295. package/templates/imessage/functions/clearDemo.ts +18 -0
  296. package/templates/imessage/functions/deleteNote.ts +11 -0
  297. package/templates/imessage/functions/deliverOutbound.ts +55 -0
  298. package/templates/imessage/functions/finishTurn.ts +84 -0
  299. package/templates/imessage/functions/fireReminder.ts +42 -0
  300. package/templates/imessage/functions/ingestInbound.ts +22 -0
  301. package/templates/imessage/functions/ownerAccount.ts +13 -0
  302. package/templates/imessage/functions/processTurn.ts +74 -0
  303. package/templates/imessage/functions/relayApply.ts +102 -0
  304. package/templates/imessage/functions/relaySync.ts +63 -0
  305. package/templates/imessage/functions/releaseOutbound.ts +18 -0
  306. package/templates/imessage/functions/resolveTurn.ts +25 -0
  307. package/templates/imessage/functions/saveContact.ts +27 -0
  308. package/templates/imessage/functions/seedDemo.ts +70 -0
  309. package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
  310. package/templates/imessage/functions/sendblueWebhook.ts +49 -0
  311. package/templates/imessage/functions/setContactStatus.ts +17 -0
  312. package/templates/imessage/functions/settleOutbound.ts +35 -0
  313. package/templates/imessage/functions/setupStatus.ts +33 -0
  314. package/templates/imessage/functions/sweep.ts +58 -0
  315. package/templates/imessage/functions/toolCancelReminder.ts +17 -0
  316. package/templates/imessage/functions/toolForgetNote.ts +15 -0
  317. package/templates/imessage/functions/toolListNotes.ts +16 -0
  318. package/templates/imessage/functions/toolListReminders.ts +17 -0
  319. package/templates/imessage/functions/toolSaveNote.ts +22 -0
  320. package/templates/imessage/functions/toolSetReminder.ts +55 -0
  321. package/templates/imessage/functions/updateSettings.ts +48 -0
  322. package/templates/imessage/gitignore +11 -0
  323. package/templates/imessage/lib/agent-call.ts +251 -0
  324. package/templates/imessage/lib/assistant.ts +74 -0
  325. package/templates/imessage/lib/chunk.ts +135 -0
  326. package/templates/imessage/lib/demo.ts +68 -0
  327. package/templates/imessage/lib/format.ts +170 -0
  328. package/templates/imessage/lib/gate.ts +104 -0
  329. package/templates/imessage/lib/handles.ts +67 -0
  330. package/templates/imessage/lib/provider.ts +38 -0
  331. package/templates/imessage/lib/relay-protocol.ts +95 -0
  332. package/templates/imessage/lib/reminders.ts +41 -0
  333. package/templates/imessage/lib/secrets.ts +59 -0
  334. package/templates/imessage/lib/sendblue.ts +173 -0
  335. package/templates/imessage/lib/store.ts +407 -0
  336. package/templates/imessage/lib/transport.ts +135 -0
  337. package/templates/imessage/lib/utils.ts +10 -0
  338. package/templates/imessage/package.json +40 -0
  339. package/templates/imessage/relay/.env.example +14 -0
  340. package/templates/imessage/relay/applescript.ts +70 -0
  341. package/templates/imessage/relay/chatdb.ts +165 -0
  342. package/templates/imessage/relay/client.ts +41 -0
  343. package/templates/imessage/relay/main.ts +137 -0
  344. package/templates/imessage/relay/state.ts +35 -0
  345. package/templates/imessage/tests/agent-call.test.ts +112 -0
  346. package/templates/imessage/tests/applescript.test.ts +88 -0
  347. package/templates/imessage/tests/chatdb.test.ts +139 -0
  348. package/templates/imessage/tests/chunk.test.ts +61 -0
  349. package/templates/imessage/tests/fake-ctx.ts +88 -0
  350. package/templates/imessage/tests/ingest.test.ts +141 -0
  351. package/templates/imessage/tests/lib.test.ts +99 -0
  352. package/templates/imessage/tests/review-fixes.test.ts +138 -0
  353. package/templates/imessage/tests/secrets.test.ts +36 -0
  354. package/templates/imessage/tests/sendblue.test.ts +143 -0
  355. package/templates/imessage/tests/setup.ts +5 -0
  356. package/templates/imessage/tsconfig.json +18 -0
  357. package/templates/inventory/AGENTS.md +1 -1
  358. package/templates/inventory/README.md +21 -0
  359. package/templates/inventory/app/apple-icon.png +0 -0
  360. package/templates/inventory/app/globals.css +29 -14
  361. package/templates/inventory/app/icon.svg +1 -0
  362. package/templates/inventory/app/layout.tsx +7 -5
  363. package/templates/inventory/app/movements/movements-view.tsx +32 -10
  364. package/templates/inventory/app/movements/page.tsx +2 -1
  365. package/templates/inventory/app/page.tsx +4 -6
  366. package/templates/inventory/app/products/[id]/page.tsx +2 -1
  367. package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
  368. package/templates/inventory/app/products-view.tsx +74 -33
  369. package/templates/inventory/app/workspace.tsx +34 -16
  370. package/templates/inventory/app.ts +10 -2
  371. package/templates/inventory/components/app-shell.tsx +94 -0
  372. package/templates/inventory/components/auth-form.tsx +35 -5
  373. package/templates/inventory/components/avatar.tsx +5 -1
  374. package/templates/inventory/components/brand-mark.tsx +25 -0
  375. package/templates/inventory/components/category-tile.tsx +49 -0
  376. package/templates/inventory/components/command-palette.tsx +17 -17
  377. package/templates/inventory/components/data-table.tsx +97 -36
  378. package/templates/inventory/components/filter-tabs.tsx +101 -0
  379. package/templates/inventory/components/nav.tsx +14 -0
  380. package/templates/inventory/components/page-header.tsx +43 -7
  381. package/templates/inventory/components/require-auth.tsx +24 -26
  382. package/templates/inventory/components/sidebar.tsx +54 -48
  383. package/templates/inventory/components/stock-level.tsx +30 -14
  384. package/templates/inventory/components/summary-bar.tsx +8 -15
  385. package/templates/inventory/components/ui/button.tsx +5 -4
  386. package/templates/inventory/components/ui/dialog.tsx +1 -1
  387. package/templates/inventory/components/ui/input.tsx +1 -1
  388. package/templates/inventory/components/ui/select.tsx +1 -1
  389. package/templates/inventory/components/ui/textarea.tsx +1 -1
  390. package/templates/inventory/functions/seedWorkspace.ts +18 -8
  391. package/templates/inventory/lib/brand.ts +9 -0
  392. package/templates/inventory/lib/demo-team.ts +51 -0
  393. package/templates/inventory/lib/search.ts +4 -3
  394. package/templates/inventory/lib/seed.ts +29 -2
  395. package/templates/inventory/lib/stock.ts +32 -0
  396. package/templates/inventory/public/favicon.ico +0 -0
  397. package/templates/inventory/tests/demo-team.test.ts +18 -0
  398. package/templates/inventory/tests/stock.test.ts +29 -0
  399. package/templates/invoices/AGENTS.md +1 -1
  400. package/templates/invoices/README.md +16 -0
  401. package/templates/invoices/app/apple-icon.png +0 -0
  402. package/templates/invoices/app/clients/clients-view.tsx +18 -6
  403. package/templates/invoices/app/clients/page.tsx +2 -1
  404. package/templates/invoices/app/globals.css +29 -14
  405. package/templates/invoices/app/icon.svg +1 -0
  406. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
  407. package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
  408. package/templates/invoices/app/invoices-view.tsx +81 -32
  409. package/templates/invoices/app/layout.tsx +7 -5
  410. package/templates/invoices/app/page.tsx +4 -6
  411. package/templates/invoices/app/workspace.tsx +26 -16
  412. package/templates/invoices/app.ts +2 -2
  413. package/templates/invoices/components/app-shell.tsx +94 -0
  414. package/templates/invoices/components/auth-form.tsx +35 -5
  415. package/templates/invoices/components/avatar.tsx +5 -1
  416. package/templates/invoices/components/brand-mark.tsx +26 -0
  417. package/templates/invoices/components/command-palette.tsx +17 -17
  418. package/templates/invoices/components/data-table.tsx +97 -36
  419. package/templates/invoices/components/filter-tabs.tsx +101 -0
  420. package/templates/invoices/components/line-items.tsx +19 -14
  421. package/templates/invoices/components/nav.tsx +14 -0
  422. package/templates/invoices/components/page-header.tsx +43 -7
  423. package/templates/invoices/components/require-auth.tsx +24 -26
  424. package/templates/invoices/components/sidebar.tsx +54 -48
  425. package/templates/invoices/components/status-badge.tsx +7 -7
  426. package/templates/invoices/components/summary-bar.tsx +5 -5
  427. package/templates/invoices/components/ui/button.tsx +5 -4
  428. package/templates/invoices/components/ui/dialog.tsx +1 -1
  429. package/templates/invoices/components/ui/input.tsx +1 -1
  430. package/templates/invoices/components/ui/select.tsx +1 -1
  431. package/templates/invoices/components/ui/textarea.tsx +1 -1
  432. package/templates/invoices/functions/recordPayment.ts +3 -0
  433. package/templates/invoices/lib/billing.ts +40 -0
  434. package/templates/invoices/lib/brand.ts +9 -0
  435. package/templates/invoices/lib/search.ts +4 -3
  436. package/templates/invoices/public/favicon.ico +0 -0
  437. package/templates/invoices/tests/billing.test.ts +27 -0
  438. package/templates/ios/consumer/apps/ios/Package.swift +4 -3
  439. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
  440. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
  441. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
  442. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
  443. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
  444. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
  445. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
  446. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
  447. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
  448. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
  449. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
  450. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
  451. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
  452. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
  453. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
  454. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
  455. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
  456. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
  457. package/templates/ios/consumer/apps/ios/project.yml +13 -9
  458. package/templates/local-service/.env.example +7 -0
  459. package/templates/local-service/AGENTS.md +1 -1
  460. package/templates/local-service/README.md +26 -2
  461. package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
  462. package/templates/local-service/app/(marketing)/page.tsx +66 -42
  463. package/templates/local-service/app/auth-form.tsx +8 -8
  464. package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
  465. package/templates/local-service/app/dashboard/page.tsx +14 -14
  466. package/templates/local-service/app/icon.svg +2 -0
  467. package/templates/local-service/app/login/page.tsx +15 -15
  468. package/templates/local-service/functions/seedDemo.ts +49 -0
  469. package/templates/local-service/lib/demo.ts +135 -0
  470. package/templates/local-service/lib/hours.ts +44 -0
  471. package/templates/local-service/lib/site.config.ts +8 -3
  472. package/templates/local-service/lib/slots.ts +3 -0
  473. package/templates/local-service/public/favicon.ico +0 -0
  474. package/templates/local-service/tests/booking.test.ts +104 -0
  475. package/templates/marketplace/AGENTS.md +1 -1
  476. package/templates/marketplace/README.md +23 -15
  477. package/templates/marketplace/app/globals.css +172 -114
  478. package/templates/marketplace/app/layout.tsx +75 -30
  479. package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
  480. package/templates/marketplace/app/me/page.tsx +4 -4
  481. package/templates/marketplace/app/page.tsx +221 -298
  482. package/templates/marketplace/app/sell/page.tsx +8 -11
  483. package/templates/marketplace/app.ts +32 -7
  484. package/templates/marketplace/client/ActivityFeed.tsx +311 -0
  485. package/templates/marketplace/client/AuthNav.tsx +24 -16
  486. package/templates/marketplace/client/ListingCard.tsx +118 -0
  487. package/templates/marketplace/client/LiveGrid.tsx +92 -0
  488. package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
  489. package/templates/marketplace/client/LoginCard.tsx +114 -136
  490. package/templates/marketplace/client/MarketProvider.tsx +3 -5
  491. package/templates/marketplace/client/MyMarket.tsx +371 -185
  492. package/templates/marketplace/client/OfferPanel.tsx +421 -287
  493. package/templates/marketplace/client/SellForm.tsx +271 -192
  494. package/templates/marketplace/client/ThemeToggle.tsx +1 -0
  495. package/templates/marketplace/client/WatchButton.tsx +16 -16
  496. package/templates/marketplace/client/market.ts +61 -14
  497. package/templates/marketplace/functions/seedMarket.ts +143 -36
  498. package/templates/marketplace/functions/seedOffers.ts +101 -0
  499. package/templates/marketplace/lib/catalog.ts +72 -1
  500. package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
  501. package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
  502. package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
  503. package/templates/marketplace/public/images/listings/duffel.webp +0 -0
  504. package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
  505. package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
  506. package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
  507. package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
  508. package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
  509. package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
  510. package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
  511. package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
  512. package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
  513. package/templates/marketplace/public/images/listings/turntable.webp +0 -0
  514. package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
  515. package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
  516. package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
  517. package/templates/marketplace/public/images/listings/watch.webp +0 -0
  518. package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
  519. package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
  520. package/templates/marketplace/tests/example.test.ts +64 -2
  521. package/templates/marketplace/tests/listing-card.test.tsx +61 -0
  522. package/templates/projects/AGENTS.md +1 -1
  523. package/templates/projects/README.md +24 -3
  524. package/templates/projects/app/apple-icon.png +0 -0
  525. package/templates/projects/app/clients/clients-view.tsx +20 -6
  526. package/templates/projects/app/clients/page.tsx +2 -1
  527. package/templates/projects/app/globals.css +29 -14
  528. package/templates/projects/app/icon.svg +1 -0
  529. package/templates/projects/app/layout.tsx +7 -5
  530. package/templates/projects/app/page.tsx +3 -5
  531. package/templates/projects/app/projects/[id]/page.tsx +2 -1
  532. package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
  533. package/templates/projects/app/projects-view.tsx +105 -52
  534. package/templates/projects/app/workspace.tsx +41 -20
  535. package/templates/projects/app.ts +2 -2
  536. package/templates/projects/components/app-shell.tsx +94 -0
  537. package/templates/projects/components/auth-form.tsx +35 -5
  538. package/templates/projects/components/avatar.tsx +5 -1
  539. package/templates/projects/components/board-skeleton.tsx +3 -3
  540. package/templates/projects/components/brand-mark.tsx +25 -0
  541. package/templates/projects/components/command-palette.tsx +17 -17
  542. package/templates/projects/components/data-table.tsx +97 -36
  543. package/templates/projects/components/filter-tabs.tsx +101 -0
  544. package/templates/projects/components/meter.tsx +48 -0
  545. package/templates/projects/components/nav.tsx +14 -0
  546. package/templates/projects/components/page-header.tsx +43 -7
  547. package/templates/projects/components/require-auth.tsx +24 -26
  548. package/templates/projects/components/sidebar.tsx +54 -48
  549. package/templates/projects/components/summary-bar.tsx +49 -0
  550. package/templates/projects/components/task-board.tsx +10 -9
  551. package/templates/projects/components/time-dialog.tsx +29 -5
  552. package/templates/projects/components/ui/button.tsx +5 -4
  553. package/templates/projects/components/ui/dialog.tsx +1 -1
  554. package/templates/projects/components/ui/input.tsx +1 -1
  555. package/templates/projects/components/ui/select.tsx +1 -1
  556. package/templates/projects/components/ui/textarea.tsx +1 -1
  557. package/templates/projects/functions/seedWorkspace.ts +26 -12
  558. package/templates/projects/lib/brand.ts +9 -0
  559. package/templates/projects/lib/demo-team.ts +51 -0
  560. package/templates/projects/lib/search.ts +4 -3
  561. package/templates/projects/lib/seed.ts +35 -1
  562. package/templates/projects/lib/work.ts +63 -0
  563. package/templates/projects/public/favicon.ico +0 -0
  564. package/templates/projects/tests/demo-team.test.ts +18 -0
  565. package/templates/projects/tests/work.test.ts +65 -0
  566. package/templates/restaurant/.env.example +7 -0
  567. package/templates/restaurant/AGENTS.md +1 -1
  568. package/templates/restaurant/README.md +29 -3
  569. package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
  570. package/templates/restaurant/app/(marketing)/page.tsx +47 -23
  571. package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
  572. package/templates/restaurant/app/auth-form.tsx +8 -8
  573. package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
  574. package/templates/restaurant/app/dashboard/page.tsx +14 -14
  575. package/templates/restaurant/app/icon.svg +4 -0
  576. package/templates/restaurant/app/login/page.tsx +15 -15
  577. package/templates/restaurant/functions/seedDemo.ts +44 -0
  578. package/templates/restaurant/lib/demo.ts +144 -0
  579. package/templates/restaurant/lib/hours.ts +45 -0
  580. package/templates/restaurant/lib/site.config.ts +3 -3
  581. package/templates/restaurant/public/favicon.ico +0 -0
  582. package/templates/restaurant/tests/reservations.test.ts +67 -0
  583. package/templates/saas/AGENTS.md +1 -1
  584. package/templates/saas/README.md +54 -35
  585. package/templates/saas/app/(auth)/auth-form.tsx +25 -23
  586. package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
  587. package/templates/saas/app/(auth)/layout.tsx +27 -33
  588. package/templates/saas/app/(auth)/login/page.tsx +1 -1
  589. package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
  590. package/templates/saas/app/(auth)/signup/page.tsx +1 -1
  591. package/templates/saas/app/(marketing)/layout.tsx +127 -253
  592. package/templates/saas/app/(marketing)/page.tsx +133 -372
  593. package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
  594. package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
  595. package/templates/saas/app/dashboard/board-client.tsx +416 -0
  596. package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
  597. package/templates/saas/app/dashboard/layout.tsx +27 -13
  598. package/templates/saas/app/dashboard/overview-client.tsx +332 -0
  599. package/templates/saas/app/dashboard/page.tsx +29 -35
  600. package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
  601. package/templates/saas/app/dashboard/projects/page.tsx +11 -7
  602. package/templates/saas/app/dashboard/projects-client.tsx +398 -0
  603. package/templates/saas/app/globals.css +5 -4
  604. package/templates/saas/app/icon.svg +1 -0
  605. package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
  606. package/templates/saas/app/onboarding/page.tsx +6 -2
  607. package/templates/saas/app.ts +68 -13
  608. package/templates/saas/components/dashboard-shell.tsx +212 -132
  609. package/templates/saas/components/marketing.tsx +137 -274
  610. package/templates/saas/components/pricing-table.tsx +12 -11
  611. package/templates/saas/components/setup-checklist.tsx +31 -29
  612. package/templates/saas/components/task-ui.tsx +199 -0
  613. package/templates/saas/functions/createProject.ts +7 -13
  614. package/templates/saas/functions/createTask.ts +67 -0
  615. package/templates/saas/functions/deleteProject.ts +17 -0
  616. package/templates/saas/functions/deleteTask.ts +14 -0
  617. package/templates/saas/functions/seedWorkspace.ts +46 -0
  618. package/templates/saas/functions/setProjectStatus.ts +41 -0
  619. package/templates/saas/functions/updateProfile.ts +13 -0
  620. package/templates/saas/functions/updateProject.ts +4 -5
  621. package/templates/saas/functions/updateTask.ts +88 -0
  622. package/templates/saas/lib/members.ts +53 -0
  623. package/templates/saas/lib/names.ts +53 -0
  624. package/templates/saas/lib/plans.ts +11 -0
  625. package/templates/saas/lib/sample-workspace.ts +117 -0
  626. package/templates/saas/lib/site.config.ts +200 -463
  627. package/templates/saas/lib/tasks.ts +101 -0
  628. package/templates/saas/lib/use-live.ts +25 -0
  629. package/templates/saas/public/favicon.ico +0 -0
  630. package/templates/saas/public/screenshots/board.png +0 -0
  631. package/templates/saas/public/screenshots/members.png +0 -0
  632. package/templates/saas/public/screenshots/overview.png +0 -0
  633. package/templates/saas/public/screenshots/projects.png +0 -0
  634. package/templates/saas/scripts/capture-screenshots.mjs +156 -0
  635. package/templates/saas/tests/names.test.ts +35 -0
  636. package/templates/saas/tests/plans.test.ts +12 -0
  637. package/templates/saas/tests/sample-workspace.test.ts +46 -0
  638. package/templates/saas/tests/tasks.test.ts +81 -0
  639. package/templates/shop/AGENTS.md +1 -1
  640. package/templates/shop/README.md +25 -13
  641. package/templates/shop/app/(marketing)/layout.tsx +94 -67
  642. package/templates/shop/app/(marketing)/not-found.tsx +13 -12
  643. package/templates/shop/app/(marketing)/page.tsx +120 -59
  644. package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
  645. package/templates/shop/app/(marketing)/success/page.tsx +75 -22
  646. package/templates/shop/app/globals.css +94 -1
  647. package/templates/shop/app/layout.tsx +4 -4
  648. package/templates/shop/app/login/page.tsx +4 -4
  649. package/templates/shop/app.ts +18 -6
  650. package/templates/shop/components/cart-button.tsx +33 -0
  651. package/templates/shop/components/marketing.tsx +17 -87
  652. package/templates/shop/components/shelf-count.tsx +36 -0
  653. package/templates/shop/functions/checkout.ts +17 -1
  654. package/templates/shop/lib/cart-store.ts +57 -0
  655. package/templates/shop/lib/shelf-store.ts +35 -0
  656. package/templates/shop/lib/shop.ts +27 -0
  657. package/templates/shop/lib/site.config.ts +108 -98
  658. package/templates/shop/public/images/hero.jpg +0 -0
  659. package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
  660. package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
  661. package/templates/shop/public/images/products/linen.jpg +0 -0
  662. package/templates/shop/public/images/products/sea-salt.jpg +0 -0
  663. package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
  664. package/templates/shop/tests/shop.test.ts +64 -0
  665. package/templates/todo/AGENTS.md +1 -1
  666. package/templates/waitlist/.env.example +7 -0
  667. package/templates/waitlist/AGENTS.md +1 -1
  668. package/templates/waitlist/README.md +29 -13
  669. package/templates/waitlist/app/(marketing)/page.tsx +9 -7
  670. package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
  671. package/templates/waitlist/app/auth-form.tsx +8 -8
  672. package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
  673. package/templates/waitlist/app/dashboard/page.tsx +17 -16
  674. package/templates/waitlist/app/icon.svg +2 -0
  675. package/templates/waitlist/app/login/page.tsx +14 -14
  676. package/templates/waitlist/app.ts +6 -6
  677. package/templates/waitlist/functions/joinWaitlist.ts +6 -18
  678. package/templates/waitlist/functions/seedDemo.ts +32 -0
  679. package/templates/waitlist/lib/demo.ts +94 -0
  680. package/templates/waitlist/lib/launch.ts +19 -0
  681. package/templates/waitlist/lib/site.config.ts +14 -9
  682. package/templates/waitlist/lib/stats.ts +35 -0
  683. package/templates/waitlist/public/favicon.ico +0 -0
  684. package/templates/waitlist/tests/counter.test.ts +107 -0
  685. package/templates/waitlist/tests/live-count.test.tsx +48 -0
  686. package/templates/ai-chat/tests/example.test.ts +0 -12
  687. package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
  688. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
  689. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
  690. package/templates/backend/consumer/apps/api/schema.ts +0 -143
  691. package/templates/chat/functions/_keep.ts +0 -13
  692. package/templates/chat/tests/example.test.ts +0 -12
  693. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
  694. package/templates/marketplace/client/LiveTicker.tsx +0 -116
  695. package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
  696. package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
@@ -0,0 +1,188 @@
1
+ // Pure chat helpers: names, colors, grouping, and labels. No Pylon imports,
2
+ // so tests/chat.test.ts runs them without a server and the server functions
3
+ // and the client share one copy.
4
+
5
+ export interface ChatMessage {
6
+ id: string;
7
+ channelId: string;
8
+ authorId: string;
9
+ text: string;
10
+ /** Absent on an optimistic row until the server stamps it. */
11
+ createdAt?: string | null;
12
+ }
13
+
14
+ export interface ChatMember {
15
+ id: string;
16
+ userId: string;
17
+ name: string;
18
+ hue: number;
19
+ }
20
+
21
+ export const MAX_MESSAGE_LENGTH = 4000;
22
+ export const MAX_NAME_LENGTH = 32;
23
+
24
+ /** Messages from one author closer together than this share one header. */
25
+ export const GROUP_GAP_MS = 5 * 60 * 1000;
26
+
27
+ const ADJECTIVES = [
28
+ "Amber", "Brisk", "Cobalt", "Dusky", "Ember", "Fern", "Gilded", "Hazel",
29
+ "Indigo", "Juniper", "Kestrel", "Lunar", "Maple", "Nimble", "Ochre", "Pewter",
30
+ "Quiet", "Russet", "Saffron", "Tidal", "Umber", "Velvet", "Willow", "Zephyr",
31
+ ];
32
+
33
+ const ANIMALS = [
34
+ "Otter", "Heron", "Lynx", "Finch", "Marten", "Ibis", "Badger", "Wren",
35
+ "Puffin", "Stoat", "Crane", "Hare", "Magpie", "Newt", "Osprey", "Plover",
36
+ "Raven", "Seal", "Tern", "Vole", "Wolf", "Yak", "Gecko", "Moth",
37
+ ];
38
+
39
+ /** FNV-1a: a small, stable string hash. The same id always maps to the same name. */
40
+ export function hashString(input: string): number {
41
+ let h = 0x811c9dc5;
42
+ for (let i = 0; i < input.length; i++) {
43
+ h ^= input.charCodeAt(i);
44
+ h = Math.imul(h, 0x01000193);
45
+ }
46
+ return h >>> 0;
47
+ }
48
+
49
+ /** "Amber Otter" style display name for a guest, derived from the user id. */
50
+ export function guestName(userId: string): string {
51
+ const h = hashString(userId);
52
+ const adjective = ADJECTIVES[h % ADJECTIVES.length];
53
+ const animal = ANIMALS[Math.floor(h / ADJECTIVES.length) % ANIMALS.length];
54
+ return `${adjective} ${animal}`;
55
+ }
56
+
57
+ /** Avatar hue (0–359) derived from the user id. */
58
+ export function guestHue(userId: string): number {
59
+ return hashString(`hue:${userId}`) % 360;
60
+ }
61
+
62
+ /** Trims, collapses whitespace, and validates a display name. Returns null when invalid. */
63
+ export function cleanName(raw: string): string | null {
64
+ // \p{Cf} covers zero-width and bidi control characters, which could make a
65
+ // name invisible or reorder the text around it.
66
+ const name = raw.replace(/\p{Cf}/gu, "").replace(/\s+/g, " ").trim();
67
+ if (name.length === 0 || name.length > MAX_NAME_LENGTH) return null;
68
+ return name;
69
+ }
70
+
71
+ /** Up to two initials: "Maya Chen" → "MC", "theo" → "T". */
72
+ export function initials(name: string): string {
73
+ const parts = name.trim().split(/\s+/).filter(Boolean);
74
+ if (parts.length === 0) return "?";
75
+ // Array.from splits by code point, so an emoji stays whole.
76
+ const first = Array.from(parts[0])[0] ?? "";
77
+ const last = parts.length > 1 ? (Array.from(parts[parts.length - 1])[0] ?? "") : "";
78
+ return (first + last).toUpperCase();
79
+ }
80
+
81
+ /**
82
+ * Chronological order. Rows the server has not stamped yet sort last. Equal
83
+ * timestamps (Postgres stores whole seconds) fall back to the id, which is
84
+ * time-ordered, so every client shows the same order.
85
+ */
86
+ export function sortMessages<T extends ChatMessage>(messages: readonly T[]): T[] {
87
+ return [...messages].sort((a, b) => {
88
+ if (!a.createdAt && !b.createdAt) return 0;
89
+ if (!a.createdAt) return 1;
90
+ if (!b.createdAt) return -1;
91
+ return a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id);
92
+ });
93
+ }
94
+
95
+ export interface MessageGroup<T extends ChatMessage = ChatMessage> {
96
+ key: string;
97
+ authorId: string;
98
+ /** Local-day key ("2026-09-26") of the first message; null while pending. */
99
+ day: string | null;
100
+ messages: T[];
101
+ }
102
+
103
+ /** Local calendar day of an ISO timestamp, as "YYYY-MM-DD". */
104
+ export function dayKey(iso: string): string {
105
+ const d = new Date(iso);
106
+ const m = String(d.getMonth() + 1).padStart(2, "0");
107
+ const day = String(d.getDate()).padStart(2, "0");
108
+ return `${d.getFullYear()}-${m}-${day}`;
109
+ }
110
+
111
+ /**
112
+ * Groups sorted messages into runs by one author. A new group starts when the
113
+ * author changes, the local day changes, or the gap exceeds GROUP_GAP_MS. A
114
+ * pending row (no createdAt) joins the previous group when the author matches.
115
+ */
116
+ export function groupMessages<T extends ChatMessage>(
117
+ sorted: readonly T[],
118
+ gapMs: number = GROUP_GAP_MS,
119
+ ): MessageGroup<T>[] {
120
+ const groups: MessageGroup<T>[] = [];
121
+ let lastTime: number | null = null;
122
+ for (const m of sorted) {
123
+ const prev = groups[groups.length - 1];
124
+ const time = m.createdAt ? Date.parse(m.createdAt) : null;
125
+ const day = m.createdAt ? dayKey(m.createdAt) : null;
126
+ const sameAuthor = prev?.authorId === m.authorId;
127
+ const sameDay = day === null || prev?.day === null || prev?.day === day;
128
+ const closeInTime = time === null || lastTime === null || time - lastTime <= gapMs;
129
+ if (prev && sameAuthor && sameDay && closeInTime) {
130
+ prev.messages.push(m);
131
+ } else {
132
+ groups.push({ key: m.id, authorId: m.authorId, day, messages: [m] });
133
+ }
134
+ if (time !== null) lastTime = time;
135
+ }
136
+ return groups;
137
+ }
138
+
139
+ /** "Today", "Yesterday", "Wednesday", or "Monday, September 21". */
140
+ export function dayLabel(iso: string, now: Date = new Date()): string {
141
+ const d = new Date(iso);
142
+ const startOf = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
143
+ const diffDays = Math.round((startOf(now) - startOf(d)) / 86_400_000);
144
+ if (diffDays === 0) return "Today";
145
+ if (diffDays === 1) return "Yesterday";
146
+ if (diffDays > 1 && diffDays < 7) {
147
+ return d.toLocaleDateString("en-US", { weekday: "long" });
148
+ }
149
+ return d.toLocaleDateString("en-US", {
150
+ weekday: "long",
151
+ month: "long",
152
+ day: "numeric",
153
+ ...(d.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }),
154
+ });
155
+ }
156
+
157
+ /** "9:41 AM" in the viewer's locale. */
158
+ export function timeLabel(iso: string): string {
159
+ return new Date(iso).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
160
+ }
161
+
162
+ /** "Maya is typing", "Maya and Theo are typing", "Maya, Theo, and 2 others are typing". */
163
+ export function typingLabel(names: readonly string[]): string {
164
+ if (names.length === 0) return "";
165
+ if (names.length === 1) return `${names[0]} is typing`;
166
+ if (names.length === 2) return `${names[0]} and ${names[1]} are typing`;
167
+ if (names.length === 3) return `${names[0]}, ${names[1]}, and ${names[2]} are typing`;
168
+ return `${names[0]}, ${names[1]}, and ${names.length - 2} others are typing`;
169
+ }
170
+
171
+ /**
172
+ * Messages from other people in a channel, newer than `lastSeen`. A channel the
173
+ * viewer has no mark for counts as read, so a first visit shows no badges.
174
+ */
175
+ export function unreadCount(
176
+ messages: readonly ChatMessage[],
177
+ channelId: string,
178
+ lastSeen: string | undefined,
179
+ selfId: string,
180
+ ): number {
181
+ if (!lastSeen) return 0;
182
+ let n = 0;
183
+ for (const m of messages) {
184
+ if (m.channelId !== channelId || m.authorId === selfId || !m.createdAt) continue;
185
+ if (m.createdAt > lastSeen) n++;
186
+ }
187
+ return n;
188
+ }
@@ -0,0 +1,78 @@
1
+ // Workspace loaded on first boot by functions/joinChat.ts. The channels are
2
+ // always created. With `includeDemoConversation` on, four demo people (a team
3
+ // shipping an offline-maps update for a trail-running app) and their messages
4
+ // are added too. Set it to false to start with empty channels.
5
+ //
6
+ // `minutesAgo` is relative to the moment the seed runs, so the conversation
7
+ // always reads as yesterday plus this morning.
8
+
9
+ export const includeDemoConversation = true;
10
+
11
+ export interface SeedChannel {
12
+ slug: string;
13
+ name: string;
14
+ topic: string;
15
+ }
16
+
17
+ export interface SeedPerson {
18
+ /** Stable author id for seeded rows. Real users have session ids instead. */
19
+ userId: string;
20
+ name: string;
21
+ hue: number;
22
+ }
23
+
24
+ export interface SeedMessage {
25
+ channel: string;
26
+ author: string;
27
+ minutesAgo: number;
28
+ text: string;
29
+ }
30
+
31
+ export const seedChannels: SeedChannel[] = [
32
+ { slug: "general", name: "general", topic: "Anything that doesn't fit elsewhere" },
33
+ { slug: "launch", name: "launch", topic: "Ridgeline 2.4 ships Thursday at 10:00" },
34
+ { slug: "design", name: "design", topic: "Screens, icons, and App Store art" },
35
+ ];
36
+
37
+ export const seedPeople: SeedPerson[] = [
38
+ { userId: "seed_maya", name: "Maya Chen", hue: 28 },
39
+ { userId: "seed_theo", name: "Theo Okafor", hue: 250 },
40
+ { userId: "seed_priya", name: "Priya Raman", hue: 150 },
41
+ { userId: "seed_jonas", name: "Jonas Weber", hue: 330 },
42
+ ];
43
+
44
+ const DAY = 24 * 60;
45
+
46
+ export const seedMessages: SeedMessage[] = [
47
+ // #launch, yesterday afternoon
48
+ { channel: "launch", author: "seed_priya", minutesAgo: DAY + 190, text: "Status check for 2.4. We still have three open items: the iPad crash, the App Store screenshots, and the release notes." },
49
+ { channel: "launch", author: "seed_maya", minutesAgo: DAY + 186, text: "The iPad crash is in the tile cache. When you download a region bigger than about 2 GB, the eviction pass runs on the main thread and the watchdog kills the app." },
50
+ { channel: "launch", author: "seed_maya", minutesAgo: DAY + 185, text: "Moving eviction to a background queue now. Should have a build tonight." },
51
+ { channel: "launch", author: "seed_jonas", minutesAgo: DAY + 171, text: "I can reproduce it every time on the 10th gen iPad with the Cascades region. Happy to run it again once the build is up." },
52
+ { channel: "launch", author: "seed_priya", minutesAgo: DAY + 168, text: "Great. If it slips past Wednesday noon we move the release to next week. I'd rather not ship offline maps that crash offline." },
53
+ { channel: "launch", author: "seed_theo", minutesAgo: DAY + 122, text: "Screenshots are 80% done. Waiting on a clean capture of the elevation profile with the new colors." },
54
+ { channel: "launch", author: "seed_maya", minutesAgo: DAY + 34, text: "TestFlight build 412 is up. Eviction runs in the background, and I added a progress bar so a big cleanup doesn't look frozen." },
55
+ { channel: "launch", author: "seed_jonas", minutesAgo: DAY + 21, text: "Installing now." },
56
+
57
+ // #launch, this morning
58
+ { channel: "launch", author: "seed_jonas", minutesAgo: 58, text: "Build 412 survived six full downloads of the Cascades region on the iPad. No crash, memory stays under 600 MB." },
59
+ { channel: "launch", author: "seed_jonas", minutesAgo: 57, text: "One small thing: the progress bar says 100% for about four seconds before it disappears." },
60
+ { channel: "launch", author: "seed_maya", minutesAgo: 49, text: "That's the index rebuild after the last tile lands. I'll change the label to \"Finishing up\" for that step instead of hiding it." },
61
+ { channel: "launch", author: "seed_priya", minutesAgo: 44, text: "Perfect. That takes the crash off the list 🎉" },
62
+ { channel: "launch", author: "seed_priya", minutesAgo: 43, text: "Remaining: screenshots (Theo) and release notes (me). Submitting for review Wednesday at 3." },
63
+ { channel: "launch", author: "seed_theo", minutesAgo: 12, text: "Screenshots are uploaded to the shared folder: 6.9\" and 13\" iPad, light and dark. Can someone check the Spanish captions?" },
64
+ { channel: "launch", author: "seed_priya", minutesAgo: 6, text: "On it after standup." },
65
+
66
+ // #design
67
+ { channel: "design", author: "seed_theo", minutesAgo: DAY + 240, text: "New elevation colors: climbs go from sand to rust, descents from sky to deep blue. The old green-to-red read like a heart rate chart." },
68
+ { channel: "design", author: "seed_maya", minutesAgo: DAY + 231, text: "Much easier to read in sunlight. Is the rust still readable on the dark map style?" },
69
+ { channel: "design", author: "seed_theo", minutesAgo: DAY + 226, text: "I bumped its lightness 8% in dark mode. Checked it on a phone outside at noon." },
70
+ { channel: "design", author: "seed_theo", minutesAgo: 95, text: "Offline badge is final: a small filled cloud with a slash, bottom-left of the region card. No more \"Saved\" text label." },
71
+ { channel: "design", author: "seed_jonas", minutesAgo: 88, text: "Support will like that. Half the tickets last month were people not knowing a region was already downloaded." },
72
+
73
+ // #general
74
+ { channel: "general", author: "seed_priya", minutesAgo: DAY + 300, text: "Welcome to the team chat. #launch is for the 2.4 release, #design for screens and art." },
75
+ { channel: "general", author: "seed_jonas", minutesAgo: DAY + 75, text: "Trail run Saturday at 8 at the Mill Creek trailhead. 12 km, easy pace, coffee after." },
76
+ { channel: "general", author: "seed_theo", minutesAgo: DAY + 70, text: "In, as long as easy pace means easy pace." },
77
+ { channel: "general", author: "seed_maya", minutesAgo: 30, text: "Standup moved to 10:30 today, Priya is on a call with the App Review team." },
78
+ ];
@@ -0,0 +1,159 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ cleanName,
4
+ dayLabel,
5
+ groupMessages,
6
+ guestHue,
7
+ guestName,
8
+ initials,
9
+ sortMessages,
10
+ typingLabel,
11
+ unreadCount,
12
+ type ChatMessage,
13
+ } from "../lib/chat";
14
+ import { seedChannels, seedMessages, seedPeople } from "../lib/seed";
15
+
16
+ const at = (iso: string, authorId = "a", id = iso, channelId = "c1"): ChatMessage => ({
17
+ id,
18
+ channelId,
19
+ authorId,
20
+ text: "x",
21
+ createdAt: iso,
22
+ });
23
+
24
+ describe("guest identity", () => {
25
+ test("the same id always gets the same name and hue", () => {
26
+ expect(guestName("guest_abc")).toBe(guestName("guest_abc"));
27
+ expect(guestHue("guest_abc")).toBe(guestHue("guest_abc"));
28
+ });
29
+
30
+ test("names are two words and hues are angles", () => {
31
+ for (const id of ["guest_1", "guest_2", "guest_ffff", "u_42"]) {
32
+ expect(guestName(id)).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+$/);
33
+ const hue = guestHue(id);
34
+ expect(hue).toBeGreaterThanOrEqual(0);
35
+ expect(hue).toBeLessThan(360);
36
+ }
37
+ });
38
+ });
39
+
40
+ describe("cleanName", () => {
41
+ test("trims and collapses whitespace", () => {
42
+ expect(cleanName(" Maya Chen ")).toBe("Maya Chen");
43
+ });
44
+ test("removes zero-width and bidi control characters", () => {
45
+ expect(cleanName("Ma\u200bya\u202e")).toBe("Maya");
46
+ expect(cleanName("\u200b\u200b")).toBeNull();
47
+ });
48
+ test("rejects empty and over-long names", () => {
49
+ expect(cleanName(" ")).toBeNull();
50
+ expect(cleanName("x".repeat(33))).toBeNull();
51
+ expect(cleanName("x".repeat(32))).toBe("x".repeat(32));
52
+ });
53
+ });
54
+
55
+ test("initials", () => {
56
+ expect(initials("Maya Chen")).toBe("MC");
57
+ expect(initials("theo")).toBe("T");
58
+ expect(initials("Priya de la Raman")).toBe("PR");
59
+ expect(initials(" ")).toBe("?");
60
+ expect(initials("🎉 Party")).toBe("🎉P");
61
+ });
62
+
63
+ describe("sortMessages", () => {
64
+ test("orders by time and puts unsent rows last", () => {
65
+ const pending: ChatMessage = { id: "p", channelId: "c1", authorId: "a", text: "x" };
66
+ const sorted = sortMessages([at("2026-09-26T10:05:00Z"), pending, at("2026-09-26T10:00:00Z")]);
67
+ expect(sorted.map((m) => m.id)).toEqual(["2026-09-26T10:00:00Z", "2026-09-26T10:05:00Z", "p"]);
68
+ });
69
+ });
70
+
71
+ test("equal timestamps fall back to id order", () => {
72
+ const sorted = sortMessages([
73
+ at("2026-09-26T10:00:00Z", "a", "b-id"),
74
+ at("2026-09-26T10:00:00Z", "a", "a-id"),
75
+ ]);
76
+ expect(sorted.map((m) => m.id)).toEqual(["a-id", "b-id"]);
77
+ });
78
+
79
+ describe("groupMessages", () => {
80
+ test("joins consecutive messages from one author within five minutes", () => {
81
+ const groups = groupMessages([
82
+ at("2026-09-26T10:00:00Z", "a"),
83
+ at("2026-09-26T10:03:00Z", "a"),
84
+ at("2026-09-26T10:04:00Z", "b"),
85
+ at("2026-09-26T10:20:00Z", "b"),
86
+ ]);
87
+ expect(groups.map((g) => [g.authorId, g.messages.length])).toEqual([
88
+ ["a", 2],
89
+ ["b", 1],
90
+ ["b", 1],
91
+ ]);
92
+ });
93
+
94
+ test("starts a new group on a new day", () => {
95
+ const groups = groupMessages([
96
+ at(new Date(2026, 8, 25, 23, 58).toISOString(), "a"),
97
+ at(new Date(2026, 8, 26, 0, 1).toISOString(), "a"),
98
+ ]);
99
+ expect(groups).toHaveLength(2);
100
+ expect(groups[0].day).not.toBe(groups[1].day);
101
+ });
102
+
103
+ test("a pending row joins the sender's last group", () => {
104
+ const pending: ChatMessage = { id: "p", channelId: "c1", authorId: "a", text: "x", createdAt: null };
105
+ const groups = groupMessages([at("2026-09-26T10:00:00Z", "a"), pending]);
106
+ expect(groups).toHaveLength(1);
107
+ expect(groups[0].messages.map((m) => m.id)).toEqual(["2026-09-26T10:00:00Z", "p"]);
108
+ });
109
+ });
110
+
111
+ describe("dayLabel", () => {
112
+ const now = new Date(2026, 8, 26, 12, 0);
113
+ test("today and yesterday", () => {
114
+ expect(dayLabel(new Date(2026, 8, 26, 8, 0).toISOString(), now)).toBe("Today");
115
+ expect(dayLabel(new Date(2026, 8, 25, 23, 0).toISOString(), now)).toBe("Yesterday");
116
+ });
117
+ test("weekday within a week, full date after", () => {
118
+ expect(dayLabel(new Date(2026, 8, 22, 9, 0).toISOString(), now)).toBe("Tuesday");
119
+ expect(dayLabel(new Date(2026, 8, 14, 9, 0).toISOString(), now)).toBe("Monday, September 14");
120
+ expect(dayLabel(new Date(2025, 8, 14, 9, 0).toISOString(), now)).toBe("Sunday, September 14, 2025");
121
+ });
122
+ });
123
+
124
+ test("typingLabel", () => {
125
+ expect(typingLabel([])).toBe("");
126
+ expect(typingLabel(["Maya"])).toBe("Maya is typing");
127
+ expect(typingLabel(["Maya", "Theo"])).toBe("Maya and Theo are typing");
128
+ expect(typingLabel(["Maya", "Theo", "Priya"])).toBe("Maya, Theo, and Priya are typing");
129
+ expect(typingLabel(["Maya", "Theo", "Priya", "Jonas"])).toBe("Maya, Theo, and 2 others are typing");
130
+ });
131
+
132
+ describe("unreadCount", () => {
133
+ const msgs = [
134
+ at("2026-09-26T10:00:00Z", "other", "1"),
135
+ at("2026-09-26T10:05:00Z", "other", "2"),
136
+ at("2026-09-26T10:06:00Z", "me", "3"),
137
+ at("2026-09-26T10:07:00Z", "other", "4", "c2"),
138
+ ];
139
+ test("counts other people's messages after the mark in that channel", () => {
140
+ expect(unreadCount(msgs, "c1", "2026-09-26T10:01:00Z", "me")).toBe(1);
141
+ });
142
+ test("a channel without a mark counts as read", () => {
143
+ expect(unreadCount(msgs, "c1", undefined, "me")).toBe(0);
144
+ });
145
+ });
146
+
147
+ describe("seed", () => {
148
+ test("every seeded message points at a seeded channel and person", () => {
149
+ const channels = new Set(seedChannels.map((c) => c.slug));
150
+ const people = new Set(seedPeople.map((p) => p.userId));
151
+ for (const m of seedMessages) {
152
+ expect(channels.has(m.channel)).toBe(true);
153
+ expect(people.has(m.author)).toBe(true);
154
+ }
155
+ });
156
+ test("seed ids cannot collide with session user ids", () => {
157
+ for (const p of seedPeople) expect(p.userId.startsWith("seed_")).toBe(true);
158
+ });
159
+ });
@@ -0,0 +1,54 @@
1
+ import { afterEach, expect, mock, test } from "bun:test";
2
+ // This project uses the classic JSX transform, so .tsx tests import React.
3
+ import React from "react";
4
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
5
+ import { Composer } from "../components/chat/composer";
6
+
7
+ afterEach(cleanup);
8
+
9
+ function setup() {
10
+ const onSend = mock((_text: string) => {});
11
+ const onTyping = mock(() => {});
12
+ const onStopTyping = mock(() => {});
13
+ render(
14
+ <Composer
15
+ channelName="launch"
16
+ channelKey="c1"
17
+ onSend={onSend}
18
+ onTyping={onTyping}
19
+ onStopTyping={onStopTyping}
20
+ />,
21
+ );
22
+ const field = screen.getByPlaceholderText("Message #launch") as HTMLTextAreaElement;
23
+ return { field, onSend, onTyping, onStopTyping };
24
+ }
25
+
26
+ test("Enter sends the trimmed text and clears the field", () => {
27
+ const { field, onSend, onStopTyping } = setup();
28
+ fireEvent.change(field, { target: { value: " ship it " } });
29
+ fireEvent.keyDown(field, { key: "Enter" });
30
+ expect(onSend).toHaveBeenCalledWith("ship it");
31
+ expect(field.value).toBe("");
32
+ expect(onStopTyping).toHaveBeenCalled();
33
+ });
34
+
35
+ test("Shift+Enter does not send", () => {
36
+ const { field, onSend } = setup();
37
+ fireEvent.change(field, { target: { value: "line one" } });
38
+ fireEvent.keyDown(field, { key: "Enter", shiftKey: true });
39
+ expect(onSend).not.toHaveBeenCalled();
40
+ });
41
+
42
+ test("whitespace-only text does not send and the button stays disabled", () => {
43
+ const { field, onSend } = setup();
44
+ fireEvent.change(field, { target: { value: " " } });
45
+ fireEvent.keyDown(field, { key: "Enter" });
46
+ expect(onSend).not.toHaveBeenCalled();
47
+ expect((screen.getByRole("button", { name: "Send message" }) as HTMLButtonElement).disabled).toBe(true);
48
+ });
49
+
50
+ test("typing reports activity", () => {
51
+ const { field, onTyping } = setup();
52
+ fireEvent.change(field, { target: { value: "h" } });
53
+ expect(onTyping).toHaveBeenCalled();
54
+ });
@@ -62,7 +62,7 @@ this pass before the user has to ask.
62
62
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
63
63
  - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
64
64
  - **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
65
- - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
65
+ - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. The call timeout is an idle timeout (`PYLON_FN_CALL_TIMEOUT`, 30s default; Stack0 Cloud 300s): each stream event resets it, and the total run is capped at 10× the timeout. Set `timeout: <secs>` on the def when a step (a slow tool call) can be silent longer than that.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
68
  - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
@@ -10,7 +10,7 @@ saved posts. Likes, comments, and follows update live in every open tab.
10
10
  __RUN_DEV__
11
11
  ```
12
12
 
13
- Open http://localhost:4321. The first visit loads twelve demo people with
13
+ Open http://localhost:4321. The first visit loads eight demo people with
14
14
  their posts, likes, comments, and follows, and gives you a guest profile.
15
15
  Post a photo with Create, like with a double tap, and open a second tab to
16
16
  watch counts change.
@@ -30,7 +30,7 @@ lib/seed.ts the demo people and posts
30
30
  lib/site.ts the app's name
31
31
  functions/ ensureProfile, updateProfile, createPost,
32
32
  addComment, seedFeed
33
- public/images/ demo avatars and photos
33
+ public/images/posts/ demo photos
34
34
  ```
35
35
 
36
36
  ## How it works
@@ -55,7 +55,7 @@ public/images/ demo avatars and photos
55
55
  move a guest's profile to the account on sign-in.
56
56
  - **Scale:** `components/social/use-social.ts` reads every row, which is fine
57
57
  for a demo. For a large network, query per screen with `where` and `limit`.
58
- - **Launch:** delete `functions/seedFeed.ts`, `lib/seed.ts`, and
58
+ - **Launch:** delete `functions/seedFeed.ts`, `lib/seed.ts`, `tests/seed.test.ts`, and
59
59
  `public/images/`, and remove the `seedFeed` call in `session.tsx`.
60
60
 
61
61
  ## Deploy
@@ -16,9 +16,12 @@
16
16
  on <html>. */
17
17
  :root {
18
18
  --radius: 0.625rem;
19
- /* The app's own colors: links and the follow button, and a liked heart. */
20
- --brand: #2563eb;
21
- --like: #e11d48;
19
+ /* The app's own colors: links and the follow button, and a liked heart.
20
+ They match the iOS app in apps/ios. */
21
+ --brand: #0095f6;
22
+ --like: #ff3040;
23
+ /* The ring around someone who posted in the last 24 hours. */
24
+ --story-ring: conic-gradient(from 180deg, #fcba45, #fa6133, #de2678, #8c38d9, #fcba45);
22
25
  --background: oklch(1 0 0);
23
26
  --foreground: oklch(0.141 0.005 285.823);
24
27
  --card: oklch(1 0 0);
@@ -147,6 +150,10 @@
147
150
  }
148
151
 
149
152
  /* The wordmark. Instrument Serif is declared in app.ts. */
153
+ .bg-story-ring {
154
+ background: var(--story-ring);
155
+ }
156
+
150
157
  .font-display {
151
158
  font-family: var(--font-display, "Iowan Old Style", Georgia, serif);
152
159
  }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><linearGradient id="g" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stop-color="#fcba45"/><stop offset=".35" stop-color="#fa6133"/><stop offset=".7" stop-color="#de2678"/><stop offset="1" stop-color="#8c38d9"/></linearGradient></defs><rect width="64" height="64" rx="14" fill="url(#g)"/><text x="32" y="45" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-style="italic" font-weight="700" font-size="40" fill="#ffffff">G</text></svg>
@@ -4,7 +4,17 @@ import React from "react";
4
4
  import { cn } from "@/lib/utils";
5
5
  import type { Profile } from "@/lib/social";
6
6
 
7
- /** A round profile photo, or the first letter of the name when there is none. */
7
+ /** The same tints, in the same order, as the iOS app's AvatarView. */
8
+ const TINTS = ["#ff9500", "#ff2d55", "#af52de", "#5856d6", "#30b0c7", "#34c759", "#007aff", "#ff3b30"];
9
+
10
+ /** A stable tint per username. Same hash as the iOS app, so a person has one color everywhere. */
11
+ export function avatarTint(username: string): string {
12
+ let h = 0;
13
+ for (const ch of username) h = (Math.imul(h, 31) + (ch.codePointAt(0) ?? 0)) & 0xffff;
14
+ return TINTS[h % TINTS.length];
15
+ }
16
+
17
+ /** A round profile photo, or the first letter of the name on a tinted circle. */
8
18
  export function Avatar({
9
19
  profile,
10
20
  size = 32,
@@ -13,11 +23,12 @@ export function Avatar({
13
23
  }: {
14
24
  profile: Pick<Profile, "username" | "displayName" | "avatarUrl"> | null | undefined;
15
25
  size?: number;
16
- /** A colored ring, for people who posted recently. */
26
+ /** The gradient ring, for people who posted in the last 24 hours. */
17
27
  ring?: boolean;
18
28
  className?: string;
19
29
  }) {
20
30
  const letter = (profile?.displayName || profile?.username || "?").trim().charAt(0).toUpperCase();
31
+ const tint = avatarTint(profile?.username ?? "");
21
32
  const inner = profile?.avatarUrl ? (
22
33
  <img
23
34
  src={profile.avatarUrl}
@@ -29,22 +40,25 @@ export function Avatar({
29
40
  />
30
41
  ) : (
31
42
  <span
32
- className="grid size-full place-items-center rounded-full bg-zinc-200 font-semibold text-zinc-600"
33
- style={{ fontSize: Math.max(10, Math.round(size * 0.4)) }}
43
+ aria-hidden
44
+ className="grid size-full place-items-center rounded-full font-semibold text-white"
45
+ style={{
46
+ fontSize: Math.max(10, Math.round(size * 0.42)),
47
+ background: `linear-gradient(to bottom, color-mix(in srgb, ${tint} 78%, white), ${tint})`,
48
+ }}
34
49
  >
35
50
  {letter}
36
51
  </span>
37
52
  );
53
+ const pad = ring ? Math.max(2, Math.round(size * 0.05)) : 0;
38
54
  return (
39
55
  <span
40
- className={cn(
41
- "inline-block shrink-0 rounded-full",
42
- ring ? "bg-brand p-[2px]" : "",
43
- className,
44
- )}
45
- style={{ width: size + (ring ? 4 : 0), height: size + (ring ? 4 : 0) }}
56
+ className={cn("inline-block shrink-0 rounded-full", ring ? "bg-story-ring" : "", className)}
57
+ style={{ width: size + pad * 4, height: size + pad * 4, padding: pad }}
46
58
  >
47
- <span className={cn("block size-full rounded-full", ring ? "bg-background p-[2px]" : "")}>{inner}</span>
59
+ <span className="block size-full rounded-full bg-background" style={{ padding: pad }}>
60
+ {inner}
61
+ </span>
48
62
  </span>
49
63
  );
50
64
  }
@@ -1,5 +1,5 @@
1
1
  import { mutation } from "@pylonsync/functions";
2
- import { shapeSeed } from "../lib/seed";
2
+ import { SEED_PROFILES, demoUserId, shapeSeed } from "../lib/seed";
3
3
 
4
4
  /**
5
5
  * Load the demo people, posts, likes, comments, and follows once. The feed
@@ -8,7 +8,7 @@ import { shapeSeed } from "../lib/seed";
8
8
  *
9
9
  * Public so an anonymous first visitor seeds it. `unsafe.insert` writes past
10
10
  * the policies, which only accept the caller's own id, so the posts read as
11
- * twelve different people. It only writes lib/seed.ts. Delete this function
11
+ * eight different people. It only writes lib/seed.ts. Delete this function
12
12
  * and lib/seed.ts when you launch.
13
13
  */
14
14
  export default mutation<Record<string, never>, { seeded: boolean }>({
@@ -16,7 +16,7 @@ export default mutation<Record<string, never>, { seeded: boolean }>({
16
16
  args: {},
17
17
  async handler(ctx) {
18
18
  await ctx.db.advisoryLock("consumer_seed_feed");
19
- const existing = await ctx.db.unsafe.query("Profile", { userId: "demo_mara_bakes", $limit: 1 });
19
+ const existing = await ctx.db.unsafe.query("Profile", { userId: demoUserId(SEED_PROFILES[0].username), $limit: 1 });
20
20
  if (existing.length > 0) return { seeded: false };
21
21
 
22
22
  const seed = shapeSeed();