@pylonsync/create-pylon 0.3.295 → 0.3.297

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 (72) hide show
  1. package/package.json +1 -1
  2. package/templates/agency/app/globals.css +8 -1
  3. package/templates/agency/app/layout.tsx +4 -6
  4. package/templates/agency/app.ts +15 -0
  5. package/templates/agency/components/marketing.tsx +2 -4
  6. package/templates/agency/lib/site.config.ts +3 -5
  7. package/templates/ai-chat/app/globals.css +8 -1
  8. package/templates/ai-chat/app/layout.tsx +4 -8
  9. package/templates/ai-chat/app.ts +18 -5
  10. package/templates/ai-chat/lib/site.config.ts +3 -3
  11. package/templates/ai-studio/app/globals.css +8 -1
  12. package/templates/ai-studio/app/layout.tsx +7 -11
  13. package/templates/ai-studio/app.ts +20 -7
  14. package/templates/ai-studio/lib/site.config.ts +2 -2
  15. package/templates/ai-studio/lib/studio.ts +1 -1
  16. package/templates/backend/b2b/apps/api/schema.ts +10 -24
  17. package/templates/backend/consumer/apps/api/schema.ts +4 -7
  18. package/templates/barebones/app/layout.tsx +3 -3
  19. package/templates/barebones/app.ts +2 -3
  20. package/templates/chat/app.ts +3 -9
  21. package/templates/consumer/app.ts +2 -3
  22. package/templates/creator/.env.example +3 -3
  23. package/templates/creator/app/globals.css +8 -1
  24. package/templates/creator/app/layout.tsx +4 -6
  25. package/templates/creator/app.ts +23 -10
  26. package/templates/creator/components/marketing.tsx +2 -4
  27. package/templates/default/app/globals.css +8 -1
  28. package/templates/default/app/layout.tsx +6 -14
  29. package/templates/default/app.ts +15 -0
  30. package/templates/default/lib/products.ts +2 -3
  31. package/templates/default/lib/site.config.ts +1 -2
  32. package/templates/default/lib/site.ts +3 -4
  33. package/templates/directory/app/auth-form.tsx +5 -5
  34. package/templates/directory/app/globals.css +8 -1
  35. package/templates/directory/app/layout.tsx +4 -6
  36. package/templates/directory/app/sitemap.ts +1 -1
  37. package/templates/directory/app.ts +15 -0
  38. package/templates/directory/lib/owner.ts +5 -5
  39. package/templates/expo/chat/apps/expo/App.tsx +2 -3
  40. package/templates/local-service/app/auth-form.tsx +4 -4
  41. package/templates/local-service/app/globals.css +8 -1
  42. package/templates/local-service/app/layout.tsx +4 -6
  43. package/templates/local-service/app/sitemap.ts +1 -1
  44. package/templates/local-service/app.ts +15 -0
  45. package/templates/local-service/components/marketing.tsx +2 -4
  46. package/templates/local-service/lib/owner.ts +3 -3
  47. package/templates/local-service/lib/site.config.ts +4 -6
  48. package/templates/marketplace/app/listing/[id]/page.tsx +3 -3
  49. package/templates/marketplace/functions/makeOffer.ts +0 -1
  50. package/templates/restaurant/app/auth-form.tsx +5 -5
  51. package/templates/restaurant/app/globals.css +8 -1
  52. package/templates/restaurant/app/layout.tsx +4 -6
  53. package/templates/restaurant/app/sitemap.ts +1 -1
  54. package/templates/restaurant/app.ts +15 -0
  55. package/templates/restaurant/lib/owner.ts +5 -5
  56. package/templates/shop/app/auth-form.tsx +5 -5
  57. package/templates/shop/app/globals.css +8 -1
  58. package/templates/shop/app/layout.tsx +5 -7
  59. package/templates/shop/app/sitemap.ts +1 -1
  60. package/templates/shop/app.ts +15 -0
  61. package/templates/shop/lib/owner.ts +6 -5
  62. package/templates/todo/app/layout.tsx +2 -2
  63. package/templates/todo/app/sitemap.ts +1 -1
  64. package/templates/todo/app.ts +4 -5
  65. package/templates/vite/todo/apps/web/vite.config.ts +5 -9
  66. package/templates/waitlist/app/globals.css +8 -1
  67. package/templates/waitlist/app/layout.tsx +4 -6
  68. package/templates/waitlist/app/waitlist-hero.tsx +4 -5
  69. package/templates/waitlist/app.ts +26 -9
  70. package/templates/waitlist/lib/site.config.ts +3 -5
  71. package/templates/web/barebones/apps/web/postcss.config.mjs +0 -1
  72. package/templates/web/barebones/apps/web/src/app/globals.css +1 -4
@@ -144,7 +144,14 @@
144
144
  body {
145
145
  background-color: var(--color-background);
146
146
  color: var(--color-foreground);
147
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
147
+ font-family: var(
148
+ --font-sans,
149
+ Inter,
150
+ ui-sans-serif,
151
+ system-ui,
152
+ -apple-system,
153
+ sans-serif
154
+ );
148
155
  -webkit-font-smoothing: antialiased;
149
156
  }
150
157
  button {
@@ -205,20 +205,12 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
205
205
  `generateMetadata` sets it. A hardcoded title in the layout would
206
206
  render first and win over the page's, so every tab would read
207
207
  "Acme". */}
208
- {/* Inter the marketing pages look best in a clean grotesk. Swap for
209
- your own font or drop this link to fall back to the system stack. */}
210
- <link rel="preconnect" href="https://fonts.googleapis.com" />
211
- <link
212
- rel="preconnect"
213
- href="https://fonts.gstatic.com"
214
- crossOrigin="anonymous"
215
- />
216
- <link
217
- rel="stylesheet"
218
- href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
219
- />
220
- {/* Tailwind is compiled by Pylon from app/globals.css and the
221
- stylesheet link is injected here automatically. */}
208
+ {/* Inter is declared in app.ts (`fonts: [font({ family: "Inter", })]`)
209
+ and self-hosted by the build the runtime injects the @font-face,
210
+ <link rel=preload>, and a size-adjusted fallback here automatically.
211
+ No third-party Google Fonts request, no layout shift. Swap the family
212
+ in app.ts to change it. Tailwind's compiled stylesheet is injected
213
+ here too. */}
222
214
  </head>
223
215
  <body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
224
216
  {isBare ? (
@@ -5,6 +5,7 @@ import {
5
5
  auth,
6
6
  buildManifest,
7
7
  discoverAppRoutes,
8
+ font,
8
9
  } from "@pylonsync/sdk";
9
10
  // Per-workspace Stripe billing — see lib/billing.ts. `billing.manifest` brings
10
11
  // the StripeSubscription entity + checkout/portal/cancel/restore/webhook actions
@@ -186,6 +187,20 @@ const manifest = buildManifest({
186
187
  // above are named with the framework defaults (Org / OrgMember / OrgInvite),
187
188
  // so `/api/auth/orgs/*` + `/api/auth/select-org` work with no extra config.
188
189
  auth: auth(),
190
+ // Self-hosted Inter (next/font parity): the build fetches the woff2, serves it
191
+ // same-origin (no third-party request, no FOUT), preloads it, and synthesizes a
192
+ // size-adjusted fallback face so there's no layout shift. globals.css reads it
193
+ // via `var(--font-sans, …)`; layout.tsx carries no font <link>.
194
+ fonts: [
195
+ font({
196
+ family: "Inter",
197
+ variable: "--font-sans",
198
+ weights: ["400", "500", "600", "700"],
199
+ subsets: ["latin"],
200
+ display: "swap",
201
+ preload: true,
202
+ }),
203
+ ],
189
204
  routes: await discoverAppRoutes(),
190
205
  });
191
206
 
@@ -1,6 +1,5 @@
1
- // The marketing "products" now live in the single site config so the whole
2
- // template can be rebranded from one file. This module re-exports them so
3
- // existing imports (`@/lib/products`) keep working. Edit lib/site.config.ts.
1
+ // Re-exports the marketing products from the single site config so existing
2
+ // `@/lib/products` imports keep working. Edit lib/site.config.ts.
4
3
  export {
5
4
  PRODUCTS,
6
5
  productBySlug,
@@ -1,8 +1,7 @@
1
1
  // THE single source of truth for everything business-specific in this template.
2
2
  // Rebrand the entire site by editing this ONE file — the marketing components
3
3
  // (hero, pricing, FAQ, footer, nav) all read from here and stay generic. The
4
- // `create-pylon` scaffolder and automated generators target this file too, so a
5
- // whole site can be themed by producing one typed object.
4
+ // `create-pylon` scaffolder and automated generators target this file too.
6
5
  //
7
6
  // Colors live here (applied as CSS variables on <html> in app/layout.tsx), so
8
7
  // you don't touch globals.css to re-theme the marketing pages.
@@ -1,7 +1,6 @@
1
- // The marketing content (solutions, resources, company, comparisons) now lives
2
- // in the single site config so the whole template can be rebranded from one
3
- // file. This module re-exports it so existing imports (`@/lib/site`) keep
4
- // working. Edit lib/site.config.ts.
1
+ // Re-exports the marketing content (solutions, resources, company, comparisons)
2
+ // from the single site config so existing `@/lib/site` imports keep working.
3
+ // Edit lib/site.config.ts.
5
4
  export {
6
5
  SOLUTIONS,
7
6
  RESOURCES,
@@ -13,15 +13,15 @@ import {
13
13
  // `/api/auth/password/*`), then `persistSession` writes the freshly-minted
14
14
  // token to local storage so the sync engine + `callFn` authenticate AS THE
15
15
  // OWNER on the next load. This step matters here specifically: the landing page
16
- // mints an anonymous guest session (for the live slot counter), and without
16
+ // mints an anonymous guest session (for the live upvotes), and without
17
17
  // persisting the real session that stale guest token would shadow the owner's
18
- // — so the owner-only `inquiriesForOwner` call would come back as a guest and
18
+ // — so the owner-only `submissionsForOwner` call would come back as a guest and
19
19
  // get rejected. We then do a full navigation to /dashboard so the SSR runtime
20
20
  // re-resolves auth from the HttpOnly cookie and renders server-side.
21
21
  //
22
- // A studio is single-tenant: there's no public sign-up, just the owner creating
23
- // their one account. Whoever signs in only sees data if their email matches
24
- // PYLON_OWNER_EMAIL — enforced by the inquiriesForOwner function.
22
+ // A directory is single-tenant: there's no public sign-up, just the owner
23
+ // creating their one account. Whoever signs in only sees data if their email
24
+ // matches PYLON_OWNER_EMAIL — enforced by the submissionsForOwner function.
25
25
  export function AuthForm() {
26
26
  const [mode, setMode] = useState<"login" | "register">("login");
27
27
  const [email, setEmail] = useState("");
@@ -139,7 +139,14 @@
139
139
  body {
140
140
  background-color: var(--color-background);
141
141
  color: var(--color-foreground);
142
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
142
+ font-family: var(
143
+ --font-sans,
144
+ Inter,
145
+ ui-sans-serif,
146
+ system-ui,
147
+ -apple-system,
148
+ sans-serif
149
+ );
143
150
  -webkit-font-smoothing: antialiased;
144
151
  }
145
152
  button {
@@ -45,12 +45,10 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
45
45
  <meta charSet="utf-8" />
46
46
  <meta name="viewport" content="width=device-width, initial-scale=1" />
47
47
  {/* No <title> here — each page's exported `metadata` sets it. */}
48
- <link rel="preconnect" href="https://fonts.googleapis.com" />
49
- <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
50
- <link
51
- rel="stylesheet"
52
- href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
53
- />
48
+ {/* Inter is declared in app.ts (fonts: [...]) and self-hosted by the
49
+ build — the runtime injects @font-face + <link rel=preload> + a
50
+ size-adjusted fallback here automatically. No third-party request,
51
+ no layout shift; change the family in app.ts. */}
54
52
  {/* Tailwind is compiled by Pylon from app/globals.css and injected here. */}
55
53
  </head>
56
54
  <body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
@@ -1,7 +1,7 @@
1
1
  import type { Sitemap } from "@pylonsync/react";
2
2
 
3
3
  // app/sitemap.ts → served at /sitemap.xml. Point SITE_URL at your domain in
4
- // production. The studio site is a single public page, so the sitemap is just "/".
4
+ // production. This lists the public homepage; add more URLs as the site grows.
5
5
  const SITE = process.env.SITE_URL ?? "http://localhost:4321";
6
6
 
7
7
  export default async function sitemap(): Promise<Sitemap> {
@@ -5,6 +5,7 @@ import {
5
5
  auth,
6
6
  buildManifest,
7
7
  discoverAppRoutes,
8
+ font,
8
9
  } from "@pylonsync/sdk";
9
10
 
10
11
  // ---------------------------------------------------------------------------
@@ -138,6 +139,20 @@ const manifest = buildManifest({
138
139
  actions: [],
139
140
  policies: [listingPolicy, submissionPolicy, userPolicy],
140
141
  auth: auth(),
142
+ // Self-hosted Inter (next/font parity): the build fetches the woff2, serves it
143
+ // same-origin (no third-party request, no FOUT), preloads it, and synthesizes a
144
+ // size-adjusted fallback face so there's no layout shift. globals.css reads it
145
+ // via `var(--font-sans, …)`; layout.tsx carries no font <link>.
146
+ fonts: [
147
+ font({
148
+ family: "Inter",
149
+ variable: "--font-sans",
150
+ weights: ["400", "500", "600", "700"],
151
+ subsets: ["latin"],
152
+ display: "swap",
153
+ preload: true,
154
+ }),
155
+ ],
141
156
  routes: await discoverAppRoutes(),
142
157
  });
143
158
 
@@ -1,11 +1,11 @@
1
- // Who owns this studio site? A studio is single-tenant — one business, one
2
- // owner — so ownership is just "the email the owner signs in with", configured
3
- // once via the PYLON_OWNER_EMAIL env var. The owner-only functions
4
- // (inquiriesForOwner etc.) read that env (via `ctx.env`) and compare it here.
1
+ // Who owns this directory? A directory is single-tenant — one curator — so
2
+ // ownership is just "the email the owner signs in with", configured once via
3
+ // the PYLON_OWNER_EMAIL env var. The owner-only functions (submissionsForOwner
4
+ // etc.) read that env (via `ctx.env`) and compare it here.
5
5
  //
6
6
  // Fail closed: if PYLON_OWNER_EMAIL is unset, NOBODY is the owner and the
7
7
  // dashboard stays locked. That's deliberate — an unset owner on a public site
8
- // must not mean "everyone can read the inquiries". Set it in .env (see
8
+ // must not mean "everyone can read the submissions". Set it in .env (see
9
9
  // .env.example) before signing in.
10
10
 
11
11
  export function normalizeOwner(raw: string | null | undefined): string | null {
@@ -62,9 +62,8 @@ export default function App() {
62
62
  }
63
63
 
64
64
  function Chat() {
65
- // Live subscriptions — sync engine pushes diffs over WebSocket.
66
- // New rooms / new messages from any other device or device update
67
- // re-render this component without polling.
65
+ // Live subscriptions — the sync engine pushes diffs over WebSocket, so new
66
+ // rooms/messages from any device re-render this component without polling.
68
67
  const { data: rooms = [] } = db.useQuery<Room>("Room", {
69
68
  orderBy: { createdAt: "asc" },
70
69
  });
@@ -13,15 +13,15 @@ import {
13
13
  // `/api/auth/password/*`), then `persistSession` writes the freshly-minted
14
14
  // token to local storage so the sync engine + `callFn` authenticate AS THE
15
15
  // OWNER on the next load. This step matters here specifically: the landing page
16
- // mints an anonymous guest session (for the live counter), and without
16
+ // mints an anonymous guest session (for the live booking picker), and without
17
17
  // persisting the real session that stale guest token would shadow the owner's
18
- // — so the owner-only `waitlistStats` call would come back as a guest and get
18
+ // — so the owner-only `bookingsForOwner` call would come back as a guest and get
19
19
  // rejected. We then do a full navigation to /dashboard so the SSR runtime
20
20
  // re-resolves auth from the HttpOnly cookie and renders server-side.
21
21
  //
22
- // A waitlist is single-tenant: there's no public signup funnel, just the owner
22
+ // A booking site is single-tenant: there's no public signup funnel, just the owner
23
23
  // creating their one account. Whoever signs in only sees data if their email
24
- // matches PYLON_OWNER_EMAIL — enforced by the waitlistStats function.
24
+ // matches PYLON_OWNER_EMAIL — enforced by the bookingsForOwner function.
25
25
  export function AuthForm() {
26
26
  const [mode, setMode] = useState<"login" | "signup">("login");
27
27
  const [email, setEmail] = useState("");
@@ -139,7 +139,14 @@
139
139
  body {
140
140
  background-color: var(--color-background);
141
141
  color: var(--color-foreground);
142
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
142
+ font-family: var(
143
+ --font-sans,
144
+ Inter,
145
+ ui-sans-serif,
146
+ system-ui,
147
+ -apple-system,
148
+ sans-serif
149
+ );
143
150
  -webkit-font-smoothing: antialiased;
144
151
  }
145
152
  button {
@@ -39,12 +39,10 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
39
39
  <head>
40
40
  <meta charSet="utf-8" />
41
41
  <meta name="viewport" content="width=device-width, initial-scale=1" />
42
- <link rel="preconnect" href="https://fonts.googleapis.com" />
43
- <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
44
- <link
45
- rel="stylesheet"
46
- href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
47
- />
42
+ {/* Inter is declared in app.ts (fonts: [...]) and self-hosted by the
43
+ build — the runtime injects @font-face + <link rel=preload> + a
44
+ size-adjusted fallback here automatically. No third-party request,
45
+ no layout shift; change the family in app.ts. */}
48
46
  </head>
49
47
  <body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
50
48
  <SectionScroller />
@@ -1,7 +1,7 @@
1
1
  import type { Sitemap } from "@pylonsync/react";
2
2
 
3
3
  // app/sitemap.ts → served at /sitemap.xml. Point SITE_URL at your domain in
4
- // production. The waitlist is a single public page, so the sitemap is just "/".
4
+ // production. This site is a single public page, so the sitemap is just "/".
5
5
  const SITE = process.env.SITE_URL ?? "http://localhost:4321";
6
6
 
7
7
  export default async function sitemap(): Promise<Sitemap> {
@@ -5,6 +5,7 @@ import {
5
5
  auth,
6
6
  buildManifest,
7
7
  discoverAppRoutes,
8
+ font,
8
9
  } from "@pylonsync/sdk";
9
10
 
10
11
  // ---------------------------------------------------------------------------
@@ -123,6 +124,20 @@ const manifest = buildManifest({
123
124
  actions: [],
124
125
  policies: [bookingPolicy, bookedSlotPolicy, userPolicy],
125
126
  auth: auth(),
127
+ // Self-hosted Inter (next/font parity): the build fetches the woff2, serves it
128
+ // same-origin (no third-party request, no FOUT), preloads it, and synthesizes a
129
+ // size-adjusted fallback face so there's no layout shift. globals.css reads it
130
+ // via `var(--font-sans, …)`; layout.tsx carries no font <link>.
131
+ fonts: [
132
+ font({
133
+ family: "Inter",
134
+ variable: "--font-sans",
135
+ weights: ["400", "500", "600", "700"],
136
+ subsets: ["latin"],
137
+ display: "swap",
138
+ preload: true,
139
+ }),
140
+ ],
126
141
  routes: await discoverAppRoutes(),
127
142
  });
128
143
 
@@ -95,10 +95,8 @@ export function initials(name: string) {
95
95
  .toUpperCase();
96
96
  }
97
97
 
98
- // A deliberately-obvious image placeholder. Real sites drop a photo here; this
99
- // makes the spot unmistakable dashed border, a photo glyph, and a one-line
100
- // "swap this" instruction telling you exactly what to replace and where. Looks
101
- // tidy enough to demo, but no one will mistake it for a finished design.
98
+ // A deliberately-obvious image placeholder dashed border, a photo glyph, and
99
+ // a one-line "swap this" instruction. Real sites drop a photo here.
102
100
  //
103
101
  // shape — "landscape" | "portrait" | "square" | "circle"
104
102
  // title — what photo belongs here ("A photo of your shop")
@@ -1,11 +1,11 @@
1
- // Who owns this waitlist? A waitlist is single-tenant — one business, one
1
+ // Who owns this site? A booking site is single-tenant — one business, one
2
2
  // owner — so ownership is just "the email the owner signs in with", configured
3
- // once via the PYLON_OWNER_EMAIL env var. The owner-only `waitlistStats`
3
+ // once via the PYLON_OWNER_EMAIL env var. The owner-only `bookingsForOwner`
4
4
  // function reads that env (via `ctx.env`) and compares it here.
5
5
  //
6
6
  // Fail closed: if PYLON_OWNER_EMAIL is unset, NOBODY is the owner and the
7
7
  // dashboard stays locked. That's deliberate — an unset owner on a public site
8
- // must not mean "everyone can read the signups". Set it in .env (see
8
+ // must not mean "everyone can read the bookings". Set it in .env (see
9
9
  // .env.example) before signing in.
10
10
 
11
11
  export function normalizeOwner(raw: string | null | undefined): string | null {
@@ -1,9 +1,7 @@
1
- // THE single source of truth for everything business-specific. Rebrand the
2
- // whole site and reconfigure the booking engine by editing this ONE file.
3
- // The landing page, layout, AND the createBooking server function all read from
4
- // here, so services, prices, weekly hours, and lead time stay in lockstep. The
5
- // create-pylon scaffolder and Mast target this file: a whole appointment site
6
- // is themed + configured by producing one typed object.
1
+ // THE single source of truth for everything business-specific. The landing
2
+ // page, layout, AND the createBooking server function all read from here, so
3
+ // services, prices, weekly hours, and lead time stay in lockstep rebrand and
4
+ // reconfigure the whole site by editing this ONE file.
7
5
  //
8
6
  // Colors live here (applied as CSS variables on <html> in app/layout.tsx).
9
7
  //
@@ -18,9 +18,6 @@ import {
18
18
  type Listing,
19
19
  } from "../../../client/market";
20
20
 
21
- // Data-driven SEO: the title + description come from the listing itself,
22
- // fetched on the server. `generateMetadata` is handed the same PageProps as
23
- // the page (params + serverData), so it reads the row directly.
24
21
  // Resolve a listing from the URL segment, which is its slug
25
22
  // ("herman-miller-aeron-a1f3"). Falls back to a raw id lookup so older
26
23
  // id-shaped links keep working.
@@ -34,6 +31,9 @@ async function resolveListing(
34
31
  );
35
32
  }
36
33
 
34
+ // Data-driven SEO: the title + description come from the listing itself,
35
+ // fetched on the server. `generateMetadata` is handed the same PageProps as
36
+ // the page (params + serverData), so it reads the row directly.
37
37
  export const generateMetadata: GenerateMetadata = async ({
38
38
  params,
39
39
  serverData,
@@ -48,7 +48,6 @@ export default mutation<MakeOfferArgs, MakeOfferResult>({
48
48
  throw ctx.error("INVALID_ARGS", "offer must be greater than zero");
49
49
 
50
50
  const id = await ctx.db.insert("Offer", {
51
- // Reuse the optimistic ghost's id so the broadcast merges in place.
52
51
  id: args._optimisticId,
53
52
  listingId: args.listingId,
54
53
  listingTitle: listing.title,
@@ -13,15 +13,15 @@ import {
13
13
  // `/api/auth/password/*`), then `persistSession` writes the freshly-minted
14
14
  // token to local storage so the sync engine + `callFn` authenticate AS THE
15
15
  // OWNER on the next load. This step matters here specifically: the landing page
16
- // mints an anonymous guest session (for the live counter), and without
16
+ // mints an anonymous guest session (for the live table picker), and without
17
17
  // persisting the real session that stale guest token would shadow the owner's
18
- // — so the owner-only `waitlistStats` call would come back as a guest and get
19
- // rejected. We then do a full navigation to /dashboard so the SSR runtime
18
+ // — so the owner-only `reservationsForOwner` call would come back as a guest and
19
+ // get rejected. We then do a full navigation to /dashboard so the SSR runtime
20
20
  // re-resolves auth from the HttpOnly cookie and renders server-side.
21
21
  //
22
- // A waitlist is single-tenant: there's no public signup funnel, just the owner
22
+ // This site is single-tenant: there's no public signup funnel, just the owner
23
23
  // creating their one account. Whoever signs in only sees data if their email
24
- // matches PYLON_OWNER_EMAIL — enforced by the waitlistStats function.
24
+ // matches PYLON_OWNER_EMAIL — enforced by the reservationsForOwner function.
25
25
  export function AuthForm() {
26
26
  const [mode, setMode] = useState<"login" | "signup">("login");
27
27
  const [email, setEmail] = useState("");
@@ -139,7 +139,14 @@
139
139
  body {
140
140
  background-color: var(--color-background);
141
141
  color: var(--color-foreground);
142
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
142
+ font-family: var(
143
+ --font-sans,
144
+ Inter,
145
+ ui-sans-serif,
146
+ system-ui,
147
+ -apple-system,
148
+ sans-serif
149
+ );
143
150
  -webkit-font-smoothing: antialiased;
144
151
  }
145
152
  button {
@@ -39,12 +39,10 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
39
39
  <head>
40
40
  <meta charSet="utf-8" />
41
41
  <meta name="viewport" content="width=device-width, initial-scale=1" />
42
- <link rel="preconnect" href="https://fonts.googleapis.com" />
43
- <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
44
- <link
45
- rel="stylesheet"
46
- href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
47
- />
42
+ {/* Inter is declared in app.ts (fonts: [...]) and self-hosted by the
43
+ build — the runtime injects @font-face + <link rel=preload> + a
44
+ size-adjusted fallback here automatically. No third-party request,
45
+ no layout shift; change the family in app.ts. */}
48
46
  </head>
49
47
  <body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
50
48
  <SectionScroller />
@@ -1,7 +1,7 @@
1
1
  import type { Sitemap } from "@pylonsync/react";
2
2
 
3
3
  // app/sitemap.ts → served at /sitemap.xml. Point SITE_URL at your domain in
4
- // production. The waitlist is a single public page, so the sitemap is just "/".
4
+ // production. The site is a single public page, so the sitemap is just "/".
5
5
  const SITE = process.env.SITE_URL ?? "http://localhost:4321";
6
6
 
7
7
  export default async function sitemap(): Promise<Sitemap> {
@@ -5,6 +5,7 @@ import {
5
5
  auth,
6
6
  buildManifest,
7
7
  discoverAppRoutes,
8
+ font,
8
9
  } from "@pylonsync/sdk";
9
10
 
10
11
  // ---------------------------------------------------------------------------
@@ -107,6 +108,20 @@ const manifest = buildManifest({
107
108
  actions: [],
108
109
  policies: [reservationPolicy, reservationSlotPolicy, userPolicy],
109
110
  auth: auth(),
111
+ // Self-hosted Inter (next/font parity): the build fetches the woff2, serves it
112
+ // same-origin (no third-party request, no FOUT), preloads it, and synthesizes a
113
+ // size-adjusted fallback face so there's no layout shift. globals.css reads it
114
+ // via `var(--font-sans, …)`; layout.tsx carries no font <link>.
115
+ fonts: [
116
+ font({
117
+ family: "Inter",
118
+ variable: "--font-sans",
119
+ weights: ["400", "500", "600", "700"],
120
+ subsets: ["latin"],
121
+ display: "swap",
122
+ preload: true,
123
+ }),
124
+ ],
110
125
  routes: await discoverAppRoutes(),
111
126
  });
112
127
 
@@ -1,11 +1,11 @@
1
- // Who owns this waitlist? A waitlist is single-tenant — one business, one
2
- // owner — so ownership is just "the email the owner signs in with", configured
3
- // once via the PYLON_OWNER_EMAIL env var. The owner-only `waitlistStats`
4
- // function reads that env (via `ctx.env`) and compares it here.
1
+ // Who owns this restaurant site? It's single-tenant — one venue, one owner —
2
+ // so ownership is just "the email the owner signs in with", configured once via
3
+ // the PYLON_OWNER_EMAIL env var. The owner-only `reservationsForOwner` function
4
+ // reads that env (via `ctx.env`) and compares it here.
5
5
  //
6
6
  // Fail closed: if PYLON_OWNER_EMAIL is unset, NOBODY is the owner and the
7
7
  // dashboard stays locked. That's deliberate — an unset owner on a public site
8
- // must not mean "everyone can read the signups". Set it in .env (see
8
+ // must not mean "everyone can read the reservations". Set it in .env (see
9
9
  // .env.example) before signing in.
10
10
 
11
11
  export function normalizeOwner(raw: string | null | undefined): string | null {
@@ -12,16 +12,16 @@ import {
12
12
  // auth API directly (`passwordLogin` / `passwordRegister` POST to
13
13
  // `/api/auth/password/*`), then `persistSession` writes the freshly-minted
14
14
  // token to local storage so the sync engine + `callFn` authenticate AS THE
15
- // OWNER on the next load. This step matters here specifically: the landing page
16
- // mints an anonymous guest session (for the live counter), and without
15
+ // OWNER on the next load. This step matters here specifically: the storefront
16
+ // mints an anonymous guest session (for the live stock grid), and without
17
17
  // persisting the real session that stale guest token would shadow the owner's
18
- // — so the owner-only `waitlistStats` call would come back as a guest and get
18
+ // — so the owner-only `ordersForOwner` call would come back as a guest and get
19
19
  // rejected. We then do a full navigation to /dashboard so the SSR runtime
20
20
  // re-resolves auth from the HttpOnly cookie and renders server-side.
21
21
  //
22
- // A waitlist is single-tenant: there's no public signup funnel, just the owner
22
+ // A shop is single-tenant: there's no public signup funnel, just the owner
23
23
  // creating their one account. Whoever signs in only sees data if their email
24
- // matches PYLON_OWNER_EMAIL — enforced by the waitlistStats function.
24
+ // matches PYLON_OWNER_EMAIL — enforced by the ordersForOwner function.
25
25
  export function AuthForm() {
26
26
  const [mode, setMode] = useState<"login" | "signup">("login");
27
27
  const [email, setEmail] = useState("");
@@ -139,7 +139,14 @@
139
139
  body {
140
140
  background-color: var(--color-background);
141
141
  color: var(--color-foreground);
142
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
142
+ font-family: var(
143
+ --font-sans,
144
+ Inter,
145
+ ui-sans-serif,
146
+ system-ui,
147
+ -apple-system,
148
+ sans-serif
149
+ );
143
150
  -webkit-font-smoothing: antialiased;
144
151
  }
145
152
  button {
@@ -15,7 +15,7 @@ interface LayoutProps {
15
15
  }
16
16
 
17
17
  export default function RootLayout({ children, url, auth }: LayoutProps) {
18
- // A guest session (minted by <EnsureGuest> for the live counter) has a
18
+ // A guest session (minted by <EnsureGuest> for the live stock grid) has a
19
19
  // `guest_…` user id — that's an anonymous visitor, NOT the signed-in owner,
20
20
  // so it shouldn't flip the nav to "Dashboard".
21
21
  const signedIn = Boolean(auth?.user_id && !auth.user_id.startsWith("guest_"));
@@ -45,12 +45,10 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
45
45
  <meta charSet="utf-8" />
46
46
  <meta name="viewport" content="width=device-width, initial-scale=1" />
47
47
  {/* No <title> here — each page's exported `metadata` sets it. */}
48
- <link rel="preconnect" href="https://fonts.googleapis.com" />
49
- <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
50
- <link
51
- rel="stylesheet"
52
- href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
53
- />
48
+ {/* Inter is declared in app.ts (fonts: [...]) and self-hosted by the
49
+ build — the runtime injects @font-face + <link rel=preload> + a
50
+ size-adjusted fallback here automatically. No third-party request,
51
+ no layout shift; change the family in app.ts. */}
54
52
  {/* Tailwind is compiled by Pylon from app/globals.css and injected here. */}
55
53
  </head>
56
54
  <body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
@@ -1,7 +1,7 @@
1
1
  import type { Sitemap } from "@pylonsync/react";
2
2
 
3
3
  // app/sitemap.ts → served at /sitemap.xml. Point SITE_URL at your domain in
4
- // production. The waitlist is a single public page, so the sitemap is just "/".
4
+ // production. The storefront is a single public page, so the sitemap is just "/".
5
5
  const SITE = process.env.SITE_URL ?? "http://localhost:4321";
6
6
 
7
7
  export default async function sitemap(): Promise<Sitemap> {