@pylonsync/create-pylon 0.3.266 → 0.3.268

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/bin/create-pylon.js +77 -14
  2. package/package.json +1 -1
  3. package/templates/b2b/AGENTS.md +61 -0
  4. package/templates/b2b/README.md +62 -0
  5. package/templates/b2b/app/auth-form.tsx +142 -0
  6. package/templates/b2b/app/dashboard/dashboard-client.tsx +192 -0
  7. package/templates/b2b/app/dashboard/page.tsx +63 -0
  8. package/templates/b2b/app/error.tsx +43 -0
  9. package/templates/b2b/app/globals.css +139 -0
  10. package/templates/b2b/app/layout.tsx +71 -0
  11. package/templates/b2b/app/login/page.tsx +47 -0
  12. package/templates/b2b/app/not-found.tsx +29 -0
  13. package/templates/b2b/app/page.tsx +114 -0
  14. package/templates/b2b/app/robots.ts +12 -0
  15. package/templates/b2b/app/signup/page.tsx +44 -0
  16. package/templates/b2b/app/sitemap.ts +27 -0
  17. package/templates/b2b/app.ts +179 -0
  18. package/templates/b2b/components/ui/button.tsx +56 -0
  19. package/templates/b2b/components/ui/card.tsx +90 -0
  20. package/templates/b2b/components.json +20 -0
  21. package/templates/b2b/functions/_keep.ts +13 -0
  22. package/templates/b2b/gitignore +10 -0
  23. package/templates/b2b/lib/utils.ts +10 -0
  24. package/templates/b2b/package.json +33 -0
  25. package/templates/b2b/tsconfig.json +18 -0
  26. package/templates/barebones/AGENTS.md +61 -0
  27. package/templates/barebones/README.md +45 -0
  28. package/templates/barebones/app/error.tsx +43 -0
  29. package/templates/barebones/app/globals.css +139 -0
  30. package/templates/barebones/app/items-client.tsx +96 -0
  31. package/templates/barebones/app/layout.tsx +27 -0
  32. package/templates/barebones/app/not-found.tsx +29 -0
  33. package/templates/barebones/app/page.tsx +28 -0
  34. package/templates/barebones/app/robots.ts +12 -0
  35. package/templates/barebones/app/sitemap.ts +27 -0
  36. package/templates/barebones/app.ts +55 -0
  37. package/templates/barebones/components/ui/button.tsx +56 -0
  38. package/templates/barebones/components/ui/card.tsx +90 -0
  39. package/templates/barebones/components.json +20 -0
  40. package/templates/barebones/functions/_keep.ts +13 -0
  41. package/templates/barebones/gitignore +10 -0
  42. package/templates/barebones/lib/utils.ts +10 -0
  43. package/templates/barebones/package.json +33 -0
  44. package/templates/barebones/tsconfig.json +18 -0
  45. package/templates/chat/AGENTS.md +61 -0
  46. package/templates/chat/README.md +51 -0
  47. package/templates/chat/app/chat-client.tsx +113 -0
  48. package/templates/chat/app/error.tsx +43 -0
  49. package/templates/chat/app/globals.css +139 -0
  50. package/templates/chat/app/layout.tsx +25 -0
  51. package/templates/chat/app/not-found.tsx +29 -0
  52. package/templates/chat/app/page.tsx +26 -0
  53. package/templates/chat/app/robots.ts +12 -0
  54. package/templates/chat/app/sitemap.ts +27 -0
  55. package/templates/chat/app.ts +59 -0
  56. package/templates/chat/components/ui/button.tsx +56 -0
  57. package/templates/chat/components/ui/card.tsx +90 -0
  58. package/templates/chat/components.json +20 -0
  59. package/templates/chat/functions/_keep.ts +13 -0
  60. package/templates/chat/gitignore +10 -0
  61. package/templates/chat/lib/utils.ts +10 -0
  62. package/templates/chat/package.json +33 -0
  63. package/templates/chat/tsconfig.json +18 -0
  64. package/templates/consumer/AGENTS.md +61 -0
  65. package/templates/consumer/README.md +52 -0
  66. package/templates/consumer/app/error.tsx +43 -0
  67. package/templates/consumer/app/feed-client.tsx +154 -0
  68. package/templates/consumer/app/globals.css +139 -0
  69. package/templates/consumer/app/layout.tsx +27 -0
  70. package/templates/consumer/app/not-found.tsx +29 -0
  71. package/templates/consumer/app/page.tsx +27 -0
  72. package/templates/consumer/app/robots.ts +12 -0
  73. package/templates/consumer/app/sitemap.ts +27 -0
  74. package/templates/consumer/app.ts +89 -0
  75. package/templates/consumer/components/ui/button.tsx +56 -0
  76. package/templates/consumer/components/ui/card.tsx +90 -0
  77. package/templates/consumer/components.json +20 -0
  78. package/templates/consumer/functions/_keep.ts +13 -0
  79. package/templates/consumer/gitignore +10 -0
  80. package/templates/consumer/lib/utils.ts +10 -0
  81. package/templates/consumer/package.json +33 -0
  82. package/templates/consumer/tsconfig.json +18 -0
  83. package/templates/ssr/app.ts +3 -0
  84. package/templates/todo/AGENTS.md +61 -0
  85. package/templates/todo/README.md +59 -0
  86. package/templates/todo/app/error.tsx +43 -0
  87. package/templates/todo/app/globals.css +139 -0
  88. package/templates/todo/app/layout.tsx +31 -0
  89. package/templates/todo/app/not-found.tsx +29 -0
  90. package/templates/todo/app/page.tsx +37 -0
  91. package/templates/todo/app/robots.ts +12 -0
  92. package/templates/todo/app/sitemap.ts +27 -0
  93. package/templates/todo/app/todo-app.tsx +133 -0
  94. package/templates/todo/app.ts +72 -0
  95. package/templates/todo/components/ui/button.tsx +56 -0
  96. package/templates/todo/components/ui/card.tsx +90 -0
  97. package/templates/todo/components.json +20 -0
  98. package/templates/todo/functions/_keep.ts +13 -0
  99. package/templates/todo/gitignore +10 -0
  100. package/templates/todo/lib/utils.ts +10 -0
  101. package/templates/todo/package.json +33 -0
  102. package/templates/todo/tsconfig.json +18 -0
@@ -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