@pylonsync/create-pylon 0.3.266 → 0.3.267

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.
@@ -86,8 +86,10 @@ const TEMPLATE_REGISTRY = {
86
86
  platforms: ["web", "ios", "mac", "expo"],
87
87
  },
88
88
  todo: {
89
- blurb: "CRUD + drag-reorder + optimistic mutations.",
90
- platforms: ["web", "vite", "ios", "mac", "expo"],
89
+ blurb:
90
+ "Live, optimistic todo list — guest auth, owner-scoped, one server, no Next.js.",
91
+ platforms: [],
92
+ unified: true,
91
93
  },
92
94
  b2b: {
93
95
  blurb: "Multi-tenant SaaS: orgs, members, roles, RBAC policies.",
@@ -116,6 +118,15 @@ const flags = {
116
118
  template: takeValue(args, "--template"),
117
119
  platforms: takeValue(args, "--platforms"),
118
120
  skipInstall: args.includes("--skip-install"),
121
+ // --skill / --no-skill: install the Pylon skill into the new project via
122
+ // `npx skills add pylonsync/pylon` — the skills.sh CLI detects the coding
123
+ // agent (Claude Code / Codex / Cursor) and drops the always-current skill
124
+ // from this repo's skills/pylon/SKILL.md. undefined => prompt (default yes).
125
+ skill: args.includes("--skill")
126
+ ? true
127
+ : args.includes("--no-skill")
128
+ ? false
129
+ : undefined,
119
130
  help: args.includes("--help") || args.includes("-h"),
120
131
  };
121
132
 
@@ -154,8 +165,8 @@ ${tmplLines.join("\n")}
154
165
 
155
166
  Examples:
156
167
  npm create @pylonsync/pylon my-app --template ssr # full-stack SSR, no Next.js
168
+ npm create @pylonsync/pylon my-app --template todo # live, optimistic todo (SSR, one port)
157
169
  npm create @pylonsync/pylon my-app
158
- npm create @pylonsync/pylon my-app --template todo --platforms web,ios
159
170
  npm create @pylonsync/pylon my-app --template b2b --platforms web,mac
160
171
  npm create @pylonsync/pylon my-app --template chat --platforms ios,mac,expo
161
172
  `);
@@ -202,6 +213,16 @@ if (!flags.pm) {
202
213
  .toLowerCase();
203
214
  flags.pm = ["bun", "pnpm", "yarn", "npm"].includes(choice) ? choice : def;
204
215
  }
216
+ if (flags.skill === undefined) {
217
+ const ans = (
218
+ await rl.question(
219
+ "Add the Pylon skill to your coding agent (Claude Code / Codex / Cursor)? [Y/n]: ",
220
+ )
221
+ )
222
+ .trim()
223
+ .toLowerCase();
224
+ flags.skill = ans !== "n" && ans !== "no";
225
+ }
205
226
  rl.close();
206
227
 
207
228
  // Unified templates own no platforms — they ARE the whole app. For
@@ -467,6 +488,34 @@ if (!flags.skipInstall) {
467
488
  }
468
489
  }
469
490
 
491
+ // ---------------------------------------------------------------------------
492
+ // Optional: install the Pylon skill into the project's coding agent.
493
+ //
494
+ // `npx skills add pylonsync/pylon` (skills.sh) detects the installed agent
495
+ // (Claude Code / Codex / Cursor) and drops the canonical skill from this
496
+ // repo's skills/pylon/SKILL.md — always current, no stale bundled copy. We
497
+ // only auto-run it when stdin is a TTY: the skills CLI prompts for scope and
498
+ // agent, which would hang a non-interactive scaffold (CI, piped input). When
499
+ // we can't run it, the footer prints the one-liner so the user can opt in.
500
+ // ---------------------------------------------------------------------------
501
+
502
+ const SKILL_INSTALL_CMD = "npx skills add pylonsync/pylon";
503
+ let skillInstalled = false;
504
+ if (flags.skill && stdin.isTTY) {
505
+ console.log("\nAdding the Pylon skill to your coding agent...");
506
+ const { spawnSync } = await import("node:child_process");
507
+ const result = spawnSync("npx", ["-y", "skills", "add", "pylonsync/pylon"], {
508
+ cwd: root,
509
+ stdio: "inherit",
510
+ });
511
+ skillInstalled = result.status === 0;
512
+ if (!skillInstalled) {
513
+ console.warn(
514
+ `\nCouldn't add the skill automatically. Run it yourself anytime:\n ${SKILL_INSTALL_CMD}\n`,
515
+ );
516
+ }
517
+ }
518
+
470
519
  // ---------------------------------------------------------------------------
471
520
  // Final instructions
472
521
  // ---------------------------------------------------------------------------
@@ -512,6 +561,12 @@ if (platforms.includes("mac"))
512
561
  if (platforms.includes("expo"))
513
562
  layoutLines.push(" apps/expo Expo + React Native");
514
563
 
564
+ const skillLine = skillInstalled
565
+ ? "\nPylon skill added to your coding agent (Claude Code / Codex / Cursor).\n"
566
+ : flags.skill
567
+ ? `\nAdd the Pylon skill to your coding agent:\n ${SKILL_INSTALL_CMD}\n`
568
+ : "";
569
+
515
570
  console.log(`
516
571
  ✓ Created ${projectName}
517
572
 
@@ -522,7 +577,7 @@ ${platformLines.join("\n")}
522
577
 
523
578
  Layout:
524
579
  ${layoutLines.join("\n")}
525
-
580
+ ${skillLine}
526
581
  Docs: https://docs.pylonsync.com
527
582
  `);
528
583
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.3.266",
3
+ "version": "0.3.267",
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"
@@ -0,0 +1,61 @@
1
+ # AGENTS.md — working in a Pylon project
2
+
3
+ Operating rules for a coding agent in this Pylon app. Pylon is a Rails-like framework for realtime apps: you declare entities, policies, and server functions in TypeScript, and a single Rust binary (`pylon`) serves the API, auth, sync, WebSocket, SSE, and native React 19 SSR — one process, one port. The full API reference is at **/llms-full.txt** (served at `/llms-full.txt`; in the repo at `apps/web/public/llms-full.txt`). Read it before guessing an API name.
4
+
5
+ ## Directory conventions
6
+
7
+ **Unified SSR app:**
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.
10
+ - `app/globals.css` — Tailwind v4 entrypoint (auto-compiled and injected).
11
+ - `functions/` — server functions, one per file, `default`-exported.
12
+ - `.pylon/` — local dev state (sqlite, jobs, sessions, uploads). Created by `pylon dev`. Do not commit.
13
+
14
+ **Monorepo app:** backend is `apps/api/` (entry `apps/api/schema.ts`, handlers in `apps/api/functions/`); frontend in `apps/web/`. `pylon.manifest.json` / `pylon.client.ts` are generated — do not hand-edit.
15
+
16
+ ## The core authoring loop
17
+
18
+ 1. **Define an entity** — `entity("Thing", { name: field.string(), done: field.boolean().default(false) })`. Modifiers: `.optional()`, `.unique()`, `.readonly()` (settable on insert, rejected on client update — use for `authorId`/`orgId`), `.serverOnly()` (never in HTTP responses), `.encrypted()` (AEAD at rest, needs `PYLON_ENCRYPTION_KEY`), `.crdt("text")` (collaborative).
19
+ 2. **Write a policy** — `policy({ entity: "Thing", allowRead, allowInsert, allowUpdate, allowDelete })` with CEL-like expressions over `auth.*` / `data.*` (e.g. `"auth.userId == data.authorId"`). **Omitted actions DENY by default.** Wide-open dev policies (`allow*: "true"`) are flagged by `pylon lint` — tighten before shipping.
20
+ 3. **Author a function** in `functions/<name>.ts` — `query` (read-only), `mutation` (transactional read+write), or `action` (external I/O, no direct `ctx.db`). Import `{ query, mutation, action, v }` from `@pylonsync/functions`. `auth` defaults to `"user"` (secure-by-default); set `"public"` explicitly for unauthenticated access. Use `ctx.db.*`, `ctx.auth.userId`, `ctx.error(code, msg)`.
21
+ 4. **Read it on the client** — `db.useQuery("Thing")` (live, re-renders on any write) or `db.useQueryOne("Thing", id)`. Call functions with `db.fn(name, args)` / `callFn`. On SSR pages, read via `use(serverData.list("Thing"))` inside `<Suspense>`.
22
+
23
+ ## Key gotchas
24
+
25
+ - **Policies deny by default; server functions BYPASS them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full DB access — enforce trust with `ctx.auth` checks inside the handler, not policies.
26
+ - **Type page props from the SDK, don't hand-roll them.** `import type { PageProps, Metadata } from "@pylonsync/react"`. Every page/layout gets `{ url, params, searchParams, auth, response, serverData }`; `PageProps<{ slug: string }>` types a `[slug]` route's params. Request headers/cookies are intentionally NOT on `PageProps` — they're server-only and stripped from hydration, so reading them in the render would mismatch.
27
+ - **Anonymous output caching is opt-in + earned.** `export const revalidate = 60` (seconds) on a page makes it CDN-cacheable (`public, s-maxage=60`) — but ONLY if the render is auth-INDEPENDENT: it must NOT read `props.auth` (reading it at all opts out, even for anonymous), set no cookie, and the app must not run strict per-caller policies (`PYLON_STRICT_FN_POLICIES`). `export const dynamic = "force-static"` caches until the next deploy; `"force-dynamic"` never caches. Fail-closed: without the opt-in (or if any condition fails) the page is `no-cache`. A page that reads `auth` or sets a cookie is never shared. The SAME earned render is also kept in an **origin disk cache** (`.pylon/.cache/ssr`): a cookie-less GET with no query string is served straight off disk for the TTL — skipping the render entirely — then re-rendered live when stale. The disk cache is namespaced per deploy (wiped on each new build) and OFF in `pylon dev` (so an edit is never masked by a stale entry); invalidation is by the `revalidate` TTL or the next deploy.
28
+ - **No-JS forms use `route.ts` + `<Form>`.** Drop `app/.../route.ts` exporting `export const POST: RouteHandler = async ({ form, db, response, auth }) => { await db.insert("X", {...}); response.redirect("/x?ok=1"); }` (303 POST-redirect-GET by default). Render `<Form action="/x">` (from @pylonsync/react) with plain `<input name=...>` — works with JS off (native POST→handler→redirect) and is enhanced to no-reload when JS is on. The handler's `db` is read+write (mutation trust model — gate on `auth`); CSRF is automatic (Origin gate + SameSite=Lax). Multipart/file uploads aren't supported yet — use urlencoded forms + `/api/files`.
29
+ - **`loading.tsx` streams a skeleton while the page's data resolves.** Drop `app/.../loading.tsx` (default export, page props) and the nearest one becomes a route-level Suspense fallback: Pylon flushes the shell + skeleton immediately, then reveals the real page when its top-level `use(serverData…)` resolves (no blank page). It only shows when the PAGE suspends — a page that wraps its own `<Suspense>` around a child (like `/dashboard` in this template) handles that itself. The skeleton is SERVER-ONLY: don't read `serverData` in it. A page with no `loading.tsx` is buffered (unchanged).
30
+ - **`export const streaming = true` streams a page's OWN inner `<Suspense>` boundaries.** Without it (and without a `loading.tsx`), a page is BUFFERED — the whole document, including suspended children, resolves before the first byte. Opt in and the shell + each inner `<Suspense>` fallback flush immediately, then each boundary's real content streams in as its data resolves (multi-boundary progressive streaming). It's opt-in because it changes the response timing contract: a streaming render commits its HTTP head BEFORE suspended subtrees finish, so (a) it's never CDN/disk cacheable — don't combine with `export const revalidate`; (b) `response.setStatus/setCookie/redirect/notFound` only take effect from the SYNCHRONOUS shell render — a call from inside a suspended subtree is dropped (the runtime logs a loud warning naming what was lost); (c) a `throw` from a deep `<Suspense>` child resolves via its nearest `error.tsx` at HTTP 200, not a 5xx. Hydration is clean for any number of boundaries (the data blob ships before hydration runs). Type the config with `import type { RouteSegmentConfig } from "@pylonsync/react"`.
31
+ - **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`) — `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
32
+ - **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive — during SSR they return defaults (empty params / "/"); for server-side URL values read the `url` / `searchParams` page props.
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
+ - **`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
+ - **`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`.
37
+ - **It's `db.useQueryOne`, not `useOne`.** Validators and field types have aliases: `v.bool`/`v.boolean`, `v.float`/`v.number`.
38
+ - **There is no `ctx.files` or `defineWorkflow`/`defineJob`.** Files go through `<FileUpload>` + `/api/files/*`; deferred execution is `ctx.scheduler.runAfter/runAt/cancel`.
39
+
40
+ ## Use the CLI — don't guess
41
+
42
+ | Need | Command |
43
+ |---|---|
44
+ | Run the app (SSR + API, hot reload, one port `:4321`) | `pylon dev` (or `npm run dev`) |
45
+ | Regenerate manifest + typed client | `pylon codegen` (Swift client: `pylon codegen client --target swift`) |
46
+ | Validate / diff / push schema | `pylon schema check` \| `diff` \| `push` |
47
+ | Migrations | `pylon migrate create <name>` \| `plan` \| `apply` |
48
+ | Lint policies (PYL001–PYL004) | `pylon lint --strict` |
49
+ | Tests | `pylon test` |
50
+ | Adversarial security probe | `pylon test:security` |
51
+ | Inspect cloud request logs (agent-safe) | `pylon logs --json --limit 50` |
52
+ | Inspect data / entities | `pylon data entities` \| `pylon data list <Entity>` |
53
+ | Call a function | `pylon fn <name> key=value` |
54
+ | Health snapshot | `pylon status` |
55
+ | Build for prod | `pylon build` |
56
+ | Deploy (Pylon Cloud by default) | `pylon deploy` |
57
+ | Look up an error code | `pylon explain <CODE>` |
58
+
59
+ `--json` works on every command for machine-readable output. Prefer one-shot/agent-safe flags (`pylon logs --limit N`, not a blocking `--follow`).
60
+
61
+ For full signatures, env vars, the complete CLI, and SSR/client/server-primitive details: **/llms-full.txt**.
@@ -0,0 +1,59 @@
1
+ # __APP_NAME__
2
+
3
+ A full-stack [Pylon](https://pylonsync.com) todo app — a server-rendered page
4
+ and a live, optimistic, per-user todo list over a synced database, all served
5
+ from one binary on one port. No Next.js, no separate API server.
6
+
7
+ ## Develop
8
+
9
+ ```bash
10
+ __RUN_DEV__
11
+ ```
12
+
13
+ Open http://localhost:4321. Add a todo — it appears instantly (optimistic) and
14
+ syncs; open a second tab to watch writes arrive live. Edit any file under
15
+ `app/` and save — the page reloads instantly.
16
+
17
+ ## Layout
18
+
19
+ ```
20
+ app.ts data model + manifest (entities, policies, auth, routes)
21
+ app/page.tsx "/" — the server-rendered page (heading + intro)
22
+ app/todo-app.tsx client island: guest session + live, optimistic todo list
23
+ app/layout.tsx root layout wrapping every page
24
+ app/globals.css Tailwind entrypoint (compiled by Pylon)
25
+ functions/ server functions (query/action) — typed RPC, if you need them
26
+ ```
27
+
28
+ ## How it works
29
+
30
+ No login wall: `app/todo-app.tsx` wraps the list in `<EnsureGuest>`, which
31
+ POSTs `/api/auth/guest` on first load so every visitor implicitly becomes their
32
+ own user. Todos are private per browser — the `todo_access` policy in `app.ts`
33
+ gates every read and write to the owner, and `userId: field.owner()` stamps the
34
+ session's id server-side so the optimistic `db.insert("Todo", { title })` can't
35
+ be spoofed. `db.useQuery("Todo")` is a live subscription; `db.insert` /
36
+ `db.update` / `db.delete` are optimistic.
37
+
38
+ To require real accounts instead, enable email/password (built in, against a
39
+ `User` entity) and swap `<EnsureGuest>` for `<SignedIn>` / `<SignedOut>` from
40
+ `@pylonsync/client`.
41
+
42
+ ## Add a route
43
+
44
+ Drop a file at `app/about/page.tsx` and visit `/about`. Pages receive
45
+ `{ url, params, searchParams, auth, response, serverData }` from the SSR
46
+ runtime — all typed via `PageProps` from `@pylonsync/react`.
47
+
48
+ ## Add data
49
+
50
+ Edit `app.ts`. Every `entity()` becomes a synced table with a REST + realtime
51
+ API and a typed client — no migrations, no resolvers.
52
+
53
+ ## Deploy
54
+
55
+ ```bash
56
+ pylon deploy
57
+ ```
58
+
59
+ Docs: https://docs.pylonsync.com
@@ -0,0 +1,43 @@
1
+ import React from "react";
2
+ import { type ErrorBoundaryProps } from "@pylonsync/react";
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ // `app/error.tsx` → the error boundary for this segment. It catches a throw
6
+ // in any page/layout below it and renders at HTTP 500. It's HYDRATED, so
7
+ // this is a real interactive client component: `reset()` re-attempts the
8
+ // route, and useState/onClick work. The thrown error reaches the client as
9
+ // `{ message, digest }` only — the stack stays in the dev overlay
10
+ // (PYLON_DEV_MODE) and the server logs, never in the page.
11
+ export default function Error({ error, reset }: ErrorBoundaryProps) {
12
+ const [tries, setTries] = React.useState(0);
13
+ return (
14
+ <div className="space-y-6">
15
+ <section>
16
+ <h1 className="text-2xl font-semibold tracking-tight">
17
+ Something went wrong
18
+ </h1>
19
+ <p className="mt-2 text-muted-foreground">{error.message}</p>
20
+ {error.digest ? (
21
+ <p className="mt-1 text-xs text-muted-foreground/70">
22
+ Reference: <code>{error.digest}</code>
23
+ </p>
24
+ ) : null}
25
+ </section>
26
+ <div className="flex items-center gap-3">
27
+ <Button
28
+ onClick={() => {
29
+ setTries((n) => n + 1);
30
+ reset();
31
+ }}
32
+ >
33
+ Try again
34
+ </Button>
35
+ {tries > 0 ? (
36
+ <span className="text-sm text-muted-foreground">
37
+ Retried {tries} {tries === 1 ? "time" : "times"}
38
+ </span>
39
+ ) : null}
40
+ </div>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,139 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ /* Tailwind v4 scans these globs for class names. `components/` is here so
5
+ shadcn/ui component classes are seen — add more @source lines if you put
6
+ markup elsewhere. */
7
+ @source "../app/**/*.{tsx,ts,jsx,js}";
8
+ @source "../components/**/*.{tsx,ts,jsx,js}";
9
+
10
+ @custom-variant dark (&:where(.dark, .dark *));
11
+
12
+ /* shadcn/ui design tokens (new-york / zinc). Edit these to re-theme the
13
+ whole app; `npx shadcn@latest add <component>` drops new components that
14
+ consume the same variables. Toggle dark mode by putting `class="dark"`
15
+ on <html>. */
16
+ :root {
17
+ --radius: 0.625rem;
18
+ --background: oklch(1 0 0);
19
+ --foreground: oklch(0.141 0.005 285.823);
20
+ --card: oklch(1 0 0);
21
+ --card-foreground: oklch(0.141 0.005 285.823);
22
+ --popover: oklch(1 0 0);
23
+ --popover-foreground: oklch(0.141 0.005 285.823);
24
+ --primary: oklch(0.21 0.006 285.885);
25
+ --primary-foreground: oklch(0.985 0 0);
26
+ --secondary: oklch(0.967 0.001 286.375);
27
+ --secondary-foreground: oklch(0.21 0.006 285.885);
28
+ --muted: oklch(0.967 0.001 286.375);
29
+ --muted-foreground: oklch(0.552 0.016 285.938);
30
+ --accent: oklch(0.967 0.001 286.375);
31
+ --accent-foreground: oklch(0.21 0.006 285.885);
32
+ --destructive: oklch(0.577 0.245 27.325);
33
+ --border: oklch(0.92 0.004 286.32);
34
+ --input: oklch(0.92 0.004 286.32);
35
+ --ring: oklch(0.705 0.015 286.067);
36
+ --chart-1: oklch(0.646 0.222 41.116);
37
+ --chart-2: oklch(0.6 0.118 184.704);
38
+ --chart-3: oklch(0.398 0.07 227.392);
39
+ --chart-4: oklch(0.828 0.189 84.429);
40
+ --chart-5: oklch(0.769 0.188 70.08);
41
+ --sidebar: oklch(0.985 0 0);
42
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
43
+ --sidebar-primary: oklch(0.21 0.006 285.885);
44
+ --sidebar-primary-foreground: oklch(0.985 0 0);
45
+ --sidebar-accent: oklch(0.967 0.001 286.375);
46
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
47
+ --sidebar-border: oklch(0.92 0.004 286.32);
48
+ --sidebar-ring: oklch(0.705 0.015 286.067);
49
+ }
50
+
51
+ .dark {
52
+ --background: oklch(0.141 0.005 285.823);
53
+ --foreground: oklch(0.985 0 0);
54
+ --card: oklch(0.21 0.006 285.885);
55
+ --card-foreground: oklch(0.985 0 0);
56
+ --popover: oklch(0.21 0.006 285.885);
57
+ --popover-foreground: oklch(0.985 0 0);
58
+ --primary: oklch(0.92 0.004 286.32);
59
+ --primary-foreground: oklch(0.21 0.006 285.885);
60
+ --secondary: oklch(0.274 0.006 286.033);
61
+ --secondary-foreground: oklch(0.985 0 0);
62
+ --muted: oklch(0.274 0.006 286.033);
63
+ --muted-foreground: oklch(0.705 0.015 286.067);
64
+ --accent: oklch(0.274 0.006 286.033);
65
+ --accent-foreground: oklch(0.985 0 0);
66
+ --destructive: oklch(0.704 0.191 22.216);
67
+ --border: oklch(1 0 0 / 10%);
68
+ --input: oklch(1 0 0 / 15%);
69
+ --ring: oklch(0.552 0.016 285.938);
70
+ --chart-1: oklch(0.488 0.243 264.376);
71
+ --chart-2: oklch(0.696 0.17 162.48);
72
+ --chart-3: oklch(0.769 0.188 70.08);
73
+ --chart-4: oklch(0.627 0.265 303.9);
74
+ --chart-5: oklch(0.645 0.246 16.439);
75
+ --sidebar: oklch(0.21 0.006 285.885);
76
+ --sidebar-foreground: oklch(0.985 0 0);
77
+ --sidebar-primary: oklch(0.488 0.243 264.376);
78
+ --sidebar-primary-foreground: oklch(0.985 0 0);
79
+ --sidebar-accent: oklch(0.274 0.006 286.033);
80
+ --sidebar-accent-foreground: oklch(0.985 0 0);
81
+ --sidebar-border: oklch(1 0 0 / 10%);
82
+ --sidebar-ring: oklch(0.552 0.016 285.938);
83
+ }
84
+
85
+ @theme inline {
86
+ --radius-sm: calc(var(--radius) - 4px);
87
+ --radius-md: calc(var(--radius) - 2px);
88
+ --radius-lg: var(--radius);
89
+ --radius-xl: calc(var(--radius) + 4px);
90
+ --color-background: var(--background);
91
+ --color-foreground: var(--foreground);
92
+ --color-card: var(--card);
93
+ --color-card-foreground: var(--card-foreground);
94
+ --color-popover: var(--popover);
95
+ --color-popover-foreground: var(--popover-foreground);
96
+ --color-primary: var(--primary);
97
+ --color-primary-foreground: var(--primary-foreground);
98
+ --color-secondary: var(--secondary);
99
+ --color-secondary-foreground: var(--secondary-foreground);
100
+ --color-muted: var(--muted);
101
+ --color-muted-foreground: var(--muted-foreground);
102
+ --color-accent: var(--accent);
103
+ --color-accent-foreground: var(--accent-foreground);
104
+ --color-destructive: var(--destructive);
105
+ --color-border: var(--border);
106
+ --color-input: var(--input);
107
+ --color-ring: var(--ring);
108
+ --color-chart-1: var(--chart-1);
109
+ --color-chart-2: var(--chart-2);
110
+ --color-chart-3: var(--chart-3);
111
+ --color-chart-4: var(--chart-4);
112
+ --color-chart-5: var(--chart-5);
113
+ --color-sidebar: var(--sidebar);
114
+ --color-sidebar-foreground: var(--sidebar-foreground);
115
+ --color-sidebar-primary: var(--sidebar-primary);
116
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
117
+ --color-sidebar-accent: var(--sidebar-accent);
118
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
119
+ --color-sidebar-border: var(--sidebar-border);
120
+ --color-sidebar-ring: var(--sidebar-ring);
121
+ }
122
+
123
+ @layer base {
124
+ *,
125
+ ::after,
126
+ ::before,
127
+ ::backdrop,
128
+ ::file-selector-button {
129
+ border-color: var(--color-border, currentColor);
130
+ outline-color: var(--color-ring);
131
+ }
132
+ body {
133
+ background-color: var(--color-background);
134
+ color: var(--color-foreground);
135
+ }
136
+ button {
137
+ cursor: pointer;
138
+ }
139
+ }
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+
3
+ // A layout wraps every page. This one is intentionally minimal — a header
4
+ // and a centered column. The page below it is server-rendered first (so the
5
+ // shell and copy are in the HTML), then hydrates into the live todo UI.
6
+ interface LayoutProps {
7
+ children: React.ReactNode;
8
+ }
9
+
10
+ export default function RootLayout({ children }: LayoutProps) {
11
+ // Add `className="dark"` to this <html> to flip every shadcn token to its
12
+ // dark value. The classes below use semantic tokens (bg-background,
13
+ // text-foreground, …) so the whole UI re-themes from app/globals.css.
14
+ return (
15
+ <html lang="en">
16
+ <head>
17
+ <meta charSet="utf-8" />
18
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
19
+ <title>__APP_NAME__</title>
20
+ {/* Tailwind is compiled by Pylon from app/globals.css and the
21
+ stylesheet link is injected here automatically — nothing to
22
+ wire up. */}
23
+ </head>
24
+ <body className="min-h-screen bg-background text-foreground antialiased">
25
+ <main className="mx-auto flex min-h-screen max-w-xl flex-col px-4 py-12">
26
+ {children}
27
+ </main>
28
+ </body>
29
+ </html>
30
+ );
31
+ }
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import { Link, useRouter, type NotFoundProps } from "@pylonsync/react";
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL (and when
6
+ // a page calls `response.notFound()`). It's HYDRATED, so it's interactive:
7
+ // the buttons below use the client router. Not-found boundaries receive the
8
+ // standard page props (and, matching Next, no `reset`).
9
+ export default function NotFound(_props: NotFoundProps) {
10
+ const router = useRouter();
11
+ return (
12
+ <div className="space-y-6">
13
+ <section>
14
+ <h1 className="text-2xl font-semibold tracking-tight">404</h1>
15
+ <p className="mt-2 text-muted-foreground">
16
+ We couldn&apos;t find that page.
17
+ </p>
18
+ </section>
19
+ <div className="flex items-center gap-3">
20
+ <Button onClick={() => router.back()} variant="outline">
21
+ ← Go back
22
+ </Button>
23
+ <Button asChild>
24
+ <Link href="/">Home</Link>
25
+ </Button>
26
+ </div>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import { type Metadata } from "@pylonsync/react";
3
+ import { TodoApp } from "./todo-app";
4
+
5
+ // SEO metadata. Export `metadata` (static) or `generateMetadata(props)`
6
+ // (dynamic) from any page or layout — Pylon renders the <title>/<meta>
7
+ // into <head> server-side. The `Metadata` type is exported from
8
+ // @pylonsync/react.
9
+ export const metadata: Metadata = {
10
+ title: "__APP_NAME__ — a live Pylon todo",
11
+ description:
12
+ "A server-rendered todo list with live, optimistic, per-user sync — one binary, one port. Open two tabs and watch them stay in sync.",
13
+ };
14
+
15
+ // `app/page.tsx` → `/`. The heading and intro are server-rendered (view
16
+ // source and they're in the HTML — good for SEO and first paint). The list
17
+ // itself is a client island: `<TodoApp>` mints a guest session and runs the
18
+ // live query + optimistic writes in the browser.
19
+ export default function IndexPage() {
20
+ return (
21
+ <div className="flex flex-1 flex-col">
22
+ <header className="mb-8">
23
+ <h1 className="text-3xl font-semibold tracking-tight">Todos</h1>
24
+ <p className="mt-1 text-sm text-muted-foreground">
25
+ Live and optimistic over one Pylon backend. Open a second tab —
26
+ changes sync instantly.
27
+ </p>
28
+ </header>
29
+ <TodoApp />
30
+ <p className="mt-8 text-xs text-muted-foreground">
31
+ Edit <code className="rounded bg-muted px-1">app/todo-app.tsx</code> for
32
+ the UI, or <code className="rounded bg-muted px-1">app.ts</code> for the
33
+ data model and access policies.
34
+ </p>
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,12 @@
1
+ import type { Robots } from "@pylonsync/react";
2
+
3
+ // app/robots.ts → served at /robots.txt. The default export may also be async.
4
+ const SITE = process.env.SITE_URL ?? "http://localhost:4321";
5
+
6
+ export default function robots(): Robots {
7
+ return {
8
+ // Keep the authenticated app and the API out of the index.
9
+ rules: { userAgent: "*", allow: "/", disallow: ["/dashboard", "/api/"] },
10
+ sitemap: `${SITE}/sitemap.xml`,
11
+ };
12
+ }
@@ -0,0 +1,27 @@
1
+ import type { Sitemap } from "@pylonsync/react";
2
+
3
+ // app/sitemap.ts → served at /sitemap.xml. The default export can be async, so
4
+ // it can enumerate dynamic pages from your database. Point SITE_URL at your
5
+ // domain in production.
6
+ const SITE = process.env.SITE_URL ?? "http://localhost:4321";
7
+
8
+ export default async function sitemap(): Promise<Sitemap> {
9
+ // Only public pages belong here — /dashboard is private (and noindex), so
10
+ // it's intentionally left out.
11
+ const staticRoutes: Sitemap = [
12
+ { url: `${SITE}/`, changeFrequency: "weekly", priority: 1 },
13
+ { url: `${SITE}/login`, changeFrequency: "yearly", priority: 0.3 },
14
+ { url: `${SITE}/signup`, changeFrequency: "yearly", priority: 0.5 },
15
+ ];
16
+
17
+ // The export is async, so you can enumerate dynamic pages from a DB read:
18
+ //
19
+ // const posts = await fetchPublishedPosts();
20
+ // const postRoutes: Sitemap = posts.map((p) => ({
21
+ // url: `${SITE}/blog/${p.slug}`,
22
+ // lastModified: p.updatedAt,
23
+ // }));
24
+ // return [...staticRoutes, ...postRoutes];
25
+
26
+ return staticRoutes;
27
+ }
@@ -0,0 +1,133 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { db } from "@pylonsync/react";
5
+ import { EnsureGuest } from "@pylonsync/client";
6
+ import { Button } from "@/components/ui/button";
7
+
8
+ export interface Todo {
9
+ id: string;
10
+ title: string;
11
+ done: boolean;
12
+ }
13
+
14
+ // `<EnsureGuest>` mints a guest session (POST /api/auth/guest) on first load
15
+ // if the browser doesn't already have one, so the list works with no login —
16
+ // every visitor implicitly becomes their own user and their todos stay
17
+ // private. The list itself is gated to the owner by the policy in app.ts.
18
+ export function TodoApp() {
19
+ return (
20
+ <EnsureGuest fallback={<ListSkeleton />}>
21
+ <TodoList />
22
+ </EnsureGuest>
23
+ );
24
+ }
25
+
26
+ // `db.useQuery` is a LIVE subscription — it re-renders the instant a Todo is
27
+ // added, toggled, or removed, in this tab or another. `db.insert` /
28
+ // `db.update` / `db.delete` are OPTIMISTIC: they apply to the local store
29
+ // immediately (zero-latency UI) and sync in the background, rolling back
30
+ // automatically if a policy rejects the write.
31
+ function TodoList() {
32
+ const [title, setTitle] = useState("");
33
+ const { data: todos, loading } = db.useQuery<Todo>("Todo");
34
+
35
+ async function addTodo(e: React.FormEvent) {
36
+ e.preventDefault();
37
+ const text = title.trim();
38
+ if (!text) return;
39
+ setTitle("");
40
+ // We don't send userId — `field.owner()` stamps it from the session
41
+ // server-side and rejects any forged value, so this optimistic insert is
42
+ // safe.
43
+ await db.insert("Todo", { title: text, done: false });
44
+ }
45
+
46
+ const remaining = todos.filter((t) => !t.done).length;
47
+
48
+ return (
49
+ <div className="space-y-5">
50
+ <form onSubmit={addTodo} className="flex items-center gap-2">
51
+ <input
52
+ value={title}
53
+ onChange={(e) => setTitle(e.target.value)}
54
+ placeholder="Add a todo…"
55
+ aria-label="Todo"
56
+ autoComplete="off"
57
+ className="flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
58
+ />
59
+ <Button type="submit">Add</Button>
60
+ </form>
61
+
62
+ {loading && todos.length === 0 ? (
63
+ <ListSkeleton />
64
+ ) : todos.length === 0 ? (
65
+ <p className="text-sm text-muted-foreground">
66
+ Nothing yet — add a todo above. It appears instantly (optimistic) and
67
+ syncs; open this page in a second tab to watch it arrive live.
68
+ </p>
69
+ ) : (
70
+ <>
71
+ <ul className="space-y-2">
72
+ {todos.map((todo) => (
73
+ <li
74
+ key={todo.id}
75
+ className="flex items-center gap-3 rounded-md border px-3 py-2.5 text-sm"
76
+ >
77
+ <button
78
+ type="button"
79
+ aria-label={todo.done ? "Mark not done" : "Mark done"}
80
+ onClick={() => db.update("Todo", todo.id, { done: !todo.done })}
81
+ className={
82
+ "flex size-5 items-center justify-center rounded-full border text-xs transition-colors " +
83
+ (todo.done
84
+ ? "border-emerald-600 bg-emerald-600 text-white"
85
+ : "border-muted-foreground/30 text-transparent hover:border-muted-foreground")
86
+ }
87
+ >
88
+
89
+ </button>
90
+ <span
91
+ className={
92
+ todo.done
93
+ ? "flex-1 text-muted-foreground line-through"
94
+ : "flex-1"
95
+ }
96
+ >
97
+ {todo.title}
98
+ </span>
99
+ <button
100
+ type="button"
101
+ aria-label="Delete todo"
102
+ onClick={() => db.delete("Todo", todo.id)}
103
+ className="text-muted-foreground/40 transition-colors hover:text-red-600"
104
+ >
105
+
106
+ </button>
107
+ </li>
108
+ ))}
109
+ </ul>
110
+ <p className="text-xs text-muted-foreground">
111
+ {remaining} {remaining === 1 ? "todo" : "todos"} left
112
+ </p>
113
+ </>
114
+ )}
115
+ </div>
116
+ );
117
+ }
118
+
119
+ function ListSkeleton() {
120
+ return (
121
+ <ul className="space-y-2" aria-hidden>
122
+ {[0, 1, 2].map((i) => (
123
+ <li
124
+ key={i}
125
+ className="flex items-center gap-3 rounded-md border px-3 py-2.5"
126
+ >
127
+ <span className="size-5 rounded-full bg-muted" />
128
+ <span className="h-4 flex-1 rounded bg-muted" />
129
+ </li>
130
+ ))}
131
+ </ul>
132
+ );
133
+ }
@@ -0,0 +1,72 @@
1
+ import {
2
+ entity,
3
+ field,
4
+ policy,
5
+ auth,
6
+ buildManifest,
7
+ discoverAppRoutes,
8
+ } from "@pylonsync/sdk";
9
+
10
+ // A todo that belongs to one person. `userId: field.owner()` is the key
11
+ // move: the framework stamps the signed-in (here: guest) user's id
12
+ // server-side on insert and rejects any forged value — so the UI can do a
13
+ // plain, optimistic `db.insert("Todo", { title })` (the row shows instantly,
14
+ // no round-trip) while ownership stays unspoofable. No createTodo function to
15
+ // write — every verb is a direct, policy-checked entity call.
16
+ const Todo = entity(
17
+ "Todo",
18
+ {
19
+ userId: field.string().owner(),
20
+ title: field.string(),
21
+ done: field.boolean().default(false),
22
+ // Float sort key so a drag-reorder can drop a row between two others by
23
+ // taking their midpoint — no renumbering the whole list. New rows seed
24
+ // with createdAt so the default order is chronological.
25
+ sortKey: field.float().optional(),
26
+ createdAt: field.datetime().defaultNow(),
27
+ },
28
+ { indexes: [{ name: "by_user", fields: ["userId"], unique: false }] },
29
+ );
30
+
31
+ // Todos are private — every read and write is gated to the owner. An entity
32
+ // with NO policy is denied to clients by default, so this is exactly what
33
+ // makes the live query + optimistic writes work, and only for your own rows.
34
+ // `auth.userId` is the session user (a guest id here); `data.userId` is the
35
+ // row's owner.
36
+ const todoPolicy = policy({
37
+ name: "todo_access",
38
+ entity: "Todo",
39
+ allowRead: "auth.userId == data.userId",
40
+ allowInsert: "auth.userId != null",
41
+ allowUpdate: "auth.userId == data.userId",
42
+ allowDelete: "auth.userId == data.userId",
43
+ });
44
+
45
+ // The manifest is your whole app in one object: data, policies, and the
46
+ // file-based routes under `app/`. `pylon dev` reads this, serves the SSR
47
+ // frontend and the API from one port, and regenerates a typed client on
48
+ // every change.
49
+ //
50
+ // This starter uses guest sessions: the page wraps its UI in `<EnsureGuest>`,
51
+ // which POSTs `/api/auth/guest` on first load so every visitor implicitly
52
+ // becomes their own user — no login wall, todos still private per browser.
53
+ // To require real accounts instead, enable email/password (it's built in
54
+ // against a `User` entity) and swap `<EnsureGuest>` for `<SignedIn>` /
55
+ // `<SignedOut>` from `@pylonsync/client`.
56
+ const manifest = buildManifest({
57
+ name: "__APP_NAME__",
58
+ version: "0.1.0",
59
+ entities: [Todo],
60
+ queries: [],
61
+ actions: [],
62
+ policies: [todoPolicy],
63
+ auth: auth(),
64
+ // File-based routing: `discoverAppRoutes()` walks `app/**/page.tsx` and
65
+ // emits one route per page. Drop `app/about/page.tsx` to add `/about`.
66
+ routes: await discoverAppRoutes(),
67
+ });
68
+
69
+ // Emit canonical manifest JSON to stdout for `pylon codegen`.
70
+ console.log(JSON.stringify(manifest, null, 2));
71
+
72
+ export default manifest;
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-9 px-4 py-2",
24
+ sm: "h-8 rounded-md px-3 text-xs",
25
+ lg: "h-10 rounded-md px-8",
26
+ icon: "h-9 w-9",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ },
34
+ );
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean;
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button";
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ );
52
+ },
53
+ );
54
+ Button.displayName = "Button";
55
+
56
+ export { Button, buttonVariants };
@@ -0,0 +1,90 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "rounded-xl border bg-card text-card-foreground shadow-sm",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function CardHeader({
19
+ className,
20
+ ...props
21
+ }: React.HTMLAttributes<HTMLDivElement>) {
22
+ return (
23
+ <div
24
+ data-slot="card-header"
25
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function CardTitle({
32
+ className,
33
+ ...props
34
+ }: React.HTMLAttributes<HTMLDivElement>) {
35
+ return (
36
+ <div
37
+ data-slot="card-title"
38
+ className={cn("font-semibold leading-none tracking-tight", className)}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ function CardDescription({
45
+ className,
46
+ ...props
47
+ }: React.HTMLAttributes<HTMLDivElement>) {
48
+ return (
49
+ <div
50
+ data-slot="card-description"
51
+ className={cn("text-sm text-muted-foreground", className)}
52
+ {...props}
53
+ />
54
+ );
55
+ }
56
+
57
+ function CardContent({
58
+ className,
59
+ ...props
60
+ }: React.HTMLAttributes<HTMLDivElement>) {
61
+ return (
62
+ <div
63
+ data-slot="card-content"
64
+ className={cn("p-6 pt-0", className)}
65
+ {...props}
66
+ />
67
+ );
68
+ }
69
+
70
+ function CardFooter({
71
+ className,
72
+ ...props
73
+ }: React.HTMLAttributes<HTMLDivElement>) {
74
+ return (
75
+ <div
76
+ data-slot="card-footer"
77
+ className={cn("flex items-center p-6 pt-0", className)}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ export {
84
+ Card,
85
+ CardHeader,
86
+ CardFooter,
87
+ CardTitle,
88
+ CardDescription,
89
+ CardContent,
90
+ };
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true
11
+ },
12
+ "aliases": {
13
+ "components": "@/components",
14
+ "utils": "@/lib/utils",
15
+ "ui": "@/components/ui",
16
+ "lib": "@/lib",
17
+ "hooks": "@/hooks"
18
+ },
19
+ "iconLibrary": "lucide"
20
+ }
@@ -0,0 +1,13 @@
1
+ // Server functions go here. Each file in this directory that exports a
2
+ // query() or action() becomes a typed RPC endpoint, callable from your
3
+ // pages and client with full type inference. Delete this placeholder when
4
+ // you add your first one.
5
+ //
6
+ // Example (functions/notes.ts):
7
+ //
8
+ // import { query } from "@pylonsync/functions";
9
+ //
10
+ // export const listNotes = query(async (ctx) => {
11
+ // return ctx.db.list("Note");
12
+ // });
13
+ export {};
@@ -0,0 +1,10 @@
1
+ node_modules/
2
+ .pylon/
3
+ pylon.manifest.json
4
+ pylon.client.ts
5
+ web/dist/
6
+ *.db
7
+ *.db-*
8
+ .env
9
+ .env.local
10
+ .DS_Store
@@ -0,0 +1,10 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ // `cn` — the shadcn class merger. clsx resolves conditional/array class
5
+ // inputs; tailwind-merge then dedupes conflicting Tailwind utilities so
6
+ // the last one wins (e.g. `cn("px-2", "px-4")` → "px-4"). Every shadcn
7
+ // component routes its className through this.
8
+ export function cn(...inputs: ClassValue[]) {
9
+ return twMerge(clsx(inputs));
10
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "__APP_NAME_KEBAB__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "pylon dev",
8
+ "deploy": "pylon deploy",
9
+ "check": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@pylonsync/react": "^__PYLON_VERSION__",
13
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
14
+ "@pylonsync/functions": "^__PYLON_VERSION__",
15
+ "@pylonsync/client": "^__PYLON_VERSION__",
16
+ "react": "^19.0.0",
17
+ "react-dom": "^19.0.0",
18
+ "tailwindcss": "^4.3.0",
19
+ "@tailwindcss/cli": "^4.3.0",
20
+ "tw-animate-css": "^1.2.0",
21
+ "class-variance-authority": "^0.7.1",
22
+ "clsx": "^2.1.1",
23
+ "tailwind-merge": "^2.5.0",
24
+ "lucide-react": "^0.460.0",
25
+ "@radix-ui/react-slot": "^1.1.0"
26
+ },
27
+ "devDependencies": {
28
+ "@pylonsync/cli": "^__PYLON_VERSION__",
29
+ "@types/react": "^19.0.0",
30
+ "@types/react-dom": "^19.0.0",
31
+ "typescript": "^5.6.0"
32
+ }
33
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react",
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "lib": ["ES2022", "DOM"],
11
+ "types": ["react", "react-dom"],
12
+ "baseUrl": ".",
13
+ "paths": {
14
+ "@/*": ["./*"]
15
+ }
16
+ },
17
+ "include": ["app.ts", "app/**/*", "components/**/*", "lib/**/*"]
18
+ }