@pylonsync/create-pylon 0.3.274 → 0.3.275

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 (323) hide show
  1. package/bin/create-pylon.js +80 -0
  2. package/package.json +1 -1
  3. package/templates/ARCHETYPES.md +339 -0
  4. package/templates/agency/.env.example +12 -0
  5. package/templates/agency/AGENTS.md +61 -0
  6. package/templates/agency/README.md +90 -0
  7. package/templates/agency/app/auth-form.tsx +129 -0
  8. package/templates/agency/app/contact-form.tsx +258 -0
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +286 -0
  10. package/templates/agency/app/dashboard/page.tsx +70 -0
  11. package/templates/agency/app/error.tsx +26 -0
  12. package/templates/agency/app/globals.css +148 -0
  13. package/templates/agency/app/layout.tsx +174 -0
  14. package/templates/agency/app/login/page.tsx +39 -0
  15. package/templates/agency/app/not-found.tsx +19 -0
  16. package/templates/agency/app/page.tsx +207 -0
  17. package/templates/agency/app/robots.ts +12 -0
  18. package/templates/agency/app/sitemap.ts +9 -0
  19. package/templates/agency/app.ts +135 -0
  20. package/templates/agency/components/marketing.tsx +148 -0
  21. package/templates/agency/components/section-scroller.tsx +35 -0
  22. package/templates/agency/components/ui/button.tsx +56 -0
  23. package/templates/agency/components/ui/card.tsx +90 -0
  24. package/templates/agency/components.json +20 -0
  25. package/templates/agency/functions/bookInquiry.ts +42 -0
  26. package/templates/agency/functions/declineInquiry.ts +41 -0
  27. package/templates/agency/functions/inquiriesForOwner.ts +31 -0
  28. package/templates/agency/functions/seedCapacity.ts +26 -0
  29. package/templates/agency/functions/setCapacity.ts +32 -0
  30. package/templates/agency/functions/submitInquiry.ts +55 -0
  31. package/templates/agency/gitignore +10 -0
  32. package/templates/agency/lib/agency.ts +27 -0
  33. package/templates/agency/lib/owner.ts +26 -0
  34. package/templates/agency/lib/site.config.ts +239 -0
  35. package/templates/agency/lib/utils.ts +10 -0
  36. package/templates/agency/package.json +34 -0
  37. package/templates/agency/tsconfig.json +18 -0
  38. package/templates/ai-chat/.env.example +33 -0
  39. package/templates/ai-chat/AGENTS.md +61 -0
  40. package/templates/ai-chat/README.md +99 -0
  41. package/templates/ai-chat/app/auth-form.tsx +124 -0
  42. package/templates/ai-chat/app/chat-client.tsx +414 -0
  43. package/templates/ai-chat/app/error.tsx +26 -0
  44. package/templates/ai-chat/app/globals.css +148 -0
  45. package/templates/ai-chat/app/layout.tsx +75 -0
  46. package/templates/ai-chat/app/login/page.tsx +39 -0
  47. package/templates/ai-chat/app/not-found.tsx +19 -0
  48. package/templates/ai-chat/app/page.tsx +23 -0
  49. package/templates/ai-chat/app.ts +121 -0
  50. package/templates/ai-chat/components.json +20 -0
  51. package/templates/ai-chat/gitignore +10 -0
  52. package/templates/ai-chat/lib/site.config.ts +103 -0
  53. package/templates/ai-chat/lib/utils.ts +10 -0
  54. package/templates/ai-chat/package.json +34 -0
  55. package/templates/ai-chat/tsconfig.json +18 -0
  56. package/templates/ai-studio/.env.example +19 -0
  57. package/templates/ai-studio/AGENTS.md +61 -0
  58. package/templates/ai-studio/README.md +83 -0
  59. package/templates/ai-studio/app/auth-form.tsx +124 -0
  60. package/templates/ai-studio/app/error.tsx +26 -0
  61. package/templates/ai-studio/app/globals.css +148 -0
  62. package/templates/ai-studio/app/layout.tsx +75 -0
  63. package/templates/ai-studio/app/login/page.tsx +39 -0
  64. package/templates/ai-studio/app/not-found.tsx +19 -0
  65. package/templates/ai-studio/app/page.tsx +34 -0
  66. package/templates/ai-studio/app/studio-client.tsx +214 -0
  67. package/templates/ai-studio/app.ts +108 -0
  68. package/templates/ai-studio/components.json +20 -0
  69. package/templates/ai-studio/functions/_getGeneration.ts +25 -0
  70. package/templates/ai-studio/functions/_updateGeneration.ts +37 -0
  71. package/templates/ai-studio/functions/generate.ts +42 -0
  72. package/templates/ai-studio/functions/pollGeneration.ts +134 -0
  73. package/templates/ai-studio/gitignore +10 -0
  74. package/templates/ai-studio/lib/site.config.ts +80 -0
  75. package/templates/ai-studio/lib/studio.ts +52 -0
  76. package/templates/ai-studio/lib/utils.ts +10 -0
  77. package/templates/ai-studio/package.json +34 -0
  78. package/templates/ai-studio/tsconfig.json +18 -0
  79. package/templates/creator/.env.example +12 -0
  80. package/templates/creator/AGENTS.md +61 -0
  81. package/templates/creator/README.md +67 -0
  82. package/templates/creator/app/auth-form.tsx +129 -0
  83. package/templates/creator/app/dashboard/dashboard-client.tsx +297 -0
  84. package/templates/creator/app/dashboard/page.tsx +70 -0
  85. package/templates/creator/app/error.tsx +26 -0
  86. package/templates/creator/app/globals.css +148 -0
  87. package/templates/creator/app/layout.tsx +160 -0
  88. package/templates/creator/app/login/page.tsx +39 -0
  89. package/templates/creator/app/newsletter-signup.tsx +162 -0
  90. package/templates/creator/app/not-found.tsx +19 -0
  91. package/templates/creator/app/page.tsx +160 -0
  92. package/templates/creator/app/robots.ts +12 -0
  93. package/templates/creator/app/sitemap.ts +9 -0
  94. package/templates/creator/app.ts +134 -0
  95. package/templates/creator/components/marketing.tsx +148 -0
  96. package/templates/creator/components/section-scroller.tsx +35 -0
  97. package/templates/creator/components/ui/button.tsx +56 -0
  98. package/templates/creator/components/ui/card.tsx +90 -0
  99. package/templates/creator/components.json +20 -0
  100. package/templates/creator/functions/subscribe.ts +82 -0
  101. package/templates/creator/functions/subscriberStats.ts +75 -0
  102. package/templates/creator/gitignore +10 -0
  103. package/templates/creator/lib/owner.ts +26 -0
  104. package/templates/creator/lib/site.config.ts +173 -0
  105. package/templates/creator/lib/stats.ts +30 -0
  106. package/templates/creator/lib/utils.ts +10 -0
  107. package/templates/creator/package.json +34 -0
  108. package/templates/creator/tsconfig.json +18 -0
  109. package/templates/default/app/layout.tsx +26 -27
  110. package/templates/default/app/page.tsx +90 -274
  111. package/templates/default/lib/products.ts +9 -122
  112. package/templates/default/lib/site.config.ts +739 -0
  113. package/templates/default/lib/site.ts +14 -261
  114. package/templates/directory/.env.example +12 -0
  115. package/templates/directory/AGENTS.md +61 -0
  116. package/templates/directory/README.md +80 -0
  117. package/templates/directory/app/auth-form.tsx +129 -0
  118. package/templates/directory/app/dashboard/dashboard-client.tsx +205 -0
  119. package/templates/directory/app/dashboard/page.tsx +70 -0
  120. package/templates/directory/app/directory-browse.tsx +328 -0
  121. package/templates/directory/app/error.tsx +26 -0
  122. package/templates/directory/app/globals.css +148 -0
  123. package/templates/directory/app/layout.tsx +171 -0
  124. package/templates/directory/app/login/page.tsx +39 -0
  125. package/templates/directory/app/not-found.tsx +19 -0
  126. package/templates/directory/app/page.tsx +50 -0
  127. package/templates/directory/app/robots.ts +12 -0
  128. package/templates/directory/app/sitemap.ts +9 -0
  129. package/templates/directory/app/submit/page.tsx +30 -0
  130. package/templates/directory/app/submit-form.tsx +151 -0
  131. package/templates/directory/app.ts +146 -0
  132. package/templates/directory/components/marketing.tsx +148 -0
  133. package/templates/directory/components/section-scroller.tsx +35 -0
  134. package/templates/directory/components/ui/button.tsx +56 -0
  135. package/templates/directory/components/ui/card.tsx +90 -0
  136. package/templates/directory/components.json +20 -0
  137. package/templates/directory/functions/approveSubmission.ts +45 -0
  138. package/templates/directory/functions/rejectSubmission.ts +20 -0
  139. package/templates/directory/functions/seedListings.ts +33 -0
  140. package/templates/directory/functions/submissionsForOwner.ts +29 -0
  141. package/templates/directory/functions/submitListing.ts +63 -0
  142. package/templates/directory/functions/upvote.ts +24 -0
  143. package/templates/directory/gitignore +10 -0
  144. package/templates/directory/lib/directory.ts +45 -0
  145. package/templates/directory/lib/owner.ts +26 -0
  146. package/templates/directory/lib/site.config.ts +130 -0
  147. package/templates/directory/lib/utils.ts +10 -0
  148. package/templates/directory/package.json +34 -0
  149. package/templates/directory/tsconfig.json +18 -0
  150. package/templates/local-service/.env.example +12 -0
  151. package/templates/local-service/AGENTS.md +61 -0
  152. package/templates/local-service/README.md +82 -0
  153. package/templates/local-service/app/auth-form.tsx +129 -0
  154. package/templates/local-service/app/booking-widget.tsx +399 -0
  155. package/templates/local-service/app/dashboard/dashboard-client.tsx +304 -0
  156. package/templates/local-service/app/dashboard/page.tsx +63 -0
  157. package/templates/local-service/app/error.tsx +26 -0
  158. package/templates/local-service/app/globals.css +148 -0
  159. package/templates/local-service/app/layout.tsx +151 -0
  160. package/templates/local-service/app/login/page.tsx +39 -0
  161. package/templates/local-service/app/not-found.tsx +19 -0
  162. package/templates/local-service/app/page.tsx +233 -0
  163. package/templates/local-service/app/robots.ts +12 -0
  164. package/templates/local-service/app/sitemap.ts +9 -0
  165. package/templates/local-service/app.ts +131 -0
  166. package/templates/local-service/components/marketing.tsx +162 -0
  167. package/templates/local-service/components/section-scroller.tsx +35 -0
  168. package/templates/local-service/components/ui/button.tsx +56 -0
  169. package/templates/local-service/components/ui/card.tsx +90 -0
  170. package/templates/local-service/components.json +20 -0
  171. package/templates/local-service/functions/bookingsForOwner.ts +30 -0
  172. package/templates/local-service/functions/cancelBooking.ts +27 -0
  173. package/templates/local-service/functions/confirmBooking.ts +18 -0
  174. package/templates/local-service/functions/createBooking.ts +98 -0
  175. package/templates/local-service/gitignore +10 -0
  176. package/templates/local-service/lib/booking.ts +24 -0
  177. package/templates/local-service/lib/owner.ts +26 -0
  178. package/templates/local-service/lib/site.config.ts +232 -0
  179. package/templates/local-service/lib/slots.ts +97 -0
  180. package/templates/local-service/lib/utils.ts +10 -0
  181. package/templates/local-service/package.json +34 -0
  182. package/templates/local-service/tsconfig.json +18 -0
  183. package/templates/marketplace/.env.example +9 -0
  184. package/templates/marketplace/AGENTS.md +61 -0
  185. package/templates/marketplace/README.md +78 -0
  186. package/templates/marketplace/app/_components/CategoryIcon.tsx +40 -0
  187. package/templates/marketplace/app/error.tsx +26 -0
  188. package/templates/marketplace/app/globals.css +64 -0
  189. package/templates/marketplace/app/layout.tsx +60 -0
  190. package/templates/marketplace/app/listing/[id]/page.tsx +163 -0
  191. package/templates/marketplace/app/me/page.tsx +15 -0
  192. package/templates/marketplace/app/not-found.tsx +20 -0
  193. package/templates/marketplace/app/page.tsx +159 -0
  194. package/templates/marketplace/app/robots.ts +12 -0
  195. package/templates/marketplace/app/sell/page.tsx +26 -0
  196. package/templates/marketplace/app/sitemap.ts +14 -0
  197. package/templates/marketplace/app.ts +190 -0
  198. package/templates/marketplace/client/AuthNav.tsx +46 -0
  199. package/templates/marketplace/client/LiveTicker.tsx +104 -0
  200. package/templates/marketplace/client/LoginCard.tsx +130 -0
  201. package/templates/marketplace/client/MarketProvider.tsx +148 -0
  202. package/templates/marketplace/client/MyMarket.tsx +180 -0
  203. package/templates/marketplace/client/OfferPanel.tsx +355 -0
  204. package/templates/marketplace/client/SeedOnEmpty.tsx +26 -0
  205. package/templates/marketplace/client/SellForm.tsx +160 -0
  206. package/templates/marketplace/client/WatchButton.tsx +88 -0
  207. package/templates/marketplace/client/market.ts +341 -0
  208. package/templates/marketplace/functions/buyNow.ts +78 -0
  209. package/templates/marketplace/functions/makeOffer.ts +65 -0
  210. package/templates/marketplace/functions/respondToOffer.ts +62 -0
  211. package/templates/marketplace/functions/seedMarket.ts +90 -0
  212. package/templates/marketplace/gitignore +10 -0
  213. package/templates/marketplace/package.json +35 -0
  214. package/templates/marketplace/tsconfig.json +14 -0
  215. package/templates/marketplace/ui/badge.tsx +30 -0
  216. package/templates/marketplace/ui/button.tsx +49 -0
  217. package/templates/marketplace/ui/card.tsx +48 -0
  218. package/templates/marketplace/ui/input.tsx +17 -0
  219. package/templates/marketplace/ui/label.tsx +18 -0
  220. package/templates/marketplace/ui/textarea.tsx +17 -0
  221. package/templates/marketplace/ui/tokens.css +32 -0
  222. package/templates/marketplace/ui/utils.ts +6 -0
  223. package/templates/restaurant/.env.example +12 -0
  224. package/templates/restaurant/AGENTS.md +61 -0
  225. package/templates/restaurant/README.md +77 -0
  226. package/templates/restaurant/app/auth-form.tsx +129 -0
  227. package/templates/restaurant/app/dashboard/dashboard-client.tsx +263 -0
  228. package/templates/restaurant/app/dashboard/page.tsx +59 -0
  229. package/templates/restaurant/app/error.tsx +26 -0
  230. package/templates/restaurant/app/globals.css +148 -0
  231. package/templates/restaurant/app/layout.tsx +151 -0
  232. package/templates/restaurant/app/login/page.tsx +39 -0
  233. package/templates/restaurant/app/not-found.tsx +19 -0
  234. package/templates/restaurant/app/page.tsx +194 -0
  235. package/templates/restaurant/app/reservation-widget.tsx +359 -0
  236. package/templates/restaurant/app/robots.ts +12 -0
  237. package/templates/restaurant/app/sitemap.ts +9 -0
  238. package/templates/restaurant/app.ts +115 -0
  239. package/templates/restaurant/components/marketing.tsx +162 -0
  240. package/templates/restaurant/components/section-scroller.tsx +35 -0
  241. package/templates/restaurant/components/ui/button.tsx +56 -0
  242. package/templates/restaurant/components/ui/card.tsx +90 -0
  243. package/templates/restaurant/components.json +20 -0
  244. package/templates/restaurant/functions/cancelReservation.ts +26 -0
  245. package/templates/restaurant/functions/confirmReservation.ts +17 -0
  246. package/templates/restaurant/functions/createReservation.ts +92 -0
  247. package/templates/restaurant/functions/reservationsForOwner.ts +28 -0
  248. package/templates/restaurant/gitignore +10 -0
  249. package/templates/restaurant/lib/owner.ts +26 -0
  250. package/templates/restaurant/lib/reservation.ts +22 -0
  251. package/templates/restaurant/lib/site.config.ts +218 -0
  252. package/templates/restaurant/lib/slots.ts +55 -0
  253. package/templates/restaurant/lib/utils.ts +10 -0
  254. package/templates/restaurant/package.json +34 -0
  255. package/templates/restaurant/tsconfig.json +18 -0
  256. package/templates/shop/.env.example +32 -0
  257. package/templates/shop/AGENTS.md +61 -0
  258. package/templates/shop/README.md +102 -0
  259. package/templates/shop/app/auth-form.tsx +129 -0
  260. package/templates/shop/app/dashboard/dashboard-client.tsx +264 -0
  261. package/templates/shop/app/dashboard/page.tsx +59 -0
  262. package/templates/shop/app/error.tsx +26 -0
  263. package/templates/shop/app/globals.css +148 -0
  264. package/templates/shop/app/layout.tsx +160 -0
  265. package/templates/shop/app/login/page.tsx +39 -0
  266. package/templates/shop/app/not-found.tsx +19 -0
  267. package/templates/shop/app/page.tsx +95 -0
  268. package/templates/shop/app/robots.ts +12 -0
  269. package/templates/shop/app/shop-client.tsx +436 -0
  270. package/templates/shop/app/sitemap.ts +9 -0
  271. package/templates/shop/app/success/page.tsx +33 -0
  272. package/templates/shop/app.ts +134 -0
  273. package/templates/shop/components/marketing.tsx +96 -0
  274. package/templates/shop/components/section-scroller.tsx +35 -0
  275. package/templates/shop/components/ui/button.tsx +56 -0
  276. package/templates/shop/components/ui/card.tsx +90 -0
  277. package/templates/shop/components.json +20 -0
  278. package/templates/shop/functions/cancelOrder.ts +33 -0
  279. package/templates/shop/functions/checkout.ts +130 -0
  280. package/templates/shop/functions/fulfillOrder.ts +17 -0
  281. package/templates/shop/functions/markGroupPaid.ts +26 -0
  282. package/templates/shop/functions/ordersForOwner.ts +28 -0
  283. package/templates/shop/functions/releaseGroup.ts +36 -0
  284. package/templates/shop/functions/reserveCart.ts +87 -0
  285. package/templates/shop/functions/restockProduct.ts +23 -0
  286. package/templates/shop/functions/seedProducts.ts +30 -0
  287. package/templates/shop/functions/stripeWebhook.ts +72 -0
  288. package/templates/shop/gitignore +10 -0
  289. package/templates/shop/lib/owner.ts +26 -0
  290. package/templates/shop/lib/shop.ts +45 -0
  291. package/templates/shop/lib/site.config.ts +198 -0
  292. package/templates/shop/lib/utils.ts +10 -0
  293. package/templates/shop/package.json +35 -0
  294. package/templates/shop/tsconfig.json +18 -0
  295. package/templates/waitlist/.env.example +12 -0
  296. package/templates/waitlist/AGENTS.md +61 -0
  297. package/templates/waitlist/README.md +81 -0
  298. package/templates/waitlist/app/auth-form.tsx +129 -0
  299. package/templates/waitlist/app/dashboard/dashboard-client.tsx +297 -0
  300. package/templates/waitlist/app/dashboard/page.tsx +70 -0
  301. package/templates/waitlist/app/error.tsx +26 -0
  302. package/templates/waitlist/app/globals.css +148 -0
  303. package/templates/waitlist/app/layout.tsx +158 -0
  304. package/templates/waitlist/app/login/page.tsx +39 -0
  305. package/templates/waitlist/app/not-found.tsx +19 -0
  306. package/templates/waitlist/app/page.tsx +119 -0
  307. package/templates/waitlist/app/robots.ts +12 -0
  308. package/templates/waitlist/app/sitemap.ts +9 -0
  309. package/templates/waitlist/app/waitlist-hero.tsx +219 -0
  310. package/templates/waitlist/app.ts +134 -0
  311. package/templates/waitlist/components/marketing.tsx +96 -0
  312. package/templates/waitlist/components/ui/button.tsx +56 -0
  313. package/templates/waitlist/components/ui/card.tsx +90 -0
  314. package/templates/waitlist/components.json +20 -0
  315. package/templates/waitlist/functions/joinWaitlist.ts +82 -0
  316. package/templates/waitlist/functions/waitlistStats.ts +75 -0
  317. package/templates/waitlist/gitignore +10 -0
  318. package/templates/waitlist/lib/owner.ts +26 -0
  319. package/templates/waitlist/lib/site.config.ts +178 -0
  320. package/templates/waitlist/lib/stats.ts +30 -0
  321. package/templates/waitlist/lib/utils.ts +10 -0
  322. package/templates/waitlist/package.json +34 -0
  323. package/templates/waitlist/tsconfig.json +18 -0
@@ -1,261 +1,14 @@
1
- // Content for the rest of the marketing site solutions, resources, company,
2
- // and comparison pages. Each collection drives a dynamic route AND the footer
3
- // columns, so the links and the pages can never drift. Fictional demo copy —
4
- // swap it for your own.
5
-
6
- export type ContentSection = { title: string; body: string };
7
-
8
- export type SitePage = {
9
- slug: string;
10
- navLabel: string; // label in nav/footer
11
- eyebrow: string;
12
- title: string; // hero headline
13
- summary: string;
14
- sections: ContentSection[];
15
- };
16
-
17
- export const SOLUTIONS: SitePage[] = [
18
- {
19
- slug: "startups",
20
- navLabel: "For startups",
21
- eyebrow: "Solutions",
22
- title: "Move fast without losing the thread.",
23
- summary:
24
- "Keep a small team aligned as everything changes weekly. Acme gives you one place to plan, build, and ship before the next pivot.",
25
- sections: [
26
- { title: "One tool, not ten", body: "Projects, tasks, and docs in one place, so you are not paying for or stitching together five apps." },
27
- { title: "Set up in minutes", body: "No admin overhead. Invite the team and start working the same day." },
28
- { title: "Grows with you", body: "The same workspace works at five people and at fifty." },
29
- ],
30
- },
31
- {
32
- slug: "agencies",
33
- navLabel: "For agencies",
34
- eyebrow: "Solutions",
35
- title: "Run every client like clockwork.",
36
- summary:
37
- "Give each client their own space, keep the work organized, and show progress without a status meeting.",
38
- sections: [
39
- { title: "A space per client", body: "Separate workspaces keep every engagement tidy and private." },
40
- { title: "Shareable views", body: "Send clients a read-only view of exactly what is in flight." },
41
- { title: "Reusable templates", body: "Start every new engagement from a proven playbook." },
42
- ],
43
- },
44
- {
45
- slug: "enterprise",
46
- navLabel: "For enterprise",
47
- eyebrow: "Solutions",
48
- title: "Scale without the chaos.",
49
- summary:
50
- "Bring hundreds of people into one system of record, with the controls and visibility a larger org needs.",
51
- sections: [
52
- { title: "SSO and roles", body: "Single sign-on and granular roles keep access where it belongs." },
53
- { title: "Audit log", body: "A complete record of who changed what, and when." },
54
- { title: "Rollups", body: "See progress across teams and departments in one view." },
55
- ],
56
- },
57
- {
58
- slug: "teams",
59
- navLabel: "For teams",
60
- eyebrow: "Solutions",
61
- title: "Built for how your team works.",
62
- summary:
63
- "Whether you build, design, market, or support, Acme adapts to your process instead of forcing a new one.",
64
- sections: [
65
- { title: "Your workflow", body: "Custom statuses and fields match the way your team already works." },
66
- { title: "Cross-team work", body: "Hand work between teams without it falling through a crack." },
67
- { title: "Less status-chasing", body: "Everyone sees the same live picture, so updates write themselves." },
68
- ],
69
- },
70
- ];
71
-
72
- export const RESOURCES: SitePage[] = [
73
- {
74
- slug: "docs",
75
- navLabel: "Docs",
76
- eyebrow: "Resources",
77
- title: "Documentation.",
78
- summary: "Everything you need to set up Acme and get your team productive.",
79
- sections: [
80
- { title: "Getting started", body: "Create a workspace, invite your team, and ship your first project." },
81
- { title: "Guides", body: "Deep dives on projects, tasks, docs, automations, and analytics." },
82
- { title: "API", body: "Build on the typed Acme API and webhooks." },
83
- ],
84
- },
85
- {
86
- slug: "guides",
87
- navLabel: "Guides",
88
- eyebrow: "Resources",
89
- title: "Guides and playbooks.",
90
- summary: "Practical walkthroughs for getting the most out of Acme.",
91
- sections: [
92
- { title: "Run a sprint", body: "Plan, track, and review a two-week cycle in Acme." },
93
- { title: "Automate intake", body: "Route incoming work to the right team automatically." },
94
- { title: "Report to leadership", body: "Build a dashboard that answers the questions you get asked." },
95
- ],
96
- },
97
- {
98
- slug: "changelog",
99
- navLabel: "Changelog",
100
- eyebrow: "Resources",
101
- title: "What's new.",
102
- summary: "Every improvement we ship, in one place.",
103
- sections: [
104
- { title: "This week", body: "Faster search, a redesigned task list, and new automation triggers." },
105
- { title: "Last week", body: "Timeline view for projects and CSV export for analytics." },
106
- { title: "Earlier", body: "Webhooks, custom fields, and version history for docs." },
107
- ],
108
- },
109
- {
110
- slug: "api",
111
- navLabel: "API reference",
112
- eyebrow: "Resources",
113
- title: "API reference.",
114
- summary: "A typed REST API and webhooks for everything in Acme.",
115
- sections: [
116
- { title: "Authentication", body: "API keys scoped to a workspace, revocable at any time." },
117
- { title: "Resources", body: "Projects, tasks, docs, and automations, all over the same API." },
118
- { title: "Webhooks", body: "Subscribe to events and react to changes in real time." },
119
- ],
120
- },
121
- {
122
- slug: "status",
123
- navLabel: "Status",
124
- eyebrow: "Resources",
125
- title: "System status.",
126
- summary: "Live status for every Acme service.",
127
- sections: [
128
- { title: "API", body: "Operational — 99.99% over the last 90 days." },
129
- { title: "Web app", body: "Operational — no incidents this week." },
130
- { title: "Webhooks", body: "Operational — delivering within seconds." },
131
- ],
132
- },
133
- ];
134
-
135
- export const COMPANY: SitePage[] = [
136
- {
137
- slug: "about",
138
- navLabel: "About",
139
- eyebrow: "Company",
140
- title: "About Acme.",
141
- summary: "We build the workspace we always wanted: fast, focused, and a pleasure to use.",
142
- sections: [
143
- { title: "Our mission", body: "Help teams do their best work without fighting their tools." },
144
- { title: "How we work", body: "Small team, weekly releases, every decision close to the user." },
145
- { title: "Where we are", body: "Remote-first, with people across a dozen time zones." },
146
- ],
147
- },
148
- {
149
- slug: "blog",
150
- navLabel: "Blog",
151
- eyebrow: "Company",
152
- title: "The Acme blog.",
153
- summary: "Notes on building Acme, and on building product in general.",
154
- sections: [
155
- { title: "Why one tool beats ten", body: "The hidden cost of stitching your stack together." },
156
- { title: "Shipping weekly", body: "How a small team keeps a steady release cadence." },
157
- { title: "Designing for focus", body: "The principles behind the Acme interface." },
158
- ],
159
- },
160
- {
161
- slug: "careers",
162
- navLabel: "Careers",
163
- eyebrow: "Company",
164
- title: "Work at Acme.",
165
- summary: "We are a small team that ships a lot. If that sounds good, come build with us.",
166
- sections: [
167
- { title: "Engineering", body: "Full-stack engineers who care about craft and speed." },
168
- { title: "Design", body: "Product designers who sweat the details." },
169
- { title: "Support", body: "People who love helping customers succeed." },
170
- ],
171
- },
172
- {
173
- slug: "contact",
174
- navLabel: "Contact",
175
- eyebrow: "Company",
176
- title: "Get in touch.",
177
- summary: "Questions, feedback, or just want to say hi? We would love to hear from you.",
178
- sections: [
179
- { title: "Sales", body: "Talk through whether Acme is a fit for your team." },
180
- { title: "Support", body: "Get help from a human, usually within a few hours." },
181
- { title: "Press", body: "Logos, screenshots, and company facts for the press." },
182
- ],
183
- },
184
- {
185
- slug: "privacy",
186
- navLabel: "Privacy",
187
- eyebrow: "Company",
188
- title: "Privacy.",
189
- summary: "How Acme handles your data, in plain language.",
190
- sections: [
191
- { title: "What we collect", body: "Only what we need to run the product and support you." },
192
- { title: "How we use it", body: "To operate Acme — never sold, never rented." },
193
- { title: "Your control", body: "Export or delete your data at any time." },
194
- ],
195
- },
196
- ];
197
-
198
- export type Comparison = {
199
- slug: string;
200
- navLabel: string;
201
- competitor: string;
202
- title: string;
203
- summary: string;
204
- rows: { dim: string; acme: string; them: string }[];
205
- };
206
-
207
- // Generic, made-up competitors so the template ships no real brand names.
208
- export const COMPARISONS: Comparison[] = [
209
- {
210
- slug: "beacon",
211
- navLabel: "Acme vs Beacon",
212
- competitor: "Beacon",
213
- title: "Acme vs Beacon",
214
- summary:
215
- "Beacon is a capable tool, but it splits projects, docs, and automation across separate products. Acme brings them into one fast workspace.",
216
- rows: [
217
- { dim: "Projects, tasks, and docs", acme: "In one workspace", them: "Separate products" },
218
- { dim: "Real-time sync", acme: "Built in", them: "Add-on" },
219
- { dim: "Automations", acme: "Included", them: "Higher tier" },
220
- { dim: "Typed API", acme: "Yes", them: "Partial" },
221
- { dim: "Setup time", acme: "Minutes", them: "Hours" },
222
- ],
223
- },
224
- {
225
- slug: "orbit",
226
- navLabel: "Acme vs Orbit",
227
- competitor: "Orbit",
228
- title: "Acme vs Orbit",
229
- summary:
230
- "Orbit is flexible but slow to set up and heavy to run. Acme gives you the same power with a fraction of the overhead.",
231
- rows: [
232
- { dim: "Time to first project", acme: "Same day", them: "Onboarding required" },
233
- { dim: "Speed", acme: "Instant, real-time", them: "Page reloads" },
234
- { dim: "Per-seat pricing", acme: "No surprises", them: "Adds up fast" },
235
- { dim: "Analytics", acme: "Built in", them: "Separate tool" },
236
- { dim: "Learning curve", acme: "Gentle", them: "Steep" },
237
- ],
238
- },
239
- {
240
- slug: "tempo",
241
- navLabel: "Acme vs Tempo",
242
- competitor: "Tempo",
243
- title: "Acme vs Tempo",
244
- summary:
245
- "Tempo is built for managers; Acme is built for the whole team. Everyone gets a fast, shared view of the work.",
246
- rows: [
247
- { dim: "Designed for", acme: "The whole team", them: "Managers" },
248
- { dim: "Daily driver", acme: "Yes", them: "Reporting layer" },
249
- { dim: "Docs included", acme: "Yes", them: "No" },
250
- { dim: "Automations", acme: "Included", them: "Limited" },
251
- { dim: "Self-serve", acme: "Yes", them: "Sales-led" },
252
- ],
253
- },
254
- ];
255
-
256
- export function bySlug<T extends { slug: string }>(
257
- list: T[],
258
- slug: string,
259
- ): T | undefined {
260
- return list.find((x) => x.slug === slug);
261
- }
1
+ // The marketing content (solutions, resources, company, comparisons) now lives
2
+ // in the single site config so the whole template can be rebranded from one
3
+ // file. This module re-exports it so existing imports (`@/lib/site`) keep
4
+ // working. Edit lib/site.config.ts.
5
+ export {
6
+ SOLUTIONS,
7
+ RESOURCES,
8
+ COMPANY,
9
+ COMPARISONS,
10
+ bySlug,
11
+ type ContentSection,
12
+ type SitePage,
13
+ type Comparison,
14
+ } from "./site.config";
@@ -0,0 +1,12 @@
1
+ # Copy to `.env` and fill in. `pylon dev` loads `.env` automatically.
2
+
3
+ # ── Owner / curator (required to use the dashboard) ──────────────────────────
4
+ # A directory is single-tenant: one curator. The /dashboard moderation queue is
5
+ # unlocked only for the account whose email matches this value, and the
6
+ # owner-only function refuses to return submissions (with their PII) otherwise.
7
+ # Set this to the email you'll sign in with, then create that account at /login.
8
+ PYLON_OWNER_EMAIL=you@yourdirectory.com
9
+
10
+ # ── Site URL (optional) ──────────────────────────────────────────────────────
11
+ # Used by robots.txt + sitemap.xml. Point it at your real domain in production.
12
+ # SITE_URL=https://yourdirectory.com
@@ -0,0 +1,61 @@
1
+ # AGENTS.md — working in a Pylon project
2
+
3
+ Operating rules for a coding agent in this Pylon app. Pylon is a Rails-like framework for realtime apps: you declare entities, policies, and server functions in TypeScript, and a single Rust binary (`pylon`) serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR — one process, one port. The full API reference is at **/llms-full.txt** (served at `/llms-full.txt`; in the repo at `apps/web/public/llms-full.txt`). Read it before guessing an API name.
4
+
5
+ ## Directory conventions
6
+
7
+ **Unified SSR app:**
8
+ - `app.ts` — data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
9
+ - `app/` — file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
10
+ - `app/globals.css` — Tailwind v4 entrypoint (auto-compiled and injected).
11
+ - `functions/` — server functions, one per file, `default`-exported.
12
+ - `.pylon/` — local dev state (sqlite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
13
+
14
+ **Monorepo app:** backend is `apps/api/` (entry `apps/api/schema.ts`, handlers in `apps/api/functions/`); frontend in `apps/web/`. `pylon.manifest.json` / `pylon.client.ts` are generated — do not hand-edit.
15
+
16
+ ## The core authoring loop
17
+
18
+ 1. **Define an entity** — `entity("Thing", { name: field.string(), done: field.boolean().default(false) })`. Modifiers: `.optional()`, `.unique()`, `.readonly()` (settable on insert, rejected on client update — use for `authorId`/`orgId`), `.serverOnly()` (never in HTTP responses), `.encrypted()` (AEAD at rest, needs `PYLON_ENCRYPTION_KEY`), `.crdt("text")` (collaborative).
19
+ 2. **Write a policy** — `policy({ entity: "Thing", allowRead, allowInsert, allowUpdate, allowDelete })` with CEL-like expressions over `auth.*` / `data.*` (e.g. `"auth.userId == data.authorId"`). **Omitted actions DENY by default.** Wide-open dev policies (`allow*: "true"`) are flagged by `pylon lint` — tighten before shipping.
20
+ 3. **Author a function** in `functions/<name>.ts` — `query` (read-only), `mutation` (transactional read+write), or `action` (external I/O, no direct `ctx.db`). Import `{ query, mutation, action, v }` from `@pylonsync/functions`. `auth` defaults to `"user"` (secure-by-default); set `"public"` explicitly for unauthenticated access. Use `ctx.db.*`, `ctx.auth.userId`, `ctx.error(code, msg)`.
21
+ 4. **Read it on the client** — `db.useQuery("Thing")` (live, re-renders on any write) or `db.useQueryOne("Thing", id)`. Call functions with `db.fn(name, args)` / `callFn`. On SSR pages, read via `use(serverData.list("Thing"))` inside `<Suspense>`.
22
+
23
+ ## Key gotchas
24
+
25
+ - **Policies deny by default; server functions BYPASS them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full DB access — enforce trust with `ctx.auth` checks inside the handler, not policies.
26
+ - **Type page props from the SDK, don't hand-roll them.** `import type { PageProps, Metadata } from "@pylonsync/react"`. Every page/layout gets `{ url, params, searchParams, auth, response, serverData }`; `PageProps<{ slug: string }>` types a `[slug]` route's params. Request headers/cookies are intentionally NOT on `PageProps` — they're server-only and stripped from hydration, so reading them in the render would mismatch.
27
+ - **Anonymous output caching is opt-in + earned.** `export const revalidate = 60` (seconds) on a page makes it CDN-cacheable (`public, s-maxage=60`) — but ONLY if the render is auth-INDEPENDENT: it must NOT read `props.auth` (reading it at all opts out, even for anonymous), set no cookie, and the app must not run strict per-caller policies (`PYLON_STRICT_FN_POLICIES`). `export const dynamic = "force-static"` caches until the next deploy; `"force-dynamic"` never caches. Fail-closed: without the opt-in (or if any condition fails) the page is `no-cache`. A page that reads `auth` or sets a cookie is never shared. The SAME earned render is also kept in an **origin disk cache** (`.pylon/.cache/ssr`): a cookie-less GET with no query string is served straight off disk for the TTL — skipping the render entirely — then re-rendered live when stale. The disk cache is namespaced per deploy (wiped on each new build) and OFF in `pylon dev` (so an edit is never masked by a stale entry); invalidation is by the `revalidate` TTL or the next deploy.
28
+ - **No-JS forms use `route.ts` + `<Form>`.** Drop `app/.../route.ts` exporting `export const POST: RouteHandler = async ({ form, db, response, auth }) => { await db.insert("X", {...}); response.redirect("/x?ok=1"); }` (303 POST-redirect-GET by default). Render `<Form action="/x">` (from @pylonsync/react) with plain `<input name=...>` — works with JS off (native POST→handler→redirect) and is enhanced to no-reload when JS is on. The handler's `db` is read+write (mutation trust model — gate on `auth`); CSRF is automatic (Origin gate + SameSite=Lax). Multipart/file uploads aren't supported yet — use urlencoded forms + `/api/files`.
29
+ - **`loading.tsx` streams a skeleton while the page's data resolves.** Drop `app/.../loading.tsx` (default export, page props) and the nearest one becomes a route-level Suspense fallback: Pylon flushes the shell + skeleton immediately, then reveals the real page when its top-level `use(serverData…)` resolves (no blank page). It only shows when the PAGE suspends — a page that wraps its own `<Suspense>` around a child (like `/dashboard` in this template) handles that itself. The skeleton is SERVER-ONLY: don't read `serverData` in it. A page with no `loading.tsx` is buffered (unchanged).
30
+ - **`export const streaming = true` streams a page's OWN inner `<Suspense>` boundaries.** Without it (and without a `loading.tsx`), a page is BUFFERED — the whole document, including suspended children, resolves before the first byte. Opt in and the shell + each inner `<Suspense>` fallback flush immediately, then each boundary's real content streams in as its data resolves (multi-boundary progressive streaming). It's opt-in because it changes the response timing contract: a streaming render commits its HTTP head BEFORE suspended subtrees finish, so (a) it's never CDN/disk cacheable — don't combine with `export const revalidate`; (b) `response.setStatus/setCookie/redirect/notFound` only take effect from the SYNCHRONOUS shell render — a call from inside a suspended subtree is dropped (the runtime logs a loud warning naming what was lost); (c) a `throw` from a deep `<Suspense>` child resolves via its nearest `error.tsx` at HTTP 200, not a 5xx. Hydration is clean for any number of boundaries (the data blob ships before hydration runs). Type the config with `import type { RouteSegmentConfig } from "@pylonsync/react"`.
31
+ - **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`) — `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
32
+ - **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive — during SSR they return defaults (empty params / "/"); for server-side URL values read the `url` / `searchParams` page props.
33
+ - **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"` — `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
34
+ - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
35
+ - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready — status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
36
+ - **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db` — use `ctx.runQuery` / `ctx.runMutation`.
37
+ - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
38
+ - **There is no `ctx.files` or `defineWorkflow`/`defineJob`.** Files go through `<FileUpload>` + `/api/files/*`; deferred execution is `ctx.scheduler.runAfter/runAt/cancel`.
39
+
40
+ ## Use the CLI — don't guess
41
+
42
+ | Need | Command |
43
+ |---|---|
44
+ | Run the app (SSR + API, hot reload, one port `:4321`) | `pylon dev` (or `npm run dev`) |
45
+ | Regenerate manifest + typed client | `pylon codegen` (Swift client: `pylon codegen client --target swift`) |
46
+ | Validate / diff / push schema | `pylon schema check` \| `diff` \| `push` |
47
+ | Migrations | `pylon migrate create <name>` \| `plan` \| `apply` |
48
+ | Lint policies (PYL001–PYL004) | `pylon lint --strict` |
49
+ | Tests | `pylon test` |
50
+ | Adversarial security probe | `pylon test:security` |
51
+ | Inspect cloud request logs (agent-safe) | `pylon logs --json --limit 50` |
52
+ | Inspect data / entities | `pylon data entities` \| `pylon data list <Entity>` |
53
+ | Call a function | `pylon fn <name> key=value` |
54
+ | Health snapshot | `pylon status` |
55
+ | Build for prod | `pylon build` |
56
+ | Deploy (Pylon Cloud by default) | `pylon deploy` |
57
+ | Look up an error code | `pylon explain <CODE>` |
58
+
59
+ `--json` works on every command for machine-readable output. Prefer one-shot/agent-safe flags (`pylon logs --limit N`, not a blocking `--follow`).
60
+
61
+ For full signatures, env vars, the complete CLI, and SSR/client/server-primitive details: **/llms-full.txt**.
@@ -0,0 +1,80 @@
1
+ # __APP_NAME__
2
+
3
+ A curated, searchable **directory** built with [Pylon](https://pylonsync.com) —
4
+ live full-text search + facets, community upvotes, and a moderated submit flow,
5
+ all from one binary on one port. No Next.js, no search service.
6
+
7
+ It's the template that shows off Pylon's **full-text search**: the browse page
8
+ is a live `db.useSearch` over the listing table — type in the box and results +
9
+ facet counts update instantly, and vote counts tick up across every open tab.
10
+
11
+ ## Develop
12
+
13
+ ```bash
14
+ __RUN_DEV__
15
+ ```
16
+
17
+ Open http://localhost:4321 — the directory seeds itself on first load. Search,
18
+ filter by category, and upvote. Then **open a second tab** and upvote something —
19
+ watch the count rise in both.
20
+
21
+ ## How it works
22
+
23
+ - **Live faceted search.** `app/directory-browse.tsx` calls
24
+ `db.useSearch("Listing", { query, filters, facets, sort })`, declared by the
25
+ `search: { text, facets, sortable }` block on the `Listing` entity in `app.ts`
26
+ (Pylon builds FTS5 + facet shadow tables). It re-runs on every keystroke AND
27
+ whenever the table is written — so it doubles as the realtime layer.
28
+ - **Live upvotes.** The public `upvote` mutation bumps `Listing.votes` under a
29
+ per-listing advisory lock; because `useSearch` is live, the new count appears
30
+ in every open tab instantly.
31
+ - **Moderated submissions.** `submitListing` (public) writes a pending
32
+ `Submission`; the curator approves it from `/dashboard`, which copies the
33
+ public fields into a new `Listing` (`approveSubmission`).
34
+
35
+ ## Privacy — read this
36
+
37
+ The `Submission` entity holds the submitter's name + email (PII), so its policy
38
+ in `app.ts` **denies every client read and write**. The public directory only
39
+ reads `Listing` (no PII). Submissions come back only through the owner-gated
40
+ `submissionsForOwner`, and `approveSubmission` copies *only* the PII-free fields
41
+ into the public `Listing` — the submitter's contact details never become public.
42
+
43
+ ## The curator dashboard
44
+
45
+ `/dashboard` is the moderation queue: pending submissions (with submitter
46
+ details), Approve / Reject, and a live count of published listings.
47
+
48
+ Set `PYLON_OWNER_EMAIL` in `.env` (see `.env.example`) to the email you'll sign
49
+ in with, then create that account at `/login`.
50
+
51
+ ## Rebrand it
52
+
53
+ Everything lives in **`lib/site.config.ts`** — brand, colors, hero copy, the
54
+ category list, the starter listings (which seed the directory), and the submit
55
+ copy. Edit that one file and the whole directory re-themes; it re-seeds on a
56
+ fresh database.
57
+
58
+ ## Layout
59
+
60
+ ```
61
+ app.ts Listing (public, FTS) + Submission (PII) + User
62
+ lib/site.config.ts ALL copy + brand + categories + seed listings
63
+ functions/seedListings.ts idempotent seed from config
64
+ functions/submitListing.ts public mutation: write a pending Submission (PII)
65
+ functions/upvote.ts public mutation: bump Listing.votes (live)
66
+ functions/submissionsForOwner.ts owner-only query: queue + submitter PII
67
+ functions/{approve,reject}Submission.ts owner-only moderation
68
+ app/page.tsx hero + browse island
69
+ app/directory-browse.tsx client island: live db.useSearch + facets + votes
70
+ app/submit/page.tsx, submit-form.tsx the submit flow
71
+ app/dashboard/ curator moderation queue (auth-gated, live)
72
+ ```
73
+
74
+ ## Deploy
75
+
76
+ ```bash
77
+ pylon deploy
78
+ ```
79
+
80
+ Docs: https://docs.pylonsync.com
@@ -0,0 +1,129 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import {
5
+ passwordLogin,
6
+ passwordRegister,
7
+ persistSession,
8
+ ApiError,
9
+ } from "@pylonsync/client";
10
+
11
+ // The owner's email/password form — one form, two modes. It calls the built-in
12
+ // auth API directly (`passwordLogin` / `passwordRegister` POST to
13
+ // `/api/auth/password/*`), then `persistSession` writes the freshly-minted
14
+ // token to local storage so the sync engine + `callFn` authenticate AS THE
15
+ // OWNER on the next load. This step matters here specifically: the landing page
16
+ // mints an anonymous guest session (for the live slot counter), and without
17
+ // persisting the real session that stale guest token would shadow the owner's
18
+ // — so the owner-only `inquiriesForOwner` call would come back as a guest and
19
+ // get rejected. We then do a full navigation to /dashboard so the SSR runtime
20
+ // re-resolves auth from the HttpOnly cookie and renders server-side.
21
+ //
22
+ // A studio is single-tenant: there's no public sign-up, just the owner creating
23
+ // their one account. Whoever signs in only sees data if their email matches
24
+ // PYLON_OWNER_EMAIL — enforced by the inquiriesForOwner function.
25
+ export function AuthForm() {
26
+ const [mode, setMode] = useState<"login" | "register">("login");
27
+ const [email, setEmail] = useState("");
28
+ const [password, setPassword] = useState("");
29
+ const [error, setError] = useState<string | null>(null);
30
+ const [pending, setPending] = useState(false);
31
+
32
+ async function onSubmit(e: React.FormEvent) {
33
+ e.preventDefault();
34
+ setError(null);
35
+ setPending(true);
36
+ try {
37
+ const session =
38
+ mode === "login"
39
+ ? await passwordLogin({ email, password })
40
+ : await passwordRegister({ email, password });
41
+ // Make this session authoritative, replacing any anonymous guest token.
42
+ persistSession(session);
43
+ window.location.assign("/dashboard");
44
+ } catch (err) {
45
+ setError(messageFor(err));
46
+ setPending(false);
47
+ }
48
+ }
49
+
50
+ return (
51
+ <div className="space-y-5">
52
+ <form onSubmit={onSubmit} className="space-y-4">
53
+ <label className="block">
54
+ <span className="mb-1.5 block text-[13px] font-medium text-zinc-700">Email</span>
55
+ <input
56
+ type="email"
57
+ value={email}
58
+ onChange={(e) => setEmail(e.target.value)}
59
+ required
60
+ autoComplete="email"
61
+ placeholder="you@yourbusiness.com"
62
+ className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
63
+ />
64
+ </label>
65
+ <label className="block">
66
+ <span className="mb-1.5 block text-[13px] font-medium text-zinc-700">Password</span>
67
+ <input
68
+ type="password"
69
+ value={password}
70
+ onChange={(e) => setPassword(e.target.value)}
71
+ required
72
+ autoComplete={mode === "login" ? "current-password" : "new-password"}
73
+ placeholder={mode === "login" ? "Your password" : "At least 10 characters"}
74
+ className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
75
+ />
76
+ </label>
77
+ {error ? (
78
+ <p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-[13px] leading-snug text-red-700">
79
+ {error}
80
+ </p>
81
+ ) : null}
82
+ <button
83
+ type="submit"
84
+ disabled={pending}
85
+ className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-zinc-900 text-sm font-medium text-white transition-colors hover:bg-zinc-700 disabled:opacity-60"
86
+ >
87
+ {pending ? "…" : mode === "login" ? "Sign in" : "Create account"}
88
+ </button>
89
+ </form>
90
+
91
+ <p className="text-center text-[13px] text-zinc-500">
92
+ {mode === "login" ? "First time here?" : "Already have an account?"}{" "}
93
+ <button
94
+ type="button"
95
+ onClick={() => {
96
+ setMode(mode === "login" ? "register" : "login");
97
+ setError(null);
98
+ }}
99
+ className="font-medium text-zinc-900 underline underline-offset-2"
100
+ >
101
+ {mode === "login" ? "Create the owner account" : "Sign in"}
102
+ </button>
103
+ </p>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ // Map the framework's auth error codes to friendly copy. `ApiError` carries a
109
+ // stable `.code` so you branch on the code, not the message.
110
+ function messageFor(err: unknown): string {
111
+ if (err instanceof ApiError) {
112
+ switch (err.code) {
113
+ case "INVALID_CREDENTIALS":
114
+ return "Wrong email or password.";
115
+ case "USER_EXISTS":
116
+ return "That email is already registered — sign in instead.";
117
+ case "WEAK_PASSWORD":
118
+ return "Pick a longer password — at least 10 characters.";
119
+ case "PWNED_PASSWORD":
120
+ return "That password has appeared in a known data breach. Choose a different one.";
121
+ case "RATE_LIMITED":
122
+ return "Too many attempts — try again in a minute.";
123
+ default:
124
+ return err.message;
125
+ }
126
+ }
127
+ if (err instanceof Error) return err.message;
128
+ return "Something went wrong. Try again.";
129
+ }