@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
@@ -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
+ });
@@ -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();