@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,2 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#0b0b0c"/>
2
+ <path d="M20 14h9v27h17v9H20z" fill="#4f6bff"/></svg>
@@ -18,21 +18,21 @@ export default function LoginPage({ auth, response }: PageProps) {
18
18
  if (auth.user_id && !auth.user_id.startsWith("guest_")) response.redirect("/dashboard");
19
19
  const { brand } = siteConfig;
20
20
  return (
21
- <div className="flex min-h-screen items-center justify-center bg-white px-6 py-12">
22
- <div className="w-full max-w-[400px] rounded-2xl border border-zinc-200/70 p-8">
23
- <Link href="/" className="inline-flex">
24
- <span className="flex size-9 items-center justify-center rounded-xl bg-zinc-900 text-base font-bold text-white">
25
- {brand.letter}
26
- </span>
21
+ <div className="flex min-h-screen flex-col bg-ink px-6 text-chalk">
22
+ <div className="mx-auto flex h-16 w-full max-w-5xl items-center">
23
+ <Link href="/" className="font-display text-[17px] font-bold tracking-tight text-chalk">
24
+ {brand.name}
27
25
  </Link>
28
- <h1 className="mt-5 text-[22px] font-semibold tracking-tight text-zinc-900">
29
- {brand.name} dashboard
30
- </h1>
31
- <p className="mt-1 text-[13px] text-zinc-500">
32
- Sign in to see who&apos;s on your waitlist.
33
- </p>
34
- <div className="mt-6">
35
- <AuthForm />
26
+ </div>
27
+ <div className="flex flex-1 items-center justify-center pb-24">
28
+ <div className="w-full max-w-[380px]">
29
+ <h1 className="font-display text-[1.75rem] font-bold leading-tight tracking-[-0.02em]">
30
+ Sign in to the waitlist
31
+ </h1>
32
+ <p className="mt-2 text-[14px] text-chalk-2">Owner access to signups and the export.</p>
33
+ <div className="mt-8 rounded-lg border border-line bg-paper p-6">
34
+ <AuthForm />
35
+ </div>
36
36
  </div>
37
37
  </div>
38
38
  </div>
@@ -44,8 +44,8 @@ const Signup = entity(
44
44
  );
45
45
 
46
46
  // A single-row, PII-FREE aggregate the public page can safely read live. It
47
- // holds only the signup count — no emails. `joinWaitlist` keeps it in sync with
48
- // the real Signup count on every new join. The landing page subscribes with
47
+ // holds only the signup count — no emails. Every Signup writer recounts into
48
+ // it (lib/stats.ts → syncWaitlistStat). The landing page subscribes with
49
49
  // `db.useQuery("WaitlistStat")`, which syncs across every open tab through the
50
50
  // replica — so the counter ticks up everywhere the instant someone joins. This
51
51
  // is the cross-tab-safe realtime primitive (entity sync), not a per-connection
@@ -81,9 +81,9 @@ const User = entity(
81
81
  // PRIVACY — the heart of the spec. Signup holds visitor emails, so it denies
82
82
  // EVERY client read and write. No `db.useQuery("Signup")` can ever pull a row,
83
83
  // and no client can insert/update/delete directly. Writes happen only inside
84
- // the server-side `joinWaitlist` mutation (functions bypass policies); reads
85
- // happen only inside `waitlistCount` (returns a bare integer) and the
86
- // owner-gated `waitlistStats`. A marketing site must never leak its own
84
+ // the server-side `joinWaitlist` and dev-only `seedDemo` mutations (functions
85
+ // bypass policies); reads happen only inside the owner-gated `waitlistStats`.
86
+ // The public page sees the count through WaitlistStat. A marketing site must never leak its own
87
87
  // customers' emails — this policy is what guarantees it.
88
88
  const signupPolicy = policy({
89
89
  name: "signup_private",
@@ -96,7 +96,7 @@ const signupPolicy = policy({
96
96
 
97
97
  // The aggregate count is public to READ (it's just a number — the whole point
98
98
  // is that the landing page shows it live to everyone). Clients can't WRITE it;
99
- // only the joinWaitlist mutation maintains it server-side.
99
+ // only the Signup writers maintain it server-side.
100
100
  const waitlistStatPolicy = policy({
101
101
  name: "waitlist_stat_public_read",
102
102
  entity: "WaitlistStat",
@@ -1,4 +1,5 @@
1
1
  import { mutation, v } from "@pylonsync/functions";
2
+ import { syncWaitlistStat } from "../lib/stats";
2
3
 
3
4
  // Normalize + sanity-check an email without pulling in a dependency. This is a
4
5
  // pragmatic "looks like an email" check, not RFC 5322 — the unique index is the
@@ -14,9 +15,8 @@ function normalizeEmail(raw: string): string | null {
14
15
 
15
16
  // joinWaitlist — the ONLY way a Signup row is ever written. It's a `mutation`
16
17
  // (not an `action`): mutations get `ctx.db` access and run as one atomic
17
- // transaction, and the insert fires a change event that re-runs the live
18
- // `waitlistCount` query for every open tab — that's what makes the counter tick
19
- // up in realtime.
18
+ // transaction. It also rewrites the public WaitlistStat row, and that change
19
+ // syncs to every open tab, which is what makes the counter tick up live.
20
20
  //
21
21
  // `auth: "public"` because a landing-page visitor has no account. Public
22
22
  // mutations are still rate-limited at the HTTP layer in production (the
@@ -61,21 +61,9 @@ export default mutation<{ email: string }, { ok: boolean; alreadyJoined: boolean
61
61
  throw e;
62
62
  }
63
63
 
64
- // Keep the public, PII-free WaitlistStat singleton in sync with the real
65
- // count. We RECOUNT (rather than +1) so the number can never drift, and
66
- // this whole handler is one transaction — on SQLite writers serialize, so
67
- // the recount-then-write is consistent. The landing page reads this row via
68
- // `db.useQuery`, which syncs the new value to every open tab.
69
- const total = (await ctx.db.unsafe.list("Signup")).length;
70
- const stat = (await ctx.db.unsafe.list("WaitlistStat"))[0] as
71
- | { id: string }
72
- | undefined;
73
- const now = new Date().toISOString();
74
- if (stat) {
75
- await ctx.db.unsafe.update("WaitlistStat", stat.id, { count: total, updatedAt: now });
76
- } else {
77
- await ctx.db.unsafe.insert("WaitlistStat", { count: total, updatedAt: now });
78
- }
64
+ // Keep the public, PII-free WaitlistStat row equal to the Signup count.
65
+ // It is a recount, not +1, so the number cannot drift from the table.
66
+ await syncWaitlistStat(ctx.db.unsafe);
79
67
 
80
68
  return { ok: true, alreadyJoined: false };
81
69
  },
@@ -0,0 +1,32 @@
1
+ import { mutation } from "@pylonsync/functions";
2
+ import { demoDataEnabled, demoSignups } from "../lib/demo";
3
+ import { syncWaitlistStat } from "../lib/stats";
4
+
5
+ // seedDemo — fill an empty waitlist with fictional signups in development.
6
+ //
7
+ // The landing page counter calls this when it finds no signups, and the
8
+ // dashboard calls it once on load. It writes nothing unless demo data is on
9
+ // (lib/demo.ts: PYLON_DEMO_DATA, else a `pylon dev` process), so `pylon start`,
10
+ // Docker, and Pylon Cloud deploys do not get demo rows. It also writes nothing
11
+ // once any Signup exists, so it cannot mix demo rows into a real list. Clearing
12
+ // every signup in development seeds the list again on the next load. The
13
+ // advisory lock stops two first visits from seeding twice.
14
+ //
15
+ // Public because the first visitor in `pylon dev` is anonymous. It returns only
16
+ // a flag, never a row.
17
+ export default mutation<Record<string, never>, { seeded: boolean }>({
18
+ auth: "public",
19
+ async handler(ctx) {
20
+ if (!demoDataEnabled(ctx.env)) return { seeded: false };
21
+
22
+ await ctx.db.advisoryLock("waitlist_seed_demo");
23
+ const existing = await ctx.db.unsafe.list("Signup");
24
+ if (existing.length > 0) return { seeded: false };
25
+
26
+ for (const row of demoSignups(Date.now())) {
27
+ await ctx.db.unsafe.insert("Signup", { email: row.email, createdAt: row.createdAt });
28
+ }
29
+ await syncWaitlistStat(ctx.db.unsafe);
30
+ return { seeded: true };
31
+ },
32
+ });
@@ -0,0 +1,94 @@
1
+ // Demo signups for local development.
2
+ //
3
+ // `pylon dev` fills an empty waitlist with about 180 fictional signups spread
4
+ // over the last 30 days, so the counter, chart, and list have data. A deploy
5
+ // does not seed: `demoDataEnabled` is true only when PYLON_DEMO_DATA is on, or
6
+ // when it is unset and the process runs under `pylon dev`. Set
7
+ // PYLON_DEMO_DATA=0 to turn it off in development too.
8
+ //
9
+ // Pure data + pure shaping, so functions/seedDemo.ts stays a thin wrapper and
10
+ // this file is testable without a server.
11
+
12
+ const ON = new Set(["1", "true", "yes", "on"]);
13
+ const OFF = new Set(["0", "false", "no", "off"]);
14
+
15
+ /**
16
+ * True when demo rows may be written. An explicit PYLON_DEMO_DATA wins.
17
+ * Otherwise it needs both PYLON_DEV_MODE on and PYLON_DEV_WATCH_DIR set. Only
18
+ * `pylon dev` sets PYLON_DEV_WATCH_DIR; `pylon start` and the Docker image do
19
+ * not, and the image turns PYLON_DEV_MODE on by default, so dev mode alone is
20
+ * not proof of a local machine.
21
+ */
22
+ export function demoDataEnabled(env: Record<string, string | undefined>): boolean {
23
+ const explicit = env.PYLON_DEMO_DATA?.trim().toLowerCase();
24
+ if (explicit && ON.has(explicit)) return true;
25
+ if (explicit && OFF.has(explicit)) return false;
26
+ const dev = env.PYLON_DEV_MODE?.trim().toLowerCase();
27
+ const devMode = dev === "1" || dev === "true";
28
+ return devMode && Boolean(env.PYLON_DEV_WATCH_DIR?.trim());
29
+ }
30
+
31
+ const FIRST = [
32
+ "priya", "marcus", "lena", "tomas", "aisha", "ben", "hana", "diego", "sofia", "kwame",
33
+ "ingrid", "raj", "noor", "felix", "maya", "owen", "yuki", "carmen", "theo", "amara",
34
+ "jonas", "leila", "sam", "ines", "dev", "clara", "malik", "rosa", "ethan", "zara",
35
+ ];
36
+ const LAST = [
37
+ "nair", "bell", "fischer", "alvarez", "khan", "osei", "sato", "ruiz", "marin", "mensah",
38
+ "berg", "patel", "haddad", "wagner", "chen", "reilly", "tanaka", "vega", "park", "okafor",
39
+ ];
40
+ // Fictional company domains on the reserved .example TLD, so no demo address
41
+ // can reach a real inbox.
42
+ const DOMAINS = [
43
+ "fieldnote.example", "northwind.example", "quarry.example", "bluefin.example", "wren.example",
44
+ "copperline.example", "tidewater.example", "larkspur.example", "kestrel.example", "orchardlabs.example",
45
+ "sable.example", "ironwood.example", "halcyon.example", "pinecrest.example", "vantage.example",
46
+ ];
47
+
48
+ /** Small deterministic PRNG (mulberry32) so the demo set is stable per seed. */
49
+ function rng(seed: number): () => number {
50
+ let a = seed >>> 0;
51
+ return () => {
52
+ a = (a + 0x6d2b79f5) >>> 0;
53
+ let t = a;
54
+ t = Math.imul(t ^ (t >>> 15), t | 1);
55
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
56
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
57
+ };
58
+ }
59
+
60
+ export interface DemoSignup {
61
+ email: string;
62
+ createdAt: string;
63
+ }
64
+
65
+ /**
66
+ * About 180 signups over the 30 days before `nowMs`, rising toward today.
67
+ * Emails are unique. Every timestamp is in the past.
68
+ */
69
+ export function demoSignups(nowMs: number, seed = 7): DemoSignup[] {
70
+ const rand = rng(seed);
71
+ const out: DemoSignup[] = [];
72
+ const seen = new Set<string>();
73
+ const DAY = 86_400_000;
74
+ for (let daysAgo = 29; daysAgo >= 0; daysAgo--) {
75
+ // Growth curve: ~2/day a month ago, ~10/day this week, plus noise.
76
+ const base = 2 + Math.round((29 - daysAgo) * 0.28);
77
+ const count = Math.max(0, base + Math.round((rand() - 0.5) * 4));
78
+ for (let i = 0; i < count; i++) {
79
+ const first = FIRST[Math.floor(rand() * FIRST.length)];
80
+ const last = LAST[Math.floor(rand() * LAST.length)];
81
+ const domain = DOMAINS[Math.floor(rand() * DOMAINS.length)];
82
+ const shape = rand();
83
+ const local = shape < 0.5 ? `${first}.${last}` : shape < 0.8 ? `${first}` : `${first[0]}${last}`;
84
+ const email = `${local}@${domain}`;
85
+ if (seen.has(email)) continue;
86
+ seen.add(email);
87
+ // A random minute inside that day, never later than `nowMs`.
88
+ const offset = Math.floor(rand() * DAY);
89
+ const ts = Math.min(nowMs - 60_000, nowMs - daysAgo * DAY - offset);
90
+ out.push({ email, createdAt: new Date(ts).toISOString() });
91
+ }
92
+ }
93
+ return out.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1));
94
+ }
@@ -0,0 +1,19 @@
1
+ // The launch line under the hero form, from `siteConfig.hero.launchDate`.
2
+ //
3
+ // Before the date it names the month ("Public launch: March 2027"). From the
4
+ // date on it says invites are going out, so the page never shows a launch date
5
+ // that has already passed. The date is read and formatted in UTC, so the
6
+ // server render and the browser produce the same text.
7
+
8
+ /** `launchDate` is "YYYY-MM-DD" (UTC). Returns null for a malformed date. */
9
+ export function launchLabel(launchDate: string, nowMs: number): string | null {
10
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(launchDate.trim());
11
+ if (!m) return null;
12
+ const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
13
+ const atMs = Date.UTC(y, mo - 1, d);
14
+ const at = new Date(atMs);
15
+ if (at.getUTCFullYear() !== y || at.getUTCMonth() !== mo - 1 || at.getUTCDate() !== d) return null;
16
+ if (nowMs >= atMs) return "Invites are going out now";
17
+ const month = at.toLocaleDateString("en-US", { month: "long", year: "numeric", timeZone: "UTC" });
18
+ return `Public launch: ${month}`;
19
+ }
@@ -36,7 +36,9 @@ export type MockRow = { title: string; tag: string; done?: boolean };
36
36
 
37
37
  export type WaitlistConfig = BaseConfig & {
38
38
  hero: {
39
- launchNote: string; // one short line, set in mono: when it ships
39
+ // "YYYY-MM-DD" (UTC). The page shows the month until this date, then
40
+ // "Invites are going out now". See lib/launch.ts.
41
+ launchDate: string;
40
42
  headline: string;
41
43
  subcopy: string;
42
44
  emailPlaceholder: string;
@@ -47,9 +49,11 @@ export type WaitlistConfig = BaseConfig & {
47
49
  counter: {
48
50
  enabled: boolean;
49
51
  label: string; // e.g. "people on the list"
50
- // A vanity baseline added to the real, live signup count so a brand-new
51
- // page doesn't read "0". Set to 0 to show only genuine signups.
52
- seedCount?: number;
52
+ labelOne: string; // the same label for a count of 1, e.g. "person on the list"
53
+ // Signups collected before this site existed (an old form, a spreadsheet),
54
+ // added to the Signup table's count. Keep 0 unless those people are real;
55
+ // the page shows the sum as one number.
56
+ importedCount: number;
53
57
  };
54
58
  // The HTML/CSS product window on the right of the hero. No screenshot.
55
59
  mock: {
@@ -93,11 +97,11 @@ export const siteConfig: WaitlistConfig = {
93
97
  seo: {
94
98
  title: "Lumo — projects, notes, and tasks in one workspace. Coming soon.",
95
99
  description:
96
- "Lumo puts your projects, notes, and tasks in one workspace. It launches this fall. Join the waitlist for an early invite.",
100
+ "Lumo puts your projects, notes, and tasks in one workspace. Join the waitlist for an early invite.",
97
101
  },
98
102
 
99
103
  hero: {
100
- launchNote: "Launch: fall 2026",
104
+ launchDate: "2027-03-02",
101
105
  headline: "Projects, notes, and tasks in one workspace.",
102
106
  subcopy:
103
107
  "Lumo is one app for the plan, the notes behind it, and the tasks that come out of it. Join the list and we send an invite before the public launch.",
@@ -110,7 +114,8 @@ export const siteConfig: WaitlistConfig = {
110
114
  counter: {
111
115
  enabled: true,
112
116
  label: "people on the list",
113
- seedCount: 1200,
117
+ labelOne: "person on the list",
118
+ importedCount: 0,
114
119
  },
115
120
 
116
121
  mock: {
@@ -148,8 +153,8 @@ export const siteConfig: WaitlistConfig = {
148
153
  headline: "Questions",
149
154
  items: [
150
155
  {
151
- q: "When does Lumo launch?",
152
- a: "Fall 2026. People on the waitlist get an invite before the public launch.",
156
+ q: "Who gets invited first?",
157
+ a: "People get invites in the order they joined. Each invite covers one workspace for up to five people.",
153
158
  },
154
159
  {
155
160
  q: "What do I get for joining?",
@@ -28,3 +28,38 @@ export interface WaitlistStatsData {
28
28
  export type WaitlistStatsResult =
29
29
  | ({ authorized: true } & WaitlistStatsData)
30
30
  | { authorized: false };
31
+
32
+ /* ------------------------- public live counter ------------------------- */
33
+
34
+ // The public counter shows the Signup table's row count, read from the
35
+ // PII-free WaitlistStat row. `importedCount` is `siteConfig.counter.importedCount`:
36
+ // signups collected before this site existed (an old form, a spreadsheet). It is
37
+ // 0 by default, so the page shows only rows in the table.
38
+ export function publicCount(
39
+ statRows: ReadonlyArray<{ count: number }>,
40
+ importedCount = 0,
41
+ ): number {
42
+ const real = statRows.length > 0 ? Math.max(0, Math.floor(statRows[0].count)) : 0;
43
+ const imported = Number.isFinite(importedCount) ? Math.max(0, Math.floor(importedCount)) : 0;
44
+ return real + imported;
45
+ }
46
+
47
+ // The subset of `ctx.db.unsafe` the recount needs. Typed structurally so the
48
+ // helper runs against the real handle and a test double.
49
+ export interface StatDb {
50
+ list(entity: string): Promise<unknown[]>;
51
+ insert(entity: string, data: Record<string, unknown>): Promise<unknown>;
52
+ update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown>;
53
+ }
54
+
55
+ // Write the Signup row count into the single WaitlistStat row, creating it if
56
+ // missing. Every writer of Signup calls this in the same transaction, so the
57
+ // public number is always the table's count.
58
+ export async function syncWaitlistStat(db: StatDb): Promise<number> {
59
+ const total = (await db.list("Signup")).length;
60
+ const stat = (await db.list("WaitlistStat"))[0] as { id: string } | undefined;
61
+ const now = new Date().toISOString();
62
+ if (stat) await db.update("WaitlistStat", stat.id, { count: total, updatedAt: now });
63
+ else await db.insert("WaitlistStat", { count: total, updatedAt: now });
64
+ return total;
65
+ }
@@ -0,0 +1,107 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { publicCount, syncWaitlistStat, type StatDb } from "../lib/stats";
3
+ import { demoDataEnabled, demoSignups } from "../lib/demo";
4
+ import { launchLabel } from "../lib/launch";
5
+ import { siteConfig } from "../lib/site.config";
6
+
7
+ describe("publicCount", () => {
8
+ test("is the table's count when nothing is imported", () => {
9
+ expect(publicCount([{ count: 3 }])).toBe(3);
10
+ expect(publicCount([])).toBe(0);
11
+ });
12
+ test("adds only an explicit, non-negative imported count", () => {
13
+ expect(publicCount([{ count: 3 }], 40)).toBe(43);
14
+ expect(publicCount([{ count: 3 }], -5)).toBe(3);
15
+ expect(publicCount([{ count: 3 }], Number.NaN)).toBe(3);
16
+ });
17
+ test("the shipped config adds nothing to the real count", () => {
18
+ expect(siteConfig.counter.importedCount).toBe(0);
19
+ });
20
+ });
21
+
22
+ // An in-memory stand-in for ctx.db.unsafe.
23
+ function fakeDb(signups: number): StatDb & { rows: Record<string, Record<string, unknown>[]> } {
24
+ const rows: Record<string, Record<string, unknown>[]> = {
25
+ Signup: Array.from({ length: signups }, (_, i) => ({ id: `s${i}` })),
26
+ WaitlistStat: [],
27
+ };
28
+ return {
29
+ rows,
30
+ async list(entity) {
31
+ return rows[entity] ?? [];
32
+ },
33
+ async insert(entity, data) {
34
+ const id = `${entity}-${rows[entity].length}`;
35
+ rows[entity].push({ id, ...data });
36
+ return id;
37
+ },
38
+ async update(entity, id, data) {
39
+ const row = rows[entity].find((r) => r.id === id);
40
+ if (row) Object.assign(row, data);
41
+ return id;
42
+ },
43
+ };
44
+ }
45
+
46
+ describe("syncWaitlistStat", () => {
47
+ test("creates the stat row with the Signup count", async () => {
48
+ const db = fakeDb(5);
49
+ expect(await syncWaitlistStat(db)).toBe(5);
50
+ expect(db.rows.WaitlistStat).toHaveLength(1);
51
+ expect(db.rows.WaitlistStat[0].count).toBe(5);
52
+ });
53
+ test("recounts into the existing row instead of adding one", async () => {
54
+ const db = fakeDb(5);
55
+ await syncWaitlistStat(db);
56
+ db.rows.Signup.push({ id: "late" });
57
+ await syncWaitlistStat(db);
58
+ expect(db.rows.WaitlistStat).toHaveLength(1);
59
+ expect(db.rows.WaitlistStat[0].count).toBe(6);
60
+ });
61
+ });
62
+
63
+ describe("demoDataEnabled", () => {
64
+ test("needs a `pylon dev` process when PYLON_DEMO_DATA is unset", () => {
65
+ expect(demoDataEnabled({ PYLON_DEV_MODE: "1", PYLON_DEV_WATCH_DIR: "/src/app" })).toBe(true);
66
+ // The Docker base image turns dev mode on by default; without `pylon dev` it must not seed.
67
+ expect(demoDataEnabled({ PYLON_DEV_MODE: "true" })).toBe(false);
68
+ expect(demoDataEnabled({ PYLON_DEV_MODE: "true", PYLON_DEV_WATCH_DIR: "/src/app" })).toBe(true);
69
+ expect(demoDataEnabled({ PYLON_DEV_MODE: "false" })).toBe(false);
70
+ expect(demoDataEnabled({})).toBe(false);
71
+ });
72
+ test("an explicit PYLON_DEMO_DATA wins", () => {
73
+ expect(demoDataEnabled({ PYLON_DEV_MODE: "1", PYLON_DEV_WATCH_DIR: "/src/app", PYLON_DEMO_DATA: "0" })).toBe(false);
74
+ expect(demoDataEnabled({ PYLON_DEV_MODE: "false", PYLON_DEMO_DATA: "1" })).toBe(true);
75
+ });
76
+ });
77
+
78
+ describe("demoSignups", () => {
79
+ const now = Date.parse("2026-09-26T15:00:00Z");
80
+ const rows = demoSignups(now);
81
+ test("unique emails, all in the past 30 days", () => {
82
+ expect(new Set(rows.map((r) => r.email)).size).toBe(rows.length);
83
+ for (const r of rows) {
84
+ const t = Date.parse(r.createdAt);
85
+ expect(t).toBeLessThan(now);
86
+ expect(t).toBeGreaterThan(now - 31 * 86_400_000);
87
+ }
88
+ });
89
+ test("enough rows to fill the chart, stable for a seed", () => {
90
+ expect(rows.length).toBeGreaterThan(120);
91
+ expect(demoSignups(now)).toEqual(rows);
92
+ });
93
+ });
94
+
95
+ describe("launchLabel", () => {
96
+ const now = Date.parse("2026-09-26T12:00:00Z");
97
+ test("names the month before the date", () => {
98
+ expect(launchLabel("2027-03-02", now)).toBe("Public launch: March 2027");
99
+ });
100
+ test("never shows a date that has passed", () => {
101
+ expect(launchLabel("2026-09-01", now)).toBe("Invites are going out now");
102
+ });
103
+ test("rejects a malformed date", () => {
104
+ expect(launchLabel("fall 2026", now)).toBeNull();
105
+ expect(launchLabel("2026-02-31", now)).toBeNull();
106
+ });
107
+ });
@@ -0,0 +1,48 @@
1
+ import { afterEach, expect, mock, test } from "bun:test";
2
+ import React from "react";
3
+ import { cleanup, render, screen } from "@testing-library/react";
4
+
5
+ // The live count reads the WaitlistStat row. Mock the data boundary so the test
6
+ // controls what the "server" says.
7
+ let statRows: { id: string; count: number; updatedAt: string }[] = [];
8
+ let loading = false;
9
+ mock.module("@pylonsync/react", () => ({
10
+ db: { useQuery: () => ({ data: statRows, loading }) },
11
+ callFn: async () => ({ seeded: false }),
12
+ }));
13
+ mock.module("@pylonsync/client", () => ({
14
+ EnsureGuest: ({ children }: { children: React.ReactNode }) => <>{children}</>,
15
+ }));
16
+ const { LiveCount } = await import("../app/(marketing)/waitlist-hero");
17
+
18
+ afterEach(() => {
19
+ cleanup();
20
+ statRows = [];
21
+ loading = false;
22
+ });
23
+
24
+ test("shows the table's count, with no vanity baseline", () => {
25
+ statRows = [{ id: "w", count: 42, updatedAt: "" }];
26
+ render(<LiveCount importedCount={0} label="people on the list" labelOne="person on the list" />);
27
+ expect(screen.getByTestId("live-count").textContent).toBe("42");
28
+ });
29
+
30
+ test("shows nothing, not a placeholder number, before the first sync", () => {
31
+ loading = true;
32
+ render(<LiveCount importedCount={0} label="people on the list" labelOne="person on the list" />);
33
+ expect(screen.queryByTestId("live-count")).toBeNull();
34
+ });
35
+
36
+ test("uses the singular label for one signup", () => {
37
+ statRows = [{ id: "w", count: 1, updatedAt: "" }];
38
+ const { container } = render(
39
+ <LiveCount importedCount={0} label="people on the list" labelOne="person on the list" />,
40
+ );
41
+ expect(container.textContent).toContain("1person on the list");
42
+ });
43
+
44
+ test("shows nothing while the list is empty", () => {
45
+ statRows = [{ id: "w", count: 0, updatedAt: "" }];
46
+ render(<LiveCount importedCount={0} label="people on the list" labelOne="person on the list" />);
47
+ expect(screen.queryByTestId("live-count")).toBeNull();
48
+ });
@@ -1,12 +0,0 @@
1
- import { expect, test } from "bun:test";
2
-
3
- // Your first test. Run the suite with: pylon test (or: npm test)
4
- //
5
- // `pylon test` discovers every *.test.ts file under tests/ (or functions/) and
6
- // runs it with Bun's test runner against an in-memory Pylon. Replace this
7
- // placeholder with tests for your functions and helpers — see AGENTS.md →
8
- // Testing for the pure-logic and HTTP-against-`pylon dev` patterns.
9
-
10
- test("placeholder — replace me with a real test", () => {
11
- expect(true).toBe(true);
12
- });
@@ -1,57 +0,0 @@
1
- import { query } from "@pylonsync/functions";
2
-
3
- /**
4
- * Public feed — every Post, newest first. Joins author Profile + the
5
- * caller's like state into the response so the client can render a
6
- * row without N+1 follow-up queries.
7
- *
8
- * For a real consumer app, paginate this (limit + cursor by createdAt)
9
- * — the scaffold returns at most 100 rows, which is plenty for a
10
- * Hello-World feed but unsustainable past a few thousand posts.
11
- */
12
- export default query({
13
- args: {},
14
- async handler(ctx) {
15
- const posts = (await ctx.db.query("Post", {})) as any[];
16
- const sorted = [...posts]
17
- .sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))
18
- .slice(0, 100);
19
-
20
- const callerProfile = ctx.auth.userId
21
- ? ((await ctx.db.query("Profile", {
22
- userId: ctx.auth.userId,
23
- })) as any[])[0]
24
- : null;
25
-
26
- const out: Array<{
27
- id: string;
28
- body: string;
29
- createdAt: string;
30
- author: { id: string; handle: string; displayName: string } | null;
31
- likeCount: number;
32
- likedByMe: boolean;
33
- }> = [];
34
- for (const p of sorted) {
35
- const author = (await ctx.db.get("Profile", p.authorId)) as any;
36
- const likes = (await ctx.db.query("Like", { postId: p.id })) as any[];
37
- const likedByMe = callerProfile
38
- ? likes.some((l) => l.profileId === callerProfile.id)
39
- : false;
40
- out.push({
41
- id: p.id,
42
- body: p.body,
43
- createdAt: p.createdAt,
44
- author: author
45
- ? {
46
- id: author.id,
47
- handle: author.handle,
48
- displayName: author.displayName,
49
- }
50
- : null,
51
- likeCount: likes.length,
52
- likedByMe,
53
- });
54
- }
55
- return out;
56
- },
57
- });
@@ -1,17 +0,0 @@
1
- import { query, v } from "@pylonsync/functions";
2
-
3
- /**
4
- * Every Post by a single Profile, newest first. Used by the profile
5
- * page on each platform.
6
- */
7
- export default query({
8
- args: { profileId: v.id("Profile") },
9
- async handler(ctx, args: { profileId: string }) {
10
- const rows = (await ctx.db.query("Post", {
11
- authorId: args.profileId,
12
- })) as any[];
13
- return rows.sort(
14
- (a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt),
15
- );
16
- },
17
- });