@pylonsync/create-pylon 0.3.274 → 0.3.276

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 (340) 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 +1440 -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 +249 -0
  17. package/templates/agency/app/robots.ts +12 -0
  18. package/templates/agency/app/seeder.tsx +26 -0
  19. package/templates/agency/app/sitemap.ts +9 -0
  20. package/templates/agency/app/work/[slug]/page.tsx +182 -0
  21. package/templates/agency/app/work/page.tsx +83 -0
  22. package/templates/agency/app.ts +284 -0
  23. package/templates/agency/components/marketing.tsx +187 -0
  24. package/templates/agency/components/section-scroller.tsx +35 -0
  25. package/templates/agency/components/ui/button.tsx +56 -0
  26. package/templates/agency/components/ui/card.tsx +90 -0
  27. package/templates/agency/components.json +20 -0
  28. package/templates/agency/functions/bookInquiry.ts +42 -0
  29. package/templates/agency/functions/clientsForOwner.ts +27 -0
  30. package/templates/agency/functions/declineInquiry.ts +41 -0
  31. package/templates/agency/functions/deleteClient.ts +27 -0
  32. package/templates/agency/functions/deleteInvoice.ts +19 -0
  33. package/templates/agency/functions/deleteProject.ts +20 -0
  34. package/templates/agency/functions/inquiriesForOwner.ts +31 -0
  35. package/templates/agency/functions/invoicesForOwner.ts +27 -0
  36. package/templates/agency/functions/seedCapacity.ts +26 -0
  37. package/templates/agency/functions/seedProjects.ts +41 -0
  38. package/templates/agency/functions/seedStudioBackoffice.ts +74 -0
  39. package/templates/agency/functions/setCapacity.ts +32 -0
  40. package/templates/agency/functions/setInvoiceStatus.ts +27 -0
  41. package/templates/agency/functions/setProjectFlags.ts +35 -0
  42. package/templates/agency/functions/submitInquiry.ts +55 -0
  43. package/templates/agency/functions/upsertClient.ts +73 -0
  44. package/templates/agency/functions/upsertInvoice.ts +113 -0
  45. package/templates/agency/functions/upsertProject.ts +97 -0
  46. package/templates/agency/gitignore +10 -0
  47. package/templates/agency/lib/agency.ts +189 -0
  48. package/templates/agency/lib/invoice-pdf.tsx +174 -0
  49. package/templates/agency/lib/owner.ts +26 -0
  50. package/templates/agency/lib/site.config.ts +418 -0
  51. package/templates/agency/lib/utils.ts +10 -0
  52. package/templates/agency/package.json +35 -0
  53. package/templates/agency/tsconfig.json +18 -0
  54. package/templates/ai-chat/.env.example +33 -0
  55. package/templates/ai-chat/AGENTS.md +61 -0
  56. package/templates/ai-chat/README.md +99 -0
  57. package/templates/ai-chat/app/auth-form.tsx +124 -0
  58. package/templates/ai-chat/app/chat-client.tsx +727 -0
  59. package/templates/ai-chat/app/error.tsx +26 -0
  60. package/templates/ai-chat/app/globals.css +148 -0
  61. package/templates/ai-chat/app/layout.tsx +75 -0
  62. package/templates/ai-chat/app/login/page.tsx +39 -0
  63. package/templates/ai-chat/app/not-found.tsx +19 -0
  64. package/templates/ai-chat/app/page.tsx +23 -0
  65. package/templates/ai-chat/app.ts +121 -0
  66. package/templates/ai-chat/components.json +20 -0
  67. package/templates/ai-chat/functions/deleteConversation.ts +33 -0
  68. package/templates/ai-chat/gitignore +10 -0
  69. package/templates/ai-chat/lib/site.config.ts +103 -0
  70. package/templates/ai-chat/lib/utils.ts +10 -0
  71. package/templates/ai-chat/package.json +34 -0
  72. package/templates/ai-chat/tsconfig.json +18 -0
  73. package/templates/ai-studio/.env.example +19 -0
  74. package/templates/ai-studio/AGENTS.md +61 -0
  75. package/templates/ai-studio/README.md +83 -0
  76. package/templates/ai-studio/app/auth-form.tsx +124 -0
  77. package/templates/ai-studio/app/error.tsx +26 -0
  78. package/templates/ai-studio/app/globals.css +148 -0
  79. package/templates/ai-studio/app/layout.tsx +75 -0
  80. package/templates/ai-studio/app/login/page.tsx +39 -0
  81. package/templates/ai-studio/app/not-found.tsx +19 -0
  82. package/templates/ai-studio/app/page.tsx +34 -0
  83. package/templates/ai-studio/app/studio-client.tsx +357 -0
  84. package/templates/ai-studio/app.ts +108 -0
  85. package/templates/ai-studio/components.json +20 -0
  86. package/templates/ai-studio/functions/_getGeneration.ts +25 -0
  87. package/templates/ai-studio/functions/_updateGeneration.ts +37 -0
  88. package/templates/ai-studio/functions/generate.ts +42 -0
  89. package/templates/ai-studio/functions/pollGeneration.ts +134 -0
  90. package/templates/ai-studio/gitignore +10 -0
  91. package/templates/ai-studio/lib/site.config.ts +80 -0
  92. package/templates/ai-studio/lib/studio.ts +52 -0
  93. package/templates/ai-studio/lib/utils.ts +10 -0
  94. package/templates/ai-studio/package.json +34 -0
  95. package/templates/ai-studio/tsconfig.json +18 -0
  96. package/templates/creator/.env.example +12 -0
  97. package/templates/creator/AGENTS.md +61 -0
  98. package/templates/creator/README.md +67 -0
  99. package/templates/creator/app/auth-form.tsx +129 -0
  100. package/templates/creator/app/dashboard/dashboard-client.tsx +297 -0
  101. package/templates/creator/app/dashboard/page.tsx +70 -0
  102. package/templates/creator/app/error.tsx +26 -0
  103. package/templates/creator/app/globals.css +148 -0
  104. package/templates/creator/app/layout.tsx +160 -0
  105. package/templates/creator/app/login/page.tsx +39 -0
  106. package/templates/creator/app/newsletter-signup.tsx +162 -0
  107. package/templates/creator/app/not-found.tsx +19 -0
  108. package/templates/creator/app/page.tsx +160 -0
  109. package/templates/creator/app/robots.ts +12 -0
  110. package/templates/creator/app/sitemap.ts +9 -0
  111. package/templates/creator/app.ts +134 -0
  112. package/templates/creator/components/marketing.tsx +148 -0
  113. package/templates/creator/components/section-scroller.tsx +35 -0
  114. package/templates/creator/components/ui/button.tsx +56 -0
  115. package/templates/creator/components/ui/card.tsx +90 -0
  116. package/templates/creator/components.json +20 -0
  117. package/templates/creator/functions/subscribe.ts +82 -0
  118. package/templates/creator/functions/subscriberStats.ts +75 -0
  119. package/templates/creator/gitignore +10 -0
  120. package/templates/creator/lib/owner.ts +26 -0
  121. package/templates/creator/lib/site.config.ts +173 -0
  122. package/templates/creator/lib/stats.ts +30 -0
  123. package/templates/creator/lib/utils.ts +10 -0
  124. package/templates/creator/package.json +34 -0
  125. package/templates/creator/tsconfig.json +18 -0
  126. package/templates/default/app/layout.tsx +26 -27
  127. package/templates/default/app/page.tsx +90 -274
  128. package/templates/default/lib/products.ts +9 -122
  129. package/templates/default/lib/site.config.ts +739 -0
  130. package/templates/default/lib/site.ts +14 -261
  131. package/templates/directory/.env.example +12 -0
  132. package/templates/directory/AGENTS.md +61 -0
  133. package/templates/directory/README.md +80 -0
  134. package/templates/directory/app/auth-form.tsx +129 -0
  135. package/templates/directory/app/dashboard/dashboard-client.tsx +205 -0
  136. package/templates/directory/app/dashboard/page.tsx +70 -0
  137. package/templates/directory/app/directory-browse.tsx +328 -0
  138. package/templates/directory/app/error.tsx +26 -0
  139. package/templates/directory/app/globals.css +148 -0
  140. package/templates/directory/app/layout.tsx +171 -0
  141. package/templates/directory/app/login/page.tsx +39 -0
  142. package/templates/directory/app/not-found.tsx +19 -0
  143. package/templates/directory/app/page.tsx +50 -0
  144. package/templates/directory/app/robots.ts +12 -0
  145. package/templates/directory/app/sitemap.ts +9 -0
  146. package/templates/directory/app/submit/page.tsx +30 -0
  147. package/templates/directory/app/submit-form.tsx +151 -0
  148. package/templates/directory/app.ts +146 -0
  149. package/templates/directory/components/marketing.tsx +148 -0
  150. package/templates/directory/components/section-scroller.tsx +35 -0
  151. package/templates/directory/components/ui/button.tsx +56 -0
  152. package/templates/directory/components/ui/card.tsx +90 -0
  153. package/templates/directory/components.json +20 -0
  154. package/templates/directory/functions/approveSubmission.ts +45 -0
  155. package/templates/directory/functions/rejectSubmission.ts +20 -0
  156. package/templates/directory/functions/seedListings.ts +33 -0
  157. package/templates/directory/functions/submissionsForOwner.ts +29 -0
  158. package/templates/directory/functions/submitListing.ts +63 -0
  159. package/templates/directory/functions/upvote.ts +24 -0
  160. package/templates/directory/gitignore +10 -0
  161. package/templates/directory/lib/directory.ts +45 -0
  162. package/templates/directory/lib/owner.ts +26 -0
  163. package/templates/directory/lib/site.config.ts +130 -0
  164. package/templates/directory/lib/utils.ts +10 -0
  165. package/templates/directory/package.json +34 -0
  166. package/templates/directory/tsconfig.json +18 -0
  167. package/templates/local-service/.env.example +12 -0
  168. package/templates/local-service/AGENTS.md +61 -0
  169. package/templates/local-service/README.md +82 -0
  170. package/templates/local-service/app/auth-form.tsx +129 -0
  171. package/templates/local-service/app/booking-widget.tsx +399 -0
  172. package/templates/local-service/app/dashboard/dashboard-client.tsx +304 -0
  173. package/templates/local-service/app/dashboard/page.tsx +63 -0
  174. package/templates/local-service/app/error.tsx +26 -0
  175. package/templates/local-service/app/globals.css +148 -0
  176. package/templates/local-service/app/layout.tsx +151 -0
  177. package/templates/local-service/app/login/page.tsx +39 -0
  178. package/templates/local-service/app/not-found.tsx +19 -0
  179. package/templates/local-service/app/page.tsx +233 -0
  180. package/templates/local-service/app/robots.ts +12 -0
  181. package/templates/local-service/app/sitemap.ts +9 -0
  182. package/templates/local-service/app.ts +131 -0
  183. package/templates/local-service/components/marketing.tsx +162 -0
  184. package/templates/local-service/components/section-scroller.tsx +35 -0
  185. package/templates/local-service/components/ui/button.tsx +56 -0
  186. package/templates/local-service/components/ui/card.tsx +90 -0
  187. package/templates/local-service/components.json +20 -0
  188. package/templates/local-service/functions/bookingsForOwner.ts +30 -0
  189. package/templates/local-service/functions/cancelBooking.ts +27 -0
  190. package/templates/local-service/functions/confirmBooking.ts +18 -0
  191. package/templates/local-service/functions/createBooking.ts +98 -0
  192. package/templates/local-service/gitignore +10 -0
  193. package/templates/local-service/lib/booking.ts +24 -0
  194. package/templates/local-service/lib/owner.ts +26 -0
  195. package/templates/local-service/lib/site.config.ts +232 -0
  196. package/templates/local-service/lib/slots.ts +97 -0
  197. package/templates/local-service/lib/utils.ts +10 -0
  198. package/templates/local-service/package.json +34 -0
  199. package/templates/local-service/tsconfig.json +18 -0
  200. package/templates/marketplace/.env.example +9 -0
  201. package/templates/marketplace/AGENTS.md +61 -0
  202. package/templates/marketplace/README.md +78 -0
  203. package/templates/marketplace/app/_components/CategoryIcon.tsx +40 -0
  204. package/templates/marketplace/app/error.tsx +26 -0
  205. package/templates/marketplace/app/globals.css +64 -0
  206. package/templates/marketplace/app/layout.tsx +60 -0
  207. package/templates/marketplace/app/listing/[id]/page.tsx +163 -0
  208. package/templates/marketplace/app/me/page.tsx +15 -0
  209. package/templates/marketplace/app/not-found.tsx +20 -0
  210. package/templates/marketplace/app/page.tsx +159 -0
  211. package/templates/marketplace/app/robots.ts +12 -0
  212. package/templates/marketplace/app/sell/page.tsx +26 -0
  213. package/templates/marketplace/app/sitemap.ts +14 -0
  214. package/templates/marketplace/app.ts +190 -0
  215. package/templates/marketplace/client/AuthNav.tsx +46 -0
  216. package/templates/marketplace/client/LiveTicker.tsx +104 -0
  217. package/templates/marketplace/client/LoginCard.tsx +130 -0
  218. package/templates/marketplace/client/MarketProvider.tsx +148 -0
  219. package/templates/marketplace/client/MyMarket.tsx +180 -0
  220. package/templates/marketplace/client/OfferPanel.tsx +355 -0
  221. package/templates/marketplace/client/SeedOnEmpty.tsx +26 -0
  222. package/templates/marketplace/client/SellForm.tsx +160 -0
  223. package/templates/marketplace/client/WatchButton.tsx +88 -0
  224. package/templates/marketplace/client/market.ts +341 -0
  225. package/templates/marketplace/functions/buyNow.ts +78 -0
  226. package/templates/marketplace/functions/makeOffer.ts +65 -0
  227. package/templates/marketplace/functions/respondToOffer.ts +62 -0
  228. package/templates/marketplace/functions/seedMarket.ts +90 -0
  229. package/templates/marketplace/gitignore +10 -0
  230. package/templates/marketplace/package.json +35 -0
  231. package/templates/marketplace/tsconfig.json +14 -0
  232. package/templates/marketplace/ui/badge.tsx +30 -0
  233. package/templates/marketplace/ui/button.tsx +49 -0
  234. package/templates/marketplace/ui/card.tsx +48 -0
  235. package/templates/marketplace/ui/input.tsx +17 -0
  236. package/templates/marketplace/ui/label.tsx +18 -0
  237. package/templates/marketplace/ui/textarea.tsx +17 -0
  238. package/templates/marketplace/ui/tokens.css +32 -0
  239. package/templates/marketplace/ui/utils.ts +6 -0
  240. package/templates/restaurant/.env.example +12 -0
  241. package/templates/restaurant/AGENTS.md +61 -0
  242. package/templates/restaurant/README.md +77 -0
  243. package/templates/restaurant/app/auth-form.tsx +129 -0
  244. package/templates/restaurant/app/dashboard/dashboard-client.tsx +263 -0
  245. package/templates/restaurant/app/dashboard/page.tsx +59 -0
  246. package/templates/restaurant/app/error.tsx +26 -0
  247. package/templates/restaurant/app/globals.css +148 -0
  248. package/templates/restaurant/app/layout.tsx +151 -0
  249. package/templates/restaurant/app/login/page.tsx +39 -0
  250. package/templates/restaurant/app/not-found.tsx +19 -0
  251. package/templates/restaurant/app/page.tsx +194 -0
  252. package/templates/restaurant/app/reservation-widget.tsx +359 -0
  253. package/templates/restaurant/app/robots.ts +12 -0
  254. package/templates/restaurant/app/sitemap.ts +9 -0
  255. package/templates/restaurant/app.ts +115 -0
  256. package/templates/restaurant/components/marketing.tsx +162 -0
  257. package/templates/restaurant/components/section-scroller.tsx +35 -0
  258. package/templates/restaurant/components/ui/button.tsx +56 -0
  259. package/templates/restaurant/components/ui/card.tsx +90 -0
  260. package/templates/restaurant/components.json +20 -0
  261. package/templates/restaurant/functions/cancelReservation.ts +26 -0
  262. package/templates/restaurant/functions/confirmReservation.ts +17 -0
  263. package/templates/restaurant/functions/createReservation.ts +92 -0
  264. package/templates/restaurant/functions/reservationsForOwner.ts +28 -0
  265. package/templates/restaurant/gitignore +10 -0
  266. package/templates/restaurant/lib/owner.ts +26 -0
  267. package/templates/restaurant/lib/reservation.ts +22 -0
  268. package/templates/restaurant/lib/site.config.ts +218 -0
  269. package/templates/restaurant/lib/slots.ts +55 -0
  270. package/templates/restaurant/lib/utils.ts +10 -0
  271. package/templates/restaurant/package.json +34 -0
  272. package/templates/restaurant/tsconfig.json +18 -0
  273. package/templates/shop/.env.example +32 -0
  274. package/templates/shop/AGENTS.md +61 -0
  275. package/templates/shop/README.md +102 -0
  276. package/templates/shop/app/auth-form.tsx +129 -0
  277. package/templates/shop/app/dashboard/dashboard-client.tsx +264 -0
  278. package/templates/shop/app/dashboard/page.tsx +59 -0
  279. package/templates/shop/app/error.tsx +26 -0
  280. package/templates/shop/app/globals.css +148 -0
  281. package/templates/shop/app/layout.tsx +160 -0
  282. package/templates/shop/app/login/page.tsx +39 -0
  283. package/templates/shop/app/not-found.tsx +19 -0
  284. package/templates/shop/app/page.tsx +95 -0
  285. package/templates/shop/app/robots.ts +12 -0
  286. package/templates/shop/app/shop-client.tsx +436 -0
  287. package/templates/shop/app/sitemap.ts +9 -0
  288. package/templates/shop/app/success/page.tsx +33 -0
  289. package/templates/shop/app.ts +134 -0
  290. package/templates/shop/components/marketing.tsx +96 -0
  291. package/templates/shop/components/section-scroller.tsx +35 -0
  292. package/templates/shop/components/ui/button.tsx +56 -0
  293. package/templates/shop/components/ui/card.tsx +90 -0
  294. package/templates/shop/components.json +20 -0
  295. package/templates/shop/functions/cancelOrder.ts +33 -0
  296. package/templates/shop/functions/checkout.ts +130 -0
  297. package/templates/shop/functions/fulfillOrder.ts +17 -0
  298. package/templates/shop/functions/markGroupPaid.ts +26 -0
  299. package/templates/shop/functions/ordersForOwner.ts +28 -0
  300. package/templates/shop/functions/releaseGroup.ts +36 -0
  301. package/templates/shop/functions/reserveCart.ts +87 -0
  302. package/templates/shop/functions/restockProduct.ts +23 -0
  303. package/templates/shop/functions/seedProducts.ts +30 -0
  304. package/templates/shop/functions/stripeWebhook.ts +72 -0
  305. package/templates/shop/gitignore +10 -0
  306. package/templates/shop/lib/owner.ts +26 -0
  307. package/templates/shop/lib/shop.ts +45 -0
  308. package/templates/shop/lib/site.config.ts +198 -0
  309. package/templates/shop/lib/utils.ts +10 -0
  310. package/templates/shop/package.json +35 -0
  311. package/templates/shop/tsconfig.json +18 -0
  312. package/templates/waitlist/.env.example +12 -0
  313. package/templates/waitlist/AGENTS.md +61 -0
  314. package/templates/waitlist/README.md +81 -0
  315. package/templates/waitlist/app/auth-form.tsx +129 -0
  316. package/templates/waitlist/app/dashboard/dashboard-client.tsx +297 -0
  317. package/templates/waitlist/app/dashboard/page.tsx +70 -0
  318. package/templates/waitlist/app/error.tsx +26 -0
  319. package/templates/waitlist/app/globals.css +148 -0
  320. package/templates/waitlist/app/layout.tsx +158 -0
  321. package/templates/waitlist/app/login/page.tsx +39 -0
  322. package/templates/waitlist/app/not-found.tsx +19 -0
  323. package/templates/waitlist/app/page.tsx +119 -0
  324. package/templates/waitlist/app/robots.ts +12 -0
  325. package/templates/waitlist/app/sitemap.ts +9 -0
  326. package/templates/waitlist/app/waitlist-hero.tsx +219 -0
  327. package/templates/waitlist/app.ts +134 -0
  328. package/templates/waitlist/components/marketing.tsx +96 -0
  329. package/templates/waitlist/components/ui/button.tsx +56 -0
  330. package/templates/waitlist/components/ui/card.tsx +90 -0
  331. package/templates/waitlist/components.json +20 -0
  332. package/templates/waitlist/functions/joinWaitlist.ts +82 -0
  333. package/templates/waitlist/functions/waitlistStats.ts +75 -0
  334. package/templates/waitlist/gitignore +10 -0
  335. package/templates/waitlist/lib/owner.ts +26 -0
  336. package/templates/waitlist/lib/site.config.ts +178 -0
  337. package/templates/waitlist/lib/stats.ts +30 -0
  338. package/templates/waitlist/lib/utils.ts +10 -0
  339. package/templates/waitlist/package.json +34 -0
  340. package/templates/waitlist/tsconfig.json +18 -0
@@ -0,0 +1,97 @@
1
+ // Pure slot math — shared by the client time-picker (which slots to show, which
2
+ // to grey out) and the server-side createBooking re-check (does this slot still
3
+ // fit). Keeping it dependency-free and in one place means the two sides agree
4
+ // exactly: the client greys a taken slot, and the server independently refuses
5
+ // to double-book it.
6
+ //
7
+ // Times are absolute instants (epoch ms / ISO-8601 UTC). Slot GENERATION uses
8
+ // local wall-clock hours (the business's open/close, interpreted in the
9
+ // runtime's local timezone); overlap + past checks compare absolute instants,
10
+ // so they're timezone-agnostic and can't be fooled by string formatting.
11
+
12
+ export interface BusyRange {
13
+ startsAt: string;
14
+ endsAt: string;
15
+ }
16
+
17
+ export interface Slot {
18
+ /** ISO-8601 start instant. */
19
+ startsAt: string;
20
+ /** ISO-8601 end instant (start + service duration). */
21
+ endsAt: string;
22
+ /** False when the slot is in the past (within lead time) or overlaps a booking. */
23
+ available: boolean;
24
+ }
25
+
26
+ /** Half-open interval overlap: [aStart,aEnd) intersects [bStart,bEnd). */
27
+ export function rangesOverlap(
28
+ aStart: number,
29
+ aEnd: number,
30
+ bStart: number,
31
+ bEnd: number,
32
+ ): boolean {
33
+ return aStart < bEnd && bStart < aEnd;
34
+ }
35
+
36
+ /**
37
+ * Generate the bookable slots for one calendar day for one service.
38
+ *
39
+ * `dayISODate` is a local "YYYY-MM-DD"; `open`/`close` are local "HH:MM". Each
40
+ * slot starts every `slotMinutes` and lasts `durationMin`; the last slot must
41
+ * end by close. A slot is unavailable if it starts before `now + leadTimeHours`
42
+ * or overlaps any `busy` range.
43
+ */
44
+ export function slotsForDay(opts: {
45
+ dayISODate: string;
46
+ open: string;
47
+ close: string;
48
+ slotMinutes: number;
49
+ durationMin: number;
50
+ leadTimeHours: number;
51
+ busy: BusyRange[];
52
+ nowMs: number;
53
+ }): Slot[] {
54
+ const { dayISODate, open, close, slotMinutes, durationMin, leadTimeHours, busy, nowMs } = opts;
55
+ const [y, m, d] = dayISODate.split("-").map(Number);
56
+ const [oh, om] = open.split(":").map(Number);
57
+ const [ch, cm] = close.split(":").map(Number);
58
+ if ([y, m, d, oh, om, ch, cm].some((n) => Number.isNaN(n))) return [];
59
+
60
+ // `new Date(y, m-1, d, h, min)` builds a LOCAL time; `.getTime()` is the
61
+ // correct absolute instant regardless of timezone.
62
+ const dayStart = new Date(y, m - 1, d, oh, om, 0, 0).getTime();
63
+ const dayEnd = new Date(y, m - 1, d, ch, cm, 0, 0).getTime();
64
+ const minStart = nowMs + leadTimeHours * 3_600_000;
65
+ const step = slotMinutes * 60_000;
66
+ const dur = durationMin * 60_000;
67
+ const busyMs = busy.map((b) => [Date.parse(b.startsAt), Date.parse(b.endsAt)] as const);
68
+
69
+ const slots: Slot[] = [];
70
+ for (let s = dayStart; s + dur <= dayEnd; s += step) {
71
+ const e = s + dur;
72
+ const past = s < minStart;
73
+ const taken = busyMs.some(([bs, be]) => rangesOverlap(s, e, bs, be));
74
+ slots.push({
75
+ startsAt: new Date(s).toISOString(),
76
+ endsAt: new Date(e).toISOString(),
77
+ available: !past && !taken,
78
+ });
79
+ }
80
+ return slots;
81
+ }
82
+
83
+ /** The weekday (0=Sun..6=Sat) for a local "YYYY-MM-DD". */
84
+ export function weekdayOf(dayISODate: string): number {
85
+ const [y, m, d] = dayISODate.split("-").map(Number);
86
+ return new Date(y, m - 1, d).getDay();
87
+ }
88
+
89
+ /** Local "YYYY-MM-DD" for an instant `daysFromToday` away (0 = today). */
90
+ export function localDateKey(daysFromToday: number, nowMs: number): string {
91
+ const d = new Date(nowMs);
92
+ d.setDate(d.getDate() + daysFromToday);
93
+ const y = d.getFullYear();
94
+ const m = String(d.getMonth() + 1).padStart(2, "0");
95
+ const day = String(d.getDate()).padStart(2, "0");
96
+ return `${y}-${m}-${day}`;
97
+ }
@@ -0,0 +1,10 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ // `cn` — the shadcn class merger. clsx resolves conditional/array class
5
+ // inputs; tailwind-merge then dedupes conflicting Tailwind utilities so
6
+ // the last one wins (e.g. `cn("px-2", "px-4")` → "px-4"). Every shadcn
7
+ // component routes its className through this.
8
+ export function cn(...inputs: ClassValue[]) {
9
+ return twMerge(clsx(inputs));
10
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "__APP_NAME_KEBAB__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "pylon dev",
8
+ "deploy": "pylon deploy",
9
+ "check": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@pylonsync/react": "^__PYLON_VERSION__",
13
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
14
+ "@pylonsync/functions": "^__PYLON_VERSION__",
15
+ "@pylonsync/client": "^__PYLON_VERSION__",
16
+ "react": "^19.0.0",
17
+ "react-dom": "^19.0.0",
18
+ "tailwindcss": "^4.3.0",
19
+ "@tailwindcss/cli": "^4.3.0",
20
+ "tw-animate-css": "^1.2.0",
21
+ "class-variance-authority": "^0.7.1",
22
+ "clsx": "^2.1.1",
23
+ "tailwind-merge": "^2.5.0",
24
+ "lucide-react": "^0.460.0",
25
+ "@radix-ui/react-slot": "^1.1.0"
26
+ },
27
+ "devDependencies": {
28
+ "@pylonsync/cli": "^__PYLON_VERSION__",
29
+ "@types/node": "^22.0.0",
30
+ "@types/react": "^19.0.0",
31
+ "@types/react-dom": "^19.0.0",
32
+ "typescript": "^5.6.0"
33
+ }
34
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react",
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "lib": ["ES2022", "DOM"],
11
+ "types": ["react", "react-dom", "node"],
12
+ "baseUrl": ".",
13
+ "paths": {
14
+ "@/*": ["./*"]
15
+ }
16
+ },
17
+ "include": ["app.ts", "app/**/*", "components/**/*", "lib/**/*", "functions/**/*"]
18
+ }
@@ -0,0 +1,9 @@
1
+ # Copy to `.env` and fill in. `pylon dev` loads `.env` automatically.
2
+ #
3
+ # The marketplace runs with ZERO config — email/password auth is built in, and
4
+ # anyone who signs up can list items and make offers. There's no single "owner";
5
+ # it's a multi-user, two-sided market.
6
+
7
+ # ── Site URL (optional) ──────────────────────────────────────────────────────
8
+ # Used by robots.txt + sitemap.xml. Point it at your real domain in production.
9
+ # SITE_URL=https://yourmarket.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,78 @@
1
+ # __APP_NAME__
2
+
3
+ A live, two-sided **marketplace** built with [Pylon](https://pylonsync.com) —
4
+ server-rendered listings (great for SEO) plus realtime offers, from one binary
5
+ on one port. No Next.js, no separate realtime service.
6
+
7
+ Anyone can list an item; anyone else can make an offer or buy it now; sellers
8
+ watch offers arrive live and accept or decline — every write fanned out to every
9
+ open tab instantly.
10
+
11
+ ## Develop
12
+
13
+ ```bash
14
+ __RUN_DEV__
15
+ ```
16
+
17
+ Open http://localhost:4321 — the grid seeds itself on first load. Then **open a
18
+ second tab**, post something from `/sell` in one, and watch it hit the "just
19
+ listed" ticker in the other with no refresh. Sign in (the `/sell` form prefills
20
+ a demo account) to list, make offers, and watch your `/me` inbox light up live.
21
+
22
+ ## How it works
23
+
24
+ - **Server-rendered for SEO + LCP.** The browse grid (`/`) and every listing
25
+ page (`/listing/:slug`) render on the server with real rows (`serverData` +
26
+ React 19 `use()`). View source and the products are *in the HTML*.
27
+ - **Realtime where it matters.** The "just listed" ticker, the live offers on a
28
+ listing, and your `/me` inbox all ride the sync engine — one `db.useQuery` per
29
+ view, no polling. The public surface connects with an anonymous **guest
30
+ session** (read-only); writing (list/offer/buy) requires a real sign-in.
31
+ - **Unspoofable ownership.** `sellerId`/`buyerId` use `field.owner()`, so the
32
+ framework stamps them from the session and rejects forged values — listings
33
+ and offers can be created with a plain optimistic `db.insert` and still can't
34
+ be spoofed. The heavier logic (accept = mark sold + auto-decline the rest)
35
+ runs in `functions/respondToOffer.ts` where it enforces "only the seller".
36
+
37
+ ## Privacy & policies
38
+
39
+ - `Listing` + `Offer` are **public-read** (buyers and sellers both see live
40
+ state); writes are owner-scoped.
41
+ - `Watch` (your saved listings) is **private** — read/write only your own rows.
42
+ - `User` rows are readable only to signed-in users (for seller/buyer names);
43
+ `passwordHash` is `serverOnly` and never serialized. Auth writes go through
44
+ `/api/auth/password/*`, not the entity API.
45
+
46
+ ## Listing photos
47
+
48
+ Listings render a **deterministic gradient + category icon** from a `seed` —
49
+ so the demo needs no image hosting. To use real photos, add an `imageUrl` field
50
+ to `Listing` in `app.ts`, collect it in `client/SellForm.tsx` (upload via
51
+ `/api/files`), and render an `<img>` in the grid + detail page instead of the
52
+ gradient.
53
+
54
+ ## Rebrand it
55
+
56
+ The brand ("Pylon Market") lives in `app/layout.tsx`; the demo catalog +
57
+ seed account are in `functions/seedMarket.ts`. The design tokens are in
58
+ `ui/tokens.css` + `app/globals.css`.
59
+
60
+ ## Layout
61
+
62
+ ```
63
+ app.ts User + Listing + Offer + Watch + policies
64
+ app/page.tsx SSR browse grid + category facets
65
+ app/listing/[id]/page.tsx SSR listing detail (+ generateMetadata)
66
+ app/sell/page.tsx list an item (sign-in gated)
67
+ app/me/page.tsx your listings, offers, watchlist (live)
68
+ functions/buyNow.ts, makeOffer.ts, respondToOffer.ts, seedMarket.ts
69
+ client/* the realtime islands (ticker, offers, sell form…)
70
+ ```
71
+
72
+ ## Deploy
73
+
74
+ ```bash
75
+ pylon deploy
76
+ ```
77
+
78
+ Docs: https://docs.pylonsync.com
@@ -0,0 +1,40 @@
1
+ import React from "react";
2
+ import {
3
+ Armchair,
4
+ Bike,
5
+ Camera,
6
+ CookingPot,
7
+ Guitar,
8
+ Headphones,
9
+ MonitorSmartphone,
10
+ Package,
11
+ Shirt,
12
+ Tent,
13
+ type LucideIcon,
14
+ } from "lucide-react";
15
+
16
+ // Category → icon. Drives the listing "photo" placeholder, so the grid +
17
+ // detail pages read as a real catalog instead of text initials. Pure SVG
18
+ // (no client hooks), so it renders straight through SSR.
19
+ const ICONS: Record<string, LucideIcon> = {
20
+ furniture: Armchair,
21
+ electronics: MonitorSmartphone,
22
+ cameras: Camera,
23
+ bikes: Bike,
24
+ audio: Headphones,
25
+ kitchen: CookingPot,
26
+ instruments: Guitar,
27
+ outdoor: Tent,
28
+ apparel: Shirt,
29
+ };
30
+
31
+ export function CategoryIcon({
32
+ category,
33
+ className,
34
+ }: {
35
+ category: string;
36
+ className?: string;
37
+ }) {
38
+ const Icon = ICONS[category] ?? Package;
39
+ return <Icon className={className} strokeWidth={1.25} aria-hidden />;
40
+ }
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { type ErrorBoundaryProps } from "@pylonsync/react";
3
+
4
+ // `app/error.tsx` → the error boundary for this segment. Hydrated + interactive:
5
+ // `reset()` re-attempts the route. The thrown error reaches the client as
6
+ // `{ message, digest }` only — the stack stays in the dev overlay / server logs.
7
+ export default function Error({ error, reset }: ErrorBoundaryProps) {
8
+ return (
9
+ <div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
10
+ <h1 className="text-2xl font-semibold tracking-tight">Something went wrong</h1>
11
+ <p className="mt-2 text-muted-foreground">{error.message}</p>
12
+ {error.digest ? (
13
+ <p className="mt-1 text-xs text-muted-foreground">
14
+ Reference: <code>{error.digest}</code>
15
+ </p>
16
+ ) : null}
17
+ <button
18
+ type="button"
19
+ onClick={reset}
20
+ className="mt-6 inline-flex h-10 items-center rounded-md bg-foreground px-5 text-sm font-medium text-background transition hover:opacity-90"
21
+ >
22
+ Try again
23
+ </button>
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,64 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "../ui/tokens.css";
4
+
5
+ /* Tailwind v4 scans these for class names (relative to this file). */
6
+ @source "./";
7
+ @source "../client";
8
+ @source "../../_shared/src";
9
+
10
+ @theme {
11
+ --font-sans: "Inter", -apple-system, system-ui, sans-serif;
12
+ --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", monospace;
13
+
14
+ --color-background: oklch(0.99 0 0);
15
+ --color-foreground: oklch(0.16 0 0);
16
+ --color-card: oklch(1 0 0);
17
+ --color-card-foreground: oklch(0.16 0 0);
18
+ --color-popover: oklch(1 0 0);
19
+ --color-popover-foreground: oklch(0.16 0 0);
20
+ --color-primary: oklch(0.21 0 0);
21
+ --color-primary-foreground: oklch(0.985 0 0);
22
+ --color-secondary: oklch(0.97 0 0);
23
+ --color-secondary-foreground: oklch(0.21 0 0);
24
+ --color-muted: oklch(0.97 0 0);
25
+ --color-muted-foreground: oklch(0.55 0 0);
26
+ --color-accent: oklch(0.97 0 0);
27
+ --color-accent-foreground: oklch(0.21 0 0);
28
+ --color-destructive: oklch(0.577 0.245 27.325);
29
+ --color-destructive-foreground: oklch(0.985 0 0);
30
+ --color-border: oklch(0.92 0 0);
31
+ --color-input: oklch(0.92 0 0);
32
+ --color-ring: oklch(0.71 0 0);
33
+
34
+ --radius-lg: 0.625rem;
35
+ --radius-md: calc(0.625rem - 2px);
36
+ --radius-sm: calc(0.625rem - 4px);
37
+ }
38
+
39
+ html,
40
+ body {
41
+ height: 100%;
42
+ }
43
+
44
+ /* Live "just listed" ticker. Items are doubled in the markup, so translating
45
+ the track by -50% loops seamlessly. */
46
+ @keyframes market-marquee {
47
+ from {
48
+ transform: translateX(0);
49
+ }
50
+ to {
51
+ transform: translateX(-50%);
52
+ }
53
+ }
54
+ .market-marquee {
55
+ animation: market-marquee 28s linear infinite;
56
+ }
57
+ .market-marquee:hover {
58
+ animation-play-state: paused;
59
+ }
60
+ @media (prefers-reduced-motion: reduce) {
61
+ .market-marquee {
62
+ animation: none;
63
+ }
64
+ }
@@ -0,0 +1,60 @@
1
+ import React from "react";
2
+ import { Link } from "@pylonsync/react";
3
+ import { AuthNav } from "../client/AuthNav";
4
+
5
+ // Root layout. Server-rendered shell; Pylon's SSR head adapter injects the
6
+ // compiled Tailwind <link> from app/globals.css automatically.
7
+ export default function RootLayout({
8
+ children,
9
+ }: {
10
+ children: React.ReactNode;
11
+ }) {
12
+ return (
13
+ <html lang="en" suppressHydrationWarning>
14
+ <head>
15
+ <meta charSet="utf-8" />
16
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
17
+ <link rel="preconnect" href="https://rsms.me/" />
18
+ <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
19
+ </head>
20
+ <body className="min-h-screen bg-background text-foreground antialiased">
21
+ <header className="sticky top-0 z-20 h-14 border-b bg-background/90 backdrop-blur">
22
+ <div className="mx-auto flex h-full max-w-5xl items-center justify-between gap-4 px-5">
23
+ <Link href="/" className="flex items-center gap-2 font-medium text-sm">
24
+ <span className="grid size-7 place-items-center rounded-md bg-foreground text-sm font-semibold text-background">
25
+ M
26
+ </span>
27
+ Pylon Market
28
+ </Link>
29
+ <nav className="flex items-center gap-1 text-sm">
30
+ <Link
31
+ href="/"
32
+ className="rounded-md px-3 py-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground"
33
+ >
34
+ Browse
35
+ </Link>
36
+ <Link
37
+ href="/sell"
38
+ className="rounded-md px-3 py-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground"
39
+ >
40
+ Sell
41
+ </Link>
42
+ <Link
43
+ href="/me"
44
+ className="rounded-md px-3 py-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground"
45
+ >
46
+ My Market
47
+ </Link>
48
+ <AuthNav />
49
+ </nav>
50
+ </div>
51
+ </header>
52
+ <main className="mx-auto max-w-5xl px-5 py-6">{children}</main>
53
+ <footer className="border-t py-6 text-center text-xs text-muted-foreground">
54
+ Pylon Market · server-rendered listings + realtime offers from one
55
+ binary
56
+ </footer>
57
+ </body>
58
+ </html>
59
+ );
60
+ }