@pylonsync/create-pylon 0.3.273 → 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,207 @@
1
+ import React from "react";
2
+ import { type Metadata } from "@pylonsync/react";
3
+ import {
4
+ WRAP,
5
+ Eyebrow,
6
+ Divider,
7
+ SectionHead,
8
+ ImagePlaceholder,
9
+ initials,
10
+ } from "@/components/marketing";
11
+ import { LiveSlots, ContactForm } from "./contact-form";
12
+ import { siteConfig } from "@/lib/site.config";
13
+
14
+ export const metadata: Metadata = {
15
+ title: siteConfig.seo.title,
16
+ description: siteConfig.seo.description,
17
+ openGraph: { title: siteConfig.seo.title, description: siteConfig.seo.description, type: "website" },
18
+ };
19
+
20
+ // `app/page.tsx` → `/`. Server-rendered studio site. Hero, services, work,
21
+ // process, team, and testimonials are static server HTML (SEO + first paint);
22
+ // the live "slots open" pill and the contact form (#contact) are client islands
23
+ // driven by the public Capacity row. All copy comes from siteConfig. Doesn't
24
+ // read `auth`, so the public page stays cacheable.
25
+ export default function LandingPage() {
26
+ const { hero, logos, services, work, process, team, testimonials, contact } = siteConfig;
27
+
28
+ return (
29
+ <div className="bg-white text-zinc-900">
30
+ {/* ============================== HERO ============================== */}
31
+ <section className={`${WRAP} pt-16 pb-14 sm:pt-20`}>
32
+ <div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-14">
33
+ <div>
34
+ <p className="font-mono text-[11px] uppercase tracking-[0.16em] text-brand">{hero.tagline}</p>
35
+ <h1 className="mt-4 text-balance text-[2.5rem] font-semibold leading-[1.04] tracking-[-0.02em] sm:text-[3.25rem]">
36
+ {hero.headline}
37
+ </h1>
38
+ <p className="mt-5 max-w-xl text-[17px] leading-relaxed text-zinc-500">{hero.subcopy}</p>
39
+ <div className="mt-7 flex flex-wrap items-center gap-4">
40
+ <a href="#contact" className="inline-flex items-center rounded-full bg-brand px-5 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-90">
41
+ {hero.ctaLabel}
42
+ </a>
43
+ <a href="#work" className="text-sm font-medium text-zinc-700 hover:text-zinc-900">
44
+ {hero.secondaryCtaLabel} →
45
+ </a>
46
+ </div>
47
+ <div className="mt-8">
48
+ <LiveSlots />
49
+ </div>
50
+ </div>
51
+
52
+ {/* Hero photo — replace the placeholder with a real shot (the studio,
53
+ the team at work, a flagship project). */}
54
+ <div className="relative mx-auto w-full max-w-sm lg:max-w-none">
55
+ <ImagePlaceholder
56
+ shape="portrait"
57
+ title="A photo of your studio or work"
58
+ hint="Swap for an <img> in app/page.tsx"
59
+ />
60
+ </div>
61
+ </div>
62
+ </section>
63
+
64
+ {/* ============================== LOGOS ============================= */}
65
+ <section className={`${WRAP} pb-6`}>
66
+ <p className="text-center font-mono text-[11px] uppercase tracking-[0.16em] text-zinc-400">
67
+ {logos.eyebrow}
68
+ </p>
69
+ <div className="mt-5 flex flex-wrap items-center justify-center gap-x-10 gap-y-4">
70
+ {logos.names.map((n) => (
71
+ <span key={n} className="text-[15px] font-semibold tracking-tight text-zinc-300">
72
+ {n}
73
+ </span>
74
+ ))}
75
+ </div>
76
+ </section>
77
+
78
+ {/* ============================ SERVICES =========================== */}
79
+ <Divider />
80
+ <section id="services" className={`${WRAP} py-16`}>
81
+ <SectionHead eyebrow={services.eyebrow} title={services.headline} />
82
+ <div className="mt-10 grid gap-x-8 gap-y-10 sm:grid-cols-2">
83
+ {services.items.map((s) => (
84
+ <div key={s.title}>
85
+ {s.icon ? (
86
+ <span className="flex size-9 items-center justify-center rounded-lg bg-brand-soft text-brand">
87
+ {s.icon}
88
+ </span>
89
+ ) : null}
90
+ <h3 className="mt-4 text-[16px] font-semibold text-zinc-900">{s.title}</h3>
91
+ <p className="mt-2 max-w-md text-[14px] leading-relaxed text-zinc-500">{s.body}</p>
92
+ </div>
93
+ ))}
94
+ </div>
95
+ </section>
96
+
97
+ {/* ============================== WORK ============================= */}
98
+ <Divider />
99
+ <section id="work" className={`${WRAP} py-16`}>
100
+ <SectionHead eyebrow={work.eyebrow} title={work.headline} />
101
+ <div className="mt-10 grid gap-6 sm:grid-cols-2">
102
+ {work.items.map((c) => (
103
+ <div key={c.title} className="group">
104
+ {/* Case-study image — drop in a real project screenshot. */}
105
+ <ImagePlaceholder
106
+ shape="landscape"
107
+ title={`${c.title} — project shot`}
108
+ hint="Swap for an <img> per case study"
109
+ />
110
+ <div className="mt-4 flex items-baseline justify-between gap-3">
111
+ <h3 className="text-[16px] font-semibold text-zinc-900">{c.title}</h3>
112
+ <span className="shrink-0 font-mono text-[11px] uppercase tracking-wide text-zinc-400">
113
+ {c.client}
114
+ </span>
115
+ </div>
116
+ <p className="mt-1.5 text-[14px] leading-relaxed text-zinc-500">{c.summary}</p>
117
+ <div className="mt-3 flex flex-wrap gap-1.5">
118
+ {c.tags.map((t) => (
119
+ <span key={t} className="rounded-full bg-zinc-100 px-2.5 py-0.5 text-[11px] font-medium text-zinc-600">
120
+ {t}
121
+ </span>
122
+ ))}
123
+ </div>
124
+ </div>
125
+ ))}
126
+ </div>
127
+ </section>
128
+
129
+ {/* ============================ PROCESS ============================ */}
130
+ <Divider />
131
+ <section className={`${WRAP} py-16`}>
132
+ <SectionHead eyebrow={process.eyebrow} title={process.headline} />
133
+ <ol className="mt-10 grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
134
+ {process.steps.map((step, i) => (
135
+ <li key={step.title}>
136
+ <span className="flex size-8 items-center justify-center rounded-full bg-zinc-900 font-mono text-[12px] font-semibold text-white">
137
+ {i + 1}
138
+ </span>
139
+ <h3 className="mt-4 text-[15px] font-semibold text-zinc-900">{step.title}</h3>
140
+ <p className="mt-2 text-[14px] leading-relaxed text-zinc-500">{step.body}</p>
141
+ </li>
142
+ ))}
143
+ </ol>
144
+ </section>
145
+
146
+ {/* ============================== TEAM ============================= */}
147
+ <Divider />
148
+ <section className={`${WRAP} py-16`}>
149
+ <SectionHead eyebrow={team.eyebrow} title={team.headline} />
150
+ <div className="mt-10 grid gap-8 sm:grid-cols-3">
151
+ {team.members.map((m) => (
152
+ <div key={m.name}>
153
+ {/* Team headshot — drop in a real photo. */}
154
+ <ImagePlaceholder shape="square" title="Headshot" hint="Replace per team member" className="max-w-[200px]" />
155
+ <h3 className="mt-4 text-[15px] font-semibold text-zinc-900">{m.name}</h3>
156
+ <p className="text-[13.5px] text-zinc-500">{m.role}</p>
157
+ </div>
158
+ ))}
159
+ </div>
160
+ </section>
161
+
162
+ {/* ========================== TESTIMONIALS ========================= */}
163
+ {testimonials ? (
164
+ <>
165
+ <Divider />
166
+ <section className={`${WRAP} py-16`}>
167
+ <SectionHead eyebrow={testimonials.eyebrow} title={testimonials.headline} />
168
+ <div className="mt-10 grid gap-6 sm:grid-cols-3">
169
+ {testimonials.items.map((t) => (
170
+ <figure key={t.name} className="flex flex-col rounded-2xl border border-zinc-200 bg-paper p-6">
171
+ <blockquote className="flex-1 text-[14px] leading-relaxed text-zinc-700">“{t.quote}”</blockquote>
172
+ <figcaption className="mt-5 flex items-center gap-3">
173
+ <span className="flex size-9 items-center justify-center rounded-full bg-zinc-200 text-[11px] font-semibold text-zinc-500">
174
+ {initials(t.name)}
175
+ </span>
176
+ <span className="text-[13px] leading-tight">
177
+ <span className="block font-medium text-zinc-900">{t.name}</span>
178
+ <span className="text-zinc-500">{t.role}</span>
179
+ </span>
180
+ </figcaption>
181
+ </figure>
182
+ ))}
183
+ </div>
184
+ </section>
185
+ </>
186
+ ) : null}
187
+
188
+ {/* ============================= CONTACT =========================== */}
189
+ <Divider />
190
+ <section id="contact" className={`${WRAP} py-16`}>
191
+ <div className="grid gap-10 lg:grid-cols-[0.9fr_1.1fr]">
192
+ <div>
193
+ <Eyebrow>{contact.eyebrow}</Eyebrow>
194
+ <h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
195
+ {contact.headline}
196
+ </h2>
197
+ <p className="mt-4 max-w-md text-[15px] leading-relaxed text-zinc-500">{contact.subcopy}</p>
198
+ <div className="mt-6">
199
+ <LiveSlots />
200
+ </div>
201
+ </div>
202
+ <ContactForm />
203
+ </div>
204
+ </section>
205
+ </div>
206
+ );
207
+ }
@@ -0,0 +1,12 @@
1
+ import type { Robots } from "@pylonsync/react";
2
+
3
+ // app/robots.ts → served at /robots.txt. Point SITE_URL at your domain in prod.
4
+ const SITE = process.env.SITE_URL ?? "http://localhost:4321";
5
+
6
+ export default function robots(): Robots {
7
+ return {
8
+ // Keep the owner dashboard and the API out of the index.
9
+ rules: { userAgent: "*", allow: "/", disallow: ["/dashboard", "/login", "/api/"] },
10
+ sitemap: `${SITE}/sitemap.xml`,
11
+ };
12
+ }
@@ -0,0 +1,9 @@
1
+ import type { Sitemap } from "@pylonsync/react";
2
+
3
+ // app/sitemap.ts → served at /sitemap.xml. Point SITE_URL at your domain in
4
+ // production. The studio site is a single public page, so the sitemap is just "/".
5
+ const SITE = process.env.SITE_URL ?? "http://localhost:4321";
6
+
7
+ export default async function sitemap(): Promise<Sitemap> {
8
+ return [{ url: `${SITE}/`, changeFrequency: "weekly", priority: 1 }];
9
+ }
@@ -0,0 +1,135 @@
1
+ import {
2
+ entity,
3
+ field,
4
+ policy,
5
+ auth,
6
+ buildManifest,
7
+ discoverAppRoutes,
8
+ } from "@pylonsync/sdk";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // agency — a site for a boutique studio that takes on a LIMITED number of
12
+ // projects at a time. The realtime hook is scarcity: the hero shows how many
13
+ // project slots are open this quarter, and the moment the owner books a new
14
+ // client from the dashboard, that number drops for EVERYONE with the page
15
+ // open — no refresh. Open it in two tabs to see it.
16
+ //
17
+ // Three entities:
18
+ // • Inquiry — a "start a project" lead, with the prospect's name, email,
19
+ // company + budget + message. Pure PII → denies ALL client
20
+ // reads/writes. The public site never reads an Inquiry; the
21
+ // owner sees them only through the owner-gated inquiriesForOwner.
22
+ // • Capacity — a single, PII-FREE row the public page reads live: the current
23
+ // booking period + how many project slots are open. This is what
24
+ // makes the hero counter realtime. Booking an inquiry decrements
25
+ // it; the owner can reset it any time.
26
+ // • User — the studio owner's account for the dashboard.
27
+ // ---------------------------------------------------------------------------
28
+
29
+ // A project inquiry. Everything here is PII or commercially sensitive, so the
30
+ // policy denies all client access; the only way in is the submitInquiry
31
+ // mutation, the only way to read is the owner-gated query. `status` tracks the
32
+ // pipeline: "new" → "booked" (consumes a slot) | "declined".
33
+ const Inquiry = entity(
34
+ "Inquiry",
35
+ {
36
+ name: field.string(),
37
+ email: field.string(),
38
+ company: field.string().optional(),
39
+ projectType: field.string().optional(),
40
+ budget: field.string().optional(),
41
+ message: field.string().optional(),
42
+ status: field.string().default("new"), // "new" | "booked" | "declined"
43
+ createdAt: field.datetime().defaultNow(),
44
+ },
45
+ { indexes: [{ name: "by_created", fields: ["createdAt"], unique: false }] },
46
+ );
47
+
48
+ // A single-row, PII-FREE aggregate the public page reads live. It holds only
49
+ // the current booking window label + the number of open project slots — no lead
50
+ // data. seedCapacity creates it from config on first visit; bookInquiry /
51
+ // setCapacity keep it current. The landing page subscribes with
52
+ // `db.useQuery("Capacity")`, so the "N slots open" counter ticks down across
53
+ // every open tab the instant the owner books someone — the cross-tab-safe
54
+ // realtime primitive (entity sync), not a per-connection server subscription.
55
+ const Capacity = entity(
56
+ "Capacity",
57
+ {
58
+ label: field.string().default(""), // e.g. "Q3 2026"
59
+ openSlots: field.int().default(0),
60
+ updatedAt: field.datetime().defaultNow(),
61
+ },
62
+ {},
63
+ );
64
+
65
+ // The studio owner's account. Email/password auth is built in against an entity
66
+ // named "User" (passwordHash is server-only). The dashboard is gated to the
67
+ // owner — see PYLON_OWNER_EMAIL in lib/owner.ts + the owner-only functions.
68
+ const User = entity(
69
+ "User",
70
+ {
71
+ email: field.string(),
72
+ displayName: field.string().optional(),
73
+ passwordHash: field.string().serverOnly().optional(),
74
+ avatarColor: field.string().optional(),
75
+ emailVerified: field.datetime().optional(),
76
+ createdAt: field.datetime().defaultNow(),
77
+ },
78
+ { indexes: [{ name: "by_email", fields: ["email"], unique: true }] },
79
+ );
80
+
81
+ // PRIVACY — Inquiry holds the prospect's name, email, company, and budget, so
82
+ // it denies EVERY client read and write. No `db.useQuery("Inquiry")` can pull a
83
+ // row; no client can write one directly. Writes happen only inside the
84
+ // submitInquiry / bookInquiry / declineInquiry mutations (functions bypass
85
+ // policies); reads happen only inside the owner-gated inquiriesForOwner. A
86
+ // studio site must never leak who's been talking to it — this guarantees it.
87
+ const inquiryPolicy = policy({
88
+ name: "inquiry_private",
89
+ entity: "Inquiry",
90
+ allowRead: "false",
91
+ allowInsert: "false",
92
+ allowUpdate: "false",
93
+ allowDelete: "false",
94
+ });
95
+
96
+ // The capacity row is public to READ (it's just a label + a number — the whole
97
+ // point is the landing page showing open slots live to everyone). Clients can't
98
+ // WRITE it; only seedCapacity / bookInquiry / setCapacity maintain it server-side.
99
+ const capacityPolicy = policy({
100
+ name: "capacity_public_read",
101
+ entity: "Capacity",
102
+ allowRead: "true",
103
+ allowInsert: "false",
104
+ allowUpdate: "false",
105
+ allowDelete: "false",
106
+ });
107
+
108
+ const userPolicy = policy({
109
+ name: "user_self",
110
+ entity: "User",
111
+ allowRead: "auth.userId == data.id",
112
+ allowInsert: "false",
113
+ allowUpdate: "false",
114
+ allowDelete: "false",
115
+ });
116
+
117
+ const manifest = buildManifest({
118
+ name: "__APP_NAME__",
119
+ version: "0.1.0",
120
+ entities: [Inquiry, Capacity, User],
121
+ // submitInquiry / seedCapacity (public) + inquiriesForOwner / bookInquiry /
122
+ // declineInquiry / setCapacity (owner-gated) live in functions/ and are
123
+ // discovered automatically — they don't need listing here.
124
+ queries: [],
125
+ actions: [],
126
+ policies: [inquiryPolicy, capacityPolicy, userPolicy],
127
+ // Email/password is on by default against the User entity above. No orgs, no
128
+ // billing — a single studio is single-tenant (one business, one owner).
129
+ auth: auth(),
130
+ routes: await discoverAppRoutes(),
131
+ });
132
+
133
+ console.log(JSON.stringify(manifest, null, 2));
134
+
135
+ export default manifest;
@@ -0,0 +1,148 @@
1
+ import React from "react";
2
+
3
+ // Reusable presentational pieces for the landing page. All server-rendered —
4
+ // no client JS. Restyle here and the whole page follows. The brand accent
5
+ // (`text-brand`, `bg-brand-soft`) comes from CSS vars set on <html> in
6
+ // app/layout.tsx, which read lib/site.config.ts — so re-theming is one edit.
7
+
8
+ // Shared container: a contained, centered column.
9
+ export const WRAP = "mx-auto w-full max-w-5xl px-6";
10
+
11
+ export function Eyebrow({ children }: { children: React.ReactNode }) {
12
+ return (
13
+ <p className="font-mono text-[11px] font-semibold uppercase tracking-[0.14em] text-brand">
14
+ {children}
15
+ </p>
16
+ );
17
+ }
18
+
19
+ // "New / Coming soon"-style pill for the hero.
20
+ export function Badge({ children }: { children: React.ReactNode }) {
21
+ return (
22
+ <span className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white py-1 pl-1.5 pr-3 text-[13px] text-zinc-600 shadow-sm">
23
+ <span className="inline-block size-1.5 rounded-full bg-brand" />
24
+ {children}
25
+ </span>
26
+ );
27
+ }
28
+
29
+ export function Divider() {
30
+ return (
31
+ <div className={WRAP}>
32
+ <div className="border-t border-zinc-200/70" />
33
+ </div>
34
+ );
35
+ }
36
+
37
+ export function SectionHead({
38
+ eyebrow,
39
+ title,
40
+ body,
41
+ }: {
42
+ eyebrow: string;
43
+ title: string;
44
+ body?: string;
45
+ }) {
46
+ return (
47
+ <div>
48
+ <Eyebrow>{eyebrow}</Eyebrow>
49
+ <h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
50
+ {title}
51
+ </h2>
52
+ {body ? (
53
+ <p className="mt-4 max-w-xl text-[15px] leading-relaxed text-zinc-500">
54
+ {body}
55
+ </p>
56
+ ) : null}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ // A grid of value props — icon + title + body.
62
+ export function FeatureGrid({
63
+ items,
64
+ }: {
65
+ items: { title: string; body: string; icon?: string }[];
66
+ }) {
67
+ return (
68
+ <div className="grid gap-6 sm:grid-cols-3">
69
+ {items.map((f) => (
70
+ <div key={f.title}>
71
+ {f.icon ? (
72
+ <span className="flex size-9 items-center justify-center rounded-lg bg-brand-soft text-brand">
73
+ {f.icon}
74
+ </span>
75
+ ) : null}
76
+ <h3 className="mt-4 text-[15px] font-semibold text-zinc-900">
77
+ {f.title}
78
+ </h3>
79
+ <p className="mt-2 text-[14px] leading-relaxed text-zinc-500">
80
+ {f.body}
81
+ </p>
82
+ </div>
83
+ ))}
84
+ </div>
85
+ );
86
+ }
87
+
88
+ // Initials for testimonial avatars, so the cards look finished without a photo.
89
+ export function initials(name: string) {
90
+ return name
91
+ .split(/\s+/)
92
+ .map((w) => w[0])
93
+ .join("")
94
+ .slice(0, 2)
95
+ .toUpperCase();
96
+ }
97
+
98
+ // A deliberately-obvious image placeholder. Real sites drop a photo here; this
99
+ // makes the spot unmistakable — dashed border, a photo glyph, and a one-line
100
+ // "swap this" instruction telling you exactly what to replace and where. Looks
101
+ // tidy enough to demo, but no one will mistake it for a finished design.
102
+ //
103
+ // shape — "landscape" | "portrait" | "square" | "circle"
104
+ // title — what photo belongs here ("Your headshot")
105
+ // hint — how to replace it ("Replace in app/page.tsx")
106
+ export function ImagePlaceholder({
107
+ shape = "landscape",
108
+ title,
109
+ hint,
110
+ className = "",
111
+ }: {
112
+ shape?: "landscape" | "portrait" | "square" | "circle";
113
+ title: string;
114
+ hint?: string;
115
+ className?: string;
116
+ }) {
117
+ const aspect =
118
+ shape === "portrait"
119
+ ? "aspect-[4/5]"
120
+ : shape === "square" || shape === "circle"
121
+ ? "aspect-square"
122
+ : "aspect-[4/3]";
123
+ const radius = shape === "circle" ? "rounded-full" : "rounded-2xl";
124
+ return (
125
+ <div
126
+ className={`relative grid place-items-center overflow-hidden border-2 border-dashed border-zinc-300 bg-zinc-50 ${aspect} ${radius} ${className}`}
127
+ >
128
+ <div className="px-4 text-center">
129
+ <svg
130
+ className="mx-auto size-7 text-zinc-300"
131
+ viewBox="0 0 24 24"
132
+ fill="none"
133
+ stroke="currentColor"
134
+ strokeWidth="1.5"
135
+ strokeLinecap="round"
136
+ strokeLinejoin="round"
137
+ aria-hidden
138
+ >
139
+ <rect x="3" y="3" width="18" height="18" rx="2" />
140
+ <circle cx="9" cy="9" r="1.6" />
141
+ <path d="m21 15-4.5-4.5L7 20" />
142
+ </svg>
143
+ <p className="mt-2 text-[12.5px] font-medium text-zinc-500">{title}</p>
144
+ {hint ? <p className="mt-1 text-[11px] leading-snug text-zinc-400">{hint}</p> : null}
145
+ </div>
146
+ </div>
147
+ );
148
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+
5
+ // Makes in-page section links work. A hydrated Pylon page updates the URL for a
6
+ // plain `<a href="#section">` click but doesn't perform the browser's native
7
+ // fragment scroll, so the page jumps nowhere. This installs ONE delegated click
8
+ // handler that catches any same-page `#`/`/#` anchor and scrolls to it smoothly.
9
+ //
10
+ // Render it once (in the root layout). Renders nothing. Real route links should
11
+ // still use `<Link>` from @pylonsync/react — this only handles `#` anchors.
12
+ export function SectionScroller() {
13
+ useEffect(() => {
14
+ function onClick(e: MouseEvent) {
15
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
16
+ return;
17
+ }
18
+ const target = e.target as Element | null;
19
+ const link = target?.closest?.('a[href^="#"], a[href^="/#"]') as HTMLAnchorElement | null;
20
+ if (!link) return;
21
+ const href = link.getAttribute("href") || "";
22
+ const id = href.slice(href.indexOf("#") + 1);
23
+ if (!id) return;
24
+ const el = document.getElementById(id);
25
+ if (!el) return; // target not on this page — leave it to the browser
26
+ e.preventDefault();
27
+ el.scrollIntoView({ block: "start" });
28
+ history.replaceState(null, "", "#" + id);
29
+ }
30
+ document.addEventListener("click", onClick);
31
+ return () => document.removeEventListener("click", onClick);
32
+ }, []);
33
+
34
+ return null;
35
+ }
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-9 px-4 py-2",
24
+ sm: "h-8 rounded-md px-3 text-xs",
25
+ lg: "h-10 rounded-md px-8",
26
+ icon: "h-9 w-9",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ },
34
+ );
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean;
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button";
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ );
52
+ },
53
+ );
54
+ Button.displayName = "Button";
55
+
56
+ export { Button, buttonVariants };