@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,129 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import {
5
+ passwordLogin,
6
+ passwordRegister,
7
+ persistSession,
8
+ ApiError,
9
+ } from "@pylonsync/client";
10
+
11
+ // The owner's email/password form — one form, two modes. It calls the built-in
12
+ // auth API directly (`passwordLogin` / `passwordRegister` POST to
13
+ // `/api/auth/password/*`), then `persistSession` writes the freshly-minted
14
+ // token to local storage so the sync engine + `callFn` authenticate AS THE
15
+ // OWNER on the next load. This step matters here specifically: the landing page
16
+ // mints an anonymous guest session (for the live counter), and without
17
+ // persisting the real session that stale guest token would shadow the owner's
18
+ // — so the owner-only `waitlistStats` call would come back as a guest and get
19
+ // rejected. We then do a full navigation to /dashboard so the SSR runtime
20
+ // re-resolves auth from the HttpOnly cookie and renders server-side.
21
+ //
22
+ // A waitlist is single-tenant: there's no public signup funnel, just the owner
23
+ // creating their one account. Whoever signs in only sees data if their email
24
+ // matches PYLON_OWNER_EMAIL — enforced by the waitlistStats function.
25
+ export function AuthForm() {
26
+ const [mode, setMode] = useState<"login" | "signup">("login");
27
+ const [email, setEmail] = useState("");
28
+ const [password, setPassword] = useState("");
29
+ const [error, setError] = useState<string | null>(null);
30
+ const [pending, setPending] = useState(false);
31
+
32
+ async function onSubmit(e: React.FormEvent) {
33
+ e.preventDefault();
34
+ setError(null);
35
+ setPending(true);
36
+ try {
37
+ const session =
38
+ mode === "login"
39
+ ? await passwordLogin({ email, password })
40
+ : await passwordRegister({ email, password });
41
+ // Make this session authoritative, replacing any anonymous guest token.
42
+ persistSession(session);
43
+ window.location.assign("/dashboard");
44
+ } catch (err) {
45
+ setError(messageFor(err));
46
+ setPending(false);
47
+ }
48
+ }
49
+
50
+ return (
51
+ <div className="space-y-5">
52
+ <form onSubmit={onSubmit} className="space-y-4">
53
+ <label className="block">
54
+ <span className="mb-1.5 block text-[13px] font-medium text-zinc-700">Email</span>
55
+ <input
56
+ type="email"
57
+ value={email}
58
+ onChange={(e) => setEmail(e.target.value)}
59
+ required
60
+ autoComplete="email"
61
+ placeholder="you@yourbusiness.com"
62
+ className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
63
+ />
64
+ </label>
65
+ <label className="block">
66
+ <span className="mb-1.5 block text-[13px] font-medium text-zinc-700">Password</span>
67
+ <input
68
+ type="password"
69
+ value={password}
70
+ onChange={(e) => setPassword(e.target.value)}
71
+ required
72
+ autoComplete={mode === "login" ? "current-password" : "new-password"}
73
+ placeholder={mode === "login" ? "Your password" : "At least 10 characters"}
74
+ className="h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-sm text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
75
+ />
76
+ </label>
77
+ {error ? (
78
+ <p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-[13px] leading-snug text-red-700">
79
+ {error}
80
+ </p>
81
+ ) : null}
82
+ <button
83
+ type="submit"
84
+ disabled={pending}
85
+ className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-zinc-900 text-sm font-medium text-white transition-colors hover:bg-zinc-700 disabled:opacity-60"
86
+ >
87
+ {pending ? "…" : mode === "login" ? "Sign in" : "Create account"}
88
+ </button>
89
+ </form>
90
+
91
+ <p className="text-center text-[13px] text-zinc-500">
92
+ {mode === "login" ? "First time here?" : "Already have an account?"}{" "}
93
+ <button
94
+ type="button"
95
+ onClick={() => {
96
+ setMode(mode === "login" ? "signup" : "login");
97
+ setError(null);
98
+ }}
99
+ className="font-medium text-zinc-900 underline underline-offset-2"
100
+ >
101
+ {mode === "login" ? "Create the owner account" : "Sign in"}
102
+ </button>
103
+ </p>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ // Map the framework's auth error codes to friendly copy. `ApiError` carries a
109
+ // stable `.code` so you branch on the code, not the message.
110
+ function messageFor(err: unknown): string {
111
+ if (err instanceof ApiError) {
112
+ switch (err.code) {
113
+ case "INVALID_CREDENTIALS":
114
+ return "Wrong email or password.";
115
+ case "USER_EXISTS":
116
+ return "That email is already registered — sign in instead.";
117
+ case "WEAK_PASSWORD":
118
+ return "Pick a longer password — at least 10 characters.";
119
+ case "PWNED_PASSWORD":
120
+ return "That password has appeared in a known data breach. Choose a different one.";
121
+ case "RATE_LIMITED":
122
+ return "Too many attempts — try again in a minute.";
123
+ default:
124
+ return err.message;
125
+ }
126
+ }
127
+ if (err instanceof Error) return err.message;
128
+ return "Something went wrong. Try again.";
129
+ }
@@ -0,0 +1,297 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useMemo, useState } from "react";
4
+ import { db, callFn } from "@pylonsync/react";
5
+ import { useAuth } from "@pylonsync/client";
6
+ import type { WaitlistStatsData, WaitlistStatsResult, SignupRow } from "@/lib/stats";
7
+
8
+ // The owner's live dashboard. Liveness rides the SAME public aggregate the
9
+ // landing page uses: `db.useQuery("WaitlistStat")` re-renders the instant the
10
+ // count changes (cross-tab, via the replica). The emails themselves never sync
11
+ // — they come from the owner-gated `waitlistStats` function, (re)fetched on
12
+ // mount and whenever the live count ticks. So the total, chart, and list all
13
+ // stay live, but PII only ever travels through the gated call.
14
+ export function WaitlistDashboard({ userEmail }: { userEmail: string }) {
15
+ const { data: statRows } = db.useQuery<{ id: string; count: number }>("WaitlistStat");
16
+ const liveCount = statRows.length > 0 ? statRows[0].count : 0;
17
+
18
+ const [data, setData] = useState<WaitlistStatsData | null>(null);
19
+ const [denied, setDenied] = useState(false);
20
+ const [error, setError] = useState<string | null>(null);
21
+
22
+ useEffect(() => {
23
+ let cancelled = false;
24
+ callFn<WaitlistStatsResult>("waitlistStats", {})
25
+ .then((r) => {
26
+ if (cancelled) return;
27
+ // The owner gate (PYLON_OWNER_EMAIL) lives in the function; a non-owner
28
+ // comes back as `{ authorized: false }` (with no data) → locked card.
29
+ if (!r.authorized) {
30
+ setDenied(true);
31
+ } else {
32
+ setData(r);
33
+ setError(null);
34
+ setDenied(false);
35
+ }
36
+ })
37
+ .catch((e) => {
38
+ if (!cancelled) setError(e instanceof Error ? e.message : String(e));
39
+ });
40
+ return () => {
41
+ cancelled = true;
42
+ };
43
+ // Re-fetch whenever a new signup moves the live count.
44
+ }, [liveCount]);
45
+
46
+ if (denied) return <OwnerOnly email={userEmail} />;
47
+ if (error) {
48
+ return (
49
+ <div className="rounded-xl border border-red-200 bg-red-50 px-5 py-4 text-sm text-red-700">
50
+ {error}
51
+ </div>
52
+ );
53
+ }
54
+ if (!data) return <Skeleton />;
55
+
56
+ return (
57
+ <div className="space-y-8">
58
+ <div>
59
+ <h1 className="text-xl font-semibold tracking-tight">Waitlist</h1>
60
+ <p className="mt-1 text-sm text-zinc-500">
61
+ Live — new signups appear here the moment they happen.
62
+ </p>
63
+ </div>
64
+
65
+ <div className="grid gap-4 sm:grid-cols-3">
66
+ <Stat label="Total signups" value={data.total} />
67
+ <Stat label="Last 7 days" value={data.last7} />
68
+ <Stat label="Today" value={data.today} />
69
+ </div>
70
+
71
+ <TrendChart daily={data.daily} />
72
+
73
+ <SignupTable signups={data.signups} />
74
+ </div>
75
+ );
76
+ }
77
+
78
+ function Stat({ label, value }: { label: string; value: number }) {
79
+ return (
80
+ <div className="rounded-xl border border-zinc-200 bg-white p-4">
81
+ <div className="text-[11px] font-medium uppercase tracking-wide text-zinc-400">
82
+ {label}
83
+ </div>
84
+ <div className="mt-1 text-2xl font-semibold tabular-nums text-zinc-900">
85
+ {value.toLocaleString()}
86
+ </div>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ /* ----------------------------- trend chart ---------------------------- */
92
+
93
+ function TrendChart({ daily }: { daily: { date: string; count: number }[] }) {
94
+ const max = Math.max(1, ...daily.map((d) => d.count));
95
+ return (
96
+ <div className="rounded-xl border border-zinc-200 bg-white p-5">
97
+ <div className="flex items-center justify-between">
98
+ <h2 className="text-sm font-semibold text-zinc-900">Last 30 days</h2>
99
+ <span className="text-[12px] text-zinc-400">
100
+ peak {max.toLocaleString()}/day
101
+ </span>
102
+ </div>
103
+ {/* Each column is a FULL-HEIGHT flex cell (h-full) that bottom-aligns its
104
+ bar — so the bar's percentage height resolves against the 7rem track,
105
+ not an auto-height parent (which would collapse it to nothing). */}
106
+ <div className="mt-4 flex h-28 items-end gap-1">
107
+ {daily.map((d) => (
108
+ <div
109
+ key={d.date}
110
+ className="group relative flex h-full flex-1 flex-col justify-end"
111
+ >
112
+ <div
113
+ className="w-full rounded-t bg-brand/80 transition-colors group-hover:bg-brand"
114
+ style={{ height: `${Math.max(2, (d.count / max) * 100)}%` }}
115
+ />
116
+ {/* Tooltip on hover — date + count. */}
117
+ <div className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-1 hidden -translate-x-1/2 whitespace-nowrap rounded bg-zinc-900 px-2 py-1 text-[11px] text-white group-hover:block">
118
+ {fmtDay(d.date)} · {d.count}
119
+ </div>
120
+ </div>
121
+ ))}
122
+ </div>
123
+ <div className="mt-2 flex justify-between text-[11px] text-zinc-400">
124
+ <span>{fmtDay(daily[0]?.date)}</span>
125
+ <span>{fmtDay(daily[daily.length - 1]?.date)}</span>
126
+ </div>
127
+ </div>
128
+ );
129
+ }
130
+
131
+ /* ----------------------------- signup list ---------------------------- */
132
+
133
+ function SignupTable({ signups }: { signups: SignupRow[] }) {
134
+ const [q, setQ] = useState("");
135
+ const filtered = useMemo(() => {
136
+ const needle = q.trim().toLowerCase();
137
+ if (!needle) return signups;
138
+ return signups.filter((s) => s.email.toLowerCase().includes(needle));
139
+ }, [q, signups]);
140
+
141
+ return (
142
+ <div className="rounded-xl border border-zinc-200 bg-white">
143
+ <div className="flex flex-col gap-3 border-b border-zinc-100 p-4 sm:flex-row sm:items-center sm:justify-between">
144
+ <h2 className="text-sm font-semibold text-zinc-900">
145
+ Signups{" "}
146
+ <span className="font-normal text-zinc-400">
147
+ ({filtered.length.toLocaleString()})
148
+ </span>
149
+ </h2>
150
+ <div className="flex items-center gap-2">
151
+ <input
152
+ value={q}
153
+ onChange={(e) => setQ(e.target.value)}
154
+ placeholder="Search email…"
155
+ aria-label="Search signups"
156
+ className="h-9 w-44 rounded-md border border-zinc-300 bg-white px-3 text-sm outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20"
157
+ />
158
+ <button
159
+ type="button"
160
+ onClick={() => exportCsv(signups)}
161
+ disabled={signups.length === 0}
162
+ className="inline-flex h-9 items-center rounded-md border border-zinc-300 px-3 text-[13px] font-medium text-zinc-700 transition-colors hover:bg-zinc-50 disabled:opacity-40"
163
+ >
164
+ Export CSV
165
+ </button>
166
+ </div>
167
+ </div>
168
+
169
+ {filtered.length === 0 ? (
170
+ <p className="p-8 text-center text-sm text-zinc-500">
171
+ {signups.length === 0
172
+ ? "No signups yet. Share your landing page and watch them roll in — live."
173
+ : "No emails match your search."}
174
+ </p>
175
+ ) : (
176
+ <ul className="divide-y divide-zinc-100">
177
+ {filtered.map((s) => (
178
+ <li key={s.id} className="flex items-center justify-between gap-3 px-4 py-2.5">
179
+ <span className="truncate text-sm text-zinc-800">{s.email}</span>
180
+ <span className="shrink-0 text-[12px] text-zinc-400">
181
+ {fmtDateTime(s.createdAt)}
182
+ </span>
183
+ </li>
184
+ ))}
185
+ </ul>
186
+ )}
187
+ </div>
188
+ );
189
+ }
190
+
191
+ /* --------------------------- owner-only gate -------------------------- */
192
+
193
+ // Shown to a signed-in user the `waitlistStats` function refused (not the
194
+ // configured owner, or no PYLON_OWNER_EMAIL set). Fails closed — no signup data
195
+ // is fetched or shown.
196
+ function OwnerOnly({ email }: { email: string }) {
197
+ return (
198
+ <div className="rounded-xl border border-dashed border-zinc-300 px-6 py-12 text-center">
199
+ <h1 className="text-lg font-semibold">This dashboard is owner-only</h1>
200
+ <p className="mx-auto mt-2 max-w-md text-sm text-zinc-500">
201
+ You&apos;re signed in as{" "}
202
+ <span className="font-medium text-zinc-700">{email || "this account"}</span>.
203
+ A waitlist is single-tenant — only the owner can see the signups. Set{" "}
204
+ <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-[12px]">
205
+ PYLON_OWNER_EMAIL={email || "you@yourbusiness.com"}
206
+ </code>{" "}
207
+ in your{" "}
208
+ <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-[12px]">.env</code>,
209
+ restart, and reload — or sign in with the owner account.
210
+ </p>
211
+ </div>
212
+ );
213
+ }
214
+
215
+ /* ----------------------------- user menu ------------------------------ */
216
+
217
+ export function UserMenu({ email }: { email: string }) {
218
+ const { signOut } = useAuth();
219
+ const initial = (email.trim()[0] || "?").toUpperCase();
220
+ async function onSignOut() {
221
+ await signOut();
222
+ window.location.assign("/");
223
+ }
224
+ return (
225
+ <details className="group relative">
226
+ <summary className="flex size-8 cursor-pointer select-none list-none items-center justify-center rounded-full bg-zinc-900 text-[12px] font-semibold text-white marker:hidden [&::-webkit-details-marker]:hidden">
227
+ {initial}
228
+ </summary>
229
+ <div className="absolute right-0 top-full z-40 mt-2 w-56 overflow-hidden rounded-xl border border-zinc-200 bg-white py-1 shadow-[0_16px_48px_-16px_rgba(0,0,0,0.25)]">
230
+ <div className="border-b border-zinc-100 px-3 py-2">
231
+ <div className="truncate text-[13px] font-medium text-zinc-900">
232
+ {email || "Signed in"}
233
+ </div>
234
+ </div>
235
+ <button
236
+ type="button"
237
+ onClick={onSignOut}
238
+ className="flex w-full items-center px-3 py-2 text-left text-[13px] text-zinc-700 transition-colors hover:bg-zinc-50"
239
+ >
240
+ Sign out
241
+ </button>
242
+ </div>
243
+ </details>
244
+ );
245
+ }
246
+
247
+ /* ------------------------------ helpers ------------------------------- */
248
+
249
+ function Skeleton() {
250
+ return (
251
+ <div className="space-y-8">
252
+ <div className="h-6 w-32 animate-pulse rounded bg-zinc-100" />
253
+ <div className="grid gap-4 sm:grid-cols-3">
254
+ {[0, 1, 2].map((i) => (
255
+ <div key={i} className="h-20 animate-pulse rounded-xl bg-zinc-100" />
256
+ ))}
257
+ </div>
258
+ <div className="h-44 animate-pulse rounded-xl bg-zinc-100" />
259
+ <div className="h-64 animate-pulse rounded-xl bg-zinc-100" />
260
+ </div>
261
+ );
262
+ }
263
+
264
+ function exportCsv(signups: SignupRow[]) {
265
+ const cell = (v: string) =>
266
+ /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
267
+ const rows = [
268
+ "email,joined_at",
269
+ ...signups.map((s) => `${cell(s.email)},${cell(s.createdAt)}`),
270
+ ];
271
+ const blob = new Blob([rows.join("\n")], { type: "text/csv;charset=utf-8" });
272
+ const url = URL.createObjectURL(blob);
273
+ const a = document.createElement("a");
274
+ a.href = url;
275
+ a.download = `waitlist-${new Date().toISOString().slice(0, 10)}.csv`;
276
+ document.body.appendChild(a);
277
+ a.click();
278
+ a.remove();
279
+ URL.revokeObjectURL(url);
280
+ }
281
+
282
+ function fmtDay(iso?: string) {
283
+ if (!iso) return "";
284
+ const d = new Date(iso + "T00:00:00Z");
285
+ return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
286
+ }
287
+
288
+ function fmtDateTime(iso: string) {
289
+ const t = Date.parse(iso);
290
+ if (Number.isNaN(t)) return "";
291
+ return new Date(t).toLocaleString(undefined, {
292
+ month: "short",
293
+ day: "numeric",
294
+ hour: "numeric",
295
+ minute: "2-digit",
296
+ });
297
+ }
@@ -0,0 +1,70 @@
1
+ import React, { use } from "react";
2
+ import { Link, type Metadata, type PageProps } from "@pylonsync/react";
3
+ import { siteConfig } from "@/lib/site.config";
4
+ import { UserMenu, WaitlistDashboard } from "./dashboard-client";
5
+
6
+ export const metadata: Metadata = {
7
+ title: `Dashboard — ${siteConfig.brand.name}`,
8
+ robots: "noindex",
9
+ };
10
+
11
+ // `app/dashboard/page.tsx` → `/dashboard`. Server-side auth gate only — any
12
+ // signed-in user can load the shell. The OWNER gate (a waitlist is
13
+ // single-tenant: only PYLON_OWNER_EMAIL may see the signups) lives in the
14
+ // `waitlistStats` function, which checks `ctx.env.PYLON_OWNER_EMAIL` — the
15
+ // authoritative server env. Non-owners get a clean "owner-only" card from the
16
+ // client when that call is denied. (Env-based config is read in the function,
17
+ // not here: an SSR page render can't reliably read arbitrary `process.env`.)
18
+ export default function DashboardPage({ auth, response, serverData }: PageProps) {
19
+ // Anonymous visitors and guest sessions (guest_… ids) get bounced to login —
20
+ // the dashboard is for the real, signed-in owner only.
21
+ if (!auth.user_id || auth.user_id.startsWith("guest_")) {
22
+ response.redirect("/login");
23
+ return null;
24
+ }
25
+
26
+ // The User self-read policy lets the signed-in user read their own row → the
27
+ // email shown in the account menu (and on the owner-only card).
28
+ const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
29
+ const email = me?.email ?? "";
30
+
31
+ return (
32
+ <Shell email={email}>
33
+ <WaitlistDashboard userEmail={email} />
34
+ </Shell>
35
+ );
36
+ }
37
+
38
+ // Dashboard chrome: a slim top bar with the logo, a link back to the public
39
+ // site, and the account menu (a client island for sign-out).
40
+ function Shell({ email, children }: { email: string; children: React.ReactNode }) {
41
+ const { brand } = siteConfig;
42
+ return (
43
+ <div className="flex min-h-screen flex-col bg-white text-zinc-900">
44
+ <header className="border-b border-zinc-200">
45
+ <div className="mx-auto flex h-14 max-w-4xl items-center justify-between px-6">
46
+ <div className="flex items-center gap-2">
47
+ <span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
48
+ {brand.letter}
49
+ </span>
50
+ <span className="text-[15px] font-semibold tracking-tight">
51
+ {brand.name} <span className="text-zinc-400">/ waitlist</span>
52
+ </span>
53
+ </div>
54
+ <div className="flex items-center gap-4">
55
+ <Link
56
+ href="/"
57
+ className="text-[13px] text-zinc-500 transition-colors hover:text-zinc-900"
58
+ >
59
+ View site ↗
60
+ </Link>
61
+ <UserMenu email={email} />
62
+ </div>
63
+ </div>
64
+ </header>
65
+ <main className="flex-1">
66
+ <div className="mx-auto max-w-4xl px-6 py-8">{children}</div>
67
+ </main>
68
+ </div>
69
+ );
70
+ }
@@ -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-zinc-500">{error.message}</p>
12
+ {error.digest ? (
13
+ <p className="mt-1 text-xs text-zinc-400">
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-full bg-zinc-900 px-5 text-sm font-medium text-white transition-colors hover:bg-zinc-700"
21
+ >
22
+ Try again
23
+ </button>
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,148 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ /* Tailwind v4 scans these globs for class names. Add more @source lines if you
5
+ put markup elsewhere. The @pylonsync/client line lets its components
6
+ (EnsureGuest, auth helpers) keep any classes they ship. */
7
+ @source "../app/**/*.{tsx,ts,jsx,js}";
8
+ @source "../components/**/*.{tsx,ts,jsx,js}";
9
+ @source "../lib/**/*.{tsx,ts,jsx,js}";
10
+ @source "../node_modules/@pylonsync/client/**/*.{tsx,ts,jsx,js}";
11
+
12
+ @custom-variant dark (&:where(.dark, .dark *));
13
+
14
+ /* shadcn/ui design tokens (new-york / zinc) + the marketing brand accent. The
15
+ three brand vars are defaults — app/layout.tsx overrides them from
16
+ lib/site.config.ts on <html>, so re-theming the whole page is one edit there. */
17
+ :root {
18
+ --radius: 0.625rem;
19
+ --brand: #4f46e5;
20
+ --brand-soft: #eef2ff;
21
+ --paper: #fafafa;
22
+ --background: oklch(1 0 0);
23
+ --foreground: oklch(0.141 0.005 285.823);
24
+ --card: oklch(1 0 0);
25
+ --card-foreground: oklch(0.141 0.005 285.823);
26
+ --popover: oklch(1 0 0);
27
+ --popover-foreground: oklch(0.141 0.005 285.823);
28
+ --primary: oklch(0.21 0.006 285.885);
29
+ --primary-foreground: oklch(0.985 0 0);
30
+ --secondary: oklch(0.967 0.001 286.375);
31
+ --secondary-foreground: oklch(0.21 0.006 285.885);
32
+ --muted: oklch(0.967 0.001 286.375);
33
+ --muted-foreground: oklch(0.552 0.016 285.938);
34
+ --accent: oklch(0.967 0.001 286.375);
35
+ --accent-foreground: oklch(0.21 0.006 285.885);
36
+ --destructive: oklch(0.577 0.245 27.325);
37
+ --border: oklch(0.92 0.004 286.32);
38
+ --input: oklch(0.92 0.004 286.32);
39
+ --ring: oklch(0.705 0.015 286.067);
40
+ --chart-1: oklch(0.646 0.222 41.116);
41
+ --chart-2: oklch(0.6 0.118 184.704);
42
+ --chart-3: oklch(0.398 0.07 227.392);
43
+ --chart-4: oklch(0.828 0.189 84.429);
44
+ --chart-5: oklch(0.769 0.188 70.08);
45
+ --sidebar: oklch(0.985 0 0);
46
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
47
+ --sidebar-primary: oklch(0.21 0.006 285.885);
48
+ --sidebar-primary-foreground: oklch(0.985 0 0);
49
+ --sidebar-accent: oklch(0.967 0.001 286.375);
50
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
51
+ --sidebar-border: oklch(0.92 0.004 286.32);
52
+ --sidebar-ring: oklch(0.705 0.015 286.067);
53
+ }
54
+
55
+ .dark {
56
+ --background: oklch(0.141 0.005 285.823);
57
+ --foreground: oklch(0.985 0 0);
58
+ --card: oklch(0.21 0.006 285.885);
59
+ --card-foreground: oklch(0.985 0 0);
60
+ --popover: oklch(0.21 0.006 285.885);
61
+ --popover-foreground: oklch(0.985 0 0);
62
+ --primary: oklch(0.92 0.004 286.32);
63
+ --primary-foreground: oklch(0.21 0.006 285.885);
64
+ --secondary: oklch(0.274 0.006 286.033);
65
+ --secondary-foreground: oklch(0.985 0 0);
66
+ --muted: oklch(0.274 0.006 286.033);
67
+ --muted-foreground: oklch(0.705 0.015 286.067);
68
+ --accent: oklch(0.274 0.006 286.033);
69
+ --accent-foreground: oklch(0.985 0 0);
70
+ --destructive: oklch(0.704 0.191 22.216);
71
+ --border: oklch(1 0 0 / 10%);
72
+ --input: oklch(1 0 0 / 15%);
73
+ --ring: oklch(0.552 0.016 285.938);
74
+ --chart-1: oklch(0.488 0.243 264.376);
75
+ --chart-2: oklch(0.696 0.17 162.48);
76
+ --chart-3: oklch(0.769 0.188 70.08);
77
+ --chart-4: oklch(0.627 0.265 303.9);
78
+ --chart-5: oklch(0.645 0.246 16.439);
79
+ --sidebar: oklch(0.21 0.006 285.885);
80
+ --sidebar-foreground: oklch(0.985 0 0);
81
+ --sidebar-primary: oklch(0.488 0.243 264.376);
82
+ --sidebar-primary-foreground: oklch(0.985 0 0);
83
+ --sidebar-accent: oklch(0.274 0.006 286.033);
84
+ --sidebar-accent-foreground: oklch(0.985 0 0);
85
+ --sidebar-border: oklch(1 0 0 / 10%);
86
+ --sidebar-ring: oklch(0.552 0.016 285.938);
87
+ }
88
+
89
+ @theme inline {
90
+ --radius-sm: calc(var(--radius) - 4px);
91
+ --radius-md: calc(var(--radius) - 2px);
92
+ --radius-lg: var(--radius);
93
+ --radius-xl: calc(var(--radius) + 4px);
94
+ --color-background: var(--background);
95
+ --color-foreground: var(--foreground);
96
+ --color-card: var(--card);
97
+ --color-card-foreground: var(--card-foreground);
98
+ --color-popover: var(--popover);
99
+ --color-popover-foreground: var(--popover-foreground);
100
+ --color-primary: var(--primary);
101
+ --color-primary-foreground: var(--primary-foreground);
102
+ --color-secondary: var(--secondary);
103
+ --color-secondary-foreground: var(--secondary-foreground);
104
+ --color-muted: var(--muted);
105
+ --color-muted-foreground: var(--muted-foreground);
106
+ --color-accent: var(--accent);
107
+ --color-accent-foreground: var(--accent-foreground);
108
+ --color-destructive: var(--destructive);
109
+ --color-border: var(--border);
110
+ --color-input: var(--input);
111
+ --color-ring: var(--ring);
112
+ --color-brand: var(--brand);
113
+ --color-brand-soft: var(--brand-soft);
114
+ --color-paper: var(--paper);
115
+ --color-chart-1: var(--chart-1);
116
+ --color-chart-2: var(--chart-2);
117
+ --color-chart-3: var(--chart-3);
118
+ --color-chart-4: var(--chart-4);
119
+ --color-chart-5: var(--chart-5);
120
+ --color-sidebar: var(--sidebar);
121
+ --color-sidebar-foreground: var(--sidebar-foreground);
122
+ --color-sidebar-primary: var(--sidebar-primary);
123
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
124
+ --color-sidebar-accent: var(--sidebar-accent);
125
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
126
+ --color-sidebar-border: var(--sidebar-border);
127
+ --color-sidebar-ring: var(--sidebar-ring);
128
+ }
129
+
130
+ @layer base {
131
+ *,
132
+ ::after,
133
+ ::before,
134
+ ::backdrop,
135
+ ::file-selector-button {
136
+ border-color: var(--color-border, currentColor);
137
+ outline-color: var(--color-ring);
138
+ }
139
+ body {
140
+ background-color: var(--color-background);
141
+ color: var(--color-foreground);
142
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
143
+ -webkit-font-smoothing: antialiased;
144
+ }
145
+ button {
146
+ cursor: pointer;
147
+ }
148
+ }