@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.
- package/bin/create-pylon.js +77 -14
- package/package.json +1 -1
- package/templates/b2b/AGENTS.md +61 -0
- package/templates/b2b/README.md +62 -0
- package/templates/b2b/app/auth-form.tsx +142 -0
- package/templates/b2b/app/dashboard/dashboard-client.tsx +192 -0
- package/templates/b2b/app/dashboard/page.tsx +63 -0
- package/templates/b2b/app/error.tsx +43 -0
- package/templates/b2b/app/globals.css +139 -0
- package/templates/b2b/app/layout.tsx +71 -0
- package/templates/b2b/app/login/page.tsx +47 -0
- package/templates/b2b/app/not-found.tsx +29 -0
- package/templates/b2b/app/page.tsx +114 -0
- package/templates/b2b/app/robots.ts +12 -0
- package/templates/b2b/app/signup/page.tsx +44 -0
- package/templates/b2b/app/sitemap.ts +27 -0
- package/templates/b2b/app.ts +179 -0
- package/templates/b2b/components/ui/button.tsx +56 -0
- package/templates/b2b/components/ui/card.tsx +90 -0
- package/templates/b2b/components.json +20 -0
- package/templates/b2b/functions/_keep.ts +13 -0
- package/templates/b2b/gitignore +10 -0
- package/templates/b2b/lib/utils.ts +10 -0
- package/templates/b2b/package.json +33 -0
- package/templates/b2b/tsconfig.json +18 -0
- package/templates/barebones/AGENTS.md +61 -0
- package/templates/barebones/README.md +45 -0
- package/templates/barebones/app/error.tsx +43 -0
- package/templates/barebones/app/globals.css +139 -0
- package/templates/barebones/app/items-client.tsx +96 -0
- package/templates/barebones/app/layout.tsx +27 -0
- package/templates/barebones/app/not-found.tsx +29 -0
- package/templates/barebones/app/page.tsx +28 -0
- package/templates/barebones/app/robots.ts +12 -0
- package/templates/barebones/app/sitemap.ts +27 -0
- package/templates/barebones/app.ts +55 -0
- package/templates/barebones/components/ui/button.tsx +56 -0
- package/templates/barebones/components/ui/card.tsx +90 -0
- package/templates/barebones/components.json +20 -0
- package/templates/barebones/functions/_keep.ts +13 -0
- package/templates/barebones/gitignore +10 -0
- package/templates/barebones/lib/utils.ts +10 -0
- package/templates/barebones/package.json +33 -0
- package/templates/barebones/tsconfig.json +18 -0
- package/templates/chat/AGENTS.md +61 -0
- package/templates/chat/README.md +51 -0
- package/templates/chat/app/chat-client.tsx +113 -0
- package/templates/chat/app/error.tsx +43 -0
- package/templates/chat/app/globals.css +139 -0
- package/templates/chat/app/layout.tsx +25 -0
- package/templates/chat/app/not-found.tsx +29 -0
- package/templates/chat/app/page.tsx +26 -0
- package/templates/chat/app/robots.ts +12 -0
- package/templates/chat/app/sitemap.ts +27 -0
- package/templates/chat/app.ts +59 -0
- package/templates/chat/components/ui/button.tsx +56 -0
- package/templates/chat/components/ui/card.tsx +90 -0
- package/templates/chat/components.json +20 -0
- package/templates/chat/functions/_keep.ts +13 -0
- package/templates/chat/gitignore +10 -0
- package/templates/chat/lib/utils.ts +10 -0
- package/templates/chat/package.json +33 -0
- package/templates/chat/tsconfig.json +18 -0
- package/templates/consumer/AGENTS.md +61 -0
- package/templates/consumer/README.md +52 -0
- package/templates/consumer/app/error.tsx +43 -0
- package/templates/consumer/app/feed-client.tsx +154 -0
- package/templates/consumer/app/globals.css +139 -0
- package/templates/consumer/app/layout.tsx +27 -0
- package/templates/consumer/app/not-found.tsx +29 -0
- package/templates/consumer/app/page.tsx +27 -0
- package/templates/consumer/app/robots.ts +12 -0
- package/templates/consumer/app/sitemap.ts +27 -0
- package/templates/consumer/app.ts +89 -0
- package/templates/consumer/components/ui/button.tsx +56 -0
- package/templates/consumer/components/ui/card.tsx +90 -0
- package/templates/consumer/components.json +20 -0
- package/templates/consumer/functions/_keep.ts +13 -0
- package/templates/consumer/gitignore +10 -0
- package/templates/consumer/lib/utils.ts +10 -0
- package/templates/consumer/package.json +33 -0
- package/templates/consumer/tsconfig.json +18 -0
- package/templates/ssr/app.ts +3 -0
- package/templates/todo/AGENTS.md +61 -0
- package/templates/todo/README.md +59 -0
- package/templates/todo/app/error.tsx +43 -0
- package/templates/todo/app/globals.css +139 -0
- package/templates/todo/app/layout.tsx +31 -0
- package/templates/todo/app/not-found.tsx +29 -0
- package/templates/todo/app/page.tsx +37 -0
- package/templates/todo/app/robots.ts +12 -0
- package/templates/todo/app/sitemap.ts +27 -0
- package/templates/todo/app/todo-app.tsx +133 -0
- package/templates/todo/app.ts +72 -0
- package/templates/todo/components/ui/button.tsx +56 -0
- package/templates/todo/components/ui/card.tsx +90 -0
- package/templates/todo/components.json +20 -0
- package/templates/todo/functions/_keep.ts +13 -0
- package/templates/todo/gitignore +10 -0
- package/templates/todo/lib/utils.ts +10 -0
- package/templates/todo/package.json +33 -0
- 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,96 @@
|
|
|
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 Item {
|
|
9
|
+
id: string;
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// `<EnsureGuest>` mints a guest session on first load (POST /api/auth/guest)
|
|
14
|
+
// so the list works with no login — every visitor becomes their own user and
|
|
15
|
+
// their items stay private (the policy in app.ts gates reads/writes to the
|
|
16
|
+
// owner).
|
|
17
|
+
export function ItemList() {
|
|
18
|
+
return (
|
|
19
|
+
<EnsureGuest fallback={<Skeleton />}>
|
|
20
|
+
<List />
|
|
21
|
+
</EnsureGuest>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// `db.useQuery` is a LIVE subscription — it re-renders the instant an Item is
|
|
26
|
+
// added, here or in another tab. `db.insert` is OPTIMISTIC: it shows the row
|
|
27
|
+
// immediately and syncs in the background, rolling back if the policy rejects.
|
|
28
|
+
function List() {
|
|
29
|
+
const [text, setText] = useState("");
|
|
30
|
+
const { data: items, loading } = db.useQuery<Item>("Item");
|
|
31
|
+
|
|
32
|
+
async function addItem(e: React.FormEvent) {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
const value = text.trim();
|
|
35
|
+
if (!value) return;
|
|
36
|
+
setText("");
|
|
37
|
+
// No userId sent — `field.owner()` stamps it server-side.
|
|
38
|
+
await db.insert("Item", { text: value });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="space-y-5">
|
|
43
|
+
<form onSubmit={addItem} className="flex items-center gap-2">
|
|
44
|
+
<input
|
|
45
|
+
value={text}
|
|
46
|
+
onChange={(e) => setText(e.target.value)}
|
|
47
|
+
placeholder="Add an item…"
|
|
48
|
+
aria-label="Item"
|
|
49
|
+
autoComplete="off"
|
|
50
|
+
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"
|
|
51
|
+
/>
|
|
52
|
+
<Button type="submit">Add</Button>
|
|
53
|
+
</form>
|
|
54
|
+
|
|
55
|
+
{loading && items.length === 0 ? (
|
|
56
|
+
<Skeleton />
|
|
57
|
+
) : items.length === 0 ? (
|
|
58
|
+
<p className="text-sm text-muted-foreground">
|
|
59
|
+
Nothing yet — add an item above. It appears instantly and syncs; open
|
|
60
|
+
a second tab to watch it arrive live.
|
|
61
|
+
</p>
|
|
62
|
+
) : (
|
|
63
|
+
<ul className="space-y-2">
|
|
64
|
+
{items.map((item) => (
|
|
65
|
+
<li
|
|
66
|
+
key={item.id}
|
|
67
|
+
className="flex items-center justify-between gap-3 rounded-md border px-3 py-2.5 text-sm"
|
|
68
|
+
>
|
|
69
|
+
<span className="truncate">{item.text}</span>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
aria-label="Delete item"
|
|
73
|
+
onClick={() => db.delete("Item", item.id)}
|
|
74
|
+
className="text-muted-foreground/40 transition-colors hover:text-red-600"
|
|
75
|
+
>
|
|
76
|
+
✕
|
|
77
|
+
</button>
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function Skeleton() {
|
|
87
|
+
return (
|
|
88
|
+
<ul className="space-y-2" aria-hidden>
|
|
89
|
+
{[0, 1, 2].map((i) => (
|
|
90
|
+
<li key={i} className="rounded-md border px-3 py-2.5">
|
|
91
|
+
<span className="block h-4 w-2/3 rounded bg-muted" />
|
|
92
|
+
</li>
|
|
93
|
+
))}
|
|
94
|
+
</ul>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
// A layout wraps every page. This one is a header and a centered column —
|
|
4
|
+
// the page renders server-side first (shell + copy in the HTML), then
|
|
5
|
+
// hydrates into the live list.
|
|
6
|
+
interface LayoutProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function RootLayout({ children }: LayoutProps) {
|
|
11
|
+
return (
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<head>
|
|
14
|
+
<meta charSet="utf-8" />
|
|
15
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
16
|
+
<title>__APP_NAME__</title>
|
|
17
|
+
{/* Tailwind is compiled by Pylon from app/globals.css and the
|
|
18
|
+
stylesheet link is injected here automatically. */}
|
|
19
|
+
</head>
|
|
20
|
+
<body className="min-h-screen bg-background text-foreground antialiased">
|
|
21
|
+
<main className="mx-auto flex min-h-screen max-w-lg flex-col px-4 py-12">
|
|
22
|
+
{children}
|
|
23
|
+
</main>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -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'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,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type Metadata } from "@pylonsync/react";
|
|
3
|
+
import { ItemList } from "./items-client";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "__APP_NAME__ — a minimal Pylon app",
|
|
7
|
+
description:
|
|
8
|
+
"One entity, a live list, and an optimistic create — server-rendered over one Pylon backend, one binary, one port.",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// `app/page.tsx` → `/`. The heading is server-rendered; the list is a client
|
|
12
|
+
// island (`<ItemList>`) that mints a guest session and runs the live query +
|
|
13
|
+
// optimistic create in the browser. This is the whole app — edit
|
|
14
|
+
// `app/items-client.tsx` for the UI or `app.ts` for the data model.
|
|
15
|
+
export default function IndexPage() {
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex flex-1 flex-col">
|
|
18
|
+
<header className="mb-8">
|
|
19
|
+
<h1 className="text-3xl font-semibold tracking-tight">Items</h1>
|
|
20
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
21
|
+
One entity, a live list, optimistic writes. The smallest working
|
|
22
|
+
Pylon app — grow it from here.
|
|
23
|
+
</p>
|
|
24
|
+
</header>
|
|
25
|
+
<ItemList />
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -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,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
entity,
|
|
3
|
+
field,
|
|
4
|
+
policy,
|
|
5
|
+
auth,
|
|
6
|
+
buildManifest,
|
|
7
|
+
discoverAppRoutes,
|
|
8
|
+
} from "@pylonsync/sdk";
|
|
9
|
+
|
|
10
|
+
// The smallest useful Pylon app: one entity, owned by its creator, with a
|
|
11
|
+
// live list and an optimistic create. `userId: field.owner()` stamps the
|
|
12
|
+
// signed-in (here: guest) user's id server-side and rejects any forged
|
|
13
|
+
// value, so the UI can do a plain `db.insert("Item", { text })` and the row
|
|
14
|
+
// stays unspoofably yours.
|
|
15
|
+
const Item = entity(
|
|
16
|
+
"Item",
|
|
17
|
+
{
|
|
18
|
+
userId: field.string().owner(),
|
|
19
|
+
text: field.string(),
|
|
20
|
+
createdAt: field.datetime().defaultNow(),
|
|
21
|
+
},
|
|
22
|
+
{ indexes: [{ name: "by_user", fields: ["userId"], unique: false }] },
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Items are private to their owner. An entity with NO policy is denied to
|
|
26
|
+
// clients by default, so this allow-list is what makes the live query +
|
|
27
|
+
// optimistic writes work — and only for your own rows.
|
|
28
|
+
const itemPolicy = policy({
|
|
29
|
+
name: "item_access",
|
|
30
|
+
entity: "Item",
|
|
31
|
+
allowRead: "auth.userId == data.userId",
|
|
32
|
+
allowInsert: "auth.userId != null",
|
|
33
|
+
allowUpdate: "auth.userId == data.userId",
|
|
34
|
+
allowDelete: "auth.userId == data.userId",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// The manifest is your whole app: data, policies, and the file-based routes
|
|
38
|
+
// under `app/`. `pylon dev` serves the SSR frontend and the API from one
|
|
39
|
+
// port. Guest sessions (via `<EnsureGuest>` on the page) mean every visitor
|
|
40
|
+
// implicitly becomes their own user — no login wall. Add fields to `Item`, or
|
|
41
|
+
// a second `entity()`, and the typed client + REST/realtime API follow.
|
|
42
|
+
const manifest = buildManifest({
|
|
43
|
+
name: "__APP_NAME__",
|
|
44
|
+
version: "0.1.0",
|
|
45
|
+
entities: [Item],
|
|
46
|
+
queries: [],
|
|
47
|
+
actions: [],
|
|
48
|
+
policies: [itemPolicy],
|
|
49
|
+
auth: auth(),
|
|
50
|
+
routes: await discoverAppRoutes(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
54
|
+
|
|
55
|
+
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
|
+
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
|
+
}
|