@pylonsync/create-pylon 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (675) hide show
  1. package/bin/create-pylon.js +30 -7
  2. package/package.json +1 -1
  3. package/templates/agency/.env.example +7 -0
  4. package/templates/agency/README.md +28 -3
  5. package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
  6. package/templates/agency/app/(marketing)/page.tsx +3 -3
  7. package/templates/agency/app/(marketing)/work/page.tsx +3 -3
  8. package/templates/agency/app/auth-form.tsx +4 -4
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
  10. package/templates/agency/app/dashboard/page.tsx +14 -16
  11. package/templates/agency/app/icon.svg +3 -0
  12. package/templates/agency/app/login/page.tsx +12 -14
  13. package/templates/agency/components/marketing.tsx +12 -5
  14. package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
  15. package/templates/agency/lib/agency.ts +22 -0
  16. package/templates/agency/lib/demo.ts +118 -0
  17. package/templates/agency/lib/site.config.ts +19 -13
  18. package/templates/agency/public/favicon.ico +0 -0
  19. package/templates/agency/tests/agency.test.ts +57 -0
  20. package/templates/ai-chat/.env.example +13 -27
  21. package/templates/ai-chat/README.md +53 -60
  22. package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
  23. package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
  24. package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
  25. package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
  26. package/templates/ai-chat/app/(chat)/page.tsx +7 -11
  27. package/templates/ai-chat/app/auth-form.tsx +45 -38
  28. package/templates/ai-chat/app/error.tsx +9 -12
  29. package/templates/ai-chat/app/globals.css +464 -102
  30. package/templates/ai-chat/app/layout.tsx +11 -19
  31. package/templates/ai-chat/app/login/page.tsx +39 -24
  32. package/templates/ai-chat/app.ts +118 -43
  33. package/templates/ai-chat/components/chat/composer.tsx +193 -0
  34. package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
  35. package/templates/ai-chat/components/chat/icons.tsx +198 -0
  36. package/templates/ai-chat/components/chat/markdown.tsx +164 -0
  37. package/templates/ai-chat/components/chat/message.tsx +183 -0
  38. package/templates/ai-chat/components/chat/setup.tsx +71 -0
  39. package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
  40. package/templates/ai-chat/functions/_beginTurn.ts +145 -0
  41. package/templates/ai-chat/functions/_finishTurn.ts +55 -0
  42. package/templates/ai-chat/functions/assistantStatus.ts +13 -0
  43. package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
  44. package/templates/ai-chat/functions/createConversation.ts +28 -0
  45. package/templates/ai-chat/functions/deleteConversation.ts +15 -20
  46. package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
  47. package/templates/ai-chat/functions/renameConversation.ts +19 -0
  48. package/templates/ai-chat/functions/respond.ts +88 -0
  49. package/templates/ai-chat/functions/seedExamples.ts +46 -0
  50. package/templates/ai-chat/functions/stopResponse.ts +30 -0
  51. package/templates/ai-chat/lib/brand-icon.ts +7 -0
  52. package/templates/ai-chat/lib/chat.ts +277 -0
  53. package/templates/ai-chat/lib/claim.ts +15 -0
  54. package/templates/ai-chat/lib/examples.ts +179 -0
  55. package/templates/ai-chat/lib/highlight.ts +107 -0
  56. package/templates/ai-chat/lib/markdown.ts +317 -0
  57. package/templates/ai-chat/lib/provider.ts +38 -0
  58. package/templates/ai-chat/lib/site.config.ts +55 -39
  59. package/templates/ai-chat/tests/chat.test.ts +174 -0
  60. package/templates/ai-chat/tests/components.test.tsx +57 -0
  61. package/templates/ai-chat/tests/markdown.test.ts +88 -0
  62. package/templates/ai-chat/tests/provider.test.ts +32 -0
  63. package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
  64. package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
  65. package/templates/backend/consumer/apps/api/app.ts +166 -0
  66. package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
  67. package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
  68. package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
  69. package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
  70. package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
  71. package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
  72. package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
  73. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
  74. package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
  75. package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
  76. package/templates/backend/consumer/apps/api/package.json +8 -2
  77. package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
  78. package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
  79. package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
  80. package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
  81. package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
  82. package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
  83. package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
  84. package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
  85. package/templates/chat/README.md +43 -21
  86. package/templates/chat/app/chat-client.tsx +495 -85
  87. package/templates/chat/app/error.tsx +1 -1
  88. package/templates/chat/app/globals.css +170 -54
  89. package/templates/chat/app/layout.tsx +12 -9
  90. package/templates/chat/app/not-found.tsx +4 -2
  91. package/templates/chat/app/page.tsx +9 -19
  92. package/templates/chat/app.ts +96 -17
  93. package/templates/chat/components/chat/avatar.tsx +45 -0
  94. package/templates/chat/components/chat/composer.tsx +110 -0
  95. package/templates/chat/components/chat/message-list.tsx +285 -0
  96. package/templates/chat/components/chat/sidebar.tsx +190 -0
  97. package/templates/chat/functions/joinChat.ts +83 -0
  98. package/templates/chat/functions/renameMember.ts +25 -0
  99. package/templates/chat/lib/chat.ts +188 -0
  100. package/templates/chat/lib/seed.ts +78 -0
  101. package/templates/chat/tests/chat.test.ts +159 -0
  102. package/templates/chat/tests/composer.test.tsx +54 -0
  103. package/templates/consumer/README.md +3 -3
  104. package/templates/consumer/app/globals.css +10 -3
  105. package/templates/consumer/app/icon.svg +1 -0
  106. package/templates/consumer/components/social/avatar.tsx +25 -11
  107. package/templates/consumer/functions/seedFeed.ts +3 -3
  108. package/templates/consumer/lib/seed.ts +43 -85
  109. package/templates/consumer/public/favicon.ico +0 -0
  110. package/templates/consumer/public/images/posts/arch.jpg +0 -0
  111. package/templates/consumer/public/images/posts/coffee.jpg +0 -0
  112. package/templates/consumer/public/images/posts/dog.jpg +0 -0
  113. package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
  114. package/templates/consumer/public/images/posts/ramen.jpg +0 -0
  115. package/templates/consumer/public/images/posts/run.jpg +0 -0
  116. package/templates/consumer/public/images/posts/sunset.jpg +0 -0
  117. package/templates/consumer/tests/seed.test.ts +39 -0
  118. package/templates/creator/.env.example +7 -0
  119. package/templates/creator/README.md +24 -5
  120. package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
  121. package/templates/creator/app/auth-form.tsx +7 -7
  122. package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
  123. package/templates/creator/app/dashboard/page.tsx +18 -18
  124. package/templates/creator/app/icon.svg +2 -0
  125. package/templates/creator/app/login/page.tsx +14 -14
  126. package/templates/creator/app.ts +4 -4
  127. package/templates/creator/functions/seedDemo.ts +32 -0
  128. package/templates/creator/functions/subscribe.ts +4 -15
  129. package/templates/creator/lib/demo.ts +94 -0
  130. package/templates/creator/lib/site.config.ts +15 -10
  131. package/templates/creator/lib/stats.ts +35 -0
  132. package/templates/creator/public/favicon.ico +0 -0
  133. package/templates/creator/tests/counter.test.ts +92 -0
  134. package/templates/creator/tests/live-count.test.tsx +45 -0
  135. package/templates/crm/README.md +22 -1
  136. package/templates/crm/app/apple-icon.png +0 -0
  137. package/templates/crm/app/companies/companies-view.tsx +32 -14
  138. package/templates/crm/app/companies/page.tsx +2 -1
  139. package/templates/crm/app/contacts/contacts-view.tsx +34 -10
  140. package/templates/crm/app/contacts/page.tsx +2 -1
  141. package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
  142. package/templates/crm/app/deals/[id]/page.tsx +2 -1
  143. package/templates/crm/app/globals.css +29 -14
  144. package/templates/crm/app/icon.svg +1 -0
  145. package/templates/crm/app/layout.tsx +7 -5
  146. package/templates/crm/app/page.tsx +2 -1
  147. package/templates/crm/app/pipeline-view.tsx +5 -5
  148. package/templates/crm/app/workspace.tsx +29 -17
  149. package/templates/crm/app.ts +5 -5
  150. package/templates/crm/components/activity-timeline.tsx +14 -4
  151. package/templates/crm/components/app-shell.tsx +94 -0
  152. package/templates/crm/components/auth-form.tsx +35 -5
  153. package/templates/crm/components/avatar.tsx +5 -1
  154. package/templates/crm/components/board-skeleton.tsx +5 -5
  155. package/templates/crm/components/brand-mark.tsx +25 -0
  156. package/templates/crm/components/command-palette.tsx +17 -17
  157. package/templates/crm/components/data-table.tsx +97 -36
  158. package/templates/crm/components/deal-card.tsx +23 -27
  159. package/templates/crm/components/filter-tabs.tsx +101 -0
  160. package/templates/crm/components/metrics-bar.tsx +6 -6
  161. package/templates/crm/components/nav.tsx +15 -0
  162. package/templates/crm/components/page-header.tsx +43 -7
  163. package/templates/crm/components/pipeline-board.tsx +16 -10
  164. package/templates/crm/components/require-auth.tsx +24 -26
  165. package/templates/crm/components/sidebar.tsx +54 -49
  166. package/templates/crm/components/stage-badge.tsx +4 -2
  167. package/templates/crm/components/ui/button.tsx +5 -4
  168. package/templates/crm/components/ui/dialog.tsx +1 -1
  169. package/templates/crm/components/ui/input.tsx +1 -1
  170. package/templates/crm/components/ui/select.tsx +1 -1
  171. package/templates/crm/components/ui/textarea.tsx +1 -1
  172. package/templates/crm/functions/seedWorkspace.ts +32 -17
  173. package/templates/crm/lib/brand.ts +9 -0
  174. package/templates/crm/lib/demo-team.ts +51 -0
  175. package/templates/crm/lib/pipeline.ts +37 -0
  176. package/templates/crm/lib/search.ts +4 -3
  177. package/templates/crm/lib/seed.ts +53 -8
  178. package/templates/crm/public/favicon.ico +0 -0
  179. package/templates/crm/tests/components.test.tsx +5 -5
  180. package/templates/crm/tests/demo-team.test.ts +18 -0
  181. package/templates/crm/tests/pipeline.test.ts +59 -3
  182. package/templates/directory/.env.example +7 -0
  183. package/templates/directory/README.md +22 -3
  184. package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
  185. package/templates/directory/app/(marketing)/page.tsx +6 -4
  186. package/templates/directory/app/auth-form.tsx +4 -4
  187. package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
  188. package/templates/directory/app/dashboard/page.tsx +16 -14
  189. package/templates/directory/app/icon.svg +4 -0
  190. package/templates/directory/app/login/page.tsx +16 -15
  191. package/templates/directory/functions/seedDemo.ts +43 -0
  192. package/templates/directory/lib/demo.ts +145 -0
  193. package/templates/directory/lib/directory.ts +27 -0
  194. package/templates/directory/lib/site.config.ts +21 -16
  195. package/templates/directory/public/favicon.ico +0 -0
  196. package/templates/directory/tests/directory.test.ts +46 -0
  197. package/templates/expo/consumer/apps/expo/App.tsx +360 -288
  198. package/templates/expo/consumer/apps/expo/app.json +9 -1
  199. package/templates/expo/consumer/apps/expo/package.json +2 -1
  200. package/templates/helpdesk/README.md +21 -0
  201. package/templates/helpdesk/app/apple-icon.png +0 -0
  202. package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
  203. package/templates/helpdesk/app/customers/page.tsx +2 -1
  204. package/templates/helpdesk/app/globals.css +29 -14
  205. package/templates/helpdesk/app/icon.svg +1 -0
  206. package/templates/helpdesk/app/inbox-view.tsx +66 -47
  207. package/templates/helpdesk/app/layout.tsx +7 -5
  208. package/templates/helpdesk/app/page.tsx +4 -7
  209. package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
  210. package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
  211. package/templates/helpdesk/app/workspace.tsx +33 -22
  212. package/templates/helpdesk/app.ts +2 -2
  213. package/templates/helpdesk/components/app-shell.tsx +94 -0
  214. package/templates/helpdesk/components/auth-form.tsx +35 -5
  215. package/templates/helpdesk/components/avatar.tsx +5 -1
  216. package/templates/helpdesk/components/brand-mark.tsx +28 -0
  217. package/templates/helpdesk/components/command-palette.tsx +17 -17
  218. package/templates/helpdesk/components/data-table.tsx +97 -36
  219. package/templates/helpdesk/components/filter-tabs.tsx +101 -0
  220. package/templates/helpdesk/components/nav.tsx +14 -0
  221. package/templates/helpdesk/components/page-header.tsx +43 -7
  222. package/templates/helpdesk/components/priority-badge.tsx +49 -6
  223. package/templates/helpdesk/components/require-auth.tsx +24 -26
  224. package/templates/helpdesk/components/sidebar.tsx +54 -48
  225. package/templates/helpdesk/components/sla-indicator.tsx +5 -8
  226. package/templates/helpdesk/components/thread.tsx +7 -7
  227. package/templates/helpdesk/components/ticket-list.tsx +79 -19
  228. package/templates/helpdesk/components/ui/button.tsx +5 -4
  229. package/templates/helpdesk/components/ui/dialog.tsx +1 -1
  230. package/templates/helpdesk/components/ui/input.tsx +1 -1
  231. package/templates/helpdesk/components/ui/select.tsx +1 -1
  232. package/templates/helpdesk/components/ui/textarea.tsx +1 -1
  233. package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
  234. package/templates/helpdesk/lib/brand.ts +9 -0
  235. package/templates/helpdesk/lib/demo-team.ts +51 -0
  236. package/templates/helpdesk/lib/search.ts +4 -3
  237. package/templates/helpdesk/lib/seed.ts +43 -2
  238. package/templates/helpdesk/lib/tickets.ts +68 -20
  239. package/templates/helpdesk/public/favicon.ico +0 -0
  240. package/templates/helpdesk/tests/components.test.tsx +10 -0
  241. package/templates/helpdesk/tests/demo-team.test.ts +18 -0
  242. package/templates/helpdesk/tests/tickets.test.ts +70 -0
  243. package/templates/imessage/.env.example +49 -0
  244. package/templates/imessage/AGENTS.md +153 -0
  245. package/templates/imessage/CLAUDE.md +7 -0
  246. package/templates/imessage/README.md +187 -0
  247. package/templates/imessage/app/console.tsx +382 -0
  248. package/templates/imessage/app/contacts/page.tsx +13 -0
  249. package/templates/imessage/app/error.tsx +16 -0
  250. package/templates/imessage/app/gate.tsx +31 -0
  251. package/templates/imessage/app/globals.css +183 -0
  252. package/templates/imessage/app/layout.tsx +17 -0
  253. package/templates/imessage/app/login/login-form.tsx +115 -0
  254. package/templates/imessage/app/login/page.tsx +30 -0
  255. package/templates/imessage/app/not-found.tsx +13 -0
  256. package/templates/imessage/app/page.tsx +14 -0
  257. package/templates/imessage/app/robots.ts +6 -0
  258. package/templates/imessage/app/setup/page.tsx +13 -0
  259. package/templates/imessage/app/sign-out.tsx +20 -0
  260. package/templates/imessage/app.ts +299 -0
  261. package/templates/imessage/bunfig +4 -0
  262. package/templates/imessage/components/avatar.tsx +46 -0
  263. package/templates/imessage/components/contact-panel.tsx +263 -0
  264. package/templates/imessage/components/contacts-view.tsx +172 -0
  265. package/templates/imessage/components/conversation-list.tsx +129 -0
  266. package/templates/imessage/components/nav-rail.tsx +67 -0
  267. package/templates/imessage/components/setup-view.tsx +408 -0
  268. package/templates/imessage/components/thread.tsx +229 -0
  269. package/templates/imessage/components/ui/badge.tsx +37 -0
  270. package/templates/imessage/components/ui/button.tsx +56 -0
  271. package/templates/imessage/components/ui/card.tsx +90 -0
  272. package/templates/imessage/components/ui/dialog.tsx +110 -0
  273. package/templates/imessage/components/ui/input.tsx +30 -0
  274. package/templates/imessage/components/ui/label.tsx +21 -0
  275. package/templates/imessage/components/ui/select.tsx +39 -0
  276. package/templates/imessage/components/ui/table.tsx +124 -0
  277. package/templates/imessage/components/ui/textarea.tsx +24 -0
  278. package/templates/imessage/components.json +20 -0
  279. package/templates/imessage/functions/answerNow.ts +34 -0
  280. package/templates/imessage/functions/assistant.ts +96 -0
  281. package/templates/imessage/functions/beginTurn.ts +101 -0
  282. package/templates/imessage/functions/cancelReminder.ts +13 -0
  283. package/templates/imessage/functions/claimOutbound.ts +44 -0
  284. package/templates/imessage/functions/clearDemo.ts +18 -0
  285. package/templates/imessage/functions/deleteNote.ts +11 -0
  286. package/templates/imessage/functions/deliverOutbound.ts +55 -0
  287. package/templates/imessage/functions/finishTurn.ts +84 -0
  288. package/templates/imessage/functions/fireReminder.ts +42 -0
  289. package/templates/imessage/functions/ingestInbound.ts +22 -0
  290. package/templates/imessage/functions/ownerAccount.ts +13 -0
  291. package/templates/imessage/functions/processTurn.ts +74 -0
  292. package/templates/imessage/functions/relayApply.ts +102 -0
  293. package/templates/imessage/functions/relaySync.ts +63 -0
  294. package/templates/imessage/functions/releaseOutbound.ts +18 -0
  295. package/templates/imessage/functions/resolveTurn.ts +25 -0
  296. package/templates/imessage/functions/saveContact.ts +27 -0
  297. package/templates/imessage/functions/seedDemo.ts +70 -0
  298. package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
  299. package/templates/imessage/functions/sendblueWebhook.ts +49 -0
  300. package/templates/imessage/functions/setContactStatus.ts +17 -0
  301. package/templates/imessage/functions/settleOutbound.ts +35 -0
  302. package/templates/imessage/functions/setupStatus.ts +33 -0
  303. package/templates/imessage/functions/sweep.ts +58 -0
  304. package/templates/imessage/functions/toolCancelReminder.ts +17 -0
  305. package/templates/imessage/functions/toolForgetNote.ts +15 -0
  306. package/templates/imessage/functions/toolListNotes.ts +16 -0
  307. package/templates/imessage/functions/toolListReminders.ts +17 -0
  308. package/templates/imessage/functions/toolSaveNote.ts +22 -0
  309. package/templates/imessage/functions/toolSetReminder.ts +55 -0
  310. package/templates/imessage/functions/updateSettings.ts +48 -0
  311. package/templates/imessage/gitignore +11 -0
  312. package/templates/imessage/lib/agent-call.ts +251 -0
  313. package/templates/imessage/lib/assistant.ts +74 -0
  314. package/templates/imessage/lib/chunk.ts +135 -0
  315. package/templates/imessage/lib/demo.ts +68 -0
  316. package/templates/imessage/lib/format.ts +170 -0
  317. package/templates/imessage/lib/gate.ts +104 -0
  318. package/templates/imessage/lib/handles.ts +67 -0
  319. package/templates/imessage/lib/provider.ts +38 -0
  320. package/templates/imessage/lib/relay-protocol.ts +95 -0
  321. package/templates/imessage/lib/reminders.ts +41 -0
  322. package/templates/imessage/lib/secrets.ts +59 -0
  323. package/templates/imessage/lib/sendblue.ts +173 -0
  324. package/templates/imessage/lib/store.ts +407 -0
  325. package/templates/imessage/lib/transport.ts +135 -0
  326. package/templates/imessage/lib/utils.ts +10 -0
  327. package/templates/imessage/package.json +40 -0
  328. package/templates/imessage/relay/.env.example +14 -0
  329. package/templates/imessage/relay/applescript.ts +70 -0
  330. package/templates/imessage/relay/chatdb.ts +165 -0
  331. package/templates/imessage/relay/client.ts +41 -0
  332. package/templates/imessage/relay/main.ts +137 -0
  333. package/templates/imessage/relay/state.ts +35 -0
  334. package/templates/imessage/tests/agent-call.test.ts +112 -0
  335. package/templates/imessage/tests/applescript.test.ts +88 -0
  336. package/templates/imessage/tests/chatdb.test.ts +139 -0
  337. package/templates/imessage/tests/chunk.test.ts +61 -0
  338. package/templates/imessage/tests/fake-ctx.ts +88 -0
  339. package/templates/imessage/tests/ingest.test.ts +141 -0
  340. package/templates/imessage/tests/lib.test.ts +99 -0
  341. package/templates/imessage/tests/review-fixes.test.ts +138 -0
  342. package/templates/imessage/tests/secrets.test.ts +36 -0
  343. package/templates/imessage/tests/sendblue.test.ts +143 -0
  344. package/templates/imessage/tests/setup.ts +5 -0
  345. package/templates/imessage/tsconfig.json +18 -0
  346. package/templates/inventory/README.md +21 -0
  347. package/templates/inventory/app/apple-icon.png +0 -0
  348. package/templates/inventory/app/globals.css +29 -14
  349. package/templates/inventory/app/icon.svg +1 -0
  350. package/templates/inventory/app/layout.tsx +7 -5
  351. package/templates/inventory/app/movements/movements-view.tsx +32 -10
  352. package/templates/inventory/app/movements/page.tsx +2 -1
  353. package/templates/inventory/app/page.tsx +4 -6
  354. package/templates/inventory/app/products/[id]/page.tsx +2 -1
  355. package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
  356. package/templates/inventory/app/products-view.tsx +74 -33
  357. package/templates/inventory/app/workspace.tsx +34 -16
  358. package/templates/inventory/app.ts +10 -2
  359. package/templates/inventory/components/app-shell.tsx +94 -0
  360. package/templates/inventory/components/auth-form.tsx +35 -5
  361. package/templates/inventory/components/avatar.tsx +5 -1
  362. package/templates/inventory/components/brand-mark.tsx +25 -0
  363. package/templates/inventory/components/category-tile.tsx +49 -0
  364. package/templates/inventory/components/command-palette.tsx +17 -17
  365. package/templates/inventory/components/data-table.tsx +97 -36
  366. package/templates/inventory/components/filter-tabs.tsx +101 -0
  367. package/templates/inventory/components/nav.tsx +14 -0
  368. package/templates/inventory/components/page-header.tsx +43 -7
  369. package/templates/inventory/components/require-auth.tsx +24 -26
  370. package/templates/inventory/components/sidebar.tsx +54 -48
  371. package/templates/inventory/components/stock-level.tsx +30 -14
  372. package/templates/inventory/components/summary-bar.tsx +8 -15
  373. package/templates/inventory/components/ui/button.tsx +5 -4
  374. package/templates/inventory/components/ui/dialog.tsx +1 -1
  375. package/templates/inventory/components/ui/input.tsx +1 -1
  376. package/templates/inventory/components/ui/select.tsx +1 -1
  377. package/templates/inventory/components/ui/textarea.tsx +1 -1
  378. package/templates/inventory/functions/seedWorkspace.ts +18 -8
  379. package/templates/inventory/lib/brand.ts +9 -0
  380. package/templates/inventory/lib/demo-team.ts +51 -0
  381. package/templates/inventory/lib/search.ts +4 -3
  382. package/templates/inventory/lib/seed.ts +29 -2
  383. package/templates/inventory/lib/stock.ts +32 -0
  384. package/templates/inventory/public/favicon.ico +0 -0
  385. package/templates/inventory/tests/demo-team.test.ts +18 -0
  386. package/templates/inventory/tests/stock.test.ts +29 -0
  387. package/templates/invoices/README.md +16 -0
  388. package/templates/invoices/app/apple-icon.png +0 -0
  389. package/templates/invoices/app/clients/clients-view.tsx +18 -6
  390. package/templates/invoices/app/clients/page.tsx +2 -1
  391. package/templates/invoices/app/globals.css +29 -14
  392. package/templates/invoices/app/icon.svg +1 -0
  393. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
  394. package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
  395. package/templates/invoices/app/invoices-view.tsx +81 -32
  396. package/templates/invoices/app/layout.tsx +7 -5
  397. package/templates/invoices/app/page.tsx +4 -6
  398. package/templates/invoices/app/workspace.tsx +26 -16
  399. package/templates/invoices/app.ts +2 -2
  400. package/templates/invoices/components/app-shell.tsx +94 -0
  401. package/templates/invoices/components/auth-form.tsx +35 -5
  402. package/templates/invoices/components/avatar.tsx +5 -1
  403. package/templates/invoices/components/brand-mark.tsx +26 -0
  404. package/templates/invoices/components/command-palette.tsx +17 -17
  405. package/templates/invoices/components/data-table.tsx +97 -36
  406. package/templates/invoices/components/filter-tabs.tsx +101 -0
  407. package/templates/invoices/components/line-items.tsx +19 -14
  408. package/templates/invoices/components/nav.tsx +14 -0
  409. package/templates/invoices/components/page-header.tsx +43 -7
  410. package/templates/invoices/components/require-auth.tsx +24 -26
  411. package/templates/invoices/components/sidebar.tsx +54 -48
  412. package/templates/invoices/components/status-badge.tsx +7 -7
  413. package/templates/invoices/components/summary-bar.tsx +5 -5
  414. package/templates/invoices/components/ui/button.tsx +5 -4
  415. package/templates/invoices/components/ui/dialog.tsx +1 -1
  416. package/templates/invoices/components/ui/input.tsx +1 -1
  417. package/templates/invoices/components/ui/select.tsx +1 -1
  418. package/templates/invoices/components/ui/textarea.tsx +1 -1
  419. package/templates/invoices/functions/recordPayment.ts +3 -0
  420. package/templates/invoices/lib/billing.ts +40 -0
  421. package/templates/invoices/lib/brand.ts +9 -0
  422. package/templates/invoices/lib/search.ts +4 -3
  423. package/templates/invoices/public/favicon.ico +0 -0
  424. package/templates/invoices/tests/billing.test.ts +27 -0
  425. package/templates/ios/consumer/apps/ios/Package.swift +4 -3
  426. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
  427. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
  428. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
  429. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
  430. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
  431. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
  432. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
  433. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
  434. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
  435. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
  436. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
  437. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
  438. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
  439. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
  440. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
  441. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
  442. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
  443. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
  444. package/templates/ios/consumer/apps/ios/project.yml +13 -9
  445. package/templates/local-service/.env.example +7 -0
  446. package/templates/local-service/README.md +26 -2
  447. package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
  448. package/templates/local-service/app/(marketing)/page.tsx +66 -42
  449. package/templates/local-service/app/auth-form.tsx +8 -8
  450. package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
  451. package/templates/local-service/app/dashboard/page.tsx +14 -14
  452. package/templates/local-service/app/icon.svg +2 -0
  453. package/templates/local-service/app/login/page.tsx +15 -15
  454. package/templates/local-service/functions/seedDemo.ts +49 -0
  455. package/templates/local-service/lib/demo.ts +135 -0
  456. package/templates/local-service/lib/hours.ts +44 -0
  457. package/templates/local-service/lib/site.config.ts +8 -3
  458. package/templates/local-service/lib/slots.ts +3 -0
  459. package/templates/local-service/public/favicon.ico +0 -0
  460. package/templates/local-service/tests/booking.test.ts +104 -0
  461. package/templates/marketplace/README.md +23 -15
  462. package/templates/marketplace/app/globals.css +172 -114
  463. package/templates/marketplace/app/layout.tsx +75 -30
  464. package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
  465. package/templates/marketplace/app/me/page.tsx +4 -4
  466. package/templates/marketplace/app/page.tsx +221 -298
  467. package/templates/marketplace/app/sell/page.tsx +8 -11
  468. package/templates/marketplace/app.ts +32 -7
  469. package/templates/marketplace/client/ActivityFeed.tsx +311 -0
  470. package/templates/marketplace/client/AuthNav.tsx +24 -16
  471. package/templates/marketplace/client/ListingCard.tsx +118 -0
  472. package/templates/marketplace/client/LiveGrid.tsx +92 -0
  473. package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
  474. package/templates/marketplace/client/LoginCard.tsx +114 -136
  475. package/templates/marketplace/client/MarketProvider.tsx +3 -5
  476. package/templates/marketplace/client/MyMarket.tsx +371 -185
  477. package/templates/marketplace/client/OfferPanel.tsx +421 -287
  478. package/templates/marketplace/client/SellForm.tsx +271 -192
  479. package/templates/marketplace/client/ThemeToggle.tsx +1 -0
  480. package/templates/marketplace/client/WatchButton.tsx +16 -16
  481. package/templates/marketplace/client/market.ts +61 -14
  482. package/templates/marketplace/functions/seedMarket.ts +143 -36
  483. package/templates/marketplace/functions/seedOffers.ts +101 -0
  484. package/templates/marketplace/lib/catalog.ts +72 -1
  485. package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
  486. package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
  487. package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
  488. package/templates/marketplace/public/images/listings/duffel.webp +0 -0
  489. package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
  490. package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
  491. package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
  492. package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
  493. package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
  494. package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
  495. package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
  496. package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
  497. package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
  498. package/templates/marketplace/public/images/listings/turntable.webp +0 -0
  499. package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
  500. package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
  501. package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
  502. package/templates/marketplace/public/images/listings/watch.webp +0 -0
  503. package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
  504. package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
  505. package/templates/marketplace/tests/example.test.ts +64 -2
  506. package/templates/marketplace/tests/listing-card.test.tsx +61 -0
  507. package/templates/projects/README.md +24 -3
  508. package/templates/projects/app/apple-icon.png +0 -0
  509. package/templates/projects/app/clients/clients-view.tsx +20 -6
  510. package/templates/projects/app/clients/page.tsx +2 -1
  511. package/templates/projects/app/globals.css +29 -14
  512. package/templates/projects/app/icon.svg +1 -0
  513. package/templates/projects/app/layout.tsx +7 -5
  514. package/templates/projects/app/page.tsx +3 -5
  515. package/templates/projects/app/projects/[id]/page.tsx +2 -1
  516. package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
  517. package/templates/projects/app/projects-view.tsx +105 -52
  518. package/templates/projects/app/workspace.tsx +41 -20
  519. package/templates/projects/app.ts +2 -2
  520. package/templates/projects/components/app-shell.tsx +94 -0
  521. package/templates/projects/components/auth-form.tsx +35 -5
  522. package/templates/projects/components/avatar.tsx +5 -1
  523. package/templates/projects/components/board-skeleton.tsx +3 -3
  524. package/templates/projects/components/brand-mark.tsx +25 -0
  525. package/templates/projects/components/command-palette.tsx +17 -17
  526. package/templates/projects/components/data-table.tsx +97 -36
  527. package/templates/projects/components/filter-tabs.tsx +101 -0
  528. package/templates/projects/components/meter.tsx +48 -0
  529. package/templates/projects/components/nav.tsx +14 -0
  530. package/templates/projects/components/page-header.tsx +43 -7
  531. package/templates/projects/components/require-auth.tsx +24 -26
  532. package/templates/projects/components/sidebar.tsx +54 -48
  533. package/templates/projects/components/summary-bar.tsx +49 -0
  534. package/templates/projects/components/task-board.tsx +10 -9
  535. package/templates/projects/components/time-dialog.tsx +29 -5
  536. package/templates/projects/components/ui/button.tsx +5 -4
  537. package/templates/projects/components/ui/dialog.tsx +1 -1
  538. package/templates/projects/components/ui/input.tsx +1 -1
  539. package/templates/projects/components/ui/select.tsx +1 -1
  540. package/templates/projects/components/ui/textarea.tsx +1 -1
  541. package/templates/projects/functions/seedWorkspace.ts +26 -12
  542. package/templates/projects/lib/brand.ts +9 -0
  543. package/templates/projects/lib/demo-team.ts +51 -0
  544. package/templates/projects/lib/search.ts +4 -3
  545. package/templates/projects/lib/seed.ts +35 -1
  546. package/templates/projects/lib/work.ts +63 -0
  547. package/templates/projects/public/favicon.ico +0 -0
  548. package/templates/projects/tests/demo-team.test.ts +18 -0
  549. package/templates/projects/tests/work.test.ts +65 -0
  550. package/templates/restaurant/.env.example +7 -0
  551. package/templates/restaurant/README.md +29 -3
  552. package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
  553. package/templates/restaurant/app/(marketing)/page.tsx +47 -23
  554. package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
  555. package/templates/restaurant/app/auth-form.tsx +8 -8
  556. package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
  557. package/templates/restaurant/app/dashboard/page.tsx +14 -14
  558. package/templates/restaurant/app/icon.svg +4 -0
  559. package/templates/restaurant/app/login/page.tsx +15 -15
  560. package/templates/restaurant/functions/seedDemo.ts +44 -0
  561. package/templates/restaurant/lib/demo.ts +144 -0
  562. package/templates/restaurant/lib/hours.ts +45 -0
  563. package/templates/restaurant/lib/site.config.ts +3 -3
  564. package/templates/restaurant/public/favicon.ico +0 -0
  565. package/templates/restaurant/tests/reservations.test.ts +67 -0
  566. package/templates/saas/README.md +54 -35
  567. package/templates/saas/app/(auth)/auth-form.tsx +25 -23
  568. package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
  569. package/templates/saas/app/(auth)/layout.tsx +27 -33
  570. package/templates/saas/app/(auth)/login/page.tsx +1 -1
  571. package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
  572. package/templates/saas/app/(auth)/signup/page.tsx +1 -1
  573. package/templates/saas/app/(marketing)/layout.tsx +127 -253
  574. package/templates/saas/app/(marketing)/page.tsx +133 -372
  575. package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
  576. package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
  577. package/templates/saas/app/dashboard/board-client.tsx +416 -0
  578. package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
  579. package/templates/saas/app/dashboard/layout.tsx +27 -13
  580. package/templates/saas/app/dashboard/overview-client.tsx +332 -0
  581. package/templates/saas/app/dashboard/page.tsx +29 -35
  582. package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
  583. package/templates/saas/app/dashboard/projects/page.tsx +11 -7
  584. package/templates/saas/app/dashboard/projects-client.tsx +398 -0
  585. package/templates/saas/app/globals.css +5 -4
  586. package/templates/saas/app/icon.svg +1 -0
  587. package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
  588. package/templates/saas/app/onboarding/page.tsx +6 -2
  589. package/templates/saas/app.ts +68 -13
  590. package/templates/saas/components/dashboard-shell.tsx +212 -132
  591. package/templates/saas/components/marketing.tsx +137 -274
  592. package/templates/saas/components/pricing-table.tsx +12 -11
  593. package/templates/saas/components/setup-checklist.tsx +31 -29
  594. package/templates/saas/components/task-ui.tsx +199 -0
  595. package/templates/saas/functions/createProject.ts +7 -13
  596. package/templates/saas/functions/createTask.ts +67 -0
  597. package/templates/saas/functions/deleteProject.ts +17 -0
  598. package/templates/saas/functions/deleteTask.ts +14 -0
  599. package/templates/saas/functions/seedWorkspace.ts +46 -0
  600. package/templates/saas/functions/setProjectStatus.ts +41 -0
  601. package/templates/saas/functions/updateProfile.ts +13 -0
  602. package/templates/saas/functions/updateProject.ts +4 -5
  603. package/templates/saas/functions/updateTask.ts +88 -0
  604. package/templates/saas/lib/members.ts +53 -0
  605. package/templates/saas/lib/names.ts +53 -0
  606. package/templates/saas/lib/plans.ts +11 -0
  607. package/templates/saas/lib/sample-workspace.ts +117 -0
  608. package/templates/saas/lib/site.config.ts +200 -463
  609. package/templates/saas/lib/tasks.ts +101 -0
  610. package/templates/saas/lib/use-live.ts +25 -0
  611. package/templates/saas/public/favicon.ico +0 -0
  612. package/templates/saas/public/screenshots/board.png +0 -0
  613. package/templates/saas/public/screenshots/members.png +0 -0
  614. package/templates/saas/public/screenshots/overview.png +0 -0
  615. package/templates/saas/public/screenshots/projects.png +0 -0
  616. package/templates/saas/scripts/capture-screenshots.mjs +156 -0
  617. package/templates/saas/tests/names.test.ts +35 -0
  618. package/templates/saas/tests/plans.test.ts +12 -0
  619. package/templates/saas/tests/sample-workspace.test.ts +46 -0
  620. package/templates/saas/tests/tasks.test.ts +81 -0
  621. package/templates/shop/README.md +25 -13
  622. package/templates/shop/app/(marketing)/layout.tsx +94 -67
  623. package/templates/shop/app/(marketing)/not-found.tsx +13 -12
  624. package/templates/shop/app/(marketing)/page.tsx +120 -59
  625. package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
  626. package/templates/shop/app/(marketing)/success/page.tsx +75 -22
  627. package/templates/shop/app/globals.css +94 -1
  628. package/templates/shop/app/layout.tsx +4 -4
  629. package/templates/shop/app/login/page.tsx +4 -4
  630. package/templates/shop/app.ts +18 -6
  631. package/templates/shop/components/cart-button.tsx +33 -0
  632. package/templates/shop/components/marketing.tsx +17 -87
  633. package/templates/shop/components/shelf-count.tsx +36 -0
  634. package/templates/shop/functions/checkout.ts +17 -1
  635. package/templates/shop/lib/cart-store.ts +57 -0
  636. package/templates/shop/lib/shelf-store.ts +35 -0
  637. package/templates/shop/lib/shop.ts +27 -0
  638. package/templates/shop/lib/site.config.ts +108 -98
  639. package/templates/shop/public/images/hero.jpg +0 -0
  640. package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
  641. package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
  642. package/templates/shop/public/images/products/linen.jpg +0 -0
  643. package/templates/shop/public/images/products/sea-salt.jpg +0 -0
  644. package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
  645. package/templates/shop/tests/shop.test.ts +64 -0
  646. package/templates/waitlist/.env.example +7 -0
  647. package/templates/waitlist/README.md +29 -13
  648. package/templates/waitlist/app/(marketing)/page.tsx +9 -7
  649. package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
  650. package/templates/waitlist/app/auth-form.tsx +8 -8
  651. package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
  652. package/templates/waitlist/app/dashboard/page.tsx +17 -16
  653. package/templates/waitlist/app/icon.svg +2 -0
  654. package/templates/waitlist/app/login/page.tsx +14 -14
  655. package/templates/waitlist/app.ts +6 -6
  656. package/templates/waitlist/functions/joinWaitlist.ts +6 -18
  657. package/templates/waitlist/functions/seedDemo.ts +32 -0
  658. package/templates/waitlist/lib/demo.ts +94 -0
  659. package/templates/waitlist/lib/launch.ts +19 -0
  660. package/templates/waitlist/lib/site.config.ts +14 -9
  661. package/templates/waitlist/lib/stats.ts +35 -0
  662. package/templates/waitlist/public/favicon.ico +0 -0
  663. package/templates/waitlist/tests/counter.test.ts +107 -0
  664. package/templates/waitlist/tests/live-count.test.tsx +48 -0
  665. package/templates/ai-chat/tests/example.test.ts +0 -12
  666. package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
  667. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
  668. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
  669. package/templates/backend/consumer/apps/api/schema.ts +0 -143
  670. package/templates/chat/functions/_keep.ts +0 -13
  671. package/templates/chat/tests/example.test.ts +0 -12
  672. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
  673. package/templates/marketplace/client/LiveTicker.tsx +0 -116
  674. package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
  675. package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
@@ -0,0 +1,285 @@
1
+ import Combine
2
+ import Foundation
3
+ import PylonClient
4
+ import PylonSync
5
+ import PylonSwiftUI
6
+
7
+ /// The live social graph. Five subscriptions (Profile, Post, Like, Comment,
8
+ /// Follow) on the sync engine, joined in memory. A row written by any
9
+ /// client reaches every open app over the engine's WebSocket, and every
10
+ /// view that reads this store redraws.
11
+ ///
12
+ /// Writes go through server functions. The server writes the row, the
13
+ /// change comes back through sync, and the view updates. Likes and follows
14
+ /// also show at once through a pending state that clears when the synced
15
+ /// row arrives.
16
+ @MainActor
17
+ final class SocialStore: ObservableObject {
18
+ let engine: SyncEngine
19
+ let client: PylonClient
20
+ let media: MediaService
21
+
22
+ @Published var myProfileId: String?
23
+ @Published private(set) var profiles: [String: Profile] = [:]
24
+ /// Newest first.
25
+ @Published private(set) var posts: [Post] = []
26
+ @Published private(set) var likesByPost: [String: [Like]] = [:]
27
+ /// Oldest first within a post.
28
+ @Published private(set) var commentsByPost: [String: [Comment]] = [:]
29
+ @Published private(set) var follows: [Follow] = []
30
+ @Published private(set) var isLoading = true
31
+ /// Post ids the user liked or unliked whose synced row has not arrived yet.
32
+ @Published private(set) var pendingLikes: [String: Bool] = [:]
33
+ @Published private(set) var pendingFollows: [String: Bool] = [:]
34
+
35
+ private let profileQuery: PylonQuery<Profile>
36
+ private let postQuery: PylonQuery<Post>
37
+ private let likeQuery: PylonQuery<Like>
38
+ private let commentQuery: PylonQuery<Comment>
39
+ private let followQuery: PylonQuery<Follow>
40
+ private var subscriptions: Set<AnyCancellable> = []
41
+
42
+ init(engine: SyncEngine, client: PylonClient, media: MediaService) {
43
+ self.engine = engine
44
+ self.client = client
45
+ self.media = media
46
+ profileQuery = PylonQuery(engine: engine, entity: "Profile")
47
+ postQuery = PylonQuery(engine: engine, entity: "Post")
48
+ likeQuery = PylonQuery(engine: engine, entity: "Like")
49
+ commentQuery = PylonQuery(engine: engine, entity: "Comment")
50
+ followQuery = PylonQuery(engine: engine, entity: "Follow")
51
+
52
+ profileQuery.$rows
53
+ .sink { [weak self] rows in
54
+ self?.profiles = Dictionary(rows.map { ($0.id, $0) }, uniquingKeysWith: { _, new in new })
55
+ }
56
+ .store(in: &subscriptions)
57
+ postQuery.$rows
58
+ .sink { [weak self] rows in
59
+ self?.posts = rows.sorted { $0.createdAt > $1.createdAt }
60
+ }
61
+ .store(in: &subscriptions)
62
+ likeQuery.$rows
63
+ .sink { [weak self] rows in
64
+ guard let self else { return }
65
+ likesByPost = Dictionary(grouping: rows, by: \.postId)
66
+ pendingLikes = pendingLikes.filter { postId, liked in
67
+ self.syncedLike(postId) != liked
68
+ }
69
+ }
70
+ .store(in: &subscriptions)
71
+ commentQuery.$rows
72
+ .sink { [weak self] rows in
73
+ self?.commentsByPost = Dictionary(grouping: rows, by: \.postId)
74
+ .mapValues { $0.sorted { $0.createdAt < $1.createdAt } }
75
+ }
76
+ .store(in: &subscriptions)
77
+ followQuery.$rows
78
+ .sink { [weak self] rows in
79
+ guard let self else { return }
80
+ follows = rows
81
+ pendingFollows = pendingFollows.filter { profileId, following in
82
+ self.syncedFollow(profileId) != following
83
+ }
84
+ }
85
+ .store(in: &subscriptions)
86
+ postQuery.$loading
87
+ .sink { [weak self] loading in self?.isLoading = loading }
88
+ .store(in: &subscriptions)
89
+ }
90
+
91
+ // MARK: - Reads
92
+
93
+ var me: Profile? { myProfileId.flatMap { profiles[$0] } }
94
+
95
+ func profile(_ id: String) -> Profile? { profiles[id] }
96
+
97
+ func posts(by profileId: String) -> [Post] {
98
+ posts.filter { $0.authorId == profileId }
99
+ }
100
+
101
+ func post(_ id: String) -> Post? { posts.first { $0.id == id } }
102
+
103
+ func isLiked(_ postId: String) -> Bool {
104
+ pendingLikes[postId] ?? syncedLike(postId)
105
+ }
106
+
107
+ func likeCount(_ postId: String) -> Int {
108
+ let synced = likesByPost[postId]?.count ?? 0
109
+ guard let pending = pendingLikes[postId], pending != syncedLike(postId) else { return synced }
110
+ return synced + (pending ? 1 : -1)
111
+ }
112
+
113
+ /// A liker to name under the photo: someone the user follows first, else the latest.
114
+ func featuredLiker(_ postId: String) -> Profile? {
115
+ let likes = (likesByPost[postId] ?? []).filter { $0.profileId != myProfileId }
116
+ let following = Set(followingIds(of: myProfileId))
117
+ let pick = likes.first { following.contains($0.profileId) } ?? likes.max { $0.createdAt < $1.createdAt }
118
+ return pick.flatMap { profiles[$0.profileId] }
119
+ }
120
+
121
+ func comments(_ postId: String) -> [Comment] { commentsByPost[postId] ?? [] }
122
+
123
+ func followerIds(of profileId: String) -> [String] {
124
+ follows.filter { $0.followingId == profileId }.map(\.followerId)
125
+ }
126
+
127
+ func followingIds(of profileId: String?) -> [String] {
128
+ guard let profileId else { return [] }
129
+ return follows.filter { $0.followerId == profileId }.map(\.followingId)
130
+ }
131
+
132
+ func isFollowing(_ profileId: String) -> Bool {
133
+ pendingFollows[profileId] ?? syncedFollow(profileId)
134
+ }
135
+
136
+ func followerCount(_ profileId: String) -> Int {
137
+ let synced = followerIds(of: profileId).count
138
+ guard let pending = pendingFollows[profileId], pending != syncedFollow(profileId) else { return synced }
139
+ return synced + (pending ? 1 : -1)
140
+ }
141
+
142
+ /// People with a post in the last 24 hours, the user first, then newest.
143
+ var storyAuthors: [Profile] {
144
+ let cutoff = Date.now.addingTimeInterval(-86_400)
145
+ var seen = Set<String>()
146
+ var out: [Profile] = []
147
+ for post in posts where post.date > cutoff {
148
+ guard !seen.contains(post.authorId), post.authorId != myProfileId,
149
+ let author = profiles[post.authorId] else { continue }
150
+ seen.insert(post.authorId)
151
+ out.append(author)
152
+ }
153
+ return out
154
+ }
155
+
156
+ func hasRecentPost(_ profileId: String) -> Bool {
157
+ let cutoff = Date.now.addingTimeInterval(-86_400)
158
+ return posts.contains { $0.authorId == profileId && $0.date > cutoff }
159
+ }
160
+
161
+ func recentPosts(by profileId: String) -> [Post] {
162
+ let cutoff = Date.now.addingTimeInterval(-86_400)
163
+ return posts.filter { $0.authorId == profileId && $0.date > cutoff }.reversed()
164
+ }
165
+
166
+ /// Likes, comments, and follows aimed at the user, newest first.
167
+ var activity: [ActivityItem] {
168
+ guard let me = myProfileId else { return [] }
169
+ let mine = Set(posts.filter { $0.authorId == me }.map(\.id))
170
+ var items: [ActivityItem] = []
171
+ for postId in mine {
172
+ for like in likesByPost[postId] ?? [] where like.profileId != me {
173
+ items.append(ActivityItem(id: "like-\(like.id)", kind: .like, actorId: like.profileId, postId: postId, text: nil, createdAt: like.createdAt))
174
+ }
175
+ for comment in commentsByPost[postId] ?? [] where comment.profileId != me {
176
+ items.append(ActivityItem(id: "comment-\(comment.id)", kind: .comment, actorId: comment.profileId, postId: postId, text: comment.text, createdAt: comment.createdAt))
177
+ }
178
+ }
179
+ for follow in follows where follow.followingId == me {
180
+ items.append(ActivityItem(id: "follow-\(follow.id)", kind: .follow, actorId: follow.followerId, postId: nil, text: nil, createdAt: follow.createdAt))
181
+ }
182
+ return items.sorted { $0.createdAt > $1.createdAt }
183
+ }
184
+
185
+ private func syncedLike(_ postId: String) -> Bool {
186
+ guard let me = myProfileId else { return false }
187
+ return likesByPost[postId]?.contains { $0.profileId == me } ?? false
188
+ }
189
+
190
+ private func syncedFollow(_ profileId: String) -> Bool {
191
+ guard let me = myProfileId else { return false }
192
+ return follows.contains { $0.followerId == me && $0.followingId == profileId }
193
+ }
194
+
195
+ // MARK: - Writes
196
+
197
+ func toggleLike(_ postId: String) {
198
+ let next = !isLiked(postId)
199
+ setLike(postId, liked: next)
200
+ }
201
+
202
+ /// Double tap only ever likes.
203
+ func like(_ postId: String) {
204
+ if !isLiked(postId) { setLike(postId, liked: true) }
205
+ }
206
+
207
+ /// Records the tap at once, then sends the wanted state. The server call
208
+ /// sets the state rather than flipping it, so taps that arrive out of
209
+ /// order still end in the last tap's state.
210
+ private func setLike(_ postId: String, liked: Bool) {
211
+ pendingLikes[postId] = liked
212
+ Task {
213
+ do {
214
+ _ = try await client.callFn("setLike", args: SetLikeArgs(postId: postId, liked: liked), as: SetLikeResult.self)
215
+ } catch {
216
+ if pendingLikes[postId] == liked { pendingLikes[postId] = nil }
217
+ }
218
+ }
219
+ }
220
+
221
+ func toggleFollow(_ profileId: String) {
222
+ let following = !isFollowing(profileId)
223
+ pendingFollows[profileId] = following
224
+ Task {
225
+ do {
226
+ _ = try await client.callFn("setFollow", args: SetFollowArgs(profileId: profileId, following: following), as: SetFollowResult.self)
227
+ } catch {
228
+ if pendingFollows[profileId] == following { pendingFollows[profileId] = nil }
229
+ }
230
+ }
231
+ }
232
+
233
+ func addComment(_ postId: String, text: String) async throws {
234
+ _ = try await client.callFn("addComment", args: AddCommentArgs(postId: postId, text: text), as: IdResult.self)
235
+ }
236
+
237
+ func deletePost(_ postId: String) async throws {
238
+ _ = try await client.callFn("deletePost", args: DeletePostArgs(id: postId), as: IdResult.self)
239
+ }
240
+
241
+ func createPost(jpeg: Data, caption: String) async throws {
242
+ let imageUrl = try await media.uploadPublicJPEG(jpeg)
243
+ _ = try await client.callFn("createPost", args: CreatePostArgs(imageUrl: imageUrl, caption: caption), as: IdResult.self)
244
+ }
245
+
246
+ func saveProfile(handle: String, displayName: String, bio: String, avatarJPEG: Data?) async throws -> Profile {
247
+ var avatarUrl: String?
248
+ if let avatarJPEG {
249
+ avatarUrl = try await media.uploadPublicJPEG(avatarJPEG)
250
+ }
251
+ return try await client.callFn(
252
+ "upsertProfile",
253
+ args: UpsertProfileArgs(handle: handle, displayName: displayName, bio: bio, avatarUrl: avatarUrl),
254
+ as: Profile.self
255
+ )
256
+ }
257
+ }
258
+
259
+ struct ActivityItem: Identifiable, Hashable {
260
+ enum Kind { case like, comment, follow }
261
+ let id: String
262
+ let kind: Kind
263
+ let actorId: String
264
+ let postId: String?
265
+ let text: String?
266
+ let createdAt: String
267
+
268
+ var date: Date { Timestamp.parse(createdAt) }
269
+ }
270
+
271
+ /// A readable sentence for an error from a server function or the network.
272
+ func friendlyMessage(_ error: Error) -> String {
273
+ if let pylon = error as? PylonError {
274
+ switch pylon {
275
+ case .http(_, _, let message?) where !message.isEmpty:
276
+ return message.prefix(1).uppercased() + message.dropFirst()
277
+ case .transport:
278
+ return "Cannot reach the server. Check your connection and try again."
279
+ default:
280
+ return "Something went wrong. Try again."
281
+ }
282
+ }
283
+ if let local = error as? LocalizedError, let text = local.errorDescription { return text }
284
+ return "Something went wrong. Try again."
285
+ }
@@ -0,0 +1,155 @@
1
+ import SwiftUI
2
+
3
+ /// Full-screen playback of each person's posts from the last 24 hours.
4
+ /// Every photo shows for five seconds. Tap the right side for the next
5
+ /// photo, the left side for the previous one, or swipe down to close.
6
+ struct StoryViewer: View {
7
+ @EnvironmentObject private var social: SocialStore
8
+ @Environment(\.dismiss) private var dismiss
9
+
10
+ let authors: [Profile]
11
+ @State var authorIndex: Int
12
+ @State private var postIndex = 0
13
+ @State private var progress: Double = 0
14
+ @State private var dragOffset: CGFloat = 0
15
+ /// Bumped to replay the current photo from the start.
16
+ @State private var restarts = 0
17
+
18
+ private let duration: Double = 5
19
+ private let tick: Double = 0.05
20
+
21
+ init(authors: [Profile], startIndex: Int) {
22
+ self.authors = authors
23
+ _authorIndex = State(initialValue: startIndex)
24
+ }
25
+
26
+ private var author: Profile? { authors.indices.contains(authorIndex) ? authors[authorIndex] : nil }
27
+ private var posts: [Post] { author.map { social.recentPosts(by: $0.id) } ?? [] }
28
+ private var post: Post? { posts.indices.contains(postIndex) ? posts[postIndex] : nil }
29
+
30
+ var body: some View {
31
+ ZStack {
32
+ Color.black.ignoresSafeArea()
33
+ if let post {
34
+ let url = social.media.url(for: post.imageUrl)
35
+ RemoteImage(url: url)
36
+ .blur(radius: 40)
37
+ .opacity(0.6)
38
+ .ignoresSafeArea()
39
+ Color.clear
40
+ .aspectRatio(4 / 5, contentMode: .fit)
41
+ .overlay { RemoteImage(url: url) }
42
+ .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
43
+ .id(post.id)
44
+ }
45
+ HStack(spacing: 0) {
46
+ Color.clear.contentShape(Rectangle()).onTapGesture(perform: previous)
47
+ Color.clear.contentShape(Rectangle()).onTapGesture(perform: next)
48
+ }
49
+ VStack(spacing: 10) {
50
+ progressBars
51
+ topBar
52
+ Spacer()
53
+ if let caption = post?.caption, !caption.isEmpty {
54
+ Text(caption)
55
+ .font(.subheadline)
56
+ .foregroundStyle(.white)
57
+ .multilineTextAlignment(.leading)
58
+ .frame(maxWidth: .infinity, alignment: .leading)
59
+ .padding(14)
60
+ .background(.black.opacity(0.35), in: RoundedRectangle(cornerRadius: 14, style: .continuous))
61
+ }
62
+ }
63
+ .padding(.horizontal, 12)
64
+ .padding(.top, 8)
65
+ }
66
+ .offset(y: dragOffset)
67
+ .gesture(
68
+ DragGesture()
69
+ .onChanged { value in dragOffset = max(0, value.translation.height) }
70
+ .onEnded { value in
71
+ if value.translation.height > 120 { dismiss() } else {
72
+ withAnimation(.spring) { dragOffset = 0 }
73
+ }
74
+ }
75
+ )
76
+ .statusBarHidden()
77
+ .task(id: "\(authorIndex)-\(postIndex)-\(restarts)") { await play() }
78
+ }
79
+
80
+ private var progressBars: some View {
81
+ HStack(spacing: 4) {
82
+ ForEach(posts.indices, id: \.self) { index in
83
+ GeometryReader { geo in
84
+ Capsule().fill(.white.opacity(0.35))
85
+ .overlay(alignment: .leading) {
86
+ Capsule().fill(.white)
87
+ .frame(width: geo.size.width * fill(for: index))
88
+ }
89
+ }
90
+ .frame(height: 2.5)
91
+ }
92
+ }
93
+ }
94
+
95
+ private var topBar: some View {
96
+ HStack(spacing: 10) {
97
+ AvatarView(profile: author, size: 32)
98
+ Text(author?.handle ?? "")
99
+ .font(.subheadline.weight(.semibold))
100
+ if let post {
101
+ Text(Timestamp.short(post.date))
102
+ .font(.subheadline)
103
+ .opacity(0.7)
104
+ }
105
+ Spacer()
106
+ Button { dismiss() } label: {
107
+ Image(systemName: "xmark")
108
+ .font(.system(size: 20, weight: .semibold))
109
+ .frame(width: 40, height: 40)
110
+ .contentShape(Rectangle())
111
+ }
112
+ .accessibilityLabel("Close")
113
+ }
114
+ .foregroundStyle(.white)
115
+ }
116
+
117
+ private func fill(for index: Int) -> Double {
118
+ if index < postIndex { return 1 }
119
+ if index == postIndex { return progress }
120
+ return 0
121
+ }
122
+
123
+ private func play() async {
124
+ progress = 0
125
+ let steps = Int(duration / tick)
126
+ for _ in 0..<steps {
127
+ try? await Task.sleep(for: .seconds(tick))
128
+ if Task.isCancelled { return }
129
+ progress += tick / duration
130
+ }
131
+ next()
132
+ }
133
+
134
+ private func next() {
135
+ if postIndex + 1 < posts.count {
136
+ postIndex += 1
137
+ } else if authorIndex + 1 < authors.count {
138
+ authorIndex += 1
139
+ postIndex = 0
140
+ } else {
141
+ dismiss()
142
+ }
143
+ }
144
+
145
+ private func previous() {
146
+ if postIndex > 0 {
147
+ postIndex -= 1
148
+ } else if authorIndex > 0 {
149
+ authorIndex -= 1
150
+ postIndex = 0
151
+ } else {
152
+ restarts += 1
153
+ }
154
+ }
155
+ }
@@ -0,0 +1,82 @@
1
+ import SwiftUI
2
+
3
+ enum Theme {
4
+ /// Filled buttons, links, and badges. Fixed so the black tab bar tint
5
+ /// does not change it.
6
+ static let accent = Color(red: 0.0, green: 0.584, blue: 0.965)
7
+ /// The like color.
8
+ static let heart = Color(red: 1.0, green: 0.19, blue: 0.25)
9
+ /// The ring around an avatar with a post from the last 24 hours.
10
+ static let storyRing = AngularGradient(
11
+ colors: [
12
+ Color(red: 0.99, green: 0.73, blue: 0.27),
13
+ Color(red: 0.98, green: 0.38, blue: 0.20),
14
+ Color(red: 0.87, green: 0.15, blue: 0.47),
15
+ Color(red: 0.55, green: 0.22, blue: 0.85),
16
+ Color(red: 0.99, green: 0.73, blue: 0.27),
17
+ ],
18
+ center: .center
19
+ )
20
+ static let hairline = Color(uiColor: .separator)
21
+ static let fieldFill = Color(uiColor: .secondarySystemBackground)
22
+ }
23
+
24
+ /// The app name in the feed header and on the sign-in screen.
25
+ struct Wordmark: View {
26
+ var size: CGFloat = 30
27
+
28
+ var body: some View {
29
+ Text("__APP_NAME__")
30
+ .font(.system(size: size, weight: .semibold, design: .serif))
31
+ .italic()
32
+ .tracking(-0.5)
33
+ }
34
+ }
35
+
36
+ /// A full-width filled button for the main action on a screen.
37
+ struct PrimaryButtonStyle: ButtonStyle {
38
+ var isLoading = false
39
+
40
+ func makeBody(configuration: Configuration) -> some View {
41
+ ZStack {
42
+ configuration.label.opacity(isLoading ? 0 : 1)
43
+ if isLoading { ProgressView().tint(.white) }
44
+ }
45
+ .font(.body.weight(.semibold))
46
+ .foregroundStyle(.white)
47
+ .frame(maxWidth: .infinity, minHeight: 50)
48
+ .background(Theme.accent, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
49
+ .opacity(configuration.isPressed ? 0.8 : 1)
50
+ }
51
+ }
52
+
53
+ /// A compact button for profile actions: Edit profile, Follow, Following.
54
+ struct ProfileButtonStyle: ButtonStyle {
55
+ var prominent = false
56
+
57
+ func makeBody(configuration: Configuration) -> some View {
58
+ configuration.label
59
+ .font(.subheadline.weight(.semibold))
60
+ .foregroundStyle(prominent ? Color.white : Color.primary)
61
+ .frame(maxWidth: .infinity, minHeight: 34)
62
+ .background(
63
+ prominent ? Theme.accent : Theme.fieldFill,
64
+ in: RoundedRectangle(cornerRadius: 9, style: .continuous)
65
+ )
66
+ .opacity(configuration.isPressed ? 0.7 : 1)
67
+ }
68
+ }
69
+
70
+ /// A rounded text field on the sign-in and profile screens.
71
+ struct FieldStyle: ViewModifier {
72
+ func body(content: Content) -> some View {
73
+ content
74
+ .padding(.horizontal, 14)
75
+ .frame(minHeight: 50)
76
+ .background(Theme.fieldFill, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
77
+ }
78
+ }
79
+
80
+ extension View {
81
+ func fieldStyle() -> some View { modifier(FieldStyle()) }
82
+ }
@@ -1,57 +1,14 @@
1
1
  import SwiftUI
2
- import PylonClient
3
- import PylonSync
4
2
 
5
3
  @main
6
4
  struct __APP_NAME_PASCAL__App: App {
7
- @StateObject private var session = AppSession()
5
+ @StateObject private var app = AppModel()
8
6
 
9
7
  var body: some Scene {
10
8
  WindowGroup {
11
- Group {
12
- if let engine = session.engine {
13
- RootView(engine: engine)
14
- .environmentObject(session)
15
- } else {
16
- ProgressView()
17
- .frame(maxWidth: .infinity, maxHeight: .infinity)
18
- }
19
- }
20
- .task { await session.bootIfNeeded() }
9
+ RootView()
10
+ .environmentObject(app)
11
+ .task { await app.boot() }
21
12
  }
22
13
  }
23
14
  }
24
-
25
- @MainActor
26
- final class AppSession: ObservableObject {
27
- let client: PylonClient
28
- @Published private(set) var engine: SyncEngine?
29
- @Published var myProfileId: String?
30
-
31
- init() {
32
- let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
33
- ?? "http://localhost:4321"
34
- guard let url = URL(string: baseURLString) else {
35
- fatalError("Invalid PYLON_BASE_URL: \(baseURLString)")
36
- }
37
- self.client = PylonClient(baseURL: url, appName: "__APP_NAME_SNAKE__")
38
- self.myProfileId = UserDefaults.standard.string(forKey: "myProfileId")
39
- }
40
-
41
- func bootIfNeeded() async {
42
- guard engine == nil else { return }
43
- let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
44
- ?? "http://localhost:4321"
45
- guard let url = URL(string: baseURLString) else { return }
46
- let config = SyncEngineConfig(baseURL: url, appName: "__APP_NAME_SNAKE__")
47
- let engine = await SyncEngine(config: config, client: client)
48
- await engine.start()
49
- self.engine = engine
50
- }
51
-
52
- func setMyProfileId(_ id: String?) {
53
- myProfileId = id
54
- if let id { UserDefaults.standard.set(id, forKey: "myProfileId") }
55
- else { UserDefaults.standard.removeObject(forKey: "myProfileId") }
56
- }
57
- }
@@ -3,18 +3,24 @@
3
3
  # Materialize the Xcode project once with:
4
4
  # brew install xcodegen
5
5
  # xcodegen generate
6
- # Then open __APP_NAME_PASCAL__.xcodeproj.
6
+ # Then open __APP_NAME_PASCAL__.xcodeproj. Run xcodegen again after you add
7
+ # or remove Swift files.
8
+ #
9
+ # The app talks to the Pylon backend in apps/api. Start it with `pylon dev`
10
+ # there. The simulator reaches http://localhost:4321; on a device, set
11
+ # PYLON_BASE_URL in the scheme's environment to your Mac's LAN address or
12
+ # your deployed API.
7
13
 
8
14
  name: __APP_NAME_PASCAL__
9
15
  options:
10
16
  bundleIdPrefix: com.example
11
17
  deploymentTarget:
12
- iOS: "16.0"
18
+ iOS: "17.0"
13
19
 
14
20
  packages:
15
21
  pylon:
16
22
  url: https://github.com/pylonsync/pylon.git
17
- from: "0.3.0"
23
+ from: "__PYLON_VERSION__"
18
24
 
19
25
  targets:
20
26
  __APP_NAME_PASCAL__:
@@ -25,15 +31,13 @@ targets:
25
31
  settings:
26
32
  base:
27
33
  GENERATE_INFOPLIST_FILE: YES
34
+ INFOPLIST_KEY_CFBundleDisplayName: "__APP_NAME__"
28
35
  INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES
29
36
  INFOPLIST_KEY_UILaunchScreen_Generation: YES
30
- INFOPLIST_KEY_UISupportedInterfaceOrientations: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
31
- TARGETED_DEVICE_FAMILY: "1,2"
32
- SWIFT_VERSION: "5.9"
37
+ INFOPLIST_KEY_UISupportedInterfaceOrientations: "UIInterfaceOrientationPortrait"
38
+ TARGETED_DEVICE_FAMILY: "1"
39
+ SWIFT_VERSION: "5.0"
33
40
  ENABLE_PREVIEWS: YES
34
- # __APP_NAME__ talks to a Pylon backend on localhost during dev.
35
- # The simulator can reach localhost directly; on a physical device
36
- # set PYLON_BASE_URL to your machine's LAN IP.
37
41
  INFOPLIST_KEY_NSAppTransportSecurity: "{NSAllowsLocalNetworking = YES;}"
38
42
  dependencies:
39
43
  - package: pylon
@@ -10,3 +10,10 @@ PYLON_OWNER_EMAIL=you@yourshop.com
10
10
  # ── Site URL (optional) ──────────────────────────────────────────────────────
11
11
  # Used by robots.txt + sitemap.xml. Point it at your real domain in production.
12
12
  # SITE_URL=https://yourshop.com
13
+
14
+ # ── Demo data ────────────────────────────────────────────────────────────────
15
+ # `pylon dev` fills an empty booking book with fictional appointments. Seeding
16
+ # runs only under `pylon dev` (it checks PYLON_DEV_MODE and
17
+ # PYLON_DEV_WATCH_DIR), so `pylon start`, Docker, and Pylon Cloud deploys never
18
+ # seed. Set 0 to start empty in development. Never set 1 on a deploy.
19
+ # PYLON_DEMO_DATA=0
@@ -45,6 +45,23 @@ Set `PYLON_OWNER_EMAIL` in `.env` (see `.env.example`) to the email you'll sign
45
45
  in with, then create that account at `/login`. Only that account can see
46
46
  bookings.
47
47
 
48
+ ## Demo data
49
+
50
+ In `pylon dev`, an empty booking book gets fictional appointments from the past
51
+ week and the next ten days (`functions/seedDemo.ts`, `lib/demo.ts`). Each one
52
+ writes a `Booking` and its `BookedSlot`, like a real booking, so the public grid
53
+ shows those times as booked and the dashboard lists them. Seeding runs only
54
+ when `PYLON_DEMO_DATA` is on, or when it is unset and the app runs under `pylon dev`,
55
+ and only while the `Booking` table is empty. `pylon start`, Docker, and Pylon Cloud deploys do not seed, because only `pylon dev` sets `PYLON_DEV_WATCH_DIR`. Set `PYLON_DEMO_DATA=0` to start empty in
56
+ development. Delete both files once you take real bookings.
57
+
58
+ ## Map or hours board
59
+
60
+ Set `location.mapEmbedUrl` in `lib/site.config.ts` to a Google Maps embed URL to
61
+ show a map beside the address. Without one, the site shows the weekly hours
62
+ (from `booking.hours`, the same hours the booking grid uses) and a directions
63
+ link. The dashboard reminds you while the URL is empty.
64
+
48
65
  ## Rebrand + reconfigure it
49
66
 
50
67
  Brand, colors, services, weekly hours, slot length, lead time, reviews,
@@ -52,6 +69,10 @@ location, and FAQ content live in **`lib/site.config.ts`**. Editing or
52
69
  generating that file updates both the site and booking engine. Services and
53
70
  hours stay in config rather than a separate database.
54
71
 
72
+ The tab icon is `app/icon.svg` (with `public/favicon.ico` for browsers that
73
+ request `/favicon.ico`). Both carry this brand's letter and colors; replace
74
+ them when you rename the brand.
75
+
55
76
  ## Layout
56
77
 
57
78
  ```
@@ -59,12 +80,15 @@ app.ts data model (Booking, BookedSlot, User) + policies
59
80
  lib/site.config.ts ALL copy + brand + services + hours (edit this)
60
81
  lib/slots.ts pure slot math, shared by picker + server re-check
61
82
  lib/booking.ts shared booking-row types
83
+ lib/hours.ts weekly hours rows + directions link
84
+ lib/demo.ts demo bookings + the dev-only gate
62
85
  lib/owner.ts owner-email gate (PYLON_OWNER_EMAIL)
63
86
  functions/createBooking.ts public mutation: re-check + book (race-safe)
64
87
  functions/bookingsForOwner.ts owner-only query: bookings + customer PII
65
88
  functions/{confirm,cancel}Booking.ts owner-only mutations
66
- app/page.tsx the landing page (server-rendered)
67
- app/booking-widget.tsx client island: live slot picker + booking form
89
+ functions/seedDemo.ts dev-only demo bookings (see Demo data)
90
+ app/(marketing)/page.tsx the landing page (server-rendered)
91
+ app/(marketing)/booking-widget.tsx client island: live slot picker + booking form
68
92
  app/login/page.tsx owner sign-in
69
93
  app/dashboard/ owner dashboard (auth-gated, live)
70
94
  app/globals.css Tailwind entrypoint (compiled by Pylon)