@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
@@ -0,0 +1,341 @@
1
+ "use client";
2
+
3
+ // Shared client-side glue for Pylon Market: types, display helpers, and the
4
+ // email/password auth bootstrap. Same-origin under native SSR, so no baseUrl —
5
+ // init() resolves window.location.origin.
6
+ import { init, configureClient, callFn, storageKey } from "@pylonsync/react";
7
+
8
+ export const APP_NAME = "market";
9
+
10
+ // Prefilled demo account — the shopper you sign in as. Owns a couple of its
11
+ // own listings so "My Market" isn't empty, but NOT the bulk of the catalog,
12
+ // so it can actually buy + bid on things.
13
+ export const DEMO = {
14
+ email: "demo@pylon.market",
15
+ password: "pylondemo123",
16
+ name: "Demo Shopper",
17
+ } as const;
18
+
19
+ // The seed seller that owns most of the catalog. Nobody logs in as this; it
20
+ // exists so the demo shopper has plenty of other people's listings to buy.
21
+ const BAZAAR = {
22
+ email: "bazaar@pylon.market",
23
+ password: "bazaarseed123",
24
+ name: "Pylon Bazaar",
25
+ } as const;
26
+
27
+ // How many of the seeded listings the bazaar owns; the rest go to the demo.
28
+ const BAZAAR_COUNT = 10;
29
+
30
+ export interface Listing {
31
+ id: string;
32
+ sellerId: string;
33
+ sellerName: string;
34
+ title: string;
35
+ slug: string;
36
+ description: string;
37
+ price: number;
38
+ category: string;
39
+ condition: string;
40
+ status: "active" | "sold";
41
+ seed: string;
42
+ createdAt: string;
43
+ }
44
+
45
+ export interface Offer {
46
+ id: string;
47
+ listingId: string;
48
+ listingTitle: string;
49
+ sellerId: string;
50
+ buyerId: string;
51
+ buyerName: string;
52
+ amount: number;
53
+ message?: string;
54
+ status: "pending" | "accepted" | "declined";
55
+ createdAt: string;
56
+ }
57
+
58
+ export interface Watch {
59
+ id: string;
60
+ userId: string;
61
+ listingId: string;
62
+ listingTitle: string;
63
+ createdAt: string;
64
+ }
65
+
66
+ export interface Identity {
67
+ userId: string;
68
+ name: string;
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Display helpers (pure)
73
+ // ---------------------------------------------------------------------------
74
+
75
+ function hash(s: string): number {
76
+ let h = 2166136261;
77
+ for (let i = 0; i < s.length; i++) {
78
+ h ^= s.charCodeAt(i);
79
+ h = Math.imul(h, 16777619);
80
+ }
81
+ return h >>> 0;
82
+ }
83
+
84
+ /** Deterministic gradient "photo" from a seed — no image hosting needed. */
85
+ export function gradient(seed: string): string {
86
+ const h = hash(seed);
87
+ const a = h % 360;
88
+ const b = (a + 40 + ((h >> 3) % 90)) % 360;
89
+ return `linear-gradient(135deg, hsl(${a} 68% 56%), hsl(${b} 72% 44%))`;
90
+ }
91
+
92
+ export function initials(title: string): string {
93
+ return (
94
+ title
95
+ .split(/\s+/)
96
+ .filter(Boolean)
97
+ .slice(0, 2)
98
+ .map((w) => w[0]?.toUpperCase() ?? "")
99
+ .join("") || "·"
100
+ );
101
+ }
102
+
103
+ export function money(n: number): string {
104
+ const v = Math.round(n * 100) / 100;
105
+ return (
106
+ "$" +
107
+ v.toLocaleString(undefined, {
108
+ minimumFractionDigits: v % 1 ? 2 : 0,
109
+ maximumFractionDigits: 2,
110
+ })
111
+ );
112
+ }
113
+
114
+ export function timeAgo(iso: string): string {
115
+ const s = (Date.now() - Date.parse(iso)) / 1000;
116
+ if (!Number.isFinite(s)) return "";
117
+ if (s < 60) return "just now";
118
+ const m = Math.floor(s / 60);
119
+ if (m < 60) return `${m}m ago`;
120
+ const h = Math.floor(m / 60);
121
+ if (h < 24) return `${h}h ago`;
122
+ return `${Math.floor(h / 24)}d ago`;
123
+ }
124
+
125
+ export function conditionLabel(c: string): string {
126
+ return (
127
+ { "like-new": "Like new", new: "New", good: "Good", fair: "Fair" }[c] ?? c
128
+ );
129
+ }
130
+
131
+ /** Title → URL-safe slug stem. */
132
+ export function slugify(s: string): string {
133
+ return s
134
+ .toLowerCase()
135
+ .trim()
136
+ .replace(/[^a-z0-9]+/g, "-")
137
+ .replace(/^-+|-+$/g, "")
138
+ .slice(0, 60);
139
+ }
140
+
141
+ /** A listing's URL key: slugified title + a short unique suffix. */
142
+ export function makeSlug(title: string, suffix: string): string {
143
+ const stem = slugify(title) || "item";
144
+ return `${stem}-${suffix}`;
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Auth — real email/password (no email verification)
149
+ // ---------------------------------------------------------------------------
150
+ //
151
+ // Storage slots:
152
+ // storageKey("token") — the bearer the sync engine sends on every request.
153
+ // A guest token for read connectivity, OR the signed-
154
+ // in user's token after login.
155
+ // storageKey("userId") — set ONLY when a real user is signed in. Its
156
+ // presence is what "logged in" means.
157
+ // market:displayName — the signed-in user's name, cached for instant UI.
158
+
159
+ const TOKEN = () => storageKey("token");
160
+ const USER_ID = () => storageKey("userId");
161
+ const DISPLAY_NAME = "market:displayName";
162
+
163
+ let booted = false;
164
+
165
+ /** Boot the sync engine once. Same-origin, so init() resolves the origin. */
166
+ export function bootClient(): void {
167
+ if (booted) return;
168
+ booted = true;
169
+ init({ appName: APP_NAME });
170
+ configureClient({ appName: APP_NAME });
171
+ }
172
+
173
+ /**
174
+ * Establish a guest session purely for READ connectivity — the public browse
175
+ * grid + "just listed" ticker run live for signed-out visitors. Writes are
176
+ * gated behind a real sign-in (see {@link signIn}), so this guest token never
177
+ * authors a row. Idempotent: skipped once any token (guest or user) exists.
178
+ */
179
+ export async function ensureReadSession(): Promise<void> {
180
+ bootClient();
181
+ if (localStorage.getItem(TOKEN())) return;
182
+ try {
183
+ const res = await fetch("/api/auth/guest", { method: "POST" });
184
+ if (res.ok) {
185
+ const body = (await res.json()) as { token?: string };
186
+ if (body.token) localStorage.setItem(TOKEN(), body.token);
187
+ configureClient({ appName: APP_NAME });
188
+ }
189
+ } catch {
190
+ // Server not reachable yet — the engine retries on connect.
191
+ }
192
+ }
193
+
194
+ function applySession(token: string, userId: string, name: string): void {
195
+ localStorage.setItem(TOKEN(), token);
196
+ localStorage.setItem(USER_ID(), userId);
197
+ localStorage.setItem(DISPLAY_NAME, name);
198
+ configureClient({ appName: APP_NAME });
199
+ // Notify in-tab + cross-tab listeners (MarketProvider) to re-read identity.
200
+ window.dispatchEvent(new Event("pylon-auth-changed"));
201
+ }
202
+
203
+ type AuthResponse = { token: string; user_id: string };
204
+
205
+ export async function signIn(email: string, password: string): Promise<void> {
206
+ const res = await fetch("/api/auth/password/login", {
207
+ method: "POST",
208
+ headers: { "Content-Type": "application/json" },
209
+ body: JSON.stringify({ email, password }),
210
+ });
211
+ const json = await res.json();
212
+ if (!res.ok) throw new Error(json.error?.message ?? "Sign-in failed");
213
+ const { token, user_id } = json as AuthResponse;
214
+ // Login doesn't return displayName; default to the email handle and let
215
+ // MarketProvider's live User query upgrade it.
216
+ applySession(token, user_id, email.split("@")[0] ?? "you");
217
+ }
218
+
219
+ export async function signUp(
220
+ email: string,
221
+ password: string,
222
+ name: string,
223
+ ): Promise<void> {
224
+ const res = await fetch("/api/auth/password/register", {
225
+ method: "POST",
226
+ headers: { "Content-Type": "application/json" },
227
+ body: JSON.stringify({
228
+ email,
229
+ password,
230
+ displayName: name || email.split("@")[0],
231
+ }),
232
+ });
233
+ const json = await res.json();
234
+ if (!res.ok) throw new Error(json.error?.message ?? "Sign-up failed");
235
+ const { token, user_id } = json as AuthResponse;
236
+ applySession(token, user_id, name || email.split("@")[0] || "you");
237
+ }
238
+
239
+ export async function signOut(): Promise<void> {
240
+ localStorage.removeItem(TOKEN());
241
+ localStorage.removeItem(USER_ID());
242
+ localStorage.removeItem(DISPLAY_NAME);
243
+ window.dispatchEvent(new Event("pylon-auth-changed"));
244
+ // Restore read connectivity for the now-anonymous visitor.
245
+ await ensureReadSession();
246
+ window.dispatchEvent(new Event("pylon-auth-changed"));
247
+ }
248
+
249
+ export function readIdentity(): Identity | null {
250
+ const userId = localStorage.getItem(USER_ID());
251
+ if (!userId) return null;
252
+ return { userId, name: localStorage.getItem(DISPLAY_NAME) ?? "you" };
253
+ }
254
+
255
+ /** Cache the freshest displayName (from the live User query) for instant UI. */
256
+ export function cacheDisplayName(name: string): void {
257
+ if (name) localStorage.setItem(DISPLAY_NAME, name);
258
+ }
259
+
260
+ /** Register an account (or log in if it already exists). Returns its token,
261
+ * or undefined on failure. Used transiently for seeding — the returned token
262
+ * is NOT persisted as the visitor's session. */
263
+ async function ensureAccount(
264
+ email: string,
265
+ password: string,
266
+ name: string,
267
+ ): Promise<string | undefined> {
268
+ try {
269
+ const reg = await fetch("/api/auth/password/register", {
270
+ method: "POST",
271
+ headers: { "Content-Type": "application/json" },
272
+ body: JSON.stringify({ email, password, displayName: name }),
273
+ });
274
+ if (reg.ok) return ((await reg.json()) as AuthResponse).token;
275
+ const login = await fetch("/api/auth/password/login", {
276
+ method: "POST",
277
+ headers: { "Content-Type": "application/json" },
278
+ body: JSON.stringify({ email, password }),
279
+ });
280
+ if (login.ok) return ((await login.json()) as AuthResponse).token;
281
+ } catch {
282
+ /* offline — caller swallows */
283
+ }
284
+ return undefined;
285
+ }
286
+
287
+ /**
288
+ * Make sure the demo accounts exist and the catalog is seeded — so the
289
+ * prefilled login is one click from a working session that can actually buy
290
+ * and sell. Idempotent and best-effort: any failure is swallowed.
291
+ *
292
+ * Two accounts are seeded:
293
+ * - **bazaar** owns the bulk of the catalog, so the demo shopper has other
294
+ * people's listings to buy + bid on (you can't buy your own);
295
+ * - **demo** (the prefilled login) owns a couple of its own listings so
296
+ * "My Market" has something in it.
297
+ *
298
+ * Both tokens are used transiently to seed and then discarded — the visitor
299
+ * stays anonymous (read-only) until they choose to sign in.
300
+ */
301
+ // Shared in-flight promise so the many islands that mount MarketProvider on
302
+ // one page (ticker, header, SeedOnEmpty) all await the SAME seed run instead
303
+ // of each firing their own — that flood was tripping the login rate limit,
304
+ // and a premature reload could abort it mid-flight.
305
+ let seedPromise: Promise<void> | null = null;
306
+
307
+ export function ensureDemoSeed(): Promise<void> {
308
+ // Already seeded in a previous visit (accounts + catalog persist
309
+ // server-side) — nothing to do.
310
+ if (localStorage.getItem("market:demo-seeded") === "1") return Promise.resolve();
311
+ if (seedPromise) return seedPromise;
312
+
313
+ seedPromise = (async () => {
314
+ const bazaarToken = await ensureAccount(
315
+ BAZAAR.email,
316
+ BAZAAR.password,
317
+ BAZAAR.name,
318
+ );
319
+ if (bazaarToken) {
320
+ await callFn(
321
+ "seedMarket",
322
+ { start: 0, end: BAZAAR_COUNT },
323
+ { token: bazaarToken },
324
+ );
325
+ }
326
+
327
+ const demoToken = await ensureAccount(DEMO.email, DEMO.password, DEMO.name);
328
+ if (demoToken) {
329
+ await callFn("seedMarket", { start: BAZAAR_COUNT }, { token: demoToken });
330
+ }
331
+
332
+ // Mark done only AFTER a successful pass, so a failed/partial run retries
333
+ // on the next load instead of being permanently skipped.
334
+ localStorage.setItem("market:demo-seeded", "1");
335
+ })().catch(() => {
336
+ // Allow a later attempt to retry from scratch.
337
+ seedPromise = null;
338
+ });
339
+
340
+ return seedPromise;
341
+ }
@@ -0,0 +1,78 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Buy a listing outright at its asking price. Records the purchase as an
5
+ * accepted Offer (so it shows up in both parties' views exactly like a
6
+ * negotiated sale), marks the listing sold, and declines any other pending
7
+ * offers. The cross-entity, multi-row work is why this is a function rather
8
+ * than a plain db.insert.
9
+ */
10
+
11
+ interface BuyNowArgs {
12
+ listingId: string;
13
+ buyerName: string;
14
+ _optimisticId?: string;
15
+ }
16
+
17
+ interface BuyNowResult {
18
+ id: string;
19
+ }
20
+
21
+ export default mutation<BuyNowArgs, BuyNowResult>({
22
+ // Defaults to auth: "user" — only signed-in members can buy.
23
+ args: {
24
+ listingId: v.id("Listing"),
25
+ buyerName: v.string(),
26
+ // Threaded by db.useMutation({ optimistic }) so the server's accepted
27
+ // Offer reuses the id of the optimistic ghost the client painted.
28
+ _optimisticId: v.optional(v.string()),
29
+ },
30
+ async handler(ctx, args) {
31
+ if (!ctx.auth.userId) throw ctx.error("UNAUTHENTICATED", "sign in first");
32
+
33
+ const listing = await ctx.db.get("Listing", args.listingId) as {
34
+ id: string;
35
+ title: string;
36
+ price: number;
37
+ status: string;
38
+ sellerId: string;
39
+ } | null;
40
+ if (!listing) throw ctx.error("NOT_FOUND", "listing not found");
41
+ if (listing.status !== "active")
42
+ throw ctx.error("INVALID_ARGS", "this listing is no longer available");
43
+ if (listing.sellerId === ctx.auth.userId)
44
+ throw ctx.error("INVALID_ARGS", "you can't buy your own listing");
45
+
46
+ const id = await ctx.db.insert("Offer", {
47
+ id: args._optimisticId,
48
+ listingId: args.listingId,
49
+ listingTitle: listing.title,
50
+ sellerId: listing.sellerId,
51
+ buyerId: ctx.auth.userId,
52
+ buyerName: args.buyerName || "anonymous",
53
+ amount: listing.price,
54
+ message: "Bought at list price",
55
+ status: "accepted",
56
+ createdAt: new Date().toISOString(),
57
+ });
58
+
59
+ // Mark sold and decline the rest — same reconciliation respondToOffer does
60
+ // on an accept.
61
+ await ctx.db.update("Listing", args.listingId, { status: "sold" });
62
+ const siblings = (await ctx.db.list("Offer")) as Array<{
63
+ id: string;
64
+ listingId: string;
65
+ status: string;
66
+ }>;
67
+ for (const o of siblings) {
68
+ if (
69
+ o.listingId === args.listingId &&
70
+ o.id !== id &&
71
+ o.status === "pending"
72
+ ) {
73
+ await ctx.db.update("Offer", o.id, { status: "declined" });
74
+ }
75
+ }
76
+ return { id };
77
+ },
78
+ });
@@ -0,0 +1,65 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Make an offer on a listing. Denormalizes the listing title + seller id
5
+ * onto the offer so the seller's inbox + the buyer's "my offers" list can
6
+ * render without a join.
7
+ */
8
+
9
+ interface MakeOfferArgs {
10
+ listingId: string;
11
+ amount: number;
12
+ message?: string;
13
+ buyerName: string;
14
+ _optimisticId?: string;
15
+ }
16
+
17
+ interface MakeOfferResult {
18
+ id: string;
19
+ }
20
+
21
+ export default mutation<MakeOfferArgs, MakeOfferResult>({
22
+ // Defaults to auth: "user" — only signed-in members can bid.
23
+ args: {
24
+ listingId: v.id("Listing"),
25
+ amount: v.number(),
26
+ message: v.optional(v.string()),
27
+ buyerName: v.string(),
28
+ // Threaded by db.useMutation({ optimistic }) so the server row reuses the
29
+ // id of the optimistic ghost the client already painted — the broadcast
30
+ // merges in place instead of flashing a temp row.
31
+ _optimisticId: v.optional(v.string()),
32
+ },
33
+ async handler(ctx, args) {
34
+ if (!ctx.auth.userId) throw ctx.error("UNAUTHENTICATED", "sign in first");
35
+
36
+ const listing = await ctx.db.get("Listing", args.listingId) as {
37
+ id: string;
38
+ title: string;
39
+ sellerId: string;
40
+ status: string;
41
+ } | null;
42
+ if (!listing) throw ctx.error("NOT_FOUND", "listing not found");
43
+ if (listing.status !== "active")
44
+ throw ctx.error("INVALID_ARGS", "this listing is no longer available");
45
+ if (listing.sellerId === ctx.auth.userId)
46
+ throw ctx.error("INVALID_ARGS", "you can't bid on your own listing");
47
+ if (args.amount <= 0)
48
+ throw ctx.error("INVALID_ARGS", "offer must be greater than zero");
49
+
50
+ const id = await ctx.db.insert("Offer", {
51
+ // Reuse the optimistic ghost's id so the broadcast merges in place.
52
+ id: args._optimisticId,
53
+ listingId: args.listingId,
54
+ listingTitle: listing.title,
55
+ sellerId: listing.sellerId,
56
+ buyerId: ctx.auth.userId,
57
+ buyerName: args.buyerName || "anonymous",
58
+ amount: Math.round(args.amount * 100) / 100,
59
+ message: (args.message ?? "").trim(),
60
+ status: "pending",
61
+ createdAt: new Date().toISOString(),
62
+ });
63
+ return { id };
64
+ },
65
+ });
@@ -0,0 +1,62 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Seller accepts or declines an offer. Accepting is the interesting path:
5
+ * it marks the offer accepted, flips the listing to "sold", and declines
6
+ * every other pending offer on that listing — all in one mutation, fanned
7
+ * out to every watching tab by the change log.
8
+ *
9
+ * Ownership is enforced here (not in a policy expression) because the rule
10
+ * spans two entities: "the caller must own the LISTING the OFFER points at."
11
+ */
12
+
13
+ interface RespondToOfferArgs {
14
+ offerId: string;
15
+ accept: boolean;
16
+ }
17
+
18
+ interface RespondToOfferResult {
19
+ accepted: boolean;
20
+ }
21
+
22
+ export default mutation<RespondToOfferArgs, RespondToOfferResult>({
23
+ // Defaults to auth: "user" — only the signed-in seller responds.
24
+ args: {
25
+ offerId: v.id("Offer"),
26
+ accept: v.boolean(),
27
+ },
28
+ async handler(ctx, args) {
29
+ if (!ctx.auth.userId) throw ctx.error("UNAUTHENTICATED", "sign in first");
30
+
31
+ const offer = await ctx.db.get("Offer", args.offerId) as {
32
+ id: string;
33
+ listingId: string;
34
+ sellerId: string;
35
+ status: string;
36
+ } | null;
37
+ if (!offer) throw ctx.error("NOT_FOUND", "offer not found");
38
+ if (offer.sellerId !== ctx.auth.userId)
39
+ throw ctx.error("UNAUTHORIZED", "only the seller can answer this offer");
40
+ if (offer.status !== "pending")
41
+ throw ctx.error("INVALID_ARGS", "this offer was already answered");
42
+
43
+ if (!args.accept) {
44
+ await ctx.db.update("Offer", args.offerId, { status: "declined" });
45
+ return { accepted: false };
46
+ }
47
+
48
+ await ctx.db.update("Offer", args.offerId, { status: "accepted" });
49
+ await ctx.db.update("Listing", String(offer.listingId), { status: "sold" });
50
+
51
+ // Decline the losers.
52
+ const siblings = await ctx.db.query("Offer", {
53
+ listingId: offer.listingId,
54
+ }) as Array<{ id: string; status: string }>;
55
+ for (const o of siblings) {
56
+ if (o.id !== args.offerId && o.status === "pending") {
57
+ await ctx.db.update("Offer", String(o.id), { status: "declined" });
58
+ }
59
+ }
60
+ return { accepted: true };
61
+ },
62
+ });
@@ -0,0 +1,90 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ // A handful of believable listings so the marketplace isn't empty on first
4
+ // run. Idempotent: seeds only when the catalog is empty, so the browse page
5
+ // can call it on load without worrying about duplicates.
6
+ const DEMO: Array<{
7
+ seller: string;
8
+ title: string;
9
+ description: string;
10
+ price: number;
11
+ category: string;
12
+ condition: string;
13
+ seed: string;
14
+ }> = [
15
+ { seller: "maple-fox", title: "Herman Miller Aeron (size B)", description: "Fully loaded, posture-fit SL. Light desk use, no squeaks.", price: 540, category: "furniture", condition: "like-new", seed: "a1f3" },
16
+ { seller: "amber-lynx", title: "Kodak Retina IIa rangefinder", description: "1950s folding 35mm. Clean glass, accurate shutter, leather case.", price: 185, category: "cameras", condition: "good", seed: "b7c2" },
17
+ { seller: "swift-otter", title: "Specialized Allez road bike, 54cm", description: "Shimano Claris, fresh bar tape, recently tuned. Fast commuter.", price: 420, category: "bikes", condition: "good", seed: "c4e9" },
18
+ { seller: "cosmic-wren", title: "Vintage Technics SL-1200 MK2", description: "Legendary direct-drive turntable. Spins true, new slipmat.", price: 650, category: "audio", condition: "good", seed: "d8a1" },
19
+ { seller: "ivory-sparrow", title: "Le Creuset 5.5qt Dutch oven", description: "Flame orange, enamel intact. The one everyone wants.", price: 120, category: "kitchen", condition: "like-new", seed: "e2b6" },
20
+ { seller: "slate-heron", title: 'LG 27" 4K UltraFine monitor', description: "USB-C, 60Hz, color-accurate. Box + cables included.", price: 240, category: "electronics", condition: "good", seed: "f5d0" },
21
+ { seller: "maple-fox", title: "Eames-style lounge + ottoman", description: "Walnut + tan leather replica. Sturdy, very comfortable.", price: 380, category: "furniture", condition: "good", seed: "a9c4" },
22
+ { seller: "amber-lynx", title: "Patagonia Black Hole 55L duffel", description: "One trip old. Weatherproof, zips perfect. Forest green.", price: 75, category: "outdoor", condition: "like-new", seed: "b3f8" },
23
+ { seller: "cosmic-wren", title: "Fender Player Stratocaster, sunburst", description: "Maple neck, plays great. Small buckle rash on back.", price: 560, category: "instruments", condition: "good", seed: "d1e7" },
24
+ { seller: "slate-heron", title: "iPad Air (5th gen) 64GB", description: "Space gray, 100% battery health, screen flawless.", price: 330, category: "electronics", condition: "like-new", seed: "f0a2" },
25
+ { seller: "ivory-sparrow", title: "Mid-century teak sideboard", description: "Three drawers, two cabinets. Real teak, refinished top.", price: 295, category: "furniture", condition: "good", seed: "e7c3" },
26
+ { seller: "swift-otter", title: "Brooks Brothers wool overcoat, 40R", description: "Charcoal herringbone, fully lined. Dry-cleaned, ready to wear.", price: 90, category: "apparel", condition: "good", seed: "c2b5" },
27
+ ];
28
+
29
+ interface SeedMarketArgs {
30
+ start?: number;
31
+ end?: number;
32
+ }
33
+
34
+ interface SeedMarketResult {
35
+ seeded: number;
36
+ }
37
+
38
+ export default mutation<SeedMarketArgs, SeedMarketResult>({
39
+ // Defaults to auth: "user" — seeds a slice of the catalog owned by the
40
+ // caller. The bootstrap calls this twice: once as a "bazaar" seller for the
41
+ // bulk of the catalog (so the demo buyer can bid on it), and once as the
42
+ // demo account for a couple of its own listings.
43
+ args: {
44
+ start: v.optional(v.number()),
45
+ end: v.optional(v.number()),
46
+ },
47
+ async handler(ctx, args) {
48
+ if (!ctx.auth.userId) throw ctx.error("UNAUTHENTICATED", "sign in first");
49
+
50
+ // Per-caller idempotency: skip if THIS seller already has listings, so the
51
+ // two seed calls (and any reloads) don't duplicate.
52
+ const all = await ctx.db.list("Listing") as Array<{ sellerId: string }>;
53
+ if (all.some((l) => l.sellerId === ctx.auth.userId)) return { seeded: 0 };
54
+
55
+ const start = args.start ?? 0;
56
+ const end = args.end ?? DEMO.length;
57
+ const slice = DEMO.slice(start, end);
58
+
59
+ // Stagger createdAt so the grid + ticker have a believable order. The
60
+ // seller id is the caller — `Listing.sellerId` is `field.owner()`, so the
61
+ // framework would reject any other value. `seller` stays a display name.
62
+ const slugify = (s: string) =>
63
+ s
64
+ .toLowerCase()
65
+ .trim()
66
+ .replace(/[^a-z0-9]+/g, "-")
67
+ .replace(/^-+|-+$/g, "")
68
+ .slice(0, 60);
69
+
70
+ const now = Date.now();
71
+ let n = 0;
72
+ for (const d of slice) {
73
+ await ctx.db.insert("Listing", {
74
+ sellerId: ctx.auth.userId,
75
+ sellerName: d.seller,
76
+ title: d.title,
77
+ slug: `${slugify(d.title) || "item"}-${d.seed}`,
78
+ description: d.description,
79
+ price: d.price,
80
+ category: d.category,
81
+ condition: d.condition,
82
+ status: "active",
83
+ seed: d.seed,
84
+ createdAt: new Date(now - (start + n) * 7 * 60_000).toISOString(),
85
+ });
86
+ n++;
87
+ }
88
+ return { seeded: n };
89
+ },
90
+ });
@@ -0,0 +1,10 @@
1
+ node_modules/
2
+ .pylon/
3
+ pylon.manifest.json
4
+ pylon.client.ts
5
+ web/dist/
6
+ *.db
7
+ *.db-*
8
+ .env
9
+ .env.local
10
+ .DS_Store
@@ -0,0 +1,35 @@
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/sync": "^__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
+ "@radix-ui/react-label": "^2.1.0"
27
+ },
28
+ "devDependencies": {
29
+ "@pylonsync/cli": "^__PYLON_VERSION__",
30
+ "@types/node": "^22.0.0",
31
+ "@types/react": "^19.0.0",
32
+ "@types/react-dom": "^19.0.0",
33
+ "typescript": "^5.6.0"
34
+ }
35
+ }