@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
@@ -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
- ImagePlaceholder,
9
- LiveBadge,
10
- } from "@/components/marketing";
3
+ import { WRAP, Divider, ImagePlaceholder, LiveBadge } from "@/components/marketing";
11
4
  import { BookingWidget } from "./booking-widget";
12
5
  import { siteConfig } from "@/lib/site.config";
13
6
 
@@ -21,10 +14,13 @@ export const metadata: Metadata = {
21
14
  },
22
15
  };
23
16
 
24
- // `app/(marketing)/page.tsx` → `/` (the group segment adds no URL prefix). Server-rendered single-page site for an appointment
25
- // business. Hero, services, reviews, location, and FAQ are static server HTML
26
- // (SEO + first paint); the booking section (#book) is a client island
27
- // (<BookingWidget>) with live slot availability. All copy comes from siteConfig.
17
+ // `app/(marketing)/page.tsx` → `/` (the group segment adds no URL prefix).
18
+ // Server-rendered single-page site for an appointment business, laid out like
19
+ // a barbershop poster: cream page, condensed uppercase display type, full-width
20
+ // rules between sections. Hero, price list, reviews, location, and FAQ are
21
+ // static server HTML (SEO + first paint); the booking section (#book) is a
22
+ // client island (<BookingWidget>) with live slot availability. All copy comes
23
+ // from siteConfig.
28
24
  //
29
25
  // Does not read `auth`, so the public page stays cacheable; the nav's signed-in
30
26
  // state is resolved in the layout.
@@ -32,45 +28,42 @@ export default function LandingPage() {
32
28
  const { hero, services, booking, reviews, location, faq } = siteConfig;
33
29
 
34
30
  return (
35
- <div className="bg-white text-zinc-900">
31
+ <div className="bg-cream text-ink">
36
32
  {/* ============================= HERO ============================= */}
37
- <section className={`${WRAP} pt-16 pb-14 sm:pt-20`}>
38
- <div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-14">
39
- <div>
40
- <p className="font-mono text-[11px] uppercase tracking-[0.16em] text-brand">
41
- {hero.tagline}
42
- </p>
43
- <h1 className="mt-4 text-balance text-[2.5rem] font-semibold leading-[1.04] tracking-[-0.02em] sm:text-[3.25rem]">
44
- {hero.headline}
45
- </h1>
46
- <p className="mt-5 max-w-xl text-[17px] leading-relaxed text-zinc-500">
47
- {hero.subcopy}
48
- </p>
49
- <div className="mt-7 flex flex-wrap items-center gap-4">
50
- <a
51
- href="#book"
52
- 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"
53
- >
54
- {hero.ctaLabel}
55
- </a>
56
- <a href="#services" className="text-sm font-medium text-zinc-700 hover:text-zinc-900">
57
- See the menu →
58
- </a>
33
+ <section className={`${WRAP} pt-10 pb-12 sm:pt-14`}>
34
+ <h1 className="font-display text-balance text-[4.25rem] leading-[0.9] tracking-[0.005em] sm:text-[7rem] lg:text-[9.5rem]">
35
+ {hero.headline}
36
+ </h1>
37
+
38
+ <div className="mt-8 grid gap-8 lg:mt-10 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)] lg:gap-12">
39
+ <div className="flex flex-col justify-between gap-8">
40
+ <p className="max-w-xl text-[17px] leading-relaxed sm:text-[19px]">{hero.subcopy}</p>
41
+ <div>
42
+ <div className="flex flex-wrap items-center gap-4">
43
+ <a
44
+ href="#book"
45
+ className="inline-flex h-12 items-center bg-brand px-7 font-display text-[22px] tracking-[0.04em] text-cream transition-opacity hover:opacity-90"
46
+ >
47
+ {hero.ctaLabel}
48
+ </a>
49
+ <a
50
+ href="#services"
51
+ className="font-display text-[22px] tracking-[0.04em] underline decoration-1 underline-offset-[6px] hover:text-brand"
52
+ >
53
+ See the prices
54
+ </a>
55
+ </div>
56
+ <dl className="mt-10 grid grid-cols-3 gap-4 border-t border-ink pt-5">
57
+ <QuickFact label="Hours" value={hero.quickFacts.hours} />
58
+ <QuickFact label="Area" value={hero.quickFacts.area} />
59
+ <QuickFact label="Call" value={hero.quickFacts.phone} />
60
+ </dl>
59
61
  </div>
60
- <dl className="mt-10 grid max-w-lg grid-cols-3 gap-4 border-t border-zinc-200/70 pt-6">
61
- <QuickFact label="Hours" value={hero.quickFacts.hours} />
62
- <QuickFact label="Area" value={hero.quickFacts.area} />
63
- <QuickFact label="Call" value={hero.quickFacts.phone} />
64
- </dl>
65
62
  </div>
66
63
 
67
- {/* Hero photo — replace the placeholder with a real shot of your shop. */}
68
- <div className="relative mx-auto w-full max-w-sm lg:max-w-none">
69
- <ImagePlaceholder
70
- shape="portrait"
71
- title="A photo of your shop"
72
- hint="Swap for an <img> in app/(marketing)/page.tsx"
73
- />
64
+ {/* Hero photo. Swap public/images/hero.jpg for a shot of your shop. */}
65
+ <div className="relative w-full">
66
+ <ImagePlaceholder shape="portrait" title="The shop" src="/images/hero.jpg" />
74
67
  <div className="absolute left-4 top-4">
75
68
  <LiveBadge>Availability updates live</LiveBadge>
76
69
  </div>
@@ -80,114 +73,130 @@ export default function LandingPage() {
80
73
 
81
74
  {/* =========================== SERVICES ========================== */}
82
75
  <Divider />
83
- <section id="services" className={`${WRAP} py-14`}>
84
- <SectionHead eyebrow={services.eyebrow} title={services.headline} />
85
- <div className="mt-8 grid gap-4 sm:grid-cols-2">
76
+ <section id="services" className={`${WRAP} py-12`}>
77
+ <h2 className="font-display text-[3rem] leading-none sm:text-[4rem]">{services.headline}</h2>
78
+ <ol className="mt-8 border-t border-ink">
86
79
  {services.items.map((s) => (
87
- <div
88
- key={s.slug}
89
- className="flex items-start justify-between gap-4 rounded-xl border border-zinc-200 bg-paper p-5"
90
- >
91
- <div>
92
- <h3 className="text-[15px] font-semibold text-zinc-900">{s.name}</h3>
93
- {s.description ? (
94
- <p className="mt-1.5 text-[13.5px] leading-relaxed text-zinc-500">
95
- {s.description}
96
- </p>
97
- ) : null}
98
- <p className="mt-2 text-[12px] uppercase tracking-wide text-zinc-400">
80
+ <li key={s.slug} className="border-b border-ink py-5">
81
+ <div className="flex items-baseline gap-4">
82
+ <h3 className="shrink-0 font-display text-[2rem] leading-none sm:text-[2.5rem]">
83
+ {s.name}
84
+ </h3>
85
+ <span className="hidden shrink-0 text-[13px] text-ink/60 sm:inline">
99
86
  {s.durationMin} min
100
- </p>
87
+ </span>
88
+ <span
89
+ aria-hidden
90
+ className="mb-[0.35em] min-w-6 flex-1 border-b border-dotted border-ink/60"
91
+ />
92
+ <span className="shrink-0 font-display text-[2rem] leading-none text-brand sm:text-[2.5rem]">
93
+ {s.price}
94
+ </span>
101
95
  </div>
102
- <div className="shrink-0 text-[15px] font-semibold text-brand">{s.price}</div>
103
- </div>
96
+ {s.description ? (
97
+ <p className="mt-2 max-w-xl text-[14px] leading-relaxed text-ink/70">
98
+ <span className="sm:hidden">{s.durationMin} min. </span>
99
+ {s.description}
100
+ </p>
101
+ ) : null}
102
+ </li>
104
103
  ))}
105
- </div>
104
+ </ol>
106
105
  </section>
107
106
 
108
107
  {/* ============================ BOOKING ========================== */}
109
108
  {booking.enabled ? (
110
- <>
111
- <Divider />
112
- <section id="book" className={`${WRAP} py-14`}>
113
- <SectionHead
114
- eyebrow={booking.eyebrow}
115
- title={booking.headline}
116
- body={booking.subcopy}
117
- />
118
- <div className="mt-8">
109
+ <section id="book" className="bg-ink text-cream">
110
+ <div className={`${WRAP} py-14`}>
111
+ <div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)] lg:gap-12">
112
+ <div>
113
+ <h2 className="font-display text-[3rem] leading-none sm:text-[4rem]">
114
+ {booking.headline}
115
+ </h2>
116
+ <p className="mt-5 max-w-sm text-[15px] leading-relaxed text-cream/75">
117
+ {booking.subcopy}
118
+ </p>
119
+ <p className="mt-6 text-[13px] text-cream/60">
120
+ Or call{" "}
121
+ <a href={`tel:${location.phone}`} className="text-cream underline underline-offset-4">
122
+ {location.phone}
123
+ </a>
124
+ </p>
125
+ </div>
119
126
  <BookingWidget />
120
127
  </div>
121
- </section>
122
- </>
128
+ </div>
129
+ </section>
123
130
  ) : null}
124
131
 
125
132
  {/* ============================ REVIEWS ========================== */}
126
133
  {reviews && reviews.items.length > 0 ? (
127
- <>
128
- <Divider />
129
- <section className={`${WRAP} py-14`}>
130
- <SectionHead eyebrow={reviews.eyebrow} title={reviews.headline} />
131
- <div className="mt-8 grid gap-5 sm:grid-cols-3">
132
- {reviews.items.map((r) => (
133
- <figure
134
- key={r.name}
135
- className="flex flex-col rounded-2xl border border-zinc-200 bg-paper p-6"
136
- >
134
+ <section className={`${WRAP} py-12`}>
135
+ <h2 className="font-display text-[3rem] leading-none sm:text-[4rem]">{reviews.headline}</h2>
136
+ <div className="mt-8 grid gap-10 border-t border-ink pt-8 md:grid-cols-3 md:gap-8">
137
+ {reviews.items.map((r) => (
138
+ <figure key={r.name}>
139
+ <blockquote className="font-display text-balance text-[1.75rem] leading-[1.05] sm:text-[2rem]">
140
+ &ldquo;{r.quote}&rdquo;
141
+ </blockquote>
142
+ <figcaption className="mt-4 text-[13px] text-ink/70">
143
+ {r.name}
137
144
  {r.rating ? (
138
- <div className="text-[13px] tracking-wide text-brand">
139
- {"★".repeat(r.rating)}
140
- <span className="text-zinc-200">{"★".repeat(5 - r.rating)}</span>
141
- </div>
145
+ <span className="ml-2 text-brand" aria-label={`${r.rating} out of 5`}>
146
+ {r.rating}/5
147
+ </span>
142
148
  ) : null}
143
- <blockquote className="mt-3 text-[14px] leading-relaxed text-zinc-600">
144
- &ldquo;{r.quote}&rdquo;
145
- </blockquote>
146
- <figcaption className="mt-4 text-[13px] font-semibold text-zinc-900">
147
- {r.name}
148
- </figcaption>
149
- </figure>
150
- ))}
151
- </div>
152
- </section>
153
- </>
149
+ </figcaption>
150
+ </figure>
151
+ ))}
152
+ </div>
153
+ </section>
154
154
  ) : null}
155
155
 
156
156
  {/* =========================== LOCATION ========================== */}
157
157
  <Divider />
158
- <section className={`${WRAP} py-14`}>
159
- <div className="grid gap-8 lg:grid-cols-[1fr_1fr]">
158
+ <section id="visit" className={`${WRAP} py-12`}>
159
+ <div className="grid gap-8 lg:grid-cols-2 lg:gap-12">
160
160
  <div>
161
- <SectionHead eyebrow={location.eyebrow} title={location.headline} />
162
- <div className="mt-6 space-y-3 text-[14px] leading-relaxed text-zinc-600">
163
- <p>{location.address}</p>
164
- <p className="text-zinc-500">{location.hoursText}</p>
165
- <p>
166
- <a href={`tel:${location.phone}`} className="font-medium text-brand">
167
- {location.phone}
168
- </a>{" "}
169
- ·{" "}
170
- <a href={`mailto:${location.email}`} className="text-zinc-600 hover:text-zinc-900">
171
- {location.email}
172
- </a>
173
- </p>
174
- <a
175
- href="#book"
176
- className="mt-2 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"
177
- >
178
- {siteConfig.hero.ctaLabel}
179
- </a>
180
- </div>
161
+ <h2 className="font-display text-[3rem] leading-none sm:text-[4rem]">{location.headline}</h2>
162
+ <dl className="mt-6 grid gap-5 text-[15px] leading-relaxed">
163
+ <div>
164
+ <dt className="text-[11px] font-medium uppercase tracking-wide text-ink/60">Address</dt>
165
+ <dd className="mt-1">{location.address}</dd>
166
+ </div>
167
+ <div>
168
+ <dt className="text-[11px] font-medium uppercase tracking-wide text-ink/60">Hours</dt>
169
+ <dd className="mt-1">{location.hoursText}</dd>
170
+ </div>
171
+ <div>
172
+ <dt className="text-[11px] font-medium uppercase tracking-wide text-ink/60">Contact</dt>
173
+ <dd className="mt-1">
174
+ <a href={`tel:${location.phone}`} className="text-brand">
175
+ {location.phone}
176
+ </a>{" "}
177
+ ·{" "}
178
+ <a href={`mailto:${location.email}`} className="underline underline-offset-4">
179
+ {location.email}
180
+ </a>
181
+ </dd>
182
+ </div>
183
+ </dl>
184
+ <a
185
+ href="#book"
186
+ className="mt-8 inline-flex h-12 items-center bg-ink px-7 font-display text-[22px] tracking-[0.04em] text-cream transition-opacity hover:opacity-90"
187
+ >
188
+ {hero.ctaLabel}
189
+ </a>
181
190
  </div>
182
191
  {location.mapEmbedUrl ? (
183
192
  <iframe
184
193
  title="Map"
185
194
  src={location.mapEmbedUrl}
186
- className="h-64 w-full rounded-2xl border border-zinc-200"
195
+ className="h-72 w-full border border-ink"
187
196
  loading="lazy"
188
197
  />
189
198
  ) : (
190
- <div className="grid h-64 place-items-center rounded-2xl border border-zinc-200 bg-paper text-sm text-zinc-400">
199
+ <div className="grid h-72 place-items-center border border-ink bg-paper text-sm text-ink/60">
191
200
  Drop a Google Maps embed URL in <code className="mx-1">location.mapEmbedUrl</code>
192
201
  </div>
193
202
  )}
@@ -198,21 +207,18 @@ export default function LandingPage() {
198
207
  {faq && faq.items.length > 0 ? (
199
208
  <>
200
209
  <Divider />
201
- <section className={`${WRAP} py-14`}>
202
- <Eyebrow>{faq.eyebrow}</Eyebrow>
203
- <h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
204
- {faq.headline}
205
- </h2>
206
- <div className="mt-8 divide-y divide-zinc-200/70 border-y border-zinc-200/70">
210
+ <section className={`${WRAP} py-12`}>
211
+ <h2 className="font-display text-[3rem] leading-none sm:text-[4rem]">{faq.headline}</h2>
212
+ <div className="mt-8 divide-y divide-ink border-y border-ink">
207
213
  {faq.items.map((f) => (
208
214
  <details key={f.q} className="group py-5">
209
- <summary className="flex cursor-pointer items-center justify-between text-[15px] font-medium text-zinc-900 marker:hidden [&::-webkit-details-marker]:hidden">
215
+ <summary className="flex cursor-pointer items-center justify-between text-[16px] font-medium marker:hidden [&::-webkit-details-marker]:hidden">
210
216
  {f.q}
211
- <span className="text-brand transition-transform group-open:rotate-45">+</span>
217
+ <span className="font-display text-[24px] leading-none text-brand transition-transform group-open:rotate-45">
218
+ +
219
+ </span>
212
220
  </summary>
213
- <p className="mt-3 max-w-2xl text-[14px] leading-relaxed text-zinc-500">
214
- {f.a}
215
- </p>
221
+ <p className="mt-3 max-w-2xl text-[14.5px] leading-relaxed text-ink/70">{f.a}</p>
216
222
  </details>
217
223
  ))}
218
224
  </div>
@@ -226,8 +232,8 @@ export default function LandingPage() {
226
232
  function QuickFact({ label, value }: { label: string; value: string }) {
227
233
  return (
228
234
  <div>
229
- <dt className="text-[11px] font-medium uppercase tracking-wide text-zinc-400">{label}</dt>
230
- <dd className="mt-1 text-[13.5px] font-medium text-zinc-900">{value}</dd>
235
+ <dt className="text-[11px] font-medium uppercase tracking-wide text-ink/60">{label}</dt>
236
+ <dd className="mt-1 text-[14px] font-medium">{value}</dd>
231
237
  </div>
232
238
  );
233
239
  }
@@ -19,6 +19,9 @@
19
19
  --brand: #4f46e5;
20
20
  --brand-soft: #eef2ff;
21
21
  --paper: #fafafa;
22
+ /* The marketing pages: a cream poster, near-black type. Set from lib/site.config.ts. */
23
+ --ink: #161412;
24
+ --cream: #f4efe6;
22
25
  --background: oklch(1 0 0);
23
26
  --foreground: oklch(0.141 0.005 285.823);
24
27
  --card: oklch(1 0 0);
@@ -112,6 +115,8 @@
112
115
  --color-brand: var(--brand);
113
116
  --color-brand-soft: var(--brand-soft);
114
117
  --color-paper: var(--paper);
118
+ --color-ink: var(--ink);
119
+ --color-cream: var(--cream);
115
120
  --color-chart-1: var(--chart-1);
116
121
  --color-chart-2: var(--chart-2);
117
122
  --color-chart-3: var(--chart-3);
@@ -153,3 +158,9 @@
153
158
  cursor: pointer;
154
159
  }
155
160
  }
161
+
162
+ /* Headlines, service names, and pull quotes. Bebas Neue is declared in app.ts. */
163
+ .font-display {
164
+ font-family: var(--font-display, "Oswald", "Arial Narrow", Impact, sans-serif);
165
+ text-transform: uppercase;
166
+ }
@@ -21,6 +21,8 @@ export default function RootLayout({ children }: LayoutProps) {
21
21
  "--brand": colors.brand,
22
22
  "--brand-soft": colors.brandSoft,
23
23
  "--paper": colors.paper,
24
+ "--ink": colors.ink,
25
+ "--cream": colors.cream,
24
26
  } as React.CSSProperties
25
27
  }
26
28
  >
@@ -143,6 +143,15 @@ const manifest = buildManifest({
143
143
  display: "swap",
144
144
  preload: true,
145
145
  }),
146
+ // The condensed display face for the headline, service names, and pull quotes.
147
+ font({
148
+ family: "Bebas Neue",
149
+ variable: "--font-display",
150
+ weights: ["400"],
151
+ subsets: ["latin"],
152
+ display: "swap",
153
+ preload: true,
154
+ }),
146
155
  ],
147
156
  routes: await discoverAppRoutes(),
148
157
  });
@@ -8,14 +8,6 @@ import React from "react";
8
8
  // Shared container: a contained, centered column.
9
9
  export const WRAP = "mx-auto w-full max-w-5xl px-6";
10
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
11
  // "New / Coming soon"-style pill for the hero.
20
12
  export function Badge({ children }: { children: React.ReactNode }) {
21
13
  return (
@@ -26,36 +18,9 @@ export function Badge({ children }: { children: React.ReactNode }) {
26
18
  );
27
19
  }
28
20
 
21
+ // A full-bleed rule between sections, like the rules on a printed price board.
29
22
  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
- );
23
+ return <div className="border-t border-ink" />;
59
24
  }
60
25
 
61
26
  // A grid of value props — icon + title + body.
@@ -105,11 +70,14 @@ export function ImagePlaceholder({
105
70
  shape = "landscape",
106
71
  title,
107
72
  hint,
73
+ src,
108
74
  className = "",
109
75
  }: {
110
76
  shape?: "landscape" | "portrait" | "square" | "circle";
111
77
  title: string;
112
78
  hint?: string;
79
+ /** A real image. The template ships one under public/images; swap it for yours. */
80
+ src?: string;
113
81
  className?: string;
114
82
  }) {
115
83
  const aspect =
@@ -118,7 +86,14 @@ export function ImagePlaceholder({
118
86
  : shape === "square" || shape === "circle"
119
87
  ? "aspect-square"
120
88
  : "aspect-[4/3]";
121
- const radius = shape === "circle" ? "rounded-full" : "rounded-2xl";
89
+ const radius = shape === "circle" ? "rounded-full" : "";
90
+ if (src) {
91
+ return (
92
+ <div className={`relative overflow-hidden bg-zinc-100 ${aspect} ${radius} ${className}`}>
93
+ <img src={src} alt={title} className="absolute inset-0 size-full object-cover" loading="lazy" />
94
+ </div>
95
+ );
96
+ }
122
97
  return (
123
98
  <div
124
99
  className={`relative grid place-items-center overflow-hidden border-2 border-dashed border-zinc-300 bg-zinc-50 ${aspect} ${radius} ${className}`}
@@ -149,7 +124,7 @@ export function ImagePlaceholder({
149
124
  // visible (e.g. "Availability updates live"). Pure decoration; no client JS.
150
125
  export function LiveBadge({ children }: { children: React.ReactNode }) {
151
126
  return (
152
- <span className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white/95 px-3 py-1.5 text-[12.5px] font-medium text-zinc-700 shadow-sm backdrop-blur">
127
+ <span className="inline-flex items-center gap-2 border border-ink bg-cream px-3 py-1.5 text-[12.5px] font-medium text-ink">
153
128
  <span className="relative flex size-2">
154
129
  <span className="absolute inline-flex size-2 animate-ping rounded-full bg-green-500/60" />
155
130
  <span className="relative inline-flex size-2 rounded-full bg-green-600" />
@@ -20,7 +20,8 @@ export type BaseConfig = {
20
20
  copyrightName: string;
21
21
  socials: Social[];
22
22
  };
23
- colors: { brand: string; brandSoft: string; paper: string };
23
+ // brand: the one accent (deep red). ink/cream: the poster's type and page colors.
24
+ colors: { brand: string; brandSoft: string; paper: string; ink: string; cream: string };
24
25
  seo: { title: string; description: string };
25
26
  };
26
27
 
@@ -46,13 +47,11 @@ export type LocalServiceConfig = BaseConfig & {
46
47
  quickFacts: { hours: string; area: string; phone: string };
47
48
  };
48
49
  services: {
49
- eyebrow: string;
50
50
  headline: string;
51
51
  items: ServiceItem[];
52
52
  };
53
53
  booking: {
54
54
  enabled: boolean;
55
- eyebrow: string;
56
55
  headline: string;
57
56
  subcopy: string;
58
57
  slotMinutes: number; // granularity of offered start times, e.g. 30
@@ -62,9 +61,8 @@ export type LocalServiceConfig = BaseConfig & {
62
61
  hours: Record<number, DayHours>;
63
62
  confirmationMessage: string;
64
63
  };
65
- reviews?: { eyebrow: string; headline: string; items: Review[] };
64
+ reviews?: { headline: string; items: Review[] };
66
65
  location: {
67
- eyebrow: string;
68
66
  headline: string;
69
67
  address: string;
70
68
  mapEmbedUrl?: string;
@@ -72,7 +70,7 @@ export type LocalServiceConfig = BaseConfig & {
72
70
  phone: string;
73
71
  email: string;
74
72
  };
75
- faq?: { eyebrow: string; headline: string; items: Faq[] };
73
+ faq?: { headline: string; items: Faq[] };
76
74
  };
77
75
 
78
76
  /* ----------------------------- config ---------------------------- */
@@ -84,7 +82,7 @@ export const siteConfig: LocalServiceConfig = {
84
82
  domain: "northgatebarbers.com",
85
83
  email: "hello@northgatebarbers.example",
86
84
  footerBlurb:
87
- "A neighborhood barbershop in Dallas. Classic cuts, hot-towel shaves, and a chair that's always ready. Book in ten seconds.",
85
+ "A neighborhood barbershop in Dallas. Haircuts, beard trims, and hot-towel shaves. Book a chair online.",
88
86
  copyrightName: "Northgate Barbers",
89
87
  socials: [
90
88
  {
@@ -95,7 +93,7 @@ export const siteConfig: LocalServiceConfig = {
95
93
  ],
96
94
  },
97
95
 
98
- colors: { brand: "#b45309", brandSoft: "#fef3c7", paper: "#fafaf9" },
96
+ colors: { brand: "#9b1c1c", brandSoft: "#f6dcd7", paper: "#ebe4d6", ink: "#161412", cream: "#f4efe6" },
99
97
 
100
98
  seo: {
101
99
  title: "Northgate Barbers — classic cuts in Dallas. Book online.",
@@ -105,9 +103,9 @@ export const siteConfig: LocalServiceConfig = {
105
103
 
106
104
  hero: {
107
105
  tagline: "Dallas · est. 2014",
108
- headline: "A proper haircut, booked in ten seconds.",
106
+ headline: "Haircuts, beard trims, and hot-towel shaves.",
109
107
  subcopy:
110
- "Classic cuts, beard work, and hot-towel shaves from experienced barbers. The booking calendar updates as chairs fill.",
108
+ "A neighborhood barbershop on Lower Greenville since 2014. Walk in or book a chair online. The calendar updates as chairs fill.",
111
109
  ctaLabel: "Book a chair",
112
110
  quickFacts: {
113
111
  hours: "Tue–Sat, 9–6",
@@ -117,8 +115,7 @@ export const siteConfig: LocalServiceConfig = {
117
115
  },
118
116
 
119
117
  services: {
120
- eyebrow: "Services",
121
- headline: "Services and prices, up front.",
118
+ headline: "Prices",
122
119
  items: [
123
120
  {
124
121
  slug: "haircut",
@@ -153,8 +150,7 @@ export const siteConfig: LocalServiceConfig = {
153
150
 
154
151
  booking: {
155
152
  enabled: true,
156
- eyebrow: "Book",
157
- headline: "Find a time that's open.",
153
+ headline: "Book a chair",
158
154
  subcopy:
159
155
  "Pick a service and a day from the live availability calendar. Book without creating an account or making a phone call.",
160
156
  slotMinutes: 30,
@@ -174,8 +170,7 @@ export const siteConfig: LocalServiceConfig = {
174
170
  },
175
171
 
176
172
  reviews: {
177
- eyebrow: "Reviews",
178
- headline: "Regulars say it best.",
173
+ headline: "What regulars say",
179
174
  items: [
180
175
  {
181
176
  quote:
@@ -199,8 +194,7 @@ export const siteConfig: LocalServiceConfig = {
199
194
  },
200
195
 
201
196
  location: {
202
- eyebrow: "Visit",
203
- headline: "Find us on Greenville.",
197
+ headline: "Find us",
204
198
  address: "1845 Greenville Ave, Dallas, TX 75206",
205
199
  mapEmbedUrl: "",
206
200
  hoursText: "Tue–Wed 9–6 · Thu–Fri 9–7 · Sat 9–4 · Sun–Mon closed",
@@ -209,20 +203,19 @@ export const siteConfig: LocalServiceConfig = {
209
203
  },
210
204
 
211
205
  faq: {
212
- eyebrow: "Questions",
213
- headline: "Good to know.",
206
+ headline: "Questions",
214
207
  items: [
215
208
  {
216
209
  q: "Do you take walk-ins?",
217
- a: "When a chair's open, sure — but booking online guarantees your time, and you can see exactly what's free.",
210
+ a: "Yes, when a chair is open. Booking online holds your time, and you can see what is free.",
218
211
  },
219
212
  {
220
213
  q: "What if I need to cancel?",
221
- a: "Just give us a call. No charge for cancellations with a few hours' notice.",
214
+ a: "Call the shop. There is no charge for cancellations with a few hours' notice.",
222
215
  },
223
216
  {
224
217
  q: "How should I pay?",
225
- a: "Cash or card in the shop. Booking online doesn't charge you anything — you pay after the cut.",
218
+ a: "Cash or card in the shop. Booking online does not charge you. You pay after the cut.",
226
219
  },
227
220
  ],
228
221
  },