@pylonsync/create-pylon 0.3.372 → 0.3.374
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.
- package/package.json +1 -1
- package/templates/_root/AGENTS.md +4 -2
- package/templates/agency/AGENTS.md +4 -2
- package/templates/agency/app/(marketing)/layout.tsx +136 -0
- package/templates/{creator/app → agency/app/(marketing)}/not-found.tsx +6 -2
- package/templates/agency/app/error.tsx +6 -3
- package/templates/agency/app/layout.tsx +8 -134
- package/templates/agency/app/login/page.tsx +3 -2
- package/templates/ai-chat/AGENTS.md +23 -2
- package/templates/ai-chat/app/(chat)/layout.tsx +46 -0
- package/templates/ai-chat/app/{not-found.tsx → (chat)/not-found.tsx} +5 -2
- package/templates/ai-chat/app/error.tsx +6 -3
- package/templates/ai-chat/app/layout.tsx +6 -39
- package/templates/ai-chat/app/login/page.tsx +2 -1
- package/templates/ai-studio/AGENTS.md +23 -2
- package/templates/ai-studio/app/(studio)/layout.tsx +46 -0
- package/templates/{agency/app → ai-studio/app/(studio)}/not-found.tsx +5 -2
- package/templates/ai-studio/app/error.tsx +6 -3
- package/templates/ai-studio/app/layout.tsx +6 -39
- package/templates/ai-studio/app/login/page.tsx +2 -1
- package/templates/barebones/AGENTS.md +4 -2
- package/templates/chat/AGENTS.md +23 -2
- package/templates/consumer/AGENTS.md +4 -2
- package/templates/creator/AGENTS.md +4 -2
- package/templates/creator/app/(marketing)/layout.tsx +122 -0
- package/templates/{ai-studio/app → creator/app/(marketing)}/not-found.tsx +6 -2
- package/templates/creator/app/error.tsx +6 -3
- package/templates/creator/app/layout.tsx +8 -120
- package/templates/creator/app/login/page.tsx +3 -2
- package/templates/crm/AGENTS.md +4 -2
- package/templates/directory/AGENTS.md +4 -2
- package/templates/directory/app/(marketing)/layout.tsx +133 -0
- package/templates/directory/app/(marketing)/not-found.tsx +23 -0
- package/templates/directory/app/error.tsx +6 -3
- package/templates/directory/app/layout.tsx +8 -131
- package/templates/directory/app/login/page.tsx +3 -2
- package/templates/helpdesk/AGENTS.md +4 -2
- package/templates/inventory/AGENTS.md +4 -2
- package/templates/invoices/AGENTS.md +4 -2
- package/templates/local-service/AGENTS.md +4 -2
- package/templates/local-service/app/(marketing)/layout.tsx +119 -0
- package/templates/local-service/app/(marketing)/not-found.tsx +23 -0
- package/templates/local-service/app/{page.tsx → (marketing)/page.tsx} +2 -2
- package/templates/local-service/app/error.tsx +3 -1
- package/templates/local-service/app/layout.tsx +8 -116
- package/templates/local-service/app/login/page.tsx +3 -2
- package/templates/local-service/components/marketing.tsx +1 -1
- package/templates/marketplace/AGENTS.md +4 -2
- package/templates/projects/AGENTS.md +4 -2
- package/templates/restaurant/AGENTS.md +4 -2
- package/templates/restaurant/app/(marketing)/layout.tsx +119 -0
- package/templates/restaurant/app/(marketing)/not-found.tsx +23 -0
- package/templates/restaurant/app/{page.tsx → (marketing)/page.tsx} +2 -2
- package/templates/restaurant/app/error.tsx +3 -1
- package/templates/restaurant/app/layout.tsx +8 -116
- package/templates/restaurant/app/login/page.tsx +3 -2
- package/templates/restaurant/components/marketing.tsx +1 -1
- package/templates/saas/AGENTS.md +4 -2
- package/templates/saas/app/(marketing)/layout.tsx +411 -0
- package/templates/saas/app/{not-found.tsx → (marketing)/not-found.tsx} +9 -5
- package/templates/saas/app/dashboard/page.tsx +3 -2
- package/templates/saas/app/error.tsx +7 -5
- package/templates/saas/app/layout.tsx +8 -408
- package/templates/saas/app/login/page.tsx +2 -1
- package/templates/saas/components/dashboard-shell.tsx +2 -2
- package/templates/shop/AGENTS.md +4 -2
- package/templates/shop/app/(marketing)/layout.tsx +122 -0
- package/templates/shop/app/(marketing)/not-found.tsx +23 -0
- package/templates/shop/app/{page.tsx → (marketing)/page.tsx} +1 -1
- package/templates/shop/app/error.tsx +3 -1
- package/templates/shop/app/layout.tsx +8 -120
- package/templates/shop/app/login/page.tsx +3 -2
- package/templates/todo/AGENTS.md +4 -2
- package/templates/waitlist/AGENTS.md +4 -2
- package/templates/waitlist/app/(marketing)/layout.tsx +122 -0
- package/templates/waitlist/app/(marketing)/not-found.tsx +23 -0
- package/templates/waitlist/app/{page.tsx → (marketing)/page.tsx} +1 -1
- package/templates/waitlist/app/error.tsx +3 -1
- package/templates/waitlist/app/layout.tsx +8 -120
- package/templates/waitlist/app/login/page.tsx +3 -2
- package/templates/directory/app/not-found.tsx +0 -19
- package/templates/local-service/app/not-found.tsx +0 -19
- package/templates/restaurant/app/not-found.tsx +0 -19
- package/templates/shop/app/not-found.tsx +0 -19
- package/templates/waitlist/app/not-found.tsx +0 -19
- /package/templates/agency/app/{contact-form.tsx → (marketing)/contact-form.tsx} +0 -0
- /package/templates/agency/app/{page.tsx → (marketing)/page.tsx} +0 -0
- /package/templates/agency/app/{seeder.tsx → (marketing)/seeder.tsx} +0 -0
- /package/templates/agency/app/{work → (marketing)/work}/[slug]/page.tsx +0 -0
- /package/templates/agency/app/{work → (marketing)/work}/page.tsx +0 -0
- /package/templates/ai-chat/app/{chat-client.tsx → (chat)/chat-client.tsx} +0 -0
- /package/templates/ai-chat/app/{page.tsx → (chat)/page.tsx} +0 -0
- /package/templates/ai-studio/app/{page.tsx → (studio)/page.tsx} +0 -0
- /package/templates/ai-studio/app/{studio-client.tsx → (studio)/studio-client.tsx} +0 -0
- /package/templates/creator/app/{newsletter-signup.tsx → (marketing)/newsletter-signup.tsx} +0 -0
- /package/templates/creator/app/{page.tsx → (marketing)/page.tsx} +0 -0
- /package/templates/directory/app/{directory-browse.tsx → (marketing)/directory-browse.tsx} +0 -0
- /package/templates/directory/app/{page.tsx → (marketing)/page.tsx} +0 -0
- /package/templates/directory/app/{submit → (marketing)/submit}/page.tsx +0 -0
- /package/templates/directory/app/{submit-form.tsx → (marketing)/submit-form.tsx} +0 -0
- /package/templates/local-service/app/{booking-widget.tsx → (marketing)/booking-widget.tsx} +0 -0
- /package/templates/restaurant/app/{reservation-widget.tsx → (marketing)/reservation-widget.tsx} +0 -0
- /package/templates/saas/app/{company → (marketing)/company}/[slug]/page.tsx +0 -0
- /package/templates/saas/app/{compare → (marketing)/compare}/[slug]/page.tsx +0 -0
- /package/templates/saas/app/{page.tsx → (marketing)/page.tsx} +0 -0
- /package/templates/saas/app/{products → (marketing)/products}/[slug]/page.tsx +0 -0
- /package/templates/saas/app/{resources → (marketing)/resources}/[slug]/page.tsx +0 -0
- /package/templates/saas/app/{solutions → (marketing)/solutions}/[slug]/page.tsx +0 -0
- /package/templates/shop/app/{shop-client.tsx → (marketing)/shop-client.tsx} +0 -0
- /package/templates/shop/app/{success → (marketing)/success}/page.tsx +0 -0
- /package/templates/waitlist/app/{waitlist-hero.tsx → (marketing)/waitlist-hero.tsx} +0 -0
|
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
8
|
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
-
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
10
|
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
11
|
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
12
|
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
@@ -33,7 +33,28 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
33
33
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
34
34
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
35
35
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
36
|
-
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
36
|
+
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
37
|
+
- **`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.
|
|
38
|
+
- **`ctx.stream.write(text)` reaches only the one client holding the HTTP response; `ctx.rooms.broadcast(room, topic, data)` reaches every subscriber of the room.** Broadcast is what makes agent output survive a closed tab or reach a second device. It 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.
|
|
39
|
+
|
|
40
|
+
Streaming agent turn — tokens to the caller, and to every device watching:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
export default action({
|
|
44
|
+
args: { chatId: v.string(), messages: v.array(v.any()) },
|
|
45
|
+
timeout: 120, // streaming does not extend the deadline
|
|
46
|
+
async handler(ctx, { chatId, messages }) {
|
|
47
|
+
const res = await ctx.llm.stream({ messages }, (e) => {
|
|
48
|
+
if (e.type === "text_delta") {
|
|
49
|
+
ctx.stream.write(e.text);
|
|
50
|
+
void ctx.rooms.broadcast(`chat-${chatId}`, "agent.delta", { text: e.text });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return res.stop_reason; // "tool_use" → run the tools and call again
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
37
58
|
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
38
59
|
- **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`.
|
|
39
60
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, type PageAuth } from "@pylonsync/react";
|
|
3
|
+
import { siteConfig } from "@/lib/site.config";
|
|
4
|
+
|
|
5
|
+
// `(studio)` is a ROUTE GROUP: the parens segment is stripped from every URL
|
|
6
|
+
// (so `(studio)/page.tsx` still serves `/`), and this layout wraps only the
|
|
7
|
+
// pages inside the group. /login sits outside it and renders bare in the
|
|
8
|
+
// root shell.
|
|
9
|
+
//
|
|
10
|
+
// App shell: a slim top bar over the studio. `auth.user_id` is resolved
|
|
11
|
+
// server-side from the session cookie before any HTML is sent, so the bar shows
|
|
12
|
+
// the account / "Sign in" with no flash. The header is h-14; the studio page
|
|
13
|
+
// fills the rest of the viewport (min-h-[calc(100vh-3.5rem)]).
|
|
14
|
+
interface LayoutProps {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
auth: PageAuth;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function StudioLayout({ children, auth }: LayoutProps) {
|
|
20
|
+
const signedIn = Boolean(auth?.user_id);
|
|
21
|
+
const { brand } = siteConfig;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<header className="flex h-14 items-center justify-between border-b border-zinc-200 bg-white px-4">
|
|
26
|
+
<Link href="/" className="flex items-center gap-2">
|
|
27
|
+
<span className="flex size-6 items-center justify-center rounded-[7px] bg-brand text-[13px] font-bold text-white">
|
|
28
|
+
{brand.letter}
|
|
29
|
+
</span>
|
|
30
|
+
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">{brand.name}</span>
|
|
31
|
+
</Link>
|
|
32
|
+
{signedIn ? (
|
|
33
|
+
<span className="text-[13px] text-zinc-400">Signed in</span>
|
|
34
|
+
) : (
|
|
35
|
+
<Link
|
|
36
|
+
href="/login"
|
|
37
|
+
className="rounded-full border border-zinc-300 px-3.5 py-1.5 text-[13px] font-medium text-zinc-700 transition-colors hover:bg-zinc-50"
|
|
38
|
+
>
|
|
39
|
+
Sign in
|
|
40
|
+
</Link>
|
|
41
|
+
)}
|
|
42
|
+
</header>
|
|
43
|
+
{children}
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Link, type NotFoundProps } from "@pylonsync/react";
|
|
3
3
|
|
|
4
|
-
// `
|
|
5
|
-
// page calls `response.notFound()`).
|
|
4
|
+
// `(studio)/not-found.tsx` → rendered at HTTP 404 for any unmatched URL (and
|
|
5
|
+
// when a page calls `response.notFound()`). It lives inside the `(studio)`
|
|
6
|
+
// group on purpose: a group segment adds no URL prefix, so this is still the
|
|
7
|
+
// root 404 boundary, but it wraps in the app-shell layout — a missing URL gets
|
|
8
|
+
// the top bar instead of a bare page. Hydrated, so the link is a client nav.
|
|
6
9
|
export default function NotFound(_props: NotFoundProps) {
|
|
7
10
|
return (
|
|
8
11
|
<div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { type ErrorBoundaryProps } from "@pylonsync/react";
|
|
3
3
|
|
|
4
|
-
// `app/error.tsx` → the error boundary
|
|
5
|
-
// `
|
|
6
|
-
//
|
|
4
|
+
// `app/error.tsx` → the GLOBAL error boundary. Sitting at the app root
|
|
5
|
+
// (outside the `(studio)` group), it catches a throw from any section — the
|
|
6
|
+
// studio or /login — and renders at HTTP 500 in the bare root shell. Hydrated +
|
|
7
|
+
// interactive: `reset()` re-attempts the route. The thrown error reaches the
|
|
8
|
+
// client as `{ message, digest }` only — the stack stays in the dev overlay /
|
|
9
|
+
// server logs.
|
|
7
10
|
export default function Error({ error, reset }: ErrorBoundaryProps) {
|
|
8
11
|
return (
|
|
9
12
|
<div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
|
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Link, type PageAuth } from "@pylonsync/react";
|
|
3
2
|
import { siteConfig } from "@/lib/site.config";
|
|
4
3
|
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// fills the rest of the viewport (min-h-[calc(100vh-3.5rem)]).
|
|
4
|
+
// The root layout is only the document shell: <html>, <head>, <body>, and the
|
|
5
|
+
// theme variables. The app shell (top bar) lives in the `(studio)` route-group
|
|
6
|
+
// layout, so /login — outside the group — renders bare with its own chrome.
|
|
9
7
|
interface LayoutProps {
|
|
10
8
|
children: React.ReactNode;
|
|
11
|
-
url: string;
|
|
12
|
-
auth: PageAuth;
|
|
13
9
|
}
|
|
14
10
|
|
|
15
|
-
export default function RootLayout({ children
|
|
16
|
-
const
|
|
17
|
-
const { brand, colors } = siteConfig;
|
|
18
|
-
|
|
19
|
-
// The auth screen brings its own chrome → render it bare.
|
|
20
|
-
const path = (url ?? "").split("?")[0];
|
|
21
|
-
const isBare = path === "/login" || path.startsWith("/login/");
|
|
11
|
+
export default function RootLayout({ children }: LayoutProps) {
|
|
12
|
+
const { colors } = siteConfig;
|
|
22
13
|
|
|
23
14
|
return (
|
|
24
15
|
<html
|
|
@@ -40,31 +31,7 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
|
40
31
|
no layout shift; change the family in app.ts. */}
|
|
41
32
|
</head>
|
|
42
33
|
<body className="bg-background text-foreground antialiased">
|
|
43
|
-
{
|
|
44
|
-
children
|
|
45
|
-
) : (
|
|
46
|
-
<>
|
|
47
|
-
<header className="flex h-14 items-center justify-between border-b border-zinc-200 bg-white px-4">
|
|
48
|
-
<Link href="/" className="flex items-center gap-2">
|
|
49
|
-
<span className="flex size-6 items-center justify-center rounded-[7px] bg-brand text-[13px] font-bold text-white">
|
|
50
|
-
{brand.letter}
|
|
51
|
-
</span>
|
|
52
|
-
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">{brand.name}</span>
|
|
53
|
-
</Link>
|
|
54
|
-
{signedIn ? (
|
|
55
|
-
<span className="text-[13px] text-zinc-400">Signed in</span>
|
|
56
|
-
) : (
|
|
57
|
-
<Link
|
|
58
|
-
href="/login"
|
|
59
|
-
className="rounded-full border border-zinc-300 px-3.5 py-1.5 text-[13px] font-medium text-zinc-700 transition-colors hover:bg-zinc-50"
|
|
60
|
-
>
|
|
61
|
-
Sign in
|
|
62
|
-
</Link>
|
|
63
|
-
)}
|
|
64
|
-
</header>
|
|
65
|
-
{children}
|
|
66
|
-
</>
|
|
67
|
-
)}
|
|
34
|
+
{children}
|
|
68
35
|
</body>
|
|
69
36
|
</html>
|
|
70
37
|
);
|
|
@@ -9,7 +9,8 @@ export const metadata: Metadata = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// `app/login/page.tsx` → `/login`. Sign-in is required to use the studio
|
|
12
|
-
// (generations are tied to your account).
|
|
12
|
+
// (generations are tied to your account). It sits outside the `(studio)` route
|
|
13
|
+
// group, so it renders bare — no app top bar. Already signed in? Skip
|
|
13
14
|
// back to the studio — `response.redirect` in the synchronous shell render is a
|
|
14
15
|
// real 307 before any HTML is sent.
|
|
15
16
|
export default function LoginPage({ auth, response }: PageProps) {
|
|
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
8
|
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
-
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
10
|
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
11
|
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
12
|
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
@@ -33,7 +33,9 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
33
33
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
34
34
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
35
35
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
36
|
-
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
36
|
+
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
37
|
+
- **`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.
|
|
38
|
+
- **`ctx.stream.write(text)` reaches only the one client holding the HTTP response; `ctx.rooms.broadcast(room, topic, data)` reaches every subscriber of the room.** Broadcast is what makes agent output survive a closed tab or reach a second device. It 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.
|
|
37
39
|
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
38
40
|
- **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`.
|
|
39
41
|
|
package/templates/chat/AGENTS.md
CHANGED
|
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
8
|
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
-
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
10
|
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
11
|
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
12
|
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
@@ -33,7 +33,28 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
33
33
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
34
34
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
35
35
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
36
|
-
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
36
|
+
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
37
|
+
- **`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.
|
|
38
|
+
- **`ctx.stream.write(text)` reaches only the one client holding the HTTP response; `ctx.rooms.broadcast(room, topic, data)` reaches every subscriber of the room.** Broadcast is what makes agent output survive a closed tab or reach a second device. It 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.
|
|
39
|
+
|
|
40
|
+
Streaming agent turn — tokens to the caller, and to every device watching:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
export default action({
|
|
44
|
+
args: { chatId: v.string(), messages: v.array(v.any()) },
|
|
45
|
+
timeout: 120, // streaming does not extend the deadline
|
|
46
|
+
async handler(ctx, { chatId, messages }) {
|
|
47
|
+
const res = await ctx.llm.stream({ messages }, (e) => {
|
|
48
|
+
if (e.type === "text_delta") {
|
|
49
|
+
ctx.stream.write(e.text);
|
|
50
|
+
void ctx.rooms.broadcast(`chat-${chatId}`, "agent.delta", { text: e.text });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return res.stop_reason; // "tool_use" → run the tools and call again
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
37
58
|
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
38
59
|
- **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`.
|
|
39
60
|
|
|
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
8
|
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
-
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
10
|
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
11
|
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
12
|
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
@@ -33,7 +33,9 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
33
33
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
34
34
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
35
35
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
36
|
-
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
36
|
+
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
37
|
+
- **`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.
|
|
38
|
+
- **`ctx.stream.write(text)` reaches only the one client holding the HTTP response; `ctx.rooms.broadcast(room, topic, data)` reaches every subscriber of the room.** Broadcast is what makes agent output survive a closed tab or reach a second device. It 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.
|
|
37
39
|
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
38
40
|
- **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`.
|
|
39
41
|
|
|
@@ -6,7 +6,7 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
6
6
|
|
|
7
7
|
**Unified SSR app:**
|
|
8
8
|
- `app.ts`: data model + manifest (`entity()` + `field.*`, queries/actions/policies, `routes: await discoverAppRoutes()`). Ends with `console.log(JSON.stringify(manifest))`.
|
|
9
|
-
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. `app/layout.tsx` is the shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
9
|
+
- `app/`: file-based SSR routes. `app/page.tsx` → `/`, `app/about/page.tsx` → `/about`, `app/blog/[slug]/page.tsx` → `/blog/:slug`. A `(group)` directory is stripped from the URL — `app/(marketing)/about/page.tsx` still serves `/about` — so a group's `layout.tsx` gives one section its own chrome (nav, footer) without changing any path; routes outside the group render without it. `app/layout.tsx` is the document shell; `app/error.tsx` / `app/not-found.tsx` are boundaries.
|
|
10
10
|
- `app/globals.css`: Tailwind v4 entrypoint (auto-compiled and injected).
|
|
11
11
|
- `functions/`: server functions, one per file, `default`-exported.
|
|
12
12
|
- `.pylon/`: local dev state (SQLite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
|
|
@@ -33,7 +33,9 @@ Pylon serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR from o
|
|
|
33
33
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
34
34
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
35
35
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
36
|
-
- **`ctx.llm` and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
36
|
+
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
37
|
+
- **`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.
|
|
38
|
+
- **`ctx.stream.write(text)` reaches only the one client holding the HTTP response; `ctx.rooms.broadcast(room, topic, data)` reaches every subscriber of the room.** Broadcast is what makes agent output survive a closed tab or reach a second device. It 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.
|
|
37
39
|
- **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
|
|
38
40
|
- **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`.
|
|
39
41
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, type PageAuth } from "@pylonsync/react";
|
|
3
|
+
import { siteConfig } from "@/lib/site.config";
|
|
4
|
+
|
|
5
|
+
// `(marketing)` is a ROUTE GROUP: the parens segment is stripped from every
|
|
6
|
+
// 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,
|
|
9
|
+
// /dashboard) render bare in the root shell. `auth.user_id` is resolved
|
|
10
|
+
// server-side from the session cookie before any HTML is sent, so the nav
|
|
11
|
+
// shows "Dashboard" once the owner is signed in and "Sign in" otherwise —
|
|
12
|
+
// no flash, no client fetch.
|
|
13
|
+
interface LayoutProps {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
auth: PageAuth;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function MarketingLayout({ children, auth }: LayoutProps) {
|
|
19
|
+
// A guest session (minted by <EnsureGuest> for the live counter) has a
|
|
20
|
+
// `guest_…` user id — that's an anonymous visitor, NOT the signed-in owner,
|
|
21
|
+
// so it shouldn't flip the nav to "Dashboard".
|
|
22
|
+
const signedIn = Boolean(auth?.user_id && !auth.user_id.startsWith("guest_"));
|
|
23
|
+
const { brand } = siteConfig;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<header className="sticky top-0 z-30 bg-white/80 backdrop-blur">
|
|
28
|
+
<div className="mx-auto flex h-14 max-w-3xl 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>
|
|
36
|
+
</Link>
|
|
37
|
+
<nav className="flex items-center gap-2">
|
|
38
|
+
{signedIn ? (
|
|
39
|
+
<Link
|
|
40
|
+
href="/dashboard"
|
|
41
|
+
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"
|
|
42
|
+
>
|
|
43
|
+
Dashboard
|
|
44
|
+
</Link>
|
|
45
|
+
) : (
|
|
46
|
+
<Link
|
|
47
|
+
href="/login"
|
|
48
|
+
className="rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900"
|
|
49
|
+
>
|
|
50
|
+
Sign in
|
|
51
|
+
</Link>
|
|
52
|
+
)}
|
|
53
|
+
</nav>
|
|
54
|
+
</div>
|
|
55
|
+
</header>
|
|
56
|
+
|
|
57
|
+
<main className="flex-1">{children}</main>
|
|
58
|
+
|
|
59
|
+
<SiteFooter />
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function SiteFooter() {
|
|
65
|
+
const { brand } = siteConfig;
|
|
66
|
+
return (
|
|
67
|
+
<footer className="border-t border-zinc-200/70 bg-white">
|
|
68
|
+
<div className="mx-auto max-w-3xl px-6 py-12">
|
|
69
|
+
<div className="flex flex-col items-start justify-between gap-6 sm:flex-row">
|
|
70
|
+
<div className="max-w-sm">
|
|
71
|
+
<Link href="/" className="inline-flex items-center gap-2">
|
|
72
|
+
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
73
|
+
{brand.letter}
|
|
74
|
+
</span>
|
|
75
|
+
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
76
|
+
{brand.name}
|
|
77
|
+
</span>
|
|
78
|
+
</Link>
|
|
79
|
+
<p className="mt-3 text-[13px] leading-relaxed text-zinc-500">
|
|
80
|
+
{brand.footerBlurb}
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="flex items-center gap-4">
|
|
84
|
+
{brand.socials.map((s) => (
|
|
85
|
+
<a
|
|
86
|
+
key={s.label}
|
|
87
|
+
href={s.href}
|
|
88
|
+
aria-label={s.label}
|
|
89
|
+
className="text-zinc-400 transition-colors hover:text-zinc-900"
|
|
90
|
+
>
|
|
91
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
92
|
+
<path d={s.path} />
|
|
93
|
+
</svg>
|
|
94
|
+
</a>
|
|
95
|
+
))}
|
|
96
|
+
<a
|
|
97
|
+
href={`mailto:${brand.email}`}
|
|
98
|
+
aria-label="Email"
|
|
99
|
+
className="text-zinc-400 transition-colors hover:text-zinc-900"
|
|
100
|
+
>
|
|
101
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
102
|
+
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
103
|
+
<path d="m3 7 9 6 9-6" />
|
|
104
|
+
</svg>
|
|
105
|
+
</a>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<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">
|
|
109
|
+
<span>
|
|
110
|
+
© {new Date().getFullYear()} {brand.copyrightName}
|
|
111
|
+
</span>
|
|
112
|
+
<span>
|
|
113
|
+
Built with{" "}
|
|
114
|
+
<a href="https://pylonsync.com" className="font-medium text-zinc-600 hover:text-zinc-900">
|
|
115
|
+
Pylon
|
|
116
|
+
</a>
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</footer>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Link, type NotFoundProps } from "@pylonsync/react";
|
|
3
3
|
|
|
4
|
-
// `
|
|
5
|
-
// page calls `response.notFound()`).
|
|
4
|
+
// `(marketing)/not-found.tsx` → rendered at HTTP 404 for any unmatched URL
|
|
5
|
+
// (and when a page calls `response.notFound()`). It lives inside the
|
|
6
|
+
// `(marketing)` group on purpose: a group segment adds no URL prefix, so this
|
|
7
|
+
// is still the root 404 boundary, but it wraps in the marketing layout — a
|
|
8
|
+
// missing URL gets the site nav + footer instead of a bare page. Hydrated, so
|
|
9
|
+
// the link is a client nav.
|
|
6
10
|
export default function NotFound(_props: NotFoundProps) {
|
|
7
11
|
return (
|
|
8
12
|
<div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { type ErrorBoundaryProps } from "@pylonsync/react";
|
|
3
3
|
|
|
4
|
-
// `app/error.tsx` → the error boundary
|
|
5
|
-
// `
|
|
6
|
-
//
|
|
4
|
+
// `app/error.tsx` → the GLOBAL error boundary. Sitting at the app root
|
|
5
|
+
// (outside the `(marketing)` group), it catches a throw from any section —
|
|
6
|
+
// marketing, auth, or dashboard — and renders at HTTP 500 in the bare root
|
|
7
|
+
// shell. Hydrated + interactive: `reset()` re-attempts the route. The thrown
|
|
8
|
+
// error reaches the client as `{ message, digest }` only — the stack stays
|
|
9
|
+
// in the dev overlay / server logs.
|
|
7
10
|
export default function Error({ error, reset }: ErrorBoundaryProps) {
|
|
8
11
|
return (
|
|
9
12
|
<div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
|
|
@@ -1,32 +1,18 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Link, type PageAuth } from "@pylonsync/react";
|
|
3
2
|
import { siteConfig } from "@/lib/site.config";
|
|
4
3
|
import { SectionScroller } from "@/components/section-scroller";
|
|
5
4
|
|
|
6
|
-
// A layout wraps every page. This marketing layout renders a slim nav up top
|
|
7
|
-
// and a footer below, both driven by lib/site.config.ts. `auth.user_id` is
|
|
8
|
-
// resolved server-side from the session cookie before any HTML is sent, so the
|
|
9
|
-
// nav shows "Dashboard" once the owner is signed in and "Sign in" otherwise —
|
|
10
|
-
// no flash, no client fetch.
|
|
11
5
|
interface LayoutProps {
|
|
12
6
|
children: React.ReactNode;
|
|
13
|
-
url: string;
|
|
14
|
-
auth: PageAuth;
|
|
15
7
|
}
|
|
16
8
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// The auth screens and the dashboard bring their own chrome, so they render
|
|
25
|
-
// bare (no marketing nav/footer). Match on the path PREFIX, not a substring.
|
|
26
|
-
const path = (url ?? "").split("?")[0];
|
|
27
|
-
const BARE_PREFIXES = ["/login", "/dashboard"];
|
|
28
|
-
const isBare = BARE_PREFIXES.some((p) => path === p || path.startsWith(p + "/"));
|
|
29
|
-
|
|
9
|
+
// The root layout is only the document shell: <html>, <head>, <body>, and the
|
|
10
|
+
// site-wide theme variables. Section chrome lives in route-group layouts —
|
|
11
|
+
// `(marketing)/layout.tsx` adds the nav + footer for every marketing page.
|
|
12
|
+
// Routes outside the group (/login, /dashboard) render bare here: the auth
|
|
13
|
+
// screen and the dashboard bring their own chrome.
|
|
14
|
+
export default function RootLayout({ children }: LayoutProps) {
|
|
15
|
+
const { colors } = siteConfig;
|
|
30
16
|
return (
|
|
31
17
|
<html
|
|
32
18
|
lang="en"
|
|
@@ -53,106 +39,8 @@ export default function RootLayout({ children, url, auth }: LayoutProps) {
|
|
|
53
39
|
</head>
|
|
54
40
|
<body className="flex min-h-screen flex-col bg-background text-foreground antialiased">
|
|
55
41
|
<SectionScroller />
|
|
56
|
-
{
|
|
57
|
-
children
|
|
58
|
-
) : (
|
|
59
|
-
<>
|
|
60
|
-
<header className="sticky top-0 z-30 bg-white/80 backdrop-blur">
|
|
61
|
-
<div className="mx-auto flex h-14 max-w-3xl items-center justify-between px-6">
|
|
62
|
-
<Link href="/" className="flex items-center gap-2">
|
|
63
|
-
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
64
|
-
{brand.letter}
|
|
65
|
-
</span>
|
|
66
|
-
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
67
|
-
{brand.name}
|
|
68
|
-
</span>
|
|
69
|
-
</Link>
|
|
70
|
-
<nav className="flex items-center gap-2">
|
|
71
|
-
{signedIn ? (
|
|
72
|
-
<Link
|
|
73
|
-
href="/dashboard"
|
|
74
|
-
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"
|
|
75
|
-
>
|
|
76
|
-
Dashboard
|
|
77
|
-
</Link>
|
|
78
|
-
) : (
|
|
79
|
-
<Link
|
|
80
|
-
href="/login"
|
|
81
|
-
className="rounded-full px-3 py-1.5 text-[13px] font-medium text-zinc-600 transition-colors hover:text-zinc-900"
|
|
82
|
-
>
|
|
83
|
-
Sign in
|
|
84
|
-
</Link>
|
|
85
|
-
)}
|
|
86
|
-
</nav>
|
|
87
|
-
</div>
|
|
88
|
-
</header>
|
|
89
|
-
|
|
90
|
-
<main className="flex-1">{children}</main>
|
|
91
|
-
|
|
92
|
-
<SiteFooter />
|
|
93
|
-
</>
|
|
94
|
-
)}
|
|
42
|
+
{children}
|
|
95
43
|
</body>
|
|
96
44
|
</html>
|
|
97
45
|
);
|
|
98
46
|
}
|
|
99
|
-
|
|
100
|
-
function SiteFooter() {
|
|
101
|
-
const { brand } = siteConfig;
|
|
102
|
-
return (
|
|
103
|
-
<footer className="border-t border-zinc-200/70 bg-white">
|
|
104
|
-
<div className="mx-auto max-w-3xl px-6 py-12">
|
|
105
|
-
<div className="flex flex-col items-start justify-between gap-6 sm:flex-row">
|
|
106
|
-
<div className="max-w-sm">
|
|
107
|
-
<Link href="/" className="inline-flex items-center gap-2">
|
|
108
|
-
<span className="flex size-6 items-center justify-center rounded-[7px] bg-zinc-900 text-[13px] font-bold text-white">
|
|
109
|
-
{brand.letter}
|
|
110
|
-
</span>
|
|
111
|
-
<span className="text-[15px] font-semibold tracking-tight text-zinc-900">
|
|
112
|
-
{brand.name}
|
|
113
|
-
</span>
|
|
114
|
-
</Link>
|
|
115
|
-
<p className="mt-3 text-[13px] leading-relaxed text-zinc-500">
|
|
116
|
-
{brand.footerBlurb}
|
|
117
|
-
</p>
|
|
118
|
-
</div>
|
|
119
|
-
<div className="flex items-center gap-4">
|
|
120
|
-
{brand.socials.map((s) => (
|
|
121
|
-
<a
|
|
122
|
-
key={s.label}
|
|
123
|
-
href={s.href}
|
|
124
|
-
aria-label={s.label}
|
|
125
|
-
className="text-zinc-400 transition-colors hover:text-zinc-900"
|
|
126
|
-
>
|
|
127
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
128
|
-
<path d={s.path} />
|
|
129
|
-
</svg>
|
|
130
|
-
</a>
|
|
131
|
-
))}
|
|
132
|
-
<a
|
|
133
|
-
href={`mailto:${brand.email}`}
|
|
134
|
-
aria-label="Email"
|
|
135
|
-
className="text-zinc-400 transition-colors hover:text-zinc-900"
|
|
136
|
-
>
|
|
137
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
138
|
-
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
139
|
-
<path d="m3 7 9 6 9-6" />
|
|
140
|
-
</svg>
|
|
141
|
-
</a>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
<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">
|
|
145
|
-
<span>
|
|
146
|
-
© {new Date().getFullYear()} {brand.copyrightName}
|
|
147
|
-
</span>
|
|
148
|
-
<span>
|
|
149
|
-
Built with{" "}
|
|
150
|
-
<a href="https://pylonsync.com" className="font-medium text-zinc-600 hover:text-zinc-900">
|
|
151
|
-
Pylon
|
|
152
|
-
</a>
|
|
153
|
-
</span>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
</footer>
|
|
157
|
-
);
|
|
158
|
-
}
|