@pylonsync/create-pylon 0.3.168 → 0.3.170
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 +28 -3
- package/package.json +1 -1
- package/templates/vite/todo/apps/web/index.html +12 -0
- package/templates/vite/todo/apps/web/package.json +31 -0
- package/templates/vite/todo/apps/web/src/App.tsx +16 -0
- package/templates/vite/todo/apps/web/src/components/TodoList.tsx +281 -0
- package/templates/vite/todo/apps/web/src/index.css +14 -0
- package/templates/vite/todo/apps/web/src/main.tsx +18 -0
- package/templates/vite/todo/apps/web/tsconfig.json +24 -0
- package/templates/vite/todo/apps/web/vite.config.ts +30 -0
package/bin/create-pylon.js
CHANGED
|
@@ -64,7 +64,11 @@ const PYLON_VERSION = JSON.parse(
|
|
|
64
64
|
// without value. Pick a different template if you want mobile.
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
// `web` and `vite` both render into apps/web — they're mutually
|
|
68
|
+
// exclusive web-frontend toolchains. `web` is Next.js 16 (SSR +
|
|
69
|
+
// server actions); `vite` is plain Vite + React for an SPA setup.
|
|
70
|
+
// Validation below rejects passing both.
|
|
71
|
+
const PLATFORMS_AVAILABLE = ["web", "vite", "ios", "mac", "expo"];
|
|
68
72
|
|
|
69
73
|
const TEMPLATE_REGISTRY = {
|
|
70
74
|
barebones: {
|
|
@@ -73,7 +77,7 @@ const TEMPLATE_REGISTRY = {
|
|
|
73
77
|
},
|
|
74
78
|
todo: {
|
|
75
79
|
blurb: "CRUD + drag-reorder + optimistic mutations.",
|
|
76
|
-
platforms: ["web", "ios", "mac", "expo"],
|
|
80
|
+
platforms: ["web", "vite", "ios", "mac", "expo"],
|
|
77
81
|
},
|
|
78
82
|
b2b: {
|
|
79
83
|
blurb: "Multi-tenant SaaS: orgs, members, roles, RBAC policies.",
|
|
@@ -201,6 +205,13 @@ if (platforms.length === 0) {
|
|
|
201
205
|
console.error(`\nError: at least one platform required.\n`);
|
|
202
206
|
exit(1);
|
|
203
207
|
}
|
|
208
|
+
if (platforms.includes("web") && platforms.includes("vite")) {
|
|
209
|
+
console.error(
|
|
210
|
+
`\nError: --platforms web and vite are mutually exclusive (both render into apps/web).\n` +
|
|
211
|
+
` Pick one: web for Next.js 16, vite for plain Vite + React.\n`,
|
|
212
|
+
);
|
|
213
|
+
exit(1);
|
|
214
|
+
}
|
|
204
215
|
if (!TEMPLATES_AVAILABLE.includes(flags.template)) {
|
|
205
216
|
console.error(
|
|
206
217
|
`\nError: unknown template "${flags.template}". Valid: ${TEMPLATES_AVAILABLE.join(", ")}\n`,
|
|
@@ -324,10 +335,17 @@ function copyTemplate(srcSubpath, destSubpath = "") {
|
|
|
324
335
|
copyTemplate("_root");
|
|
325
336
|
copyTemplate(`backend/${flags.template}`);
|
|
326
337
|
|
|
338
|
+
// `web` (Next.js) and `vite` are alternative web-frontend toolchains;
|
|
339
|
+
// the mutex check above guarantees at most one of them is set. Either
|
|
340
|
+
// way we also pull in packages/ui so the shared primitives are present.
|
|
327
341
|
if (platforms.includes("web")) {
|
|
328
342
|
copyTemplate("ui");
|
|
329
343
|
copyTemplate(`web/${flags.template}`);
|
|
330
344
|
}
|
|
345
|
+
if (platforms.includes("vite")) {
|
|
346
|
+
copyTemplate("ui");
|
|
347
|
+
copyTemplate(`vite/${flags.template}`);
|
|
348
|
+
}
|
|
331
349
|
for (const p of ["ios", "mac", "expo"]) {
|
|
332
350
|
if (platforms.includes(p)) copyTemplate(`${p}/${flags.template}`);
|
|
333
351
|
}
|
|
@@ -374,7 +392,8 @@ const rootPkg = {
|
|
|
374
392
|
workspaces: ["apps/*", "packages/*"].filter((p) => {
|
|
375
393
|
// Only declare packages/* as a workspace if we actually scaffolded
|
|
376
394
|
// packages/ui — otherwise the empty match warns on bun install.
|
|
377
|
-
if (p === "packages/*")
|
|
395
|
+
if (p === "packages/*")
|
|
396
|
+
return platforms.includes("web") || platforms.includes("vite");
|
|
378
397
|
return true;
|
|
379
398
|
}),
|
|
380
399
|
scripts: {
|
|
@@ -421,6 +440,8 @@ const platformLines = [];
|
|
|
421
440
|
platformLines.push(" → api http://localhost:4321 (Pylon control plane)");
|
|
422
441
|
if (platforms.includes("web"))
|
|
423
442
|
platformLines.push(" → web http://localhost:3000 (Next.js)");
|
|
443
|
+
if (platforms.includes("vite"))
|
|
444
|
+
platformLines.push(" → web http://localhost:3000 (Vite + React)");
|
|
424
445
|
if (platforms.includes("expo"))
|
|
425
446
|
platformLines.push(` → expo ${runDev} (Metro + simulator, alongside web/api)`);
|
|
426
447
|
if (platforms.includes("ios"))
|
|
@@ -433,6 +454,10 @@ if (platforms.includes("web")) {
|
|
|
433
454
|
layoutLines.push(" apps/web Next.js 16 + React 19 + Tailwind v4");
|
|
434
455
|
layoutLines.push(" packages/ui shared UI primitives");
|
|
435
456
|
}
|
|
457
|
+
if (platforms.includes("vite")) {
|
|
458
|
+
layoutLines.push(" apps/web Vite + React 19 + Tailwind v4");
|
|
459
|
+
layoutLines.push(" packages/ui shared UI primitives");
|
|
460
|
+
}
|
|
436
461
|
if (platforms.includes("ios"))
|
|
437
462
|
layoutLines.push(" apps/ios Swift / SwiftUI (iOS)");
|
|
438
463
|
if (platforms.includes("mac"))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/create-pylon",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.170",
|
|
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,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>__APP_NAME__</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@__APP_NAME_KEBAB__/web",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite --port 3000",
|
|
8
|
+
"build": "tsc --noEmit && vite build",
|
|
9
|
+
"preview": "vite preview --port 3000"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@__APP_NAME_KEBAB__/ui": "__WORKSPACE_DEP__",
|
|
13
|
+
"@pylonsync/react": "^__PYLON_VERSION__",
|
|
14
|
+
"@pylonsync/sync": "^__PYLON_VERSION__",
|
|
15
|
+
"@dnd-kit/core": "^6.3.1",
|
|
16
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
17
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
18
|
+
"react": "^19.0.0",
|
|
19
|
+
"react-dom": "^19.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
23
|
+
"@types/node": "^20.0.0",
|
|
24
|
+
"@types/react": "^19.0.0",
|
|
25
|
+
"@types/react-dom": "^19.0.0",
|
|
26
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
27
|
+
"tailwindcss": "^4.0.0",
|
|
28
|
+
"typescript": "^5.5.0",
|
|
29
|
+
"vite": "^5.4.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TodoList } from "./components/TodoList";
|
|
2
|
+
|
|
3
|
+
export function App() {
|
|
4
|
+
return (
|
|
5
|
+
<main className="mx-auto max-w-2xl px-6 py-12 space-y-8">
|
|
6
|
+
<header className="space-y-2">
|
|
7
|
+
<h1 className="text-3xl font-semibold tracking-tight">__APP_NAME__</h1>
|
|
8
|
+
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
|
9
|
+
Drag rows to reorder, double-click a title to edit, hover for
|
|
10
|
+
delete. All mutations are optimistic with revert-on-failure.
|
|
11
|
+
</p>
|
|
12
|
+
</header>
|
|
13
|
+
<TodoList />
|
|
14
|
+
</main>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { useState, useTransition, useRef, useEffect } from "react";
|
|
2
|
+
import { Button, Input } from "@__APP_NAME_KEBAB__/ui";
|
|
3
|
+
import { db } from "@pylonsync/react";
|
|
4
|
+
import {
|
|
5
|
+
DndContext,
|
|
6
|
+
closestCenter,
|
|
7
|
+
KeyboardSensor,
|
|
8
|
+
PointerSensor,
|
|
9
|
+
useSensor,
|
|
10
|
+
useSensors,
|
|
11
|
+
type DragEndEvent,
|
|
12
|
+
} from "@dnd-kit/core";
|
|
13
|
+
import {
|
|
14
|
+
arrayMove,
|
|
15
|
+
SortableContext,
|
|
16
|
+
sortableKeyboardCoordinates,
|
|
17
|
+
useSortable,
|
|
18
|
+
verticalListSortingStrategy,
|
|
19
|
+
} from "@dnd-kit/sortable";
|
|
20
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
21
|
+
|
|
22
|
+
type Todo = {
|
|
23
|
+
id: string;
|
|
24
|
+
title: string;
|
|
25
|
+
done: boolean;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
position?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Live query — db.useQuery subscribes to Todo and re-renders on every
|
|
31
|
+
// server-pushed change. Compare to a Next.js Server Component which
|
|
32
|
+
// fetches once at request time and goes stale. The order matches
|
|
33
|
+
// listTodos: explicit `position` first, falling back to createdAt
|
|
34
|
+
// for legacy rows.
|
|
35
|
+
function useTodos(): Todo[] {
|
|
36
|
+
const { data } = db.useQuery<Todo>("Todo");
|
|
37
|
+
const rows = data ?? [];
|
|
38
|
+
return [...rows].sort((a, b) => {
|
|
39
|
+
const ap =
|
|
40
|
+
typeof a.position === "number" ? a.position : Date.parse(a.createdAt) || 0;
|
|
41
|
+
const bp =
|
|
42
|
+
typeof b.position === "number" ? b.position : Date.parse(b.createdAt) || 0;
|
|
43
|
+
return ap - bp;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function TodoList() {
|
|
48
|
+
const todos = useTodos();
|
|
49
|
+
const [title, setTitle] = useState("");
|
|
50
|
+
const [pending, startTransition] = useTransition();
|
|
51
|
+
const sensors = useSensors(
|
|
52
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
53
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
function add() {
|
|
57
|
+
if (!title.trim()) return;
|
|
58
|
+
const newTitle = title;
|
|
59
|
+
setTitle("");
|
|
60
|
+
startTransition(async () => {
|
|
61
|
+
await db.fn<Todo>("addTodo", { title: newTitle }).catch(() => null);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toggle(t: Todo) {
|
|
66
|
+
startTransition(async () => {
|
|
67
|
+
await db
|
|
68
|
+
.fn("toggleTodo", { id: t.id, done: !t.done })
|
|
69
|
+
.catch(() => null);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function remove(t: Todo) {
|
|
74
|
+
startTransition(async () => {
|
|
75
|
+
await db.fn("deleteTodo", { id: t.id }).catch(() => null);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function rename(t: Todo, newTitle: string) {
|
|
80
|
+
const trimmed = newTitle.trim();
|
|
81
|
+
if (!trimmed || trimmed === t.title) return;
|
|
82
|
+
startTransition(async () => {
|
|
83
|
+
await db.fn("editTodo", { id: t.id, title: trimmed }).catch(() => null);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function onDragEnd(e: DragEndEvent) {
|
|
88
|
+
const { active, over } = e;
|
|
89
|
+
if (!over || active.id === over.id) return;
|
|
90
|
+
const oldIndex = todos.findIndex((t) => t.id === active.id);
|
|
91
|
+
const newIndex = todos.findIndex((t) => t.id === over.id);
|
|
92
|
+
if (oldIndex < 0 || newIndex < 0) return;
|
|
93
|
+
const reordered = arrayMove(todos, oldIndex, newIndex);
|
|
94
|
+
const prev = reordered[newIndex - 1];
|
|
95
|
+
const next = reordered[newIndex + 1];
|
|
96
|
+
const prevPos = prev?.position ?? Date.parse(prev?.createdAt ?? "") ?? 0;
|
|
97
|
+
const nextPos = next?.position ?? Date.parse(next?.createdAt ?? "") ?? 0;
|
|
98
|
+
let position: number;
|
|
99
|
+
if (prev && next) position = (prevPos + nextPos) / 2;
|
|
100
|
+
else if (prev) position = prevPos + 1024;
|
|
101
|
+
else if (next) position = nextPos - 1024;
|
|
102
|
+
else position = 1024;
|
|
103
|
+
const movedId = String(active.id);
|
|
104
|
+
startTransition(async () => {
|
|
105
|
+
await db.fn("reorderTodo", { id: movedId, position }).catch(() => null);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="space-y-4">
|
|
111
|
+
<form
|
|
112
|
+
onSubmit={(e) => {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
add();
|
|
115
|
+
}}
|
|
116
|
+
className="flex gap-2"
|
|
117
|
+
>
|
|
118
|
+
<Input
|
|
119
|
+
value={title}
|
|
120
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
121
|
+
placeholder="What needs doing?"
|
|
122
|
+
disabled={pending}
|
|
123
|
+
className="flex-1"
|
|
124
|
+
/>
|
|
125
|
+
<Button
|
|
126
|
+
type="submit"
|
|
127
|
+
variant="primary"
|
|
128
|
+
disabled={pending || !title.trim()}
|
|
129
|
+
>
|
|
130
|
+
Add
|
|
131
|
+
</Button>
|
|
132
|
+
</form>
|
|
133
|
+
|
|
134
|
+
{todos.length === 0 ? (
|
|
135
|
+
<p className="text-sm text-neutral-500 dark:text-neutral-400 text-center py-8">
|
|
136
|
+
No todos yet. Add one above.
|
|
137
|
+
</p>
|
|
138
|
+
) : (
|
|
139
|
+
<DndContext
|
|
140
|
+
sensors={sensors}
|
|
141
|
+
collisionDetection={closestCenter}
|
|
142
|
+
onDragEnd={onDragEnd}
|
|
143
|
+
>
|
|
144
|
+
<SortableContext
|
|
145
|
+
items={todos.map((t) => t.id)}
|
|
146
|
+
strategy={verticalListSortingStrategy}
|
|
147
|
+
>
|
|
148
|
+
<ul className="divide-y divide-neutral-200 dark:divide-neutral-800 rounded-md border border-neutral-200 dark:border-neutral-800">
|
|
149
|
+
{todos.map((t) => (
|
|
150
|
+
<SortableRow
|
|
151
|
+
key={t.id}
|
|
152
|
+
todo={t}
|
|
153
|
+
pending={pending}
|
|
154
|
+
onToggle={() => toggle(t)}
|
|
155
|
+
onRemove={() => remove(t)}
|
|
156
|
+
onRename={(next) => rename(t, next)}
|
|
157
|
+
/>
|
|
158
|
+
))}
|
|
159
|
+
</ul>
|
|
160
|
+
</SortableContext>
|
|
161
|
+
</DndContext>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function SortableRow({
|
|
168
|
+
todo,
|
|
169
|
+
pending,
|
|
170
|
+
onToggle,
|
|
171
|
+
onRemove,
|
|
172
|
+
onRename,
|
|
173
|
+
}: {
|
|
174
|
+
todo: Todo;
|
|
175
|
+
pending: boolean;
|
|
176
|
+
onToggle: () => void;
|
|
177
|
+
onRemove: () => void;
|
|
178
|
+
onRename: (next: string) => void;
|
|
179
|
+
}) {
|
|
180
|
+
const {
|
|
181
|
+
attributes,
|
|
182
|
+
listeners,
|
|
183
|
+
setNodeRef,
|
|
184
|
+
transform,
|
|
185
|
+
transition,
|
|
186
|
+
isDragging,
|
|
187
|
+
} = useSortable({ id: todo.id });
|
|
188
|
+
const style = {
|
|
189
|
+
transform: CSS.Transform.toString(transform),
|
|
190
|
+
transition,
|
|
191
|
+
opacity: isDragging ? 0.4 : 1,
|
|
192
|
+
};
|
|
193
|
+
const [editing, setEditing] = useState(false);
|
|
194
|
+
const [draft, setDraft] = useState(todo.title);
|
|
195
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (editing) {
|
|
199
|
+
setDraft(todo.title);
|
|
200
|
+
requestAnimationFrame(() => {
|
|
201
|
+
inputRef.current?.focus();
|
|
202
|
+
inputRef.current?.select();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}, [editing, todo.title]);
|
|
206
|
+
|
|
207
|
+
function commit() {
|
|
208
|
+
setEditing(false);
|
|
209
|
+
onRename(draft);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<li
|
|
214
|
+
ref={setNodeRef}
|
|
215
|
+
style={style}
|
|
216
|
+
className="flex items-center gap-3 px-4 py-3 text-sm group bg-white dark:bg-neutral-950"
|
|
217
|
+
>
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
{...attributes}
|
|
221
|
+
{...listeners}
|
|
222
|
+
className="cursor-grab active:cursor-grabbing text-neutral-300 hover:text-neutral-500 select-none touch-none"
|
|
223
|
+
aria-label="Drag to reorder"
|
|
224
|
+
tabIndex={-1}
|
|
225
|
+
>
|
|
226
|
+
⋮⋮
|
|
227
|
+
</button>
|
|
228
|
+
<input
|
|
229
|
+
type="checkbox"
|
|
230
|
+
checked={todo.done}
|
|
231
|
+
onChange={onToggle}
|
|
232
|
+
disabled={pending}
|
|
233
|
+
className="size-4 cursor-pointer"
|
|
234
|
+
aria-label={`Mark "${todo.title}" as ${todo.done ? "not done" : "done"}`}
|
|
235
|
+
/>
|
|
236
|
+
{editing ? (
|
|
237
|
+
<input
|
|
238
|
+
ref={inputRef}
|
|
239
|
+
value={draft}
|
|
240
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
241
|
+
onBlur={commit}
|
|
242
|
+
onKeyDown={(e) => {
|
|
243
|
+
if (e.key === "Enter") commit();
|
|
244
|
+
else if (e.key === "Escape") {
|
|
245
|
+
setEditing(false);
|
|
246
|
+
setDraft(todo.title);
|
|
247
|
+
}
|
|
248
|
+
}}
|
|
249
|
+
className="flex-1 bg-transparent border-b border-neutral-300 dark:border-neutral-700 outline-none text-sm"
|
|
250
|
+
aria-label="Edit title"
|
|
251
|
+
/>
|
|
252
|
+
) : (
|
|
253
|
+
<button
|
|
254
|
+
type="button"
|
|
255
|
+
onDoubleClick={() => setEditing(true)}
|
|
256
|
+
className={`flex-1 text-left ${todo.done ? "line-through text-neutral-400" : ""}`}
|
|
257
|
+
title="Double-click to edit"
|
|
258
|
+
>
|
|
259
|
+
{todo.title}
|
|
260
|
+
</button>
|
|
261
|
+
)}
|
|
262
|
+
<button
|
|
263
|
+
type="button"
|
|
264
|
+
onClick={() => setEditing(true)}
|
|
265
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity text-xs text-neutral-500 hover:text-neutral-800 dark:hover:text-neutral-200"
|
|
266
|
+
aria-label={`Edit "${todo.title}"`}
|
|
267
|
+
>
|
|
268
|
+
Edit
|
|
269
|
+
</button>
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
onClick={onRemove}
|
|
273
|
+
disabled={pending}
|
|
274
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity text-xs text-neutral-500 hover:text-red-500"
|
|
275
|
+
aria-label={`Delete "${todo.title}"`}
|
|
276
|
+
>
|
|
277
|
+
Delete
|
|
278
|
+
</button>
|
|
279
|
+
</li>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@source "../../../packages/ui/src/**/*.{ts,tsx}";
|
|
3
|
+
|
|
4
|
+
:root { color-scheme: light dark; }
|
|
5
|
+
html, body, #root { height: 100%; }
|
|
6
|
+
body {
|
|
7
|
+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
8
|
+
margin: 0;
|
|
9
|
+
background: white;
|
|
10
|
+
color: #171717;
|
|
11
|
+
}
|
|
12
|
+
@media (prefers-color-scheme: dark) {
|
|
13
|
+
body { background: #0a0a0a; color: #f5f5f5; }
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { init } from "@pylonsync/react";
|
|
4
|
+
import { App } from "./App";
|
|
5
|
+
import "./index.css";
|
|
6
|
+
|
|
7
|
+
// Same-origin baseUrl — the Vite dev server proxies /api/* to the
|
|
8
|
+
// Pylon backend (see vite.config.ts). In production, configure your
|
|
9
|
+
// CDN / reverse proxy the same way and this works unchanged.
|
|
10
|
+
init({ baseUrl: "", appName: "__APP_NAME_SNAKE__" });
|
|
11
|
+
|
|
12
|
+
const root = document.getElementById("root");
|
|
13
|
+
if (!root) throw new Error("#root not found");
|
|
14
|
+
createRoot(root).render(
|
|
15
|
+
<StrictMode>
|
|
16
|
+
<App />
|
|
17
|
+
</StrictMode>,
|
|
18
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"allowImportingTsExtensions": false,
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"useDefineForClassFields": true,
|
|
16
|
+
"esModuleInterop": true,
|
|
17
|
+
"allowSyntheticDefaultImports": true,
|
|
18
|
+
"baseUrl": ".",
|
|
19
|
+
"paths": {
|
|
20
|
+
"@/*": ["./src/*"]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"include": ["src", "vite.config.ts"]
|
|
24
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineConfig, type ProxyOptions } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
4
|
+
|
|
5
|
+
// Backend origin. `pylon dev` defaults to :4321 — override via the
|
|
6
|
+
// PYLON_TARGET env if you're running it elsewhere. In a production
|
|
7
|
+
// build (`vite build` → static hosting + a separate API origin),
|
|
8
|
+
// configure your reverse proxy / CDN to route /api/* to the same
|
|
9
|
+
// place this proxy points at.
|
|
10
|
+
const PYLON_TARGET = process.env.PYLON_TARGET ?? "http://localhost:4321";
|
|
11
|
+
|
|
12
|
+
const proxyConfig: Record<string, string | ProxyOptions> = {
|
|
13
|
+
// HTTP — /api/auth/*, /api/fn/*, /api/sync/pull, etc.
|
|
14
|
+
"/api": {
|
|
15
|
+
target: PYLON_TARGET,
|
|
16
|
+
changeOrigin: true,
|
|
17
|
+
// Vite's http-proxy needs ws:true for the WebSocket upgrade
|
|
18
|
+
// to propagate. Without it `/api/sync/ws` 404s and db.useQuery
|
|
19
|
+
// hangs in a "connecting" state forever.
|
|
20
|
+
ws: true,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
plugins: [react(), tailwindcss()],
|
|
26
|
+
server: {
|
|
27
|
+
port: 3000,
|
|
28
|
+
proxy: proxyConfig,
|
|
29
|
+
},
|
|
30
|
+
});
|