@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
@@ -106,7 +106,7 @@ const TEMPLATE_REGISTRY = {
106
106
  },
107
107
  consumer: {
108
108
  blurb:
109
- "Social feed — live posts + likes, public-read, owner-write. One SSR app.",
109
+ "Photo sharing — feed, explore, post pages, profiles, follows, likes, comments, and saves. One SSR app.",
110
110
  platforms: [],
111
111
  unified: true,
112
112
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
4
4
  "description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -57,7 +57,7 @@ this pass before the user has to ask.
57
57
  - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
58
58
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
59
59
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
60
- - **There is no `ctx.files` or `defineWorkflow`/`defineJob`.** Files go through `<FileUpload>` + `/api/files/*`; deferred execution is `ctx.scheduler.runAfter/runAt/cancel`.
60
+ - **There is no `ctx.files` or `defineWorkflow`/`defineJob`.** Files go through `<FileUpload>` + `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; deferred execution is `ctx.scheduler.runAfter/runAt/cancel`.
61
61
 
62
62
  ## Use the CLI
63
63
 
@@ -65,7 +65,7 @@ this pass before the user has to ask.
65
65
  - **`ctx.llm.stream(request, onEvent)` streams tokens as they generate** and still resolves with the full response, so `stop_reason === "tool_use"` drives an agent tool loop. Events are `text_delta` / `tool_use_start` / `tool_input_delta` / `done`. Same auth + model-allowlist gating as `ctx.llm.complete`. Streaming does NOT extend the call deadline (`PYLON_FN_CALL_TIMEOUT`, 30s default) — set `timeout: <secs>` on the def for a long run.
66
66
  - **`ctx.stream.write(text)` streams to the caller — and every fn stream is RESUMABLE**: the server buffers frames under a stream id (`streamFn`'s `onStreamId`), so a dropped connection or closed tab catches up via `resumeStream(id)`, including the final result after the handler finished. `ctx.rooms.broadcast(room, topic, data)` fans out to every CURRENTLY-CONNECTED subscriber (second device, second tab) but does not replay missed messages — use the stream id for anything that must survive a gap. Broadcast resolves `{ delivered: false }` when the room has no members — a no-op, not an error. Clients receive with `useRoomMessages(room, cb)` from `@pylonsync/react`; `useRoom` is the send side.
67
67
  - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
68
- - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
68
+ - **Use the supported file and scheduling APIs.** Files go through `<FileUpload>` and `/api/files/*`; an upload is readable by its uploader only, so pass `visibility: "public"` to `uploadFile` or `<FileUpload>` for files every visitor sees; there is no `ctx.files`. One-shot work uses `ctx.scheduler.runAfter`, `runAt`, or `cancel`; there is no `defineWorkflow` or `defineJob`. Recurring work uses `cron("0 * * * *", "fnName")` in `buildManifest({ crons: [...] })`, imported from `@pylonsync/sdk`. Make the target function `internal: true`. It runs with anonymous auth, but its own `ctx.db.*` calls are server-side and bypass policies. Use `ctx.auth.elevate({ admin: true, reason: "..." })`, with a mandatory reason, only when chaining another internal function through `ctx.scheduler`.
69
69
 
70
70
  ## Testing
71
71
 
@@ -9,7 +9,7 @@ import { siteConfig } from "@/lib/site.config";
9
9
  // row via `db.useQuery("Capacity")` — so when the owner books a project from the
10
10
  // dashboard, the "N slots open" number drops live in every open tab. No refresh.
11
11
  //
12
- // • <LiveSlots> — the hero pill ("3 project slots open · Q3 2026").
12
+ // • <LiveSlots> — the hero line ("3 project slots open, Q3 2026").
13
13
  // • <ContactForm> — the "start a project" form. submitInquiry is a public
14
14
  // mutation, so it works for anonymous visitors; the Inquiry
15
15
  // it writes is pure PII and can never be read back by a
@@ -33,7 +33,7 @@ function useSeedCapacity() {
33
33
  }, []);
34
34
  }
35
35
 
36
- /* ------------------------------ hero pill ------------------------------ */
36
+ /* ------------------------------ hero line ------------------------------ */
37
37
 
38
38
  export function LiveSlots() {
39
39
  return (
@@ -65,7 +65,7 @@ function SlotsPill({
65
65
  const period = label || siteConfig.capacity.label;
66
66
  const bookedOut = open <= 0;
67
67
  return (
68
- <span className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white px-3.5 py-1.5 text-[13px] font-medium text-zinc-700 shadow-sm">
68
+ <span className="inline-flex items-center gap-2.5 text-[14px] text-zinc-700">
69
69
  {loading ? (
70
70
  <span className="inline-flex size-2 rounded-full bg-zinc-300" />
71
71
  ) : (
@@ -82,8 +82,8 @@ function SlotsPill({
82
82
  <span>Booked through {period}</span>
83
83
  ) : (
84
84
  <span>
85
- <span className="tabular-nums text-zinc-900">{open}</span>{" "}
86
- {open === 1 ? "project slot" : "project slots"} open · {period}
85
+ <span className="tabular-nums text-ink">{open}</span>{" "}
86
+ {open === 1 ? "project slot" : "project slots"} open, {period}
87
87
  </span>
88
88
  )}
89
89
  </span>
@@ -156,7 +156,7 @@ function FormShell({
156
156
  setError(
157
157
  /valid email|INVALID_ARGS/i.test(msg)
158
158
  ? "Please enter a valid email address."
159
- : "Something went wrong — try again in a moment.",
159
+ : "Something went wrong. Try again in a moment.",
160
160
  );
161
161
  setStatus("idle");
162
162
  }
@@ -164,23 +164,21 @@ function FormShell({
164
164
 
165
165
  if (status === "done") {
166
166
  return (
167
- <div className="rounded-2xl border border-brand/30 bg-brand-soft/50 px-6 py-8 text-center">
168
- <div className="mx-auto flex size-10 items-center justify-center rounded-full bg-brand text-white">
169
- <CheckIcon />
170
- </div>
171
- <p className="mt-3 text-[15px] font-semibold text-zinc-900">{contact.confirmationMessage}</p>
167
+ <div className="border-t border-ink pt-6">
168
+ <p className="font-display text-[1.75rem] leading-tight text-ink">Sent.</p>
169
+ <p className="mt-2 text-[15px] leading-relaxed text-zinc-700">{contact.confirmationMessage}</p>
172
170
  </div>
173
171
  );
174
172
  }
175
173
 
176
174
  return (
177
- <form onSubmit={onSubmit} className="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm sm:p-7">
175
+ <form onSubmit={onSubmit}>
178
176
  {bookedOut ? (
179
- <p className="mb-4 rounded-lg bg-amber-50 px-3 py-2 text-[13px] text-amber-700">
180
- We&apos;re fully booked{period ? ` through ${period}` : ""} — send a note anyway and we&apos;ll reach out when a slot opens.
177
+ <p className="mb-5 border-l-2 border-ink pl-3 text-[14px] text-zinc-700">
178
+ We are fully booked{period ? ` through ${period}` : ""}. Send a note anyway and we will write when a slot opens.
181
179
  </p>
182
180
  ) : null}
183
- <div className="grid gap-3 sm:grid-cols-2">
181
+ <div className="grid gap-5 sm:grid-cols-2">
184
182
  <Field label="Name" required>
185
183
  <input value={form.name} onChange={set("name")} autoComplete="name" aria-label="Name" className={inputCls} disabled={disabled} />
186
184
  </Field>
@@ -199,7 +197,7 @@ function FormShell({
199
197
  </select>
200
198
  </Field>
201
199
  </div>
202
- <div className="mt-3">
200
+ <div className="mt-5">
203
201
  <Field label="Budget">
204
202
  <select value={form.budget} onChange={set("budget")} aria-label="Budget" className={inputCls} disabled={disabled}>
205
203
  <option value="">Select…</option>
@@ -209,24 +207,24 @@ function FormShell({
209
207
  </select>
210
208
  </Field>
211
209
  </div>
212
- <div className="mt-3">
210
+ <div className="mt-5">
213
211
  <Field label="What are you building?">
214
212
  <textarea
215
213
  value={form.message}
216
214
  onChange={set("message")}
217
215
  rows={4}
218
216
  aria-label="Message"
219
- placeholder="A sentence or two about the project, timeline, and what success looks like."
220
- className={inputCls + " resize-none py-2.5"}
217
+ placeholder="A sentence or two about the project and the timeline."
218
+ className={inputCls + " h-auto resize-none py-2.5"}
221
219
  disabled={disabled}
222
220
  />
223
221
  </Field>
224
222
  </div>
225
- {error ? <p className="mt-3 text-[13px] text-red-600">{error}</p> : null}
223
+ {error ? <p className="mt-4 text-[14px] text-red-700">{error}</p> : null}
226
224
  <button
227
225
  type="submit"
228
226
  disabled={status === "sending" || disabled}
229
- className="mt-5 inline-flex h-11 w-full items-center justify-center rounded-full bg-brand text-[15px] font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-60 sm:w-auto sm:px-7"
227
+ className="mt-6 inline-flex h-11 w-full items-center justify-center bg-ink px-7 text-[15px] font-medium text-white transition-colors hover:bg-zinc-700 disabled:opacity-60 sm:w-auto"
230
228
  >
231
229
  {status === "sending" ? "Sending…" : "Send inquiry"}
232
230
  </button>
@@ -237,9 +235,9 @@ function FormShell({
237
235
  function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) {
238
236
  return (
239
237
  <label className="block">
240
- <span className="mb-1.5 block text-[12.5px] font-medium text-zinc-600">
238
+ <span className="mb-1.5 block text-[11px] font-medium uppercase tracking-[0.08em] text-zinc-600">
241
239
  {label}
242
- {required ? <span className="text-brand"> *</span> : null}
240
+ {required ? <span className="text-zinc-500"> *</span> : null}
243
241
  </span>
244
242
  {children}
245
243
  </label>
@@ -247,12 +245,4 @@ function Field({ label, required, children }: { label: string; required?: boolea
247
245
  }
248
246
 
249
247
  const inputCls =
250
- "h-10 w-full rounded-lg border border-zinc-300 bg-white px-3 text-[14px] text-zinc-900 outline-none transition placeholder:text-zinc-400 focus:border-brand focus:ring-2 focus:ring-brand/20 disabled:opacity-60";
251
-
252
- function CheckIcon() {
253
- return (
254
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
255
- <path d="M20 6 9 17l-5-5" />
256
- </svg>
257
- );
258
- }
248
+ "h-11 w-full rounded-none border-0 border-b border-zinc-400 bg-transparent px-0 text-[15px] text-ink outline-none transition-colors placeholder:text-zinc-400 focus:border-ink disabled:opacity-60";
@@ -1,11 +1,12 @@
1
1
  import React from "react";
2
2
  import { Link, type PageAuth } from "@pylonsync/react";
3
3
  import { siteConfig } from "@/lib/site.config";
4
+ import { WRAP } from "@/components/marketing";
4
5
 
5
6
  // `(marketing)` is a ROUTE GROUP: the parens segment is stripped from every
6
7
  // URL (so `(marketing)/page.tsx` still serves `/`), and this layout wraps
7
- // only the pages inside the group — a slim nav up top and a footer below,
8
- // both driven by lib/site.config.ts. Routes outside the group (/login,
8
+ // only the pages inside the group — a plain text nav up top and a footer
9
+ // below, both driven by lib/site.config.ts. Routes outside the group (/login,
9
10
  // /dashboard) render bare in the root shell. `auth.user_id` is resolved
10
11
  // server-side from the session cookie before any HTML is sent, so the nav
11
12
  // shows "Dashboard" once the owner is signed in and "Sign in" otherwise —
@@ -15,6 +16,8 @@ interface LayoutProps {
15
16
  auth: PageAuth;
16
17
  }
17
18
 
19
+ const NAV_LINK = "text-[14px] text-zinc-700 transition-colors hover:text-ink";
20
+
18
21
  export default function MarketingLayout({ children, auth }: LayoutProps) {
19
22
  // A guest session (minted by <EnsureGuest> for the live counter) has a
20
23
  // `guest_…` user id — that's an anonymous visitor, NOT the signed-in owner,
@@ -24,43 +27,29 @@ export default function MarketingLayout({ children, auth }: LayoutProps) {
24
27
 
25
28
  return (
26
29
  <>
27
- <header className="sticky top-0 z-30 border-b border-zinc-200/70 bg-white/85 backdrop-blur">
28
- <div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-6">
29
- <Link href="/" className="flex items-center gap-2">
30
- <span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
31
- {brand.letter}
32
- </span>
33
- <span className="text-[15px] font-semibold tracking-tight text-zinc-900">
34
- {brand.name}
35
- </span>
30
+ <header className="bg-white">
31
+ <div className={`${WRAP} flex h-16 items-center justify-between`}>
32
+ <Link href="/" className="font-display text-[1.5rem] leading-none text-ink">
33
+ {brand.name}
36
34
  </Link>
37
- <nav className="flex items-center gap-1 sm:gap-2">
38
- <a href="/work" className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex">
35
+ <nav className="flex items-center gap-5 sm:gap-7">
36
+ <a href="/work" className={NAV_LINK}>
39
37
  Work
40
38
  </a>
41
- <a href="/#services" className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex">
39
+ <a href="/#services" className={`hidden sm:inline ${NAV_LINK}`}>
42
40
  Services
43
41
  </a>
44
42
  {signedIn ? (
45
- <Link
46
- href="/dashboard"
47
- className="inline-flex items-center rounded-full bg-zinc-900 px-3.5 py-1.5 text-[13px] font-medium text-white transition-colors hover:bg-zinc-700"
48
- >
43
+ <Link href="/dashboard" className={NAV_LINK}>
49
44
  Dashboard
50
45
  </Link>
51
46
  ) : (
52
47
  <>
53
- <Link
54
- href="/login"
55
- className="hidden rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex"
56
- >
48
+ <Link href="/login" className={`hidden sm:inline ${NAV_LINK}`}>
57
49
  Sign in
58
50
  </Link>
59
- <a
60
- href="/#contact"
61
- className="inline-flex items-center rounded-full bg-brand px-3.5 py-1.5 text-[13px] font-medium text-white transition-opacity hover:opacity-90"
62
- >
63
- {siteConfig.hero.ctaLabel}
51
+ <a href="/#contact" className={NAV_LINK}>
52
+ Contact
64
53
  </a>
65
54
  </>
66
55
  )}
@@ -78,54 +67,37 @@ export default function MarketingLayout({ children, auth }: LayoutProps) {
78
67
  function SiteFooter() {
79
68
  const { brand } = siteConfig;
80
69
  return (
81
- <footer className="border-t border-zinc-200/70 bg-white">
82
- <div className="mx-auto max-w-5xl px-6 py-12">
83
- <div className="flex flex-col items-start justify-between gap-6 sm:flex-row">
70
+ <footer className="border-t border-zinc-200 bg-white text-ink">
71
+ <div className={`${WRAP} py-12`}>
72
+ <div className="grid gap-8 sm:grid-cols-[1fr_auto]">
84
73
  <div className="max-w-sm">
85
- <Link href="/" className="inline-flex items-center gap-2">
86
- <span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
87
- {brand.letter}
88
- </span>
89
- <span className="text-[15px] font-semibold tracking-tight text-zinc-900">
90
- {brand.name}
91
- </span>
74
+ <Link href="/" className="font-display text-[1.5rem] leading-none">
75
+ {brand.name}
92
76
  </Link>
93
- <p className="mt-3 text-[13px] leading-relaxed text-zinc-500">
94
- {brand.footerBlurb}
95
- </p>
77
+ <p className="mt-3 text-[14px] leading-relaxed text-zinc-600">{brand.footerBlurb}</p>
96
78
  </div>
97
- <div className="flex items-center gap-4">
98
- {brand.socials.map((s) => (
99
- <a
100
- key={s.label}
101
- href={s.href}
102
- aria-label={s.label}
103
- className="text-zinc-400 transition-colors hover:text-zinc-900"
104
- >
105
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
106
- <path d={s.path} />
107
- </svg>
79
+ <ul className="flex flex-col gap-2 text-[14px] sm:text-right">
80
+ <li>
81
+ <a href={`mailto:${brand.email}`} className="text-zinc-700 transition-colors hover:text-ink">
82
+ {brand.email}
108
83
  </a>
84
+ </li>
85
+ {brand.socials.map((s) => (
86
+ <li key={s.label}>
87
+ <a href={s.href} className="text-zinc-700 transition-colors hover:text-ink">
88
+ {s.label}
89
+ </a>
90
+ </li>
109
91
  ))}
110
- <a
111
- href={`mailto:${brand.email}`}
112
- aria-label="Email"
113
- className="text-zinc-400 transition-colors hover:text-zinc-900"
114
- >
115
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
116
- <rect x="3" y="5" width="18" height="14" rx="2" />
117
- <path d="m3 7 9 6 9-6" />
118
- </svg>
119
- </a>
120
- </div>
92
+ </ul>
121
93
  </div>
122
- <div className="mt-10 flex flex-col items-start justify-between gap-3 border-t border-zinc-200/70 pt-6 text-[12px] text-zinc-400 sm:flex-row sm:items-center">
94
+ <div className="mt-10 flex flex-col gap-2 border-t border-zinc-200 pt-6 text-[13px] text-zinc-500 sm:flex-row sm:justify-between">
123
95
  <span>
124
96
  © {new Date().getFullYear()} {brand.copyrightName}
125
97
  </span>
126
98
  <span>
127
99
  Built with{" "}
128
- <a href="https://pylonsync.com" className="font-medium text-zinc-600 hover:text-zinc-900">
100
+ <a href="https://pylonsync.com" className="text-zinc-700 hover:text-ink">
129
101
  Pylon
130
102
  </a>
131
103
  </span>
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { Link, type NotFoundProps } from "@pylonsync/react";
3
+ import { WRAP_NARROW, TEXT_LINK } from "@/components/marketing";
3
4
 
4
5
  // `(marketing)/not-found.tsx` → rendered at HTTP 404 for any unmatched URL
5
6
  // (and when a page calls `response.notFound()`). It lives inside the
@@ -9,14 +10,11 @@ import { Link, type NotFoundProps } from "@pylonsync/react";
9
10
  // the link is a client nav.
10
11
  export default function NotFound(_props: NotFoundProps) {
11
12
  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>
15
- <Link
16
- 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"
18
- >
19
- Back home
13
+ <div className={`${WRAP_NARROW} flex min-h-[60vh] flex-col justify-center py-20 text-ink`}>
14
+ <h1 className="font-display text-[2.75rem] leading-none">Page not found</h1>
15
+ <p className="mt-4 text-[16px] text-zinc-600">There is nothing at this address.</p>
16
+ <Link href="/" className={`mt-6 inline-block self-start text-[15px] ${TEXT_LINK}`}>
17
+ Back to the homepage
20
18
  </Link>
21
19
  </div>
22
20
  );