@pylonsync/create-pylon 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (675) hide show
  1. package/bin/create-pylon.js +30 -7
  2. package/package.json +1 -1
  3. package/templates/agency/.env.example +7 -0
  4. package/templates/agency/README.md +28 -3
  5. package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
  6. package/templates/agency/app/(marketing)/page.tsx +3 -3
  7. package/templates/agency/app/(marketing)/work/page.tsx +3 -3
  8. package/templates/agency/app/auth-form.tsx +4 -4
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
  10. package/templates/agency/app/dashboard/page.tsx +14 -16
  11. package/templates/agency/app/icon.svg +3 -0
  12. package/templates/agency/app/login/page.tsx +12 -14
  13. package/templates/agency/components/marketing.tsx +12 -5
  14. package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
  15. package/templates/agency/lib/agency.ts +22 -0
  16. package/templates/agency/lib/demo.ts +118 -0
  17. package/templates/agency/lib/site.config.ts +19 -13
  18. package/templates/agency/public/favicon.ico +0 -0
  19. package/templates/agency/tests/agency.test.ts +57 -0
  20. package/templates/ai-chat/.env.example +13 -27
  21. package/templates/ai-chat/README.md +53 -60
  22. package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
  23. package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
  24. package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
  25. package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
  26. package/templates/ai-chat/app/(chat)/page.tsx +7 -11
  27. package/templates/ai-chat/app/auth-form.tsx +45 -38
  28. package/templates/ai-chat/app/error.tsx +9 -12
  29. package/templates/ai-chat/app/globals.css +464 -102
  30. package/templates/ai-chat/app/layout.tsx +11 -19
  31. package/templates/ai-chat/app/login/page.tsx +39 -24
  32. package/templates/ai-chat/app.ts +118 -43
  33. package/templates/ai-chat/components/chat/composer.tsx +193 -0
  34. package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
  35. package/templates/ai-chat/components/chat/icons.tsx +198 -0
  36. package/templates/ai-chat/components/chat/markdown.tsx +164 -0
  37. package/templates/ai-chat/components/chat/message.tsx +183 -0
  38. package/templates/ai-chat/components/chat/setup.tsx +71 -0
  39. package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
  40. package/templates/ai-chat/functions/_beginTurn.ts +145 -0
  41. package/templates/ai-chat/functions/_finishTurn.ts +55 -0
  42. package/templates/ai-chat/functions/assistantStatus.ts +13 -0
  43. package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
  44. package/templates/ai-chat/functions/createConversation.ts +28 -0
  45. package/templates/ai-chat/functions/deleteConversation.ts +15 -20
  46. package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
  47. package/templates/ai-chat/functions/renameConversation.ts +19 -0
  48. package/templates/ai-chat/functions/respond.ts +88 -0
  49. package/templates/ai-chat/functions/seedExamples.ts +46 -0
  50. package/templates/ai-chat/functions/stopResponse.ts +30 -0
  51. package/templates/ai-chat/lib/brand-icon.ts +7 -0
  52. package/templates/ai-chat/lib/chat.ts +277 -0
  53. package/templates/ai-chat/lib/claim.ts +15 -0
  54. package/templates/ai-chat/lib/examples.ts +179 -0
  55. package/templates/ai-chat/lib/highlight.ts +107 -0
  56. package/templates/ai-chat/lib/markdown.ts +317 -0
  57. package/templates/ai-chat/lib/provider.ts +38 -0
  58. package/templates/ai-chat/lib/site.config.ts +55 -39
  59. package/templates/ai-chat/tests/chat.test.ts +174 -0
  60. package/templates/ai-chat/tests/components.test.tsx +57 -0
  61. package/templates/ai-chat/tests/markdown.test.ts +88 -0
  62. package/templates/ai-chat/tests/provider.test.ts +32 -0
  63. package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
  64. package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
  65. package/templates/backend/consumer/apps/api/app.ts +166 -0
  66. package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
  67. package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
  68. package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
  69. package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
  70. package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
  71. package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
  72. package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
  73. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
  74. package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
  75. package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
  76. package/templates/backend/consumer/apps/api/package.json +8 -2
  77. package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
  78. package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
  79. package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
  80. package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
  81. package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
  82. package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
  83. package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
  84. package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
  85. package/templates/chat/README.md +43 -21
  86. package/templates/chat/app/chat-client.tsx +495 -85
  87. package/templates/chat/app/error.tsx +1 -1
  88. package/templates/chat/app/globals.css +170 -54
  89. package/templates/chat/app/layout.tsx +12 -9
  90. package/templates/chat/app/not-found.tsx +4 -2
  91. package/templates/chat/app/page.tsx +9 -19
  92. package/templates/chat/app.ts +96 -17
  93. package/templates/chat/components/chat/avatar.tsx +45 -0
  94. package/templates/chat/components/chat/composer.tsx +110 -0
  95. package/templates/chat/components/chat/message-list.tsx +285 -0
  96. package/templates/chat/components/chat/sidebar.tsx +190 -0
  97. package/templates/chat/functions/joinChat.ts +83 -0
  98. package/templates/chat/functions/renameMember.ts +25 -0
  99. package/templates/chat/lib/chat.ts +188 -0
  100. package/templates/chat/lib/seed.ts +78 -0
  101. package/templates/chat/tests/chat.test.ts +159 -0
  102. package/templates/chat/tests/composer.test.tsx +54 -0
  103. package/templates/consumer/README.md +3 -3
  104. package/templates/consumer/app/globals.css +10 -3
  105. package/templates/consumer/app/icon.svg +1 -0
  106. package/templates/consumer/components/social/avatar.tsx +25 -11
  107. package/templates/consumer/functions/seedFeed.ts +3 -3
  108. package/templates/consumer/lib/seed.ts +43 -85
  109. package/templates/consumer/public/favicon.ico +0 -0
  110. package/templates/consumer/public/images/posts/arch.jpg +0 -0
  111. package/templates/consumer/public/images/posts/coffee.jpg +0 -0
  112. package/templates/consumer/public/images/posts/dog.jpg +0 -0
  113. package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
  114. package/templates/consumer/public/images/posts/ramen.jpg +0 -0
  115. package/templates/consumer/public/images/posts/run.jpg +0 -0
  116. package/templates/consumer/public/images/posts/sunset.jpg +0 -0
  117. package/templates/consumer/tests/seed.test.ts +39 -0
  118. package/templates/creator/.env.example +7 -0
  119. package/templates/creator/README.md +24 -5
  120. package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
  121. package/templates/creator/app/auth-form.tsx +7 -7
  122. package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
  123. package/templates/creator/app/dashboard/page.tsx +18 -18
  124. package/templates/creator/app/icon.svg +2 -0
  125. package/templates/creator/app/login/page.tsx +14 -14
  126. package/templates/creator/app.ts +4 -4
  127. package/templates/creator/functions/seedDemo.ts +32 -0
  128. package/templates/creator/functions/subscribe.ts +4 -15
  129. package/templates/creator/lib/demo.ts +94 -0
  130. package/templates/creator/lib/site.config.ts +15 -10
  131. package/templates/creator/lib/stats.ts +35 -0
  132. package/templates/creator/public/favicon.ico +0 -0
  133. package/templates/creator/tests/counter.test.ts +92 -0
  134. package/templates/creator/tests/live-count.test.tsx +45 -0
  135. package/templates/crm/README.md +22 -1
  136. package/templates/crm/app/apple-icon.png +0 -0
  137. package/templates/crm/app/companies/companies-view.tsx +32 -14
  138. package/templates/crm/app/companies/page.tsx +2 -1
  139. package/templates/crm/app/contacts/contacts-view.tsx +34 -10
  140. package/templates/crm/app/contacts/page.tsx +2 -1
  141. package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
  142. package/templates/crm/app/deals/[id]/page.tsx +2 -1
  143. package/templates/crm/app/globals.css +29 -14
  144. package/templates/crm/app/icon.svg +1 -0
  145. package/templates/crm/app/layout.tsx +7 -5
  146. package/templates/crm/app/page.tsx +2 -1
  147. package/templates/crm/app/pipeline-view.tsx +5 -5
  148. package/templates/crm/app/workspace.tsx +29 -17
  149. package/templates/crm/app.ts +5 -5
  150. package/templates/crm/components/activity-timeline.tsx +14 -4
  151. package/templates/crm/components/app-shell.tsx +94 -0
  152. package/templates/crm/components/auth-form.tsx +35 -5
  153. package/templates/crm/components/avatar.tsx +5 -1
  154. package/templates/crm/components/board-skeleton.tsx +5 -5
  155. package/templates/crm/components/brand-mark.tsx +25 -0
  156. package/templates/crm/components/command-palette.tsx +17 -17
  157. package/templates/crm/components/data-table.tsx +97 -36
  158. package/templates/crm/components/deal-card.tsx +23 -27
  159. package/templates/crm/components/filter-tabs.tsx +101 -0
  160. package/templates/crm/components/metrics-bar.tsx +6 -6
  161. package/templates/crm/components/nav.tsx +15 -0
  162. package/templates/crm/components/page-header.tsx +43 -7
  163. package/templates/crm/components/pipeline-board.tsx +16 -10
  164. package/templates/crm/components/require-auth.tsx +24 -26
  165. package/templates/crm/components/sidebar.tsx +54 -49
  166. package/templates/crm/components/stage-badge.tsx +4 -2
  167. package/templates/crm/components/ui/button.tsx +5 -4
  168. package/templates/crm/components/ui/dialog.tsx +1 -1
  169. package/templates/crm/components/ui/input.tsx +1 -1
  170. package/templates/crm/components/ui/select.tsx +1 -1
  171. package/templates/crm/components/ui/textarea.tsx +1 -1
  172. package/templates/crm/functions/seedWorkspace.ts +32 -17
  173. package/templates/crm/lib/brand.ts +9 -0
  174. package/templates/crm/lib/demo-team.ts +51 -0
  175. package/templates/crm/lib/pipeline.ts +37 -0
  176. package/templates/crm/lib/search.ts +4 -3
  177. package/templates/crm/lib/seed.ts +53 -8
  178. package/templates/crm/public/favicon.ico +0 -0
  179. package/templates/crm/tests/components.test.tsx +5 -5
  180. package/templates/crm/tests/demo-team.test.ts +18 -0
  181. package/templates/crm/tests/pipeline.test.ts +59 -3
  182. package/templates/directory/.env.example +7 -0
  183. package/templates/directory/README.md +22 -3
  184. package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
  185. package/templates/directory/app/(marketing)/page.tsx +6 -4
  186. package/templates/directory/app/auth-form.tsx +4 -4
  187. package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
  188. package/templates/directory/app/dashboard/page.tsx +16 -14
  189. package/templates/directory/app/icon.svg +4 -0
  190. package/templates/directory/app/login/page.tsx +16 -15
  191. package/templates/directory/functions/seedDemo.ts +43 -0
  192. package/templates/directory/lib/demo.ts +145 -0
  193. package/templates/directory/lib/directory.ts +27 -0
  194. package/templates/directory/lib/site.config.ts +21 -16
  195. package/templates/directory/public/favicon.ico +0 -0
  196. package/templates/directory/tests/directory.test.ts +46 -0
  197. package/templates/expo/consumer/apps/expo/App.tsx +360 -288
  198. package/templates/expo/consumer/apps/expo/app.json +9 -1
  199. package/templates/expo/consumer/apps/expo/package.json +2 -1
  200. package/templates/helpdesk/README.md +21 -0
  201. package/templates/helpdesk/app/apple-icon.png +0 -0
  202. package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
  203. package/templates/helpdesk/app/customers/page.tsx +2 -1
  204. package/templates/helpdesk/app/globals.css +29 -14
  205. package/templates/helpdesk/app/icon.svg +1 -0
  206. package/templates/helpdesk/app/inbox-view.tsx +66 -47
  207. package/templates/helpdesk/app/layout.tsx +7 -5
  208. package/templates/helpdesk/app/page.tsx +4 -7
  209. package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
  210. package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
  211. package/templates/helpdesk/app/workspace.tsx +33 -22
  212. package/templates/helpdesk/app.ts +2 -2
  213. package/templates/helpdesk/components/app-shell.tsx +94 -0
  214. package/templates/helpdesk/components/auth-form.tsx +35 -5
  215. package/templates/helpdesk/components/avatar.tsx +5 -1
  216. package/templates/helpdesk/components/brand-mark.tsx +28 -0
  217. package/templates/helpdesk/components/command-palette.tsx +17 -17
  218. package/templates/helpdesk/components/data-table.tsx +97 -36
  219. package/templates/helpdesk/components/filter-tabs.tsx +101 -0
  220. package/templates/helpdesk/components/nav.tsx +14 -0
  221. package/templates/helpdesk/components/page-header.tsx +43 -7
  222. package/templates/helpdesk/components/priority-badge.tsx +49 -6
  223. package/templates/helpdesk/components/require-auth.tsx +24 -26
  224. package/templates/helpdesk/components/sidebar.tsx +54 -48
  225. package/templates/helpdesk/components/sla-indicator.tsx +5 -8
  226. package/templates/helpdesk/components/thread.tsx +7 -7
  227. package/templates/helpdesk/components/ticket-list.tsx +79 -19
  228. package/templates/helpdesk/components/ui/button.tsx +5 -4
  229. package/templates/helpdesk/components/ui/dialog.tsx +1 -1
  230. package/templates/helpdesk/components/ui/input.tsx +1 -1
  231. package/templates/helpdesk/components/ui/select.tsx +1 -1
  232. package/templates/helpdesk/components/ui/textarea.tsx +1 -1
  233. package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
  234. package/templates/helpdesk/lib/brand.ts +9 -0
  235. package/templates/helpdesk/lib/demo-team.ts +51 -0
  236. package/templates/helpdesk/lib/search.ts +4 -3
  237. package/templates/helpdesk/lib/seed.ts +43 -2
  238. package/templates/helpdesk/lib/tickets.ts +68 -20
  239. package/templates/helpdesk/public/favicon.ico +0 -0
  240. package/templates/helpdesk/tests/components.test.tsx +10 -0
  241. package/templates/helpdesk/tests/demo-team.test.ts +18 -0
  242. package/templates/helpdesk/tests/tickets.test.ts +70 -0
  243. package/templates/imessage/.env.example +49 -0
  244. package/templates/imessage/AGENTS.md +153 -0
  245. package/templates/imessage/CLAUDE.md +7 -0
  246. package/templates/imessage/README.md +187 -0
  247. package/templates/imessage/app/console.tsx +382 -0
  248. package/templates/imessage/app/contacts/page.tsx +13 -0
  249. package/templates/imessage/app/error.tsx +16 -0
  250. package/templates/imessage/app/gate.tsx +31 -0
  251. package/templates/imessage/app/globals.css +183 -0
  252. package/templates/imessage/app/layout.tsx +17 -0
  253. package/templates/imessage/app/login/login-form.tsx +115 -0
  254. package/templates/imessage/app/login/page.tsx +30 -0
  255. package/templates/imessage/app/not-found.tsx +13 -0
  256. package/templates/imessage/app/page.tsx +14 -0
  257. package/templates/imessage/app/robots.ts +6 -0
  258. package/templates/imessage/app/setup/page.tsx +13 -0
  259. package/templates/imessage/app/sign-out.tsx +20 -0
  260. package/templates/imessage/app.ts +299 -0
  261. package/templates/imessage/bunfig +4 -0
  262. package/templates/imessage/components/avatar.tsx +46 -0
  263. package/templates/imessage/components/contact-panel.tsx +263 -0
  264. package/templates/imessage/components/contacts-view.tsx +172 -0
  265. package/templates/imessage/components/conversation-list.tsx +129 -0
  266. package/templates/imessage/components/nav-rail.tsx +67 -0
  267. package/templates/imessage/components/setup-view.tsx +408 -0
  268. package/templates/imessage/components/thread.tsx +229 -0
  269. package/templates/imessage/components/ui/badge.tsx +37 -0
  270. package/templates/imessage/components/ui/button.tsx +56 -0
  271. package/templates/imessage/components/ui/card.tsx +90 -0
  272. package/templates/imessage/components/ui/dialog.tsx +110 -0
  273. package/templates/imessage/components/ui/input.tsx +30 -0
  274. package/templates/imessage/components/ui/label.tsx +21 -0
  275. package/templates/imessage/components/ui/select.tsx +39 -0
  276. package/templates/imessage/components/ui/table.tsx +124 -0
  277. package/templates/imessage/components/ui/textarea.tsx +24 -0
  278. package/templates/imessage/components.json +20 -0
  279. package/templates/imessage/functions/answerNow.ts +34 -0
  280. package/templates/imessage/functions/assistant.ts +96 -0
  281. package/templates/imessage/functions/beginTurn.ts +101 -0
  282. package/templates/imessage/functions/cancelReminder.ts +13 -0
  283. package/templates/imessage/functions/claimOutbound.ts +44 -0
  284. package/templates/imessage/functions/clearDemo.ts +18 -0
  285. package/templates/imessage/functions/deleteNote.ts +11 -0
  286. package/templates/imessage/functions/deliverOutbound.ts +55 -0
  287. package/templates/imessage/functions/finishTurn.ts +84 -0
  288. package/templates/imessage/functions/fireReminder.ts +42 -0
  289. package/templates/imessage/functions/ingestInbound.ts +22 -0
  290. package/templates/imessage/functions/ownerAccount.ts +13 -0
  291. package/templates/imessage/functions/processTurn.ts +74 -0
  292. package/templates/imessage/functions/relayApply.ts +102 -0
  293. package/templates/imessage/functions/relaySync.ts +63 -0
  294. package/templates/imessage/functions/releaseOutbound.ts +18 -0
  295. package/templates/imessage/functions/resolveTurn.ts +25 -0
  296. package/templates/imessage/functions/saveContact.ts +27 -0
  297. package/templates/imessage/functions/seedDemo.ts +70 -0
  298. package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
  299. package/templates/imessage/functions/sendblueWebhook.ts +49 -0
  300. package/templates/imessage/functions/setContactStatus.ts +17 -0
  301. package/templates/imessage/functions/settleOutbound.ts +35 -0
  302. package/templates/imessage/functions/setupStatus.ts +33 -0
  303. package/templates/imessage/functions/sweep.ts +58 -0
  304. package/templates/imessage/functions/toolCancelReminder.ts +17 -0
  305. package/templates/imessage/functions/toolForgetNote.ts +15 -0
  306. package/templates/imessage/functions/toolListNotes.ts +16 -0
  307. package/templates/imessage/functions/toolListReminders.ts +17 -0
  308. package/templates/imessage/functions/toolSaveNote.ts +22 -0
  309. package/templates/imessage/functions/toolSetReminder.ts +55 -0
  310. package/templates/imessage/functions/updateSettings.ts +48 -0
  311. package/templates/imessage/gitignore +11 -0
  312. package/templates/imessage/lib/agent-call.ts +251 -0
  313. package/templates/imessage/lib/assistant.ts +74 -0
  314. package/templates/imessage/lib/chunk.ts +135 -0
  315. package/templates/imessage/lib/demo.ts +68 -0
  316. package/templates/imessage/lib/format.ts +170 -0
  317. package/templates/imessage/lib/gate.ts +104 -0
  318. package/templates/imessage/lib/handles.ts +67 -0
  319. package/templates/imessage/lib/provider.ts +38 -0
  320. package/templates/imessage/lib/relay-protocol.ts +95 -0
  321. package/templates/imessage/lib/reminders.ts +41 -0
  322. package/templates/imessage/lib/secrets.ts +59 -0
  323. package/templates/imessage/lib/sendblue.ts +173 -0
  324. package/templates/imessage/lib/store.ts +407 -0
  325. package/templates/imessage/lib/transport.ts +135 -0
  326. package/templates/imessage/lib/utils.ts +10 -0
  327. package/templates/imessage/package.json +40 -0
  328. package/templates/imessage/relay/.env.example +14 -0
  329. package/templates/imessage/relay/applescript.ts +70 -0
  330. package/templates/imessage/relay/chatdb.ts +165 -0
  331. package/templates/imessage/relay/client.ts +41 -0
  332. package/templates/imessage/relay/main.ts +137 -0
  333. package/templates/imessage/relay/state.ts +35 -0
  334. package/templates/imessage/tests/agent-call.test.ts +112 -0
  335. package/templates/imessage/tests/applescript.test.ts +88 -0
  336. package/templates/imessage/tests/chatdb.test.ts +139 -0
  337. package/templates/imessage/tests/chunk.test.ts +61 -0
  338. package/templates/imessage/tests/fake-ctx.ts +88 -0
  339. package/templates/imessage/tests/ingest.test.ts +141 -0
  340. package/templates/imessage/tests/lib.test.ts +99 -0
  341. package/templates/imessage/tests/review-fixes.test.ts +138 -0
  342. package/templates/imessage/tests/secrets.test.ts +36 -0
  343. package/templates/imessage/tests/sendblue.test.ts +143 -0
  344. package/templates/imessage/tests/setup.ts +5 -0
  345. package/templates/imessage/tsconfig.json +18 -0
  346. package/templates/inventory/README.md +21 -0
  347. package/templates/inventory/app/apple-icon.png +0 -0
  348. package/templates/inventory/app/globals.css +29 -14
  349. package/templates/inventory/app/icon.svg +1 -0
  350. package/templates/inventory/app/layout.tsx +7 -5
  351. package/templates/inventory/app/movements/movements-view.tsx +32 -10
  352. package/templates/inventory/app/movements/page.tsx +2 -1
  353. package/templates/inventory/app/page.tsx +4 -6
  354. package/templates/inventory/app/products/[id]/page.tsx +2 -1
  355. package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
  356. package/templates/inventory/app/products-view.tsx +74 -33
  357. package/templates/inventory/app/workspace.tsx +34 -16
  358. package/templates/inventory/app.ts +10 -2
  359. package/templates/inventory/components/app-shell.tsx +94 -0
  360. package/templates/inventory/components/auth-form.tsx +35 -5
  361. package/templates/inventory/components/avatar.tsx +5 -1
  362. package/templates/inventory/components/brand-mark.tsx +25 -0
  363. package/templates/inventory/components/category-tile.tsx +49 -0
  364. package/templates/inventory/components/command-palette.tsx +17 -17
  365. package/templates/inventory/components/data-table.tsx +97 -36
  366. package/templates/inventory/components/filter-tabs.tsx +101 -0
  367. package/templates/inventory/components/nav.tsx +14 -0
  368. package/templates/inventory/components/page-header.tsx +43 -7
  369. package/templates/inventory/components/require-auth.tsx +24 -26
  370. package/templates/inventory/components/sidebar.tsx +54 -48
  371. package/templates/inventory/components/stock-level.tsx +30 -14
  372. package/templates/inventory/components/summary-bar.tsx +8 -15
  373. package/templates/inventory/components/ui/button.tsx +5 -4
  374. package/templates/inventory/components/ui/dialog.tsx +1 -1
  375. package/templates/inventory/components/ui/input.tsx +1 -1
  376. package/templates/inventory/components/ui/select.tsx +1 -1
  377. package/templates/inventory/components/ui/textarea.tsx +1 -1
  378. package/templates/inventory/functions/seedWorkspace.ts +18 -8
  379. package/templates/inventory/lib/brand.ts +9 -0
  380. package/templates/inventory/lib/demo-team.ts +51 -0
  381. package/templates/inventory/lib/search.ts +4 -3
  382. package/templates/inventory/lib/seed.ts +29 -2
  383. package/templates/inventory/lib/stock.ts +32 -0
  384. package/templates/inventory/public/favicon.ico +0 -0
  385. package/templates/inventory/tests/demo-team.test.ts +18 -0
  386. package/templates/inventory/tests/stock.test.ts +29 -0
  387. package/templates/invoices/README.md +16 -0
  388. package/templates/invoices/app/apple-icon.png +0 -0
  389. package/templates/invoices/app/clients/clients-view.tsx +18 -6
  390. package/templates/invoices/app/clients/page.tsx +2 -1
  391. package/templates/invoices/app/globals.css +29 -14
  392. package/templates/invoices/app/icon.svg +1 -0
  393. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
  394. package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
  395. package/templates/invoices/app/invoices-view.tsx +81 -32
  396. package/templates/invoices/app/layout.tsx +7 -5
  397. package/templates/invoices/app/page.tsx +4 -6
  398. package/templates/invoices/app/workspace.tsx +26 -16
  399. package/templates/invoices/app.ts +2 -2
  400. package/templates/invoices/components/app-shell.tsx +94 -0
  401. package/templates/invoices/components/auth-form.tsx +35 -5
  402. package/templates/invoices/components/avatar.tsx +5 -1
  403. package/templates/invoices/components/brand-mark.tsx +26 -0
  404. package/templates/invoices/components/command-palette.tsx +17 -17
  405. package/templates/invoices/components/data-table.tsx +97 -36
  406. package/templates/invoices/components/filter-tabs.tsx +101 -0
  407. package/templates/invoices/components/line-items.tsx +19 -14
  408. package/templates/invoices/components/nav.tsx +14 -0
  409. package/templates/invoices/components/page-header.tsx +43 -7
  410. package/templates/invoices/components/require-auth.tsx +24 -26
  411. package/templates/invoices/components/sidebar.tsx +54 -48
  412. package/templates/invoices/components/status-badge.tsx +7 -7
  413. package/templates/invoices/components/summary-bar.tsx +5 -5
  414. package/templates/invoices/components/ui/button.tsx +5 -4
  415. package/templates/invoices/components/ui/dialog.tsx +1 -1
  416. package/templates/invoices/components/ui/input.tsx +1 -1
  417. package/templates/invoices/components/ui/select.tsx +1 -1
  418. package/templates/invoices/components/ui/textarea.tsx +1 -1
  419. package/templates/invoices/functions/recordPayment.ts +3 -0
  420. package/templates/invoices/lib/billing.ts +40 -0
  421. package/templates/invoices/lib/brand.ts +9 -0
  422. package/templates/invoices/lib/search.ts +4 -3
  423. package/templates/invoices/public/favicon.ico +0 -0
  424. package/templates/invoices/tests/billing.test.ts +27 -0
  425. package/templates/ios/consumer/apps/ios/Package.swift +4 -3
  426. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
  427. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
  428. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
  429. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
  430. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
  431. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
  432. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
  433. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
  434. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
  435. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
  436. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
  437. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
  438. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
  439. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
  440. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
  441. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
  442. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
  443. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
  444. package/templates/ios/consumer/apps/ios/project.yml +13 -9
  445. package/templates/local-service/.env.example +7 -0
  446. package/templates/local-service/README.md +26 -2
  447. package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
  448. package/templates/local-service/app/(marketing)/page.tsx +66 -42
  449. package/templates/local-service/app/auth-form.tsx +8 -8
  450. package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
  451. package/templates/local-service/app/dashboard/page.tsx +14 -14
  452. package/templates/local-service/app/icon.svg +2 -0
  453. package/templates/local-service/app/login/page.tsx +15 -15
  454. package/templates/local-service/functions/seedDemo.ts +49 -0
  455. package/templates/local-service/lib/demo.ts +135 -0
  456. package/templates/local-service/lib/hours.ts +44 -0
  457. package/templates/local-service/lib/site.config.ts +8 -3
  458. package/templates/local-service/lib/slots.ts +3 -0
  459. package/templates/local-service/public/favicon.ico +0 -0
  460. package/templates/local-service/tests/booking.test.ts +104 -0
  461. package/templates/marketplace/README.md +23 -15
  462. package/templates/marketplace/app/globals.css +172 -114
  463. package/templates/marketplace/app/layout.tsx +75 -30
  464. package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
  465. package/templates/marketplace/app/me/page.tsx +4 -4
  466. package/templates/marketplace/app/page.tsx +221 -298
  467. package/templates/marketplace/app/sell/page.tsx +8 -11
  468. package/templates/marketplace/app.ts +32 -7
  469. package/templates/marketplace/client/ActivityFeed.tsx +311 -0
  470. package/templates/marketplace/client/AuthNav.tsx +24 -16
  471. package/templates/marketplace/client/ListingCard.tsx +118 -0
  472. package/templates/marketplace/client/LiveGrid.tsx +92 -0
  473. package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
  474. package/templates/marketplace/client/LoginCard.tsx +114 -136
  475. package/templates/marketplace/client/MarketProvider.tsx +3 -5
  476. package/templates/marketplace/client/MyMarket.tsx +371 -185
  477. package/templates/marketplace/client/OfferPanel.tsx +421 -287
  478. package/templates/marketplace/client/SellForm.tsx +271 -192
  479. package/templates/marketplace/client/ThemeToggle.tsx +1 -0
  480. package/templates/marketplace/client/WatchButton.tsx +16 -16
  481. package/templates/marketplace/client/market.ts +61 -14
  482. package/templates/marketplace/functions/seedMarket.ts +143 -36
  483. package/templates/marketplace/functions/seedOffers.ts +101 -0
  484. package/templates/marketplace/lib/catalog.ts +72 -1
  485. package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
  486. package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
  487. package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
  488. package/templates/marketplace/public/images/listings/duffel.webp +0 -0
  489. package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
  490. package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
  491. package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
  492. package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
  493. package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
  494. package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
  495. package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
  496. package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
  497. package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
  498. package/templates/marketplace/public/images/listings/turntable.webp +0 -0
  499. package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
  500. package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
  501. package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
  502. package/templates/marketplace/public/images/listings/watch.webp +0 -0
  503. package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
  504. package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
  505. package/templates/marketplace/tests/example.test.ts +64 -2
  506. package/templates/marketplace/tests/listing-card.test.tsx +61 -0
  507. package/templates/projects/README.md +24 -3
  508. package/templates/projects/app/apple-icon.png +0 -0
  509. package/templates/projects/app/clients/clients-view.tsx +20 -6
  510. package/templates/projects/app/clients/page.tsx +2 -1
  511. package/templates/projects/app/globals.css +29 -14
  512. package/templates/projects/app/icon.svg +1 -0
  513. package/templates/projects/app/layout.tsx +7 -5
  514. package/templates/projects/app/page.tsx +3 -5
  515. package/templates/projects/app/projects/[id]/page.tsx +2 -1
  516. package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
  517. package/templates/projects/app/projects-view.tsx +105 -52
  518. package/templates/projects/app/workspace.tsx +41 -20
  519. package/templates/projects/app.ts +2 -2
  520. package/templates/projects/components/app-shell.tsx +94 -0
  521. package/templates/projects/components/auth-form.tsx +35 -5
  522. package/templates/projects/components/avatar.tsx +5 -1
  523. package/templates/projects/components/board-skeleton.tsx +3 -3
  524. package/templates/projects/components/brand-mark.tsx +25 -0
  525. package/templates/projects/components/command-palette.tsx +17 -17
  526. package/templates/projects/components/data-table.tsx +97 -36
  527. package/templates/projects/components/filter-tabs.tsx +101 -0
  528. package/templates/projects/components/meter.tsx +48 -0
  529. package/templates/projects/components/nav.tsx +14 -0
  530. package/templates/projects/components/page-header.tsx +43 -7
  531. package/templates/projects/components/require-auth.tsx +24 -26
  532. package/templates/projects/components/sidebar.tsx +54 -48
  533. package/templates/projects/components/summary-bar.tsx +49 -0
  534. package/templates/projects/components/task-board.tsx +10 -9
  535. package/templates/projects/components/time-dialog.tsx +29 -5
  536. package/templates/projects/components/ui/button.tsx +5 -4
  537. package/templates/projects/components/ui/dialog.tsx +1 -1
  538. package/templates/projects/components/ui/input.tsx +1 -1
  539. package/templates/projects/components/ui/select.tsx +1 -1
  540. package/templates/projects/components/ui/textarea.tsx +1 -1
  541. package/templates/projects/functions/seedWorkspace.ts +26 -12
  542. package/templates/projects/lib/brand.ts +9 -0
  543. package/templates/projects/lib/demo-team.ts +51 -0
  544. package/templates/projects/lib/search.ts +4 -3
  545. package/templates/projects/lib/seed.ts +35 -1
  546. package/templates/projects/lib/work.ts +63 -0
  547. package/templates/projects/public/favicon.ico +0 -0
  548. package/templates/projects/tests/demo-team.test.ts +18 -0
  549. package/templates/projects/tests/work.test.ts +65 -0
  550. package/templates/restaurant/.env.example +7 -0
  551. package/templates/restaurant/README.md +29 -3
  552. package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
  553. package/templates/restaurant/app/(marketing)/page.tsx +47 -23
  554. package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
  555. package/templates/restaurant/app/auth-form.tsx +8 -8
  556. package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
  557. package/templates/restaurant/app/dashboard/page.tsx +14 -14
  558. package/templates/restaurant/app/icon.svg +4 -0
  559. package/templates/restaurant/app/login/page.tsx +15 -15
  560. package/templates/restaurant/functions/seedDemo.ts +44 -0
  561. package/templates/restaurant/lib/demo.ts +144 -0
  562. package/templates/restaurant/lib/hours.ts +45 -0
  563. package/templates/restaurant/lib/site.config.ts +3 -3
  564. package/templates/restaurant/public/favicon.ico +0 -0
  565. package/templates/restaurant/tests/reservations.test.ts +67 -0
  566. package/templates/saas/README.md +54 -35
  567. package/templates/saas/app/(auth)/auth-form.tsx +25 -23
  568. package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
  569. package/templates/saas/app/(auth)/layout.tsx +27 -33
  570. package/templates/saas/app/(auth)/login/page.tsx +1 -1
  571. package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
  572. package/templates/saas/app/(auth)/signup/page.tsx +1 -1
  573. package/templates/saas/app/(marketing)/layout.tsx +127 -253
  574. package/templates/saas/app/(marketing)/page.tsx +133 -372
  575. package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
  576. package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
  577. package/templates/saas/app/dashboard/board-client.tsx +416 -0
  578. package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
  579. package/templates/saas/app/dashboard/layout.tsx +27 -13
  580. package/templates/saas/app/dashboard/overview-client.tsx +332 -0
  581. package/templates/saas/app/dashboard/page.tsx +29 -35
  582. package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
  583. package/templates/saas/app/dashboard/projects/page.tsx +11 -7
  584. package/templates/saas/app/dashboard/projects-client.tsx +398 -0
  585. package/templates/saas/app/globals.css +5 -4
  586. package/templates/saas/app/icon.svg +1 -0
  587. package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
  588. package/templates/saas/app/onboarding/page.tsx +6 -2
  589. package/templates/saas/app.ts +68 -13
  590. package/templates/saas/components/dashboard-shell.tsx +212 -132
  591. package/templates/saas/components/marketing.tsx +137 -274
  592. package/templates/saas/components/pricing-table.tsx +12 -11
  593. package/templates/saas/components/setup-checklist.tsx +31 -29
  594. package/templates/saas/components/task-ui.tsx +199 -0
  595. package/templates/saas/functions/createProject.ts +7 -13
  596. package/templates/saas/functions/createTask.ts +67 -0
  597. package/templates/saas/functions/deleteProject.ts +17 -0
  598. package/templates/saas/functions/deleteTask.ts +14 -0
  599. package/templates/saas/functions/seedWorkspace.ts +46 -0
  600. package/templates/saas/functions/setProjectStatus.ts +41 -0
  601. package/templates/saas/functions/updateProfile.ts +13 -0
  602. package/templates/saas/functions/updateProject.ts +4 -5
  603. package/templates/saas/functions/updateTask.ts +88 -0
  604. package/templates/saas/lib/members.ts +53 -0
  605. package/templates/saas/lib/names.ts +53 -0
  606. package/templates/saas/lib/plans.ts +11 -0
  607. package/templates/saas/lib/sample-workspace.ts +117 -0
  608. package/templates/saas/lib/site.config.ts +200 -463
  609. package/templates/saas/lib/tasks.ts +101 -0
  610. package/templates/saas/lib/use-live.ts +25 -0
  611. package/templates/saas/public/favicon.ico +0 -0
  612. package/templates/saas/public/screenshots/board.png +0 -0
  613. package/templates/saas/public/screenshots/members.png +0 -0
  614. package/templates/saas/public/screenshots/overview.png +0 -0
  615. package/templates/saas/public/screenshots/projects.png +0 -0
  616. package/templates/saas/scripts/capture-screenshots.mjs +156 -0
  617. package/templates/saas/tests/names.test.ts +35 -0
  618. package/templates/saas/tests/plans.test.ts +12 -0
  619. package/templates/saas/tests/sample-workspace.test.ts +46 -0
  620. package/templates/saas/tests/tasks.test.ts +81 -0
  621. package/templates/shop/README.md +25 -13
  622. package/templates/shop/app/(marketing)/layout.tsx +94 -67
  623. package/templates/shop/app/(marketing)/not-found.tsx +13 -12
  624. package/templates/shop/app/(marketing)/page.tsx +120 -59
  625. package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
  626. package/templates/shop/app/(marketing)/success/page.tsx +75 -22
  627. package/templates/shop/app/globals.css +94 -1
  628. package/templates/shop/app/layout.tsx +4 -4
  629. package/templates/shop/app/login/page.tsx +4 -4
  630. package/templates/shop/app.ts +18 -6
  631. package/templates/shop/components/cart-button.tsx +33 -0
  632. package/templates/shop/components/marketing.tsx +17 -87
  633. package/templates/shop/components/shelf-count.tsx +36 -0
  634. package/templates/shop/functions/checkout.ts +17 -1
  635. package/templates/shop/lib/cart-store.ts +57 -0
  636. package/templates/shop/lib/shelf-store.ts +35 -0
  637. package/templates/shop/lib/shop.ts +27 -0
  638. package/templates/shop/lib/site.config.ts +108 -98
  639. package/templates/shop/public/images/hero.jpg +0 -0
  640. package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
  641. package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
  642. package/templates/shop/public/images/products/linen.jpg +0 -0
  643. package/templates/shop/public/images/products/sea-salt.jpg +0 -0
  644. package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
  645. package/templates/shop/tests/shop.test.ts +64 -0
  646. package/templates/waitlist/.env.example +7 -0
  647. package/templates/waitlist/README.md +29 -13
  648. package/templates/waitlist/app/(marketing)/page.tsx +9 -7
  649. package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
  650. package/templates/waitlist/app/auth-form.tsx +8 -8
  651. package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
  652. package/templates/waitlist/app/dashboard/page.tsx +17 -16
  653. package/templates/waitlist/app/icon.svg +2 -0
  654. package/templates/waitlist/app/login/page.tsx +14 -14
  655. package/templates/waitlist/app.ts +6 -6
  656. package/templates/waitlist/functions/joinWaitlist.ts +6 -18
  657. package/templates/waitlist/functions/seedDemo.ts +32 -0
  658. package/templates/waitlist/lib/demo.ts +94 -0
  659. package/templates/waitlist/lib/launch.ts +19 -0
  660. package/templates/waitlist/lib/site.config.ts +14 -9
  661. package/templates/waitlist/lib/stats.ts +35 -0
  662. package/templates/waitlist/public/favicon.ico +0 -0
  663. package/templates/waitlist/tests/counter.test.ts +107 -0
  664. package/templates/waitlist/tests/live-count.test.tsx +48 -0
  665. package/templates/ai-chat/tests/example.test.ts +0 -12
  666. package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
  667. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
  668. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
  669. package/templates/backend/consumer/apps/api/schema.ts +0 -143
  670. package/templates/chat/functions/_keep.ts +0 -13
  671. package/templates/chat/tests/example.test.ts +0 -12
  672. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
  673. package/templates/marketplace/client/LiveTicker.tsx +0 -116
  674. package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
  675. package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
@@ -1,33 +1,86 @@
1
1
  import React from "react";
2
- import { Link, type Metadata } from "@pylonsync/react";
3
- import { WRAP } from "@/components/marketing";
2
+ import { Link, type Metadata, type PageProps } from "@pylonsync/react";
3
+ import { Check } from "lucide-react";
4
+ import { WRAP, BUTTON_DARK } from "@/components/marketing";
4
5
  import { siteConfig } from "@/lib/site.config";
5
6
 
6
7
  export const metadata: Metadata = {
7
- title: `Thank you — ${siteConfig.brand.name}`,
8
+ title: `Thank you | ${siteConfig.brand.name}`,
8
9
  description: "Your order is confirmed.",
9
10
  };
10
11
 
11
- // `app/success/page.tsx` → `/success`. Where Stripe sends the shopper after a
12
- // completed Checkout (the `successUrl` passed to the `checkout` action). The
13
- // order is settled server-side by the signed `stripeWebhook` — this page is
14
- // just the friendly landing, so it doesn't need to read the session id Stripe
15
- // appends. Rendered with the normal marketing chrome (nav + footer).
16
- export default function CheckoutSuccess() {
12
+ // `app/(marketing)/success/page.tsx` → `/success`. Two ways in:
13
+ // • Stripe sends the shopper here after a completed Checkout (the
14
+ // `successUrl` passed to the `checkout` action). The signed `stripeWebhook`
15
+ // settles the order, so this page doesn't read the session id Stripe appends.
16
+ // • With no Stripe key, the cart sends the shopper here with `?order=reserved`
17
+ // (plus `&sold=` naming any line that sold out during checkout).
18
+ // The page shows no order details: Order rows hold PII and are owner-only.
19
+ export default function CheckoutSuccess({ searchParams }: PageProps) {
20
+ const reserved = searchParams.order === "reserved";
21
+ const sold = (searchParams.sold ?? "").split(",").filter(Boolean);
22
+ const { checkout, brand } = siteConfig;
23
+
24
+ const steps = reserved
25
+ ? ["Order reserved", "Payment link sent by email", "Packed and shipped from Dallas"]
26
+ : ["Payment received", "Packed in the studio", "Shipped from Dallas"];
27
+
17
28
  return (
18
- <section className={`${WRAP} flex min-h-[60vh] flex-col items-center justify-center py-20 text-center`}>
19
- <div className="flex size-12 items-center justify-center rounded-full bg-brand text-xl text-white">✓</div>
20
- <h1 className="mt-5 text-2xl font-semibold tracking-tight text-zinc-900">Thank you for your order</h1>
21
- <p className="mx-auto mt-3 max-w-md text-[15px] leading-relaxed text-zinc-500">
22
- Your payment went through and we&apos;ve emailed your receipt. We&apos;ll be in touch when it ships —
23
- everything is hand-made, so give us a couple of business days.
24
- </p>
25
- <Link
26
- href="/#shop"
27
- className="mt-7 inline-flex h-11 items-center rounded-full bg-brand px-6 text-sm font-medium text-white transition-opacity hover:opacity-90"
28
- >
29
- Keep shopping
30
- </Link>
29
+ <section className={`${WRAP} grid min-h-[72vh] items-center gap-14 py-16 md:grid-cols-[1.4fr_1fr] md:gap-20 md:py-20`}>
30
+ <div>
31
+ <h1 className="rise font-display text-[3rem] font-light leading-[1] tracking-[-0.03em] text-ink sm:text-[4.25rem]">
32
+ {reserved ? "Your order is reserved." : "Thank you for your order."}
33
+ </h1>
34
+ <p
35
+ className="rise mt-6 max-w-[46ch] text-[17px] leading-[1.65] text-ink-2"
36
+ style={{ animationDelay: "100ms" }}
37
+ >
38
+ {reserved ? checkout.reservedMessage : checkout.paidMessage}
39
+ </p>
40
+ {sold.length > 0 ? (
41
+ <p className="mt-4 max-w-[46ch] text-[14.5px] leading-relaxed text-brand">
42
+ {sold.join(", ")} sold out before we could hold {sold.length === 1 ? "it" : "them"}, so{" "}
43
+ {sold.length === 1 ? "it is" : "they are"} not in this order.
44
+ </p>
45
+ ) : null}
46
+
47
+ <ol className="rise mt-10 max-w-md border-t border-ink/15" style={{ animationDelay: "200ms" }}>
48
+ {steps.map((step, i) => (
49
+ <li key={step} className="flex items-center gap-4 border-b border-ink/15 py-4 text-[15px]">
50
+ <span
51
+ className={
52
+ "flex size-7 shrink-0 items-center justify-center rounded-full text-[12px] tabular-nums " +
53
+ (i === 0 ? "bg-ink text-warm-white" : "border border-ink/20 text-ink-2")
54
+ }
55
+ >
56
+ {i === 0 ? <Check className="size-3.5" strokeWidth={2} /> : i + 1}
57
+ </span>
58
+ <span className={i === 0 ? "font-medium text-ink" : "text-ink-2"}>{step}</span>
59
+ </li>
60
+ ))}
61
+ </ol>
62
+
63
+ <div className="rise mt-10 flex flex-wrap items-center gap-5" style={{ animationDelay: "300ms" }}>
64
+ <Link href="/#shop" className={BUTTON_DARK}>
65
+ Back to the shop
66
+ </Link>
67
+ <a
68
+ href={`mailto:${brand.email}`}
69
+ className="text-[14px] text-ink-2 underline decoration-ink/20 underline-offset-4 transition-colors hover:text-ink hover:decoration-ink"
70
+ >
71
+ Questions? {brand.email}
72
+ </a>
73
+ </div>
74
+ </div>
75
+
76
+ <div className="relative aspect-[4/5] overflow-hidden bg-stone">
77
+ {/* eslint-disable-next-line @next/next/no-img-element */}
78
+ <img
79
+ src={checkout.image}
80
+ alt=""
81
+ className="settle size-full object-cover"
82
+ />
83
+ </div>
31
84
  </section>
32
85
  );
33
86
  }
@@ -19,6 +19,14 @@
19
19
  --brand: #4f46e5;
20
20
  --brand-soft: #eef2ff;
21
21
  --paper: #fafafa;
22
+ /* Storefront palette: bone page, warm near-black type, stone for image
23
+ wells, and a dark espresso band. */
24
+ --ink: #1f1b17;
25
+ --ink-2: #6d645a;
26
+ --warm-white: #f6f2eb;
27
+ --stone: #e6ded2;
28
+ --espresso: #221d18;
29
+ --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
22
30
  --background: oklch(1 0 0);
23
31
  --foreground: oklch(0.141 0.005 285.823);
24
32
  --card: oklch(1 0 0);
@@ -112,6 +120,12 @@
112
120
  --color-brand: var(--brand);
113
121
  --color-brand-soft: var(--brand-soft);
114
122
  --color-paper: var(--paper);
123
+ --color-ink: var(--ink);
124
+ --color-ink-2: var(--ink-2);
125
+ --color-warm-white: var(--warm-white);
126
+ --color-stone: var(--stone);
127
+ --color-espresso: var(--espresso);
128
+ --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
115
129
  --color-chart-1: var(--chart-1);
116
130
  --color-chart-2: var(--chart-2);
117
131
  --color-chart-3: var(--chart-3);
@@ -141,7 +155,7 @@
141
155
  color: var(--color-foreground);
142
156
  font-family: var(
143
157
  --font-sans,
144
- Inter,
158
+ "DM Sans",
145
159
  ui-sans-serif,
146
160
  system-ui,
147
161
  -apple-system,
@@ -153,3 +167,82 @@
153
167
  cursor: pointer;
154
168
  }
155
169
  }
170
+
171
+ .font-display {
172
+ font-family: var(--font-display, "Newsreader", Georgia, "Times New Roman", serif);
173
+ font-optical-sizing: auto;
174
+ }
175
+
176
+ /* Storefront motion. Every animation uses transform and opacity only, and
177
+ all of it stops under prefers-reduced-motion. */
178
+ @keyframes rise {
179
+ from {
180
+ opacity: 0;
181
+ transform: translateY(18px);
182
+ }
183
+ to {
184
+ opacity: 1;
185
+ transform: none;
186
+ }
187
+ }
188
+ @keyframes settle {
189
+ from {
190
+ transform: scale(1.06);
191
+ }
192
+ to {
193
+ transform: scale(1);
194
+ }
195
+ }
196
+ @keyframes stock-tick {
197
+ 0% {
198
+ background-color: color-mix(in srgb, var(--brand) 22%, transparent);
199
+ }
200
+ 100% {
201
+ background-color: transparent;
202
+ }
203
+ }
204
+ @keyframes ping-soft {
205
+ 0% {
206
+ transform: scale(1);
207
+ opacity: 0.55;
208
+ }
209
+ 80%,
210
+ 100% {
211
+ transform: scale(2.6);
212
+ opacity: 0;
213
+ }
214
+ }
215
+
216
+ .rise {
217
+ animation: rise 1s var(--ease-out-quint) both;
218
+ }
219
+ .settle {
220
+ animation: settle 2.2s var(--ease-out-quint) both;
221
+ }
222
+ .stock-tick {
223
+ animation: stock-tick 1.6s var(--ease-out-quint) both;
224
+ }
225
+ .ping-soft {
226
+ animation: ping-soft 2s cubic-bezier(0.2, 0.6, 0.3, 1) infinite;
227
+ }
228
+
229
+ @media (prefers-reduced-motion: reduce) {
230
+ .rise,
231
+ .settle,
232
+ .stock-tick,
233
+ .ping-soft {
234
+ animation: none;
235
+ }
236
+ }
237
+
238
+ /* Paper grain over the storefront. Fixed and non-interactive, so it never
239
+ repaints on scroll. */
240
+ .grain::after {
241
+ content: "";
242
+ position: fixed;
243
+ inset: 0;
244
+ z-index: 60;
245
+ pointer-events: none;
246
+ opacity: 0.035;
247
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
248
+ }
@@ -31,10 +31,10 @@ export default function RootLayout({ children }: LayoutProps) {
31
31
  <meta charSet="utf-8" />
32
32
  <meta name="viewport" content="width=device-width, initial-scale=1" />
33
33
  {/* No <title> here — each page's exported `metadata` sets it. */}
34
- {/* Inter is declared in app.ts (fonts: [...]) and self-hosted by the
35
- build — the runtime injects @font-face + <link rel=preload> + a
36
- size-adjusted fallback here automatically. No third-party request,
37
- no layout shift; change the family in app.ts. */}
34
+ {/* DM Sans and Newsreader are declared in app.ts (fonts: [...]) and
35
+ self-hosted by the build. The runtime injects @font-face + <link
36
+ rel=preload> + a size-adjusted fallback here automatically. Change
37
+ the families in app.ts. */}
38
38
  {/* Tailwind is compiled by Pylon from app/globals.css and injected here. */}
39
39
  </head>
40
40
  <body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
@@ -18,14 +18,14 @@ 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">
21
+ <div className="flex min-h-screen items-center justify-center bg-warm-white px-6 py-12">
22
+ <div className="w-full max-w-[400px] rounded-2xl border border-ink/10 bg-white p-8">
23
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">
24
+ <span className="font-display flex size-9 items-center justify-center rounded-full bg-ink text-[18px] text-warm-white">
25
25
  {brand.letter}
26
26
  </span>
27
27
  </Link>
28
- <h1 className="mt-5 text-[22px] font-semibold tracking-tight text-zinc-900">
28
+ <h1 className="font-display mt-5 text-[26px] font-normal tracking-[-0.01em] text-ink">
29
29
  {brand.name} dashboard
30
30
  </h1>
31
31
  <p className="mt-1 text-[13px] text-zinc-500">
@@ -46,7 +46,7 @@ const Product = entity(
46
46
  name: field.string(),
47
47
  priceCents: field.int(),
48
48
  description: field.string().optional(),
49
- image: field.string().optional(), // emoji or image URL
49
+ image: field.string().optional(), // photo URL or /public path
50
50
  stock: field.int().default(0),
51
51
  },
52
52
  { indexes: [{ name: "by_slug", fields: ["slug"], unique: true }] },
@@ -133,15 +133,27 @@ const manifest = buildManifest({
133
133
  actions: fns.actions,
134
134
  policies: [productPolicy, orderPolicy, userPolicy],
135
135
  auth: auth(),
136
- // Self-hosted Inter (next/font parity): the build fetches the woff2, serves it
136
+ // Self-hosted fonts (next/font parity): the build fetches the woff2, serves it
137
137
  // same-origin (no third-party request, no FOUT), preloads it, and synthesizes a
138
- // size-adjusted fallback face so there's no layout shift. globals.css reads it
139
- // via `var(--font-sans, …)`; layout.tsx carries no font <link>.
138
+ // size-adjusted fallback face so there's no layout shift. globals.css reads
139
+ // them via `var(--font-sans, …)` and `var(--font-display, …)`; layout.tsx
140
+ // carries no font <link>.
140
141
  fonts: [
142
+ // Body text, labels, buttons, prices.
141
143
  font({
142
- family: "Inter",
144
+ family: "DM Sans",
143
145
  variable: "--font-sans",
144
- weights: ["400", "500", "600", "700"],
146
+ weights: ["400", "500", "600"],
147
+ subsets: ["latin"],
148
+ display: "swap",
149
+ preload: true,
150
+ }),
151
+ // The serif for the brand name, headings and product names.
152
+ font({
153
+ family: "Newsreader",
154
+ variable: "--font-display",
155
+ weights: ["300", "400", "500"],
156
+ styles: ["normal", "italic"],
145
157
  subsets: ["latin"],
146
158
  display: "swap",
147
159
  preload: true,
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { ShoppingBag } from "lucide-react";
5
+ import { cart, countItems, useCart } from "@/lib/cart-store";
6
+
7
+ // The nav cart button. Reads the shared cart store so the count matches the
8
+ // grid, and opens the cart panel rendered by the shop island. The count
9
+ // re-mounts on change, which replays its pop-in animation.
10
+ export function CartButton() {
11
+ const { lines } = useCart();
12
+ const count = countItems(lines);
13
+ return (
14
+ <button
15
+ type="button"
16
+ onClick={cart.open}
17
+ aria-label={count === 1 ? "Cart, 1 item" : `Cart, ${count} items`}
18
+ className="inline-flex h-10 items-center gap-2 rounded-full pr-1 pl-3 text-[14px] text-ink transition-colors duration-300 hover:bg-ink/[0.05]"
19
+ >
20
+ <ShoppingBag className="size-[18px]" strokeWidth={1.25} />
21
+ <span className="hidden sm:inline">Cart</span>
22
+ <span
23
+ key={count}
24
+ className={
25
+ "inline-flex h-6 min-w-6 items-center justify-center rounded-full px-1.5 text-[12px] font-medium tabular-nums transition-colors duration-300 " +
26
+ (count > 0 ? "animate-in zoom-in-50 bg-ink text-warm-white duration-300" : "bg-ink/[0.07] text-ink/60")
27
+ }
28
+ >
29
+ {count}
30
+ </span>
31
+ </button>
32
+ );
33
+ }
@@ -1,96 +1,26 @@
1
1
  import React from "react";
2
2
 
3
- // Reusable presentational pieces for the landing page. All server-rendered —
4
- // no client JS. Restyle here and the whole page follows. The brand accent
5
- // (`text-brand`, `bg-brand-soft`) comes from CSS vars set on <html> in
6
- // app/layout.tsx, which read lib/site.config.ts — so re-theming is one edit.
3
+ // Server-rendered layout helpers for the storefront pages. Restyle here and
4
+ // every page follows. The brand accent (`text-brand`, `bg-brand`) comes from
5
+ // CSS vars set on <html> in app/layout.tsx, which read lib/site.config.ts.
7
6
 
8
- // Shared container: a contained, centered column.
9
- export const WRAP = "mx-auto w-full max-w-3xl px-6";
7
+ // Shared container. The storefront runs wide so product photos stay large.
8
+ export const WRAP = "mx-auto w-full max-w-[1360px] px-5 sm:px-8 lg:px-12";
10
9
 
11
- export function Eyebrow({ children }: { children: React.ReactNode }) {
12
- return (
13
- <p className="font-mono text-[11px] font-semibold uppercase tracking-[0.14em] text-brand">
14
- {children}
15
- </p>
16
- );
17
- }
10
+ // Pill buttons. `BUTTON_DARK` is the primary action; `BUTTON_LINE` sits beside it.
11
+ export const BUTTON_DARK =
12
+ "inline-flex h-12 items-center justify-center gap-2 rounded-full bg-ink px-7 text-[14.5px] font-medium text-warm-white transition-[background-color,transform] duration-300 ease-out-quint hover:bg-brand active:scale-[0.98]";
13
+ export const BUTTON_LINE =
14
+ "inline-flex h-12 items-center justify-center gap-2 rounded-full border border-ink/25 px-7 text-[14.5px] font-medium text-ink transition-[background-color,border-color,transform] duration-300 ease-out-quint hover:border-ink hover:bg-ink/[0.04] active:scale-[0.98]";
18
15
 
19
- // "New / Coming soon"-style pill for the hero.
20
- export function Badge({ children }: { children: React.ReactNode }) {
16
+ // A section heading in the display serif.
17
+ export function SectionHeading({ id, children }: { id?: string; children: React.ReactNode }) {
21
18
  return (
22
- <span className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white py-1 pl-1.5 pr-3 text-[13px] text-zinc-600 shadow-sm">
23
- <span className="inline-block size-1.5 rounded-full bg-brand" />
19
+ <h2
20
+ id={id}
21
+ className="font-display text-[2.5rem] font-light leading-[1.02] tracking-[-0.02em] text-balance sm:text-[3.5rem]"
22
+ >
24
23
  {children}
25
- </span>
26
- );
27
- }
28
-
29
- export function Divider() {
30
- return (
31
- <div className={WRAP}>
32
- <div className="border-t border-zinc-200/70" />
33
- </div>
34
- );
35
- }
36
-
37
- export function SectionHead({
38
- eyebrow,
39
- title,
40
- body,
41
- }: {
42
- eyebrow: string;
43
- title: string;
44
- body?: string;
45
- }) {
46
- return (
47
- <div>
48
- <Eyebrow>{eyebrow}</Eyebrow>
49
- <h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
50
- {title}
51
- </h2>
52
- {body ? (
53
- <p className="mt-4 max-w-xl text-[15px] leading-relaxed text-zinc-500">
54
- {body}
55
- </p>
56
- ) : null}
57
- </div>
24
+ </h2>
58
25
  );
59
26
  }
60
-
61
- // A grid of value props — icon + title + body.
62
- export function FeatureGrid({
63
- items,
64
- }: {
65
- items: { title: string; body: string; icon?: string }[];
66
- }) {
67
- return (
68
- <div className="grid gap-6 sm:grid-cols-3">
69
- {items.map((f) => (
70
- <div key={f.title}>
71
- {f.icon ? (
72
- <span className="flex size-9 items-center justify-center rounded-lg bg-brand-soft text-brand">
73
- {f.icon}
74
- </span>
75
- ) : null}
76
- <h3 className="mt-4 text-[15px] font-semibold text-zinc-900">
77
- {f.title}
78
- </h3>
79
- <p className="mt-2 text-[14px] leading-relaxed text-zinc-500">
80
- {f.body}
81
- </p>
82
- </div>
83
- ))}
84
- </div>
85
- );
86
- }
87
-
88
- // Initials for testimonial avatars, so the cards look finished without a photo.
89
- export function initials(name: string) {
90
- return name
91
- .split(/\s+/)
92
- .map((w) => w[0])
93
- .join("")
94
- .slice(0, 2)
95
- .toUpperCase();
96
- }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { useShelf } from "@/lib/shelf-store";
5
+
6
+ // The live stock line in the hero. The product grid publishes shelf totals to
7
+ // lib/shelf-store.ts from its live Product query, so this count drops for
8
+ // every open page when someone checks out. Renders a fixed-width placeholder
9
+ // until the first totals arrive, so the hero does not shift.
10
+ export function ShelfCount() {
11
+ const shelf = useShelf();
12
+ return (
13
+ <p className="flex items-center gap-3 text-[13.5px] text-ink/75" aria-live="polite">
14
+ <span className="relative flex size-2">
15
+ <span className="ping-soft absolute inset-0 rounded-full bg-[#5d7a4a]" />
16
+ <span className="relative size-2 rounded-full bg-[#5d7a4a]" />
17
+ </span>
18
+ {shelf ? (
19
+ <span>
20
+ <span key={shelf.units} className="stock-tick rounded px-0.5 font-medium tabular-nums text-ink">
21
+ {shelf.units}
22
+ </span>{" "}
23
+ pieces on the studio shelf
24
+ {shelf.low > 0 ? (
25
+ <>
26
+ <span className="mx-2 text-ink/30">/</span>
27
+ <span className="tabular-nums">{shelf.low}</span> running low
28
+ </>
29
+ ) : null}
30
+ </span>
31
+ ) : (
32
+ <span className="inline-block h-4 w-56 rounded bg-ink/[0.06]" />
33
+ )}
34
+ </p>
35
+ );
36
+ }
@@ -1,6 +1,7 @@
1
1
  import { action, v } from "@pylonsync/functions";
2
2
  import { stripeRequest, assertSafeRedirectUrl } from "@pylonsync/stripe";
3
- import type { CheckoutResult } from "../lib/shop";
3
+ import { shippingCents, type CheckoutResult } from "../lib/shop";
4
+ import { siteConfig } from "../lib/site.config";
4
5
 
5
6
  const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6
7
  const CURRENCY = "usd";
@@ -92,6 +93,11 @@ export default action<
92
93
  // If the session can't be created (bad key, network error, Stripe down) we
93
94
  // MUST release the held stock — there's no session that would ever expire to
94
95
  // do it for us, so without this the units would be stranded off the shelf.
96
+ // Shipping follows the same terms the cart shows: a flat rate, free at or
97
+ // above the threshold. Prices come from the held lines, not the client.
98
+ const subtotal = held.lines.reduce((s, l) => s + l.unitPriceCents * l.qty, 0);
99
+ const shipping = shippingCents(subtotal, siteConfig.checkout.shipping);
100
+
95
101
  let session: { id: string; url: string } | undefined;
96
102
  try {
97
103
  session = await stripeRequest<{ id: string; url: string }>(
@@ -114,6 +120,16 @@ export default action<
114
120
  product_data: { name: l.name },
115
121
  },
116
122
  })),
123
+ shipping_address_collection: { allowed_countries: ["US"] },
124
+ shipping_options: [
125
+ {
126
+ shipping_rate_data: {
127
+ type: "fixed_amount",
128
+ display_name: shipping === 0 ? "Free shipping" : "Standard shipping",
129
+ fixed_amount: { amount: shipping, currency: CURRENCY },
130
+ },
131
+ },
132
+ ],
117
133
  },
118
134
  );
119
135
  } catch {
@@ -0,0 +1,57 @@
1
+ // A tiny client-side cart store shared by the product grid (app/(marketing)/
2
+ // shop-client.tsx) and the nav cart button (components/cart-button.tsx). The
3
+ // two live in different client islands, so React state can't be lifted between
4
+ // them; this module holds the cart and both islands subscribe with
5
+ // `useSyncExternalStore`. Cart contents are slug → qty; the grid clamps them to
6
+ // live stock when it renders.
7
+
8
+ import { useSyncExternalStore } from "react";
9
+
10
+ export type CartLines = Record<string, number>;
11
+
12
+ type CartState = { lines: CartLines; open: boolean };
13
+
14
+ let state: CartState = { lines: {}, open: false };
15
+ const listeners = new Set<() => void>();
16
+
17
+ function emit() {
18
+ listeners.forEach((l) => l());
19
+ }
20
+
21
+ function subscribe(listener: () => void) {
22
+ listeners.add(listener);
23
+ return () => listeners.delete(listener);
24
+ }
25
+
26
+ function getSnapshot() {
27
+ return state;
28
+ }
29
+
30
+ // The server render has no cart; every island sees the same empty snapshot.
31
+ const SERVER_STATE: CartState = { lines: {}, open: false };
32
+ function getServerSnapshot() {
33
+ return SERVER_STATE;
34
+ }
35
+
36
+ export function useCart() {
37
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
38
+ }
39
+
40
+ export const cart = {
41
+ setLines(update: (lines: CartLines) => CartLines) {
42
+ state = { ...state, lines: update(state.lines) };
43
+ emit();
44
+ },
45
+ open() {
46
+ state = { ...state, open: true };
47
+ emit();
48
+ },
49
+ close() {
50
+ state = { ...state, open: false };
51
+ emit();
52
+ },
53
+ };
54
+
55
+ export function countItems(lines: CartLines) {
56
+ return Object.values(lines).reduce((s, q) => s + q, 0);
57
+ }
@@ -0,0 +1,35 @@
1
+ // Live shelf totals, shared between client islands. The product grid
2
+ // (app/(marketing)/shop-client.tsx) owns the live Product query and publishes
3
+ // the totals here; the hero's shelf count (components/shelf-count.tsx) reads
4
+ // them. One query and one guest session serve both islands.
5
+
6
+ import { useSyncExternalStore } from "react";
7
+
8
+ export type ShelfTotals = { units: number; low: number; soldOut: number } | null;
9
+
10
+ let totals: ShelfTotals = null;
11
+ const listeners = new Set<() => void>();
12
+
13
+ function subscribe(listener: () => void) {
14
+ listeners.add(listener);
15
+ return () => listeners.delete(listener);
16
+ }
17
+
18
+ export function publishShelf(stocks: number[], lowAt: number) {
19
+ const next = {
20
+ units: stocks.reduce((s, n) => s + Math.max(0, n), 0),
21
+ low: stocks.filter((n) => n > 0 && n <= lowAt).length,
22
+ soldOut: stocks.filter((n) => n <= 0).length,
23
+ };
24
+ if (totals && totals.units === next.units && totals.low === next.low && totals.soldOut === next.soldOut) return;
25
+ totals = next;
26
+ listeners.forEach((l) => l());
27
+ }
28
+
29
+ export function useShelf(): ShelfTotals {
30
+ return useSyncExternalStore(
31
+ subscribe,
32
+ () => totals,
33
+ () => null,
34
+ );
35
+ }
@@ -43,3 +43,30 @@ export type CheckoutResult =
43
43
  export function formatPrice(cents: number): string {
44
44
  return `$${(cents / 100).toFixed(cents % 100 === 0 ? 0 : 2)}`;
45
45
  }
46
+
47
+ // How a product's stock reads on the storefront. "low" at 3 or fewer.
48
+ export const LOW_STOCK_AT = 3;
49
+
50
+ export type StockStatus =
51
+ | { kind: "sold_out"; label: string }
52
+ | { kind: "low"; label: string }
53
+ | { kind: "in_stock"; label: string };
54
+
55
+ export function stockStatus(stock: number): StockStatus {
56
+ if (stock <= 0) return { kind: "sold_out", label: "Sold out" };
57
+ if (stock <= LOW_STOCK_AT) return { kind: "low", label: stock === 1 ? "Last one" : `Only ${stock} left` };
58
+ return { kind: "in_stock", label: `${stock} in stock` };
59
+ }
60
+
61
+ // Shipping terms: a flat rate, free at or above a subtotal threshold. The cart
62
+ // shows these numbers and `checkout` charges them on the Stripe session.
63
+ export type ShippingTerms = { flatCents: number; freeOverCents: number };
64
+
65
+ // Cents still needed to reach free shipping; 0 once the subtotal qualifies.
66
+ export function freeShippingRemaining(subtotalCents: number, terms: ShippingTerms): number {
67
+ return Math.max(0, terms.freeOverCents - subtotalCents);
68
+ }
69
+
70
+ export function shippingCents(subtotalCents: number, terms: ShippingTerms): number {
71
+ return freeShippingRemaining(subtotalCents, terms) === 0 ? 0 : terms.flatCents;
72
+ }