@pylonsync/create-pylon 0.11.1 → 0.11.3

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 (142) hide show
  1. package/bin/create-pylon.js +1 -1
  2. package/package.json +1 -1
  3. package/templates/_root/AGENTS.md +1 -1
  4. package/templates/agency/AGENTS.md +1 -1
  5. package/templates/agency/app/(marketing)/contact-form.tsx +22 -32
  6. package/templates/agency/app/(marketing)/layout.tsx +36 -64
  7. package/templates/agency/app/(marketing)/not-found.tsx +6 -8
  8. package/templates/agency/app/(marketing)/page.tsx +93 -144
  9. package/templates/agency/app/(marketing)/work/[slug]/page.tsx +69 -71
  10. package/templates/agency/app/(marketing)/work/page.tsx +20 -17
  11. package/templates/agency/app/globals.css +15 -6
  12. package/templates/agency/app.ts +10 -0
  13. package/templates/agency/components/marketing.tsx +40 -104
  14. package/templates/agency/lib/site.config.ts +34 -52
  15. package/templates/agency/public/images/hero.jpg +0 -0
  16. package/templates/agency/public/images/team/claire-donovan.jpg +0 -0
  17. package/templates/agency/public/images/team/dana-okafor.jpg +0 -0
  18. package/templates/agency/public/images/team/marcus-lee.jpg +0 -0
  19. package/templates/agency/public/images/work/atlas-health.jpg +0 -0
  20. package/templates/agency/public/images/work/cohort.jpg +0 -0
  21. package/templates/agency/public/images/work/ledger.jpg +0 -0
  22. package/templates/agency/public/images/work/vela.jpg +0 -0
  23. package/templates/ai-chat/AGENTS.md +1 -1
  24. package/templates/ai-studio/AGENTS.md +1 -1
  25. package/templates/barebones/AGENTS.md +1 -1
  26. package/templates/chat/AGENTS.md +1 -1
  27. package/templates/consumer/AGENTS.md +1 -1
  28. package/templates/consumer/README.md +39 -23
  29. package/templates/consumer/app/(app)/explore/page.tsx +13 -0
  30. package/templates/consumer/app/(app)/layout.tsx +17 -0
  31. package/templates/consumer/app/(app)/p/[id]/page.tsx +13 -0
  32. package/templates/consumer/app/(app)/page.tsx +15 -0
  33. package/templates/consumer/app/(app)/u/[username]/page.tsx +14 -0
  34. package/templates/consumer/app/error.tsx +1 -1
  35. package/templates/consumer/app/globals.css +25 -0
  36. package/templates/consumer/app/layout.tsx +3 -10
  37. package/templates/consumer/app/llms.ts +10 -10
  38. package/templates/consumer/app/not-found.tsx +9 -22
  39. package/templates/consumer/app/robots.ts +3 -4
  40. package/templates/consumer/app/sitemap.ts +7 -21
  41. package/templates/consumer/app.ts +152 -33
  42. package/templates/consumer/components/social/app-chrome.tsx +109 -0
  43. package/templates/consumer/components/social/avatar.tsx +50 -0
  44. package/templates/consumer/components/social/comment-form.tsx +51 -0
  45. package/templates/consumer/components/social/create-post.tsx +155 -0
  46. package/templates/consumer/components/social/edit-profile.tsx +103 -0
  47. package/templates/consumer/components/social/explore-screen.tsx +23 -0
  48. package/templates/consumer/components/social/home-screen.tsx +161 -0
  49. package/templates/consumer/components/social/post-card.tsx +77 -0
  50. package/templates/consumer/components/social/post-grid.tsx +46 -0
  51. package/templates/consumer/components/social/post-parts.tsx +146 -0
  52. package/templates/consumer/components/social/post-screen.tsx +173 -0
  53. package/templates/consumer/components/social/profile-screen.tsx +146 -0
  54. package/templates/consumer/components/social/rich-text.tsx +22 -0
  55. package/templates/consumer/components/social/session.tsx +43 -0
  56. package/templates/consumer/components/social/use-social.ts +86 -0
  57. package/templates/consumer/functions/addComment.ts +27 -0
  58. package/templates/consumer/functions/createPost.ts +39 -0
  59. package/templates/consumer/functions/ensureProfile.ts +39 -0
  60. package/templates/consumer/functions/seedFeed.ts +38 -0
  61. package/templates/consumer/functions/updateProfile.ts +61 -0
  62. package/templates/consumer/lib/seed.ts +192 -0
  63. package/templates/consumer/lib/site.ts +6 -0
  64. package/templates/consumer/lib/social.ts +245 -0
  65. package/templates/consumer/tests/social.test.ts +157 -0
  66. package/templates/creator/AGENTS.md +1 -1
  67. package/templates/creator/app/(marketing)/layout.tsx +35 -69
  68. package/templates/creator/app/(marketing)/newsletter-signup.tsx +15 -26
  69. package/templates/creator/app/(marketing)/not-found.tsx +8 -6
  70. package/templates/creator/app/(marketing)/page.tsx +90 -106
  71. package/templates/creator/app/globals.css +14 -0
  72. package/templates/creator/app/layout.tsx +1 -0
  73. package/templates/creator/app.ts +10 -0
  74. package/templates/creator/components/marketing.tsx +28 -89
  75. package/templates/creator/lib/site.config.ts +51 -29
  76. package/templates/creator/public/images/headshot.jpg +0 -0
  77. package/templates/crm/AGENTS.md +1 -1
  78. package/templates/crm/lib/seed.ts +153 -21
  79. package/templates/directory/AGENTS.md +1 -1
  80. package/templates/directory/app/(marketing)/directory-browse.tsx +99 -137
  81. package/templates/directory/app/(marketing)/layout.tsx +37 -88
  82. package/templates/directory/app/(marketing)/not-found.tsx +11 -10
  83. package/templates/directory/app/(marketing)/page.tsx +12 -33
  84. package/templates/directory/app/(marketing)/submit/page.tsx +9 -10
  85. package/templates/directory/app/(marketing)/submit-form.tsx +26 -26
  86. package/templates/directory/app/globals.css +15 -4
  87. package/templates/directory/app.ts +10 -0
  88. package/templates/directory/components/marketing.tsx +27 -91
  89. package/templates/directory/lib/site.config.ts +13 -24
  90. package/templates/helpdesk/AGENTS.md +1 -1
  91. package/templates/helpdesk/lib/seed.ts +188 -104
  92. package/templates/inventory/AGENTS.md +1 -1
  93. package/templates/inventory/lib/seed.ts +175 -44
  94. package/templates/invoices/AGENTS.md +1 -1
  95. package/templates/invoices/lib/seed.ts +74 -89
  96. package/templates/local-service/AGENTS.md +1 -1
  97. package/templates/local-service/app/(marketing)/booking-widget.tsx +31 -31
  98. package/templates/local-service/app/(marketing)/layout.tsx +25 -37
  99. package/templates/local-service/app/(marketing)/not-found.tsx +4 -4
  100. package/templates/local-service/app/(marketing)/page.tsx +145 -139
  101. package/templates/local-service/app/globals.css +11 -0
  102. package/templates/local-service/app/layout.tsx +2 -0
  103. package/templates/local-service/app.ts +9 -0
  104. package/templates/local-service/components/marketing.tsx +14 -39
  105. package/templates/local-service/lib/site.config.ts +16 -23
  106. package/templates/local-service/public/images/hero.jpg +0 -0
  107. package/templates/marketplace/AGENTS.md +1 -1
  108. package/templates/marketplace/client/SellForm.tsx +2 -0
  109. package/templates/projects/AGENTS.md +1 -1
  110. package/templates/projects/lib/seed.ts +103 -32
  111. package/templates/restaurant/AGENTS.md +1 -1
  112. package/templates/restaurant/app/(marketing)/layout.tsx +26 -48
  113. package/templates/restaurant/app/(marketing)/page.tsx +146 -123
  114. package/templates/restaurant/app/globals.css +11 -0
  115. package/templates/restaurant/app/layout.tsx +1 -0
  116. package/templates/restaurant/app.ts +10 -0
  117. package/templates/restaurant/components/marketing.tsx +10 -0
  118. package/templates/restaurant/lib/site.config.ts +16 -14
  119. package/templates/restaurant/public/images/bar.jpg +0 -0
  120. package/templates/restaurant/public/images/branzino.jpg +0 -0
  121. package/templates/restaurant/public/images/hearth.jpg +0 -0
  122. package/templates/restaurant/public/images/hero.jpg +0 -0
  123. package/templates/saas/AGENTS.md +1 -1
  124. package/templates/shop/AGENTS.md +1 -1
  125. package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
  126. package/templates/shop/public/images/products/linen.jpg +0 -0
  127. package/templates/todo/AGENTS.md +1 -1
  128. package/templates/waitlist/AGENTS.md +1 -1
  129. package/templates/waitlist/app/(marketing)/layout.tsx +39 -85
  130. package/templates/waitlist/app/(marketing)/not-found.tsx +8 -9
  131. package/templates/waitlist/app/(marketing)/page.tsx +51 -86
  132. package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +43 -83
  133. package/templates/waitlist/app/globals.css +23 -0
  134. package/templates/waitlist/app/layout.tsx +1 -0
  135. package/templates/waitlist/app.ts +18 -0
  136. package/templates/waitlist/components/marketing.tsx +67 -81
  137. package/templates/waitlist/lib/site.config.ts +52 -59
  138. package/templates/consumer/app/feed-client.tsx +0 -159
  139. package/templates/consumer/app/page.tsx +0 -27
  140. package/templates/consumer/functions/_keep.ts +0 -13
  141. package/templates/consumer/functions/seedPosts.ts +0 -56
  142. package/templates/consumer/tests/example.test.ts +0 -12
@@ -4,19 +4,21 @@ import { Link, type NotFoundProps } from "@pylonsync/react";
4
4
  // `(marketing)/not-found.tsx` → rendered at HTTP 404 for any unmatched URL
5
5
  // (and when a page calls `response.notFound()`). It lives inside the
6
6
  // `(marketing)` group on purpose: a group segment adds no URL prefix, so this
7
- // is still the root 404 boundary, but it wraps in the marketing layout — a
7
+ // is still the root 404 boundary, but it wraps in the marketing layout, so a
8
8
  // missing URL gets the site nav + footer instead of a bare page. Hydrated, so
9
9
  // the link is a client nav.
10
10
  export default function NotFound(_props: NotFoundProps) {
11
11
  return (
12
- <div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
13
- <h1 className="text-3xl font-semibold tracking-tight">404</h1>
14
- <p className="mt-2 text-zinc-500">We couldn&apos;t find that page.</p>
12
+ <div className="mx-auto w-full max-w-[40rem] px-6 py-24">
13
+ <h1 className="text-[2rem] font-medium leading-tight text-ink">Page not found</h1>
14
+ <p className="mt-4 text-[17.5px] leading-[1.65] text-ink-2">
15
+ There is nothing at this address.
16
+ </p>
15
17
  <Link
16
18
  href="/"
17
- 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"
19
+ className="mt-6 inline-block text-[17.5px] font-medium text-ink underline decoration-brand/50 underline-offset-4 hover:decoration-brand"
18
20
  >
19
- Back home
21
+ Back to the home page
20
22
  </Link>
21
23
  </div>
22
24
  );
@@ -1,13 +1,6 @@
1
1
  import React from "react";
2
2
  import { type Metadata } from "@pylonsync/react";
3
- import {
4
- WRAP,
5
- Eyebrow,
6
- Divider,
7
- SectionHead,
8
- initials,
9
- ImagePlaceholder,
10
- } from "@/components/marketing";
3
+ import { WRAP, PROSE, Divider, SectionTitle, ImagePlaceholder } from "@/components/marketing";
11
4
  import { NewsletterSignup } from "./newsletter-signup";
12
5
  import { siteConfig } from "@/lib/site.config";
13
6
 
@@ -17,97 +10,105 @@ export const metadata: Metadata = {
17
10
  openGraph: { title: siteConfig.seo.title, description: siteConfig.seo.description, type: "website" },
18
11
  };
19
12
 
20
- // `app/page.tsx` → `/`. A server-rendered personal-brand site. Hero, about,
21
- // offerings, testimonials, and links are static server HTML (SEO + first paint);
22
- // the newsletter section (#newsletter) is a client island with a live
23
- // subscriber counter. All copy comes from siteConfig. Doesn't read `auth`, so
24
- // the public page stays cacheable.
13
+ // `app/page.tsx` → `/`. A server-rendered personal site set in one narrow
14
+ // column. Hero, about, offerings, writing, testimonials, and the closing
15
+ // section are static server HTML (SEO + first paint); the newsletter form in
16
+ // the hero is a client island with a live subscriber counter. All copy comes
17
+ // from siteConfig. Doesn't read `auth`, so the public page stays cacheable.
25
18
  export default function LandingPage() {
26
- const { hero, about, offerings, testimonials, newsletter, links } = siteConfig;
19
+ const { hero, about, offerings, writing, testimonials, newsletter, links } = siteConfig;
27
20
 
28
21
  return (
29
- <div className="bg-white text-zinc-900">
22
+ <>
30
23
  {/* HERO */}
31
- <section className={`${WRAP} pt-16 pb-14 sm:pt-20`}>
32
- <div className="grid items-center gap-10 sm:grid-cols-[1fr_auto]">
24
+ <section className={`${WRAP} pt-14 pb-12 sm:pt-20`}>
25
+ <div className="flex items-center gap-5">
26
+ <div className="w-20 shrink-0 sm:w-24">
27
+ <ImagePlaceholder shape="circle" title={hero.name} src="/images/headshot.jpg" />
28
+ </div>
33
29
  <div>
34
- <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">
35
- <span className="inline-block size-1.5 rounded-full bg-brand" />
36
- {hero.badge}
37
- </span>
38
- <h1 className="mt-5 text-balance text-[2.5rem] font-semibold leading-[1.04] tracking-[-0.02em] sm:text-[3rem]">
30
+ <h1 className="text-[2rem] font-medium leading-[1.1] text-ink sm:text-[2.5rem]">
39
31
  {hero.name}
40
32
  </h1>
41
- <p className="mt-2 text-[18px] font-medium text-brand">{hero.tagline}</p>
42
- <p className="mt-5 max-w-xl text-[16px] leading-relaxed text-zinc-500">{hero.intro}</p>
43
- <div className="mt-7 flex flex-wrap items-center gap-4">
44
- <a href="#newsletter" className="inline-flex items-center rounded-full bg-zinc-900 px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-zinc-700">
45
- Read the newsletter
46
- </a>
47
- <a href="#work" className="text-sm font-medium text-zinc-700 hover:text-zinc-900">
48
- Work with me →
49
- </a>
50
- </div>
33
+ <p className="mt-1.5 text-[17.5px] italic leading-snug text-ink-2">{hero.tagline}</p>
51
34
  </div>
52
- {/* Profile photo — replace the placeholder with a real headshot. */}
53
- <div className="order-first w-32 sm:order-none sm:w-44">
54
- <ImagePlaceholder
55
- shape="square"
56
- title="Your headshot"
57
- hint="Replace in app/page.tsx"
58
- />
35
+ </div>
36
+ <p className={`mt-8 ${PROSE}`}>{hero.intro}</p>
37
+ <div id="newsletter" className="mt-8 scroll-mt-20">
38
+ <p className={PROSE}>
39
+ <em>{newsletter.name}</em> is my newsletter. {newsletter.subcopy}
40
+ </p>
41
+ <div className="mt-4">
42
+ <NewsletterSignup newsletter={newsletter} />
59
43
  </div>
60
44
  </div>
61
45
  </section>
62
46
 
63
47
  {/* ABOUT */}
64
48
  <Divider />
65
- <section className={`${WRAP} py-14`}>
66
- <div className="grid gap-8 lg:grid-cols-[0.8fr_1.2fr]">
67
- <SectionHead eyebrow={about.eyebrow} title={about.headline} />
68
- <div className="space-y-4 text-[15px] leading-relaxed text-zinc-600">
69
- {about.paragraphs.map((p) => (
70
- <p key={p}>{p}</p>
71
- ))}
72
- </div>
49
+ <section className={`${WRAP} py-12`}>
50
+ <SectionTitle>{about.headline}</SectionTitle>
51
+ <div className={`mt-5 space-y-5 ${PROSE}`}>
52
+ {about.paragraphs.map((p) => (
53
+ <p key={p}>{p}</p>
54
+ ))}
73
55
  </div>
74
56
  </section>
75
57
 
76
- {/* OFFERINGS */}
58
+ {/* WHAT I DO */}
77
59
  <Divider />
78
- <section id="work" className={`${WRAP} py-14`}>
79
- <SectionHead eyebrow={offerings.eyebrow} title={offerings.headline} />
80
- <div className="mt-8 grid gap-5 sm:grid-cols-3">
60
+ <section className={`${WRAP} py-12`}>
61
+ <SectionTitle>{offerings.headline}</SectionTitle>
62
+ <dl className="mt-5 divide-y divide-rule">
81
63
  {offerings.items.map((o) => (
82
- <div key={o.title} className="flex flex-col rounded-2xl border border-zinc-200 bg-paper p-6">
83
- <h3 className="text-[15px] font-semibold text-zinc-900">{o.title}</h3>
84
- <p className="mt-2 flex-1 text-[14px] leading-relaxed text-zinc-500">{o.body}</p>
85
- {o.price ? (
86
- <p className="mt-4 text-[13px] font-medium uppercase tracking-wide text-brand">{o.price}</p>
87
- ) : null}
64
+ <div key={o.title} className="py-5 first:pt-0 last:pb-0">
65
+ <dt className="flex flex-wrap items-baseline justify-between gap-x-6 gap-y-1">
66
+ <span className="text-[18px] font-medium text-ink">{o.title}</span>
67
+ {o.price ? <span className="text-[16px] italic text-brand">{o.price}</span> : null}
68
+ </dt>
69
+ <dd className={`mt-1.5 ${PROSE}`}>{o.body}</dd>
88
70
  </div>
89
71
  ))}
90
- </div>
72
+ </dl>
91
73
  </section>
92
74
 
75
+ {/* RECENT WRITING */}
76
+ {writing && writing.items.length > 0 ? (
77
+ <>
78
+ <Divider />
79
+ <section className={`${WRAP} py-12`}>
80
+ <SectionTitle>{writing.headline}</SectionTitle>
81
+ <ul className="mt-5 divide-y divide-rule">
82
+ {writing.items.map((issue) => (
83
+ <li key={issue.title} className="py-4 first:pt-0 last:pb-0">
84
+ <a href={issue.href} className="group block">
85
+ <div className="flex flex-wrap items-baseline justify-between gap-x-6 gap-y-1">
86
+ <span className="text-[18px] font-medium text-ink underline-offset-4 group-hover:underline">
87
+ {issue.title}
88
+ </span>
89
+ <time className="text-[15px] text-ink-2">{issue.date}</time>
90
+ </div>
91
+ {issue.summary ? <p className={`mt-1 ${PROSE}`}>{issue.summary}</p> : null}
92
+ </a>
93
+ </li>
94
+ ))}
95
+ </ul>
96
+ </section>
97
+ </>
98
+ ) : null}
99
+
93
100
  {/* TESTIMONIALS */}
94
101
  {testimonials && testimonials.items.length > 0 ? (
95
102
  <>
96
103
  <Divider />
97
- <section className={`${WRAP} py-14`}>
98
- <SectionHead eyebrow={testimonials.eyebrow} title={testimonials.headline} />
99
- <div className="mt-8 grid gap-5 sm:grid-cols-3">
104
+ <section className={`${WRAP} py-12`}>
105
+ <SectionTitle>{testimonials.headline}</SectionTitle>
106
+ <div className="mt-5 space-y-7">
100
107
  {testimonials.items.map((t) => (
101
- <figure key={t.name} className="flex flex-col rounded-2xl border border-zinc-200 bg-paper p-6">
102
- <blockquote className="text-[14px] leading-relaxed text-zinc-600">&ldquo;{t.quote}&rdquo;</blockquote>
103
- <figcaption className="mt-5 flex items-center gap-3">
104
- <span className="flex size-8 items-center justify-center rounded-full bg-zinc-200 text-[11px] font-semibold text-zinc-500">
105
- {initials(t.name)}
106
- </span>
107
- <div className="leading-tight">
108
- <div className="text-[13px] font-semibold">{t.name}</div>
109
- <div className="text-[12px] text-zinc-500">{t.role}</div>
110
- </div>
108
+ <figure key={t.name} className="border-l-2 border-brand pl-5">
109
+ <blockquote className={`italic ${PROSE}`}>&ldquo;{t.quote}&rdquo;</blockquote>
110
+ <figcaption className="mt-2 text-[15px] text-ink-2">
111
+ {t.name}, {t.role}
111
112
  </figcaption>
112
113
  </figure>
113
114
  ))}
@@ -116,45 +117,28 @@ export default function LandingPage() {
116
117
  </>
117
118
  ) : null}
118
119
 
119
- {/* NEWSLETTER (live) */}
120
- <Divider />
121
- <section id="newsletter" className={`${WRAP} py-14`}>
122
- <div className="rounded-2xl border border-zinc-200 bg-paper p-7 sm:p-10">
123
- <Eyebrow>{newsletter.eyebrow}</Eyebrow>
124
- <h2 className="mt-4 max-w-2xl text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
125
- {newsletter.headline}
126
- </h2>
127
- <p className="mt-4 max-w-xl text-[15px] leading-relaxed text-zinc-500">{newsletter.subcopy}</p>
128
- <div className="mt-7">
129
- <NewsletterSignup newsletter={newsletter} />
130
- </div>
131
- </div>
132
- </section>
133
-
134
- {/* LINKS */}
135
- {links && links.items.length > 0 ? (
120
+ {/* WORK WITH ME */}
121
+ {links ? (
136
122
  <>
137
123
  <Divider />
138
- <section className={`${WRAP} py-14`}>
139
- <SectionHead eyebrow={links.eyebrow} title={links.headline} />
140
- <div className="mt-8 grid gap-3 sm:grid-cols-3">
141
- {links.items.map((l) => (
142
- <a
143
- key={l.label}
144
- href={l.href}
145
- className="group rounded-xl border border-zinc-200 bg-white px-5 py-4 transition-colors hover:border-zinc-300"
146
- >
147
- <div className="flex items-center justify-between text-[14px] font-medium text-zinc-900">
148
- {l.label}
149
- <span className="text-zinc-300 transition-colors group-hover:text-brand">→</span>
150
- </div>
151
- {l.note ? <p className="mt-1 text-[12.5px] text-zinc-500">{l.note}</p> : null}
152
- </a>
153
- ))}
154
- </div>
124
+ <section id="work" className={`${WRAP} py-12 scroll-mt-20`}>
125
+ <SectionTitle>{links.headline}</SectionTitle>
126
+ <p className={`mt-5 ${PROSE}`}>{links.body}</p>
127
+ {links.items.length > 0 ? (
128
+ <ul className={`mt-5 space-y-2 ${PROSE}`}>
129
+ {links.items.map((l) => (
130
+ <li key={l.label}>
131
+ <a href={l.href} className="font-medium text-ink underline decoration-brand/50 underline-offset-4 hover:decoration-brand">
132
+ {l.label}
133
+ </a>
134
+ {l.note ? <span className="text-ink-2"> · {l.note}</span> : null}
135
+ </li>
136
+ ))}
137
+ </ul>
138
+ ) : null}
155
139
  </section>
156
140
  </>
157
141
  ) : null}
158
- </div>
142
+ </>
159
143
  );
160
144
  }
@@ -19,6 +19,10 @@
19
19
  --brand: #4f46e5;
20
20
  --brand-soft: #eef2ff;
21
21
  --paper: #fafafa;
22
+ /* The marketing pages: warm paper, dark warm-grey type. Set from lib/site.config.ts. */
23
+ --ink: #2a2622;
24
+ --ink-2: #5b554e;
25
+ --rule: #d9d2c5;
22
26
  --background: oklch(1 0 0);
23
27
  --foreground: oklch(0.141 0.005 285.823);
24
28
  --card: oklch(1 0 0);
@@ -112,6 +116,9 @@
112
116
  --color-brand: var(--brand);
113
117
  --color-brand-soft: var(--brand-soft);
114
118
  --color-paper: var(--paper);
119
+ --color-ink: var(--ink);
120
+ --color-ink-2: var(--ink-2);
121
+ --color-rule: var(--rule);
115
122
  --color-chart-1: var(--chart-1);
116
123
  --color-chart-2: var(--chart-2);
117
124
  --color-chart-3: var(--chart-3);
@@ -153,3 +160,10 @@
153
160
  cursor: pointer;
154
161
  }
155
162
  }
163
+
164
+ /* The marketing pages. Source Serif 4 is declared in app.ts; /login and
165
+ /dashboard keep Inter. */
166
+ .font-display {
167
+ font-family: var(--font-display, "Iowan Old Style", "Palatino Linotype", Georgia, serif);
168
+ font-optical-sizing: auto;
169
+ }
@@ -24,6 +24,7 @@ export default function RootLayout({ children }: LayoutProps) {
24
24
  "--brand": colors.brand,
25
25
  "--brand-soft": colors.brandSoft,
26
26
  "--paper": colors.paper,
27
+ "--ink": colors.ink,
27
28
  } as React.CSSProperties
28
29
  }
29
30
  >
@@ -143,6 +143,16 @@ const manifest = buildManifest({
143
143
  display: "swap",
144
144
  preload: true,
145
145
  }),
146
+ // The marketing pages are set in a serif, headings and running text alike.
147
+ font({
148
+ family: "Source Serif 4",
149
+ variable: "--font-display",
150
+ weights: ["400", "500", "600"],
151
+ styles: ["normal", "italic"],
152
+ subsets: ["latin"],
153
+ display: "swap",
154
+ preload: true,
155
+ }),
146
156
  ],
147
157
  routes: await discoverAppRoutes(),
148
158
  });
@@ -1,101 +1,30 @@
1
1
  import React from "react";
2
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.
3
+ // Presentational pieces for the marketing pages. All server-rendered, no client
4
+ // JS. Colors come from CSS vars set on <html> in app/layout.tsx, which read
5
+ // lib/site.config.ts, so re-theming is one edit there.
7
6
 
8
- // Shared container: a contained, centered column.
9
- export const WRAP = "mx-auto w-full max-w-3xl 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
- }
7
+ // One narrow column, left aligned, like a well-set essay page.
8
+ export const WRAP = "mx-auto w-full max-w-[40rem] px-6";
28
9
 
10
+ // A rule between sections.
29
11
  export function Divider() {
30
12
  return (
31
13
  <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}
14
+ <hr className="border-0 border-t border-rule" />
57
15
  </div>
58
16
  );
59
17
  }
60
18
 
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
- );
19
+ // Section heading. One size, one weight, no label above it.
20
+ export function SectionTitle({ children }: { children: React.ReactNode }) {
21
+ return <h2 className="text-[1.5rem] font-medium leading-[1.2] text-ink">{children}</h2>;
86
22
  }
87
23
 
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
- }
24
+ // Running text: 17.5px with generous leading, the same serif as the headings.
25
+ export const PROSE = "text-[17.5px] leading-[1.65] text-ink-2";
97
26
 
98
- // A deliberately-obvious image placeholder — dashed border, photo glyph, and a
27
+ // A deliberately-obvious image placeholder: dashed border, photo glyph, and a
99
28
  // "swap this" hint. Real sites drop a photo here.
100
29
  //
101
30
  // shape — "landscape" | "portrait" | "square" | "circle"
@@ -105,11 +34,14 @@ export function ImagePlaceholder({
105
34
  shape = "landscape",
106
35
  title,
107
36
  hint,
37
+ src,
108
38
  className = "",
109
39
  }: {
110
40
  shape?: "landscape" | "portrait" | "square" | "circle";
111
41
  title: string;
112
42
  hint?: string;
43
+ /** A real image. The template ships one under public/images; swap it for yours. */
44
+ src?: string;
113
45
  className?: string;
114
46
  }) {
115
47
  const aspect =
@@ -118,14 +50,21 @@ export function ImagePlaceholder({
118
50
  : shape === "square" || shape === "circle"
119
51
  ? "aspect-square"
120
52
  : "aspect-[4/3]";
121
- const radius = shape === "circle" ? "rounded-full" : "rounded-2xl";
53
+ const radius = shape === "circle" ? "rounded-full" : "rounded-lg";
54
+ if (src) {
55
+ return (
56
+ <div className={`relative overflow-hidden bg-rule ${aspect} ${radius} ${className}`}>
57
+ <img src={src} alt={title} className="absolute inset-0 size-full object-cover" />
58
+ </div>
59
+ );
60
+ }
122
61
  return (
123
62
  <div
124
- className={`relative grid place-items-center overflow-hidden border-2 border-dashed border-zinc-300 bg-zinc-50 ${aspect} ${radius} ${className}`}
63
+ className={`relative grid place-items-center overflow-hidden border-2 border-dashed border-rule bg-paper ${aspect} ${radius} ${className}`}
125
64
  >
126
65
  <div className="px-4 text-center">
127
66
  <svg
128
- className="mx-auto size-7 text-zinc-300"
67
+ className="mx-auto size-7 text-rule"
129
68
  viewBox="0 0 24 24"
130
69
  fill="none"
131
70
  stroke="currentColor"
@@ -138,8 +77,8 @@ export function ImagePlaceholder({
138
77
  <circle cx="9" cy="9" r="1.6" />
139
78
  <path d="m21 15-4.5-4.5L7 20" />
140
79
  </svg>
141
- <p className="mt-2 text-[12.5px] font-medium text-zinc-500">{title}</p>
142
- {hint ? <p className="mt-1 text-[11px] leading-snug text-zinc-400">{hint}</p> : null}
80
+ <p className="mt-2 text-[12.5px] font-medium text-ink-2">{title}</p>
81
+ {hint ? <p className="mt-1 text-[11px] leading-snug text-ink-2/70">{hint}</p> : null}
143
82
  </div>
144
83
  </div>
145
84
  );