@pylonsync/create-pylon 0.3.50 → 0.3.51
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 +495 -495
- package/package.json +1 -1
package/bin/create-pylon.js
CHANGED
|
@@ -254,12 +254,12 @@ writeJson("apps/api/tsconfig.json", {
|
|
|
254
254
|
write(
|
|
255
255
|
"apps/api/schema.ts",
|
|
256
256
|
`import {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
257
|
+
entity,
|
|
258
|
+
field,
|
|
259
|
+
query,
|
|
260
|
+
action,
|
|
261
|
+
policy,
|
|
262
|
+
buildManifest,
|
|
263
263
|
} from "@pylonsync/sdk";
|
|
264
264
|
|
|
265
265
|
// ---------------------------------------------------------------------------
|
|
@@ -267,14 +267,14 @@ write(
|
|
|
267
267
|
// ---------------------------------------------------------------------------
|
|
268
268
|
|
|
269
269
|
const Todo = entity("Todo", {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
270
|
+
title: field.string(),
|
|
271
|
+
done: field.bool(),
|
|
272
|
+
createdAt: field.datetime(),
|
|
273
|
+
// Float position so drag-reorder can insert between two existing
|
|
274
|
+
// rows without renumbering the whole list. Frontend computes
|
|
275
|
+
// (prev.position + next.position) / 2 on drop. Optional for
|
|
276
|
+
// backwards compat with legacy rows.
|
|
277
|
+
position: field.float().optional(),
|
|
278
278
|
});
|
|
279
279
|
|
|
280
280
|
// ---------------------------------------------------------------------------
|
|
@@ -285,29 +285,29 @@ const Todo = entity("Todo", {
|
|
|
285
285
|
const listTodos = query("listTodos");
|
|
286
286
|
|
|
287
287
|
const addTodo = action("addTodo", {
|
|
288
|
-
|
|
288
|
+
input: [{ name: "title", type: "string" }],
|
|
289
289
|
});
|
|
290
290
|
|
|
291
291
|
const toggleTodo = action("toggleTodo", {
|
|
292
|
-
|
|
292
|
+
input: [{ name: "id", type: "id(Todo)" }, { name: "done", type: "bool" }],
|
|
293
293
|
});
|
|
294
294
|
|
|
295
295
|
const deleteTodo = action("deleteTodo", {
|
|
296
|
-
|
|
296
|
+
input: [{ name: "id", type: "id(Todo)" }],
|
|
297
297
|
});
|
|
298
298
|
|
|
299
299
|
const editTodo = action("editTodo", {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
input: [
|
|
301
|
+
{ name: "id", type: "id(Todo)" },
|
|
302
|
+
{ name: "title", type: "string" },
|
|
303
|
+
],
|
|
304
304
|
});
|
|
305
305
|
|
|
306
306
|
const reorderTodo = action("reorderTodo", {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
307
|
+
input: [
|
|
308
|
+
{ name: "id", type: "id(Todo)" },
|
|
309
|
+
{ name: "position", type: "float" },
|
|
310
|
+
],
|
|
311
311
|
});
|
|
312
312
|
|
|
313
313
|
// ---------------------------------------------------------------------------
|
|
@@ -315,12 +315,12 @@ const reorderTodo = action("reorderTodo", {
|
|
|
315
315
|
// ---------------------------------------------------------------------------
|
|
316
316
|
|
|
317
317
|
const todoPolicy = policy({
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
318
|
+
name: "todo_open",
|
|
319
|
+
entity: "Todo",
|
|
320
|
+
allowRead: "true",
|
|
321
|
+
allowInsert: "true",
|
|
322
|
+
allowUpdate: "true",
|
|
323
|
+
allowDelete: "true",
|
|
324
324
|
});
|
|
325
325
|
|
|
326
326
|
// ---------------------------------------------------------------------------
|
|
@@ -331,13 +331,13 @@ const todoPolicy = policy({
|
|
|
331
331
|
// manifest off stdout. The framework expects JSON, not the JS object —
|
|
332
332
|
// every Pylon entry file ends with this console.log line.
|
|
333
333
|
const manifest = buildManifest({
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
334
|
+
name: "${projectName}",
|
|
335
|
+
version: "0.0.1",
|
|
336
|
+
entities: [Todo],
|
|
337
|
+
queries: [listTodos],
|
|
338
|
+
actions: [addTodo, toggleTodo, deleteTodo, editTodo, reorderTodo],
|
|
339
|
+
policies: [todoPolicy],
|
|
340
|
+
routes: [],
|
|
341
341
|
});
|
|
342
342
|
|
|
343
343
|
console.log(JSON.stringify(manifest));
|
|
@@ -354,63 +354,63 @@ write(
|
|
|
354
354
|
* a fallback so the list stays deterministic.
|
|
355
355
|
*/
|
|
356
356
|
export default query({
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
357
|
+
args: {},
|
|
358
|
+
async handler(ctx) {
|
|
359
|
+
const rows = await ctx.db.query("Todo", {});
|
|
360
|
+
return [...rows].sort((a: any, b: any) => {
|
|
361
|
+
const ap =
|
|
362
|
+
typeof a.position === "number"
|
|
363
|
+
? a.position
|
|
364
|
+
: Date.parse(a.createdAt) || 0;
|
|
365
|
+
const bp =
|
|
366
|
+
typeof b.position === "number"
|
|
367
|
+
? b.position
|
|
368
|
+
: Date.parse(b.createdAt) || 0;
|
|
369
|
+
return ap - bp;
|
|
370
|
+
});
|
|
371
|
+
},
|
|
372
372
|
});
|
|
373
373
|
`,
|
|
374
374
|
);
|
|
375
375
|
|
|
376
376
|
write(
|
|
377
|
-
|
|
378
|
-
|
|
377
|
+
"apps/api/functions/editTodo.ts",
|
|
378
|
+
`import { mutation, v } from "@pylonsync/functions";
|
|
379
379
|
|
|
380
380
|
/**
|
|
381
381
|
* Rename a Todo. Trims whitespace; rejects empty titles.
|
|
382
382
|
*/
|
|
383
383
|
export default mutation({
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
384
|
+
args: { id: v.id("Todo"), title: v.string() },
|
|
385
|
+
async handler(ctx, args: { id: string; title: string }) {
|
|
386
|
+
const trimmed = args.title.trim();
|
|
387
|
+
if (!trimmed) {
|
|
388
|
+
throw ctx.error("EMPTY_TITLE", "title cannot be empty");
|
|
389
|
+
}
|
|
390
|
+
await ctx.db.update("Todo", args.id, { title: trimmed });
|
|
391
|
+
return await ctx.db.get("Todo", args.id);
|
|
392
|
+
},
|
|
393
393
|
});
|
|
394
|
-
|
|
394
|
+
`,
|
|
395
395
|
);
|
|
396
396
|
|
|
397
397
|
write(
|
|
398
|
-
|
|
399
|
-
|
|
398
|
+
"apps/api/functions/reorderTodo.ts",
|
|
399
|
+
`import { mutation, v } from "@pylonsync/functions";
|
|
400
400
|
|
|
401
401
|
/**
|
|
402
|
-
* Drag-reorder. Frontend computes
|
|
402
|
+
* Drag-reorder. Frontend computes \`position\` as the midpoint of the
|
|
403
403
|
* drop target's neighbors; we just write it. Floats give us ~52 inserts
|
|
404
404
|
* between any two rows before precision matters.
|
|
405
405
|
*/
|
|
406
406
|
export default mutation({
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
407
|
+
args: { id: v.id("Todo"), position: v.number() },
|
|
408
|
+
async handler(ctx, args: { id: string; position: number }) {
|
|
409
|
+
await ctx.db.update("Todo", args.id, { position: args.position });
|
|
410
|
+
return await ctx.db.get("Todo", args.id);
|
|
411
|
+
},
|
|
412
412
|
});
|
|
413
|
-
|
|
413
|
+
`,
|
|
414
414
|
);
|
|
415
415
|
|
|
416
416
|
write(
|
|
@@ -422,11 +422,11 @@ write(
|
|
|
422
422
|
* \`ctx.db.update\` which is only on writable ctx variants.
|
|
423
423
|
*/
|
|
424
424
|
export default mutation({
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
425
|
+
args: { id: v.id("Todo"), done: v.bool() },
|
|
426
|
+
async handler(ctx, args: { id: string; done: boolean }) {
|
|
427
|
+
await ctx.db.update("Todo", args.id, { done: args.done });
|
|
428
|
+
return await ctx.db.get("Todo", args.id);
|
|
429
|
+
},
|
|
430
430
|
});
|
|
431
431
|
`,
|
|
432
432
|
);
|
|
@@ -440,12 +440,12 @@ write(
|
|
|
440
440
|
* the client can show a "todo removed" toast or animate it out.
|
|
441
441
|
*/
|
|
442
442
|
export default mutation({
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
443
|
+
args: { id: v.id("Todo") },
|
|
444
|
+
async handler(ctx, args: { id: string }) {
|
|
445
|
+
const snapshot = await ctx.db.get("Todo", args.id);
|
|
446
|
+
await ctx.db.delete("Todo", args.id);
|
|
447
|
+
return snapshot;
|
|
448
|
+
},
|
|
449
449
|
});
|
|
450
450
|
`,
|
|
451
451
|
);
|
|
@@ -460,24 +460,24 @@ write(
|
|
|
460
460
|
* room for inserts-between without needing global renumber.
|
|
461
461
|
*/
|
|
462
462
|
export default mutation({
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
463
|
+
args: { title: v.string() },
|
|
464
|
+
async handler(ctx, args: { title: string }) {
|
|
465
|
+
const existing = await ctx.db.query("Todo", {});
|
|
466
|
+
const maxPos = existing.reduce((acc: number, row: any) => {
|
|
467
|
+
const p =
|
|
468
|
+
typeof row.position === "number"
|
|
469
|
+
? row.position
|
|
470
|
+
: Date.parse(row.createdAt) || 0;
|
|
471
|
+
return p > acc ? p : acc;
|
|
472
|
+
}, 0);
|
|
473
|
+
const id = await ctx.db.insert("Todo", {
|
|
474
|
+
title: args.title,
|
|
475
|
+
done: false,
|
|
476
|
+
createdAt: new Date().toISOString(),
|
|
477
|
+
position: maxPos + 1024,
|
|
478
|
+
});
|
|
479
|
+
return await ctx.db.get("Todo", id);
|
|
480
|
+
},
|
|
481
481
|
});
|
|
482
482
|
`,
|
|
483
483
|
);
|
|
@@ -541,7 +541,7 @@ import { twMerge } from "tailwind-merge";
|
|
|
541
541
|
* primitive's bg-neutral-900 base).
|
|
542
542
|
*/
|
|
543
543
|
export function cn(...inputs: ClassValue[]): string {
|
|
544
|
-
|
|
544
|
+
return twMerge(clsx(inputs));
|
|
545
545
|
}
|
|
546
546
|
`,
|
|
547
547
|
);
|
|
@@ -555,42 +555,42 @@ type Variant = "default" | "primary" | "ghost";
|
|
|
555
555
|
type Size = "sm" | "md";
|
|
556
556
|
|
|
557
557
|
const variants: Record<Variant, string> = {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
558
|
+
default:
|
|
559
|
+
"bg-neutral-100 hover:bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:text-neutral-100",
|
|
560
|
+
primary:
|
|
561
|
+
"bg-neutral-900 hover:bg-neutral-800 text-white dark:bg-white dark:hover:bg-neutral-200 dark:text-neutral-900",
|
|
562
|
+
ghost:
|
|
563
|
+
"bg-transparent hover:bg-neutral-100 text-neutral-700 dark:hover:bg-neutral-800 dark:text-neutral-300",
|
|
564
564
|
};
|
|
565
565
|
|
|
566
566
|
const sizes: Record<Size, string> = {
|
|
567
|
-
|
|
568
|
-
|
|
567
|
+
sm: "h-8 px-3 text-[13px]",
|
|
568
|
+
md: "h-9 px-4 text-sm",
|
|
569
569
|
};
|
|
570
570
|
|
|
571
571
|
export interface ButtonProps
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
572
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
573
|
+
variant?: Variant;
|
|
574
|
+
size?: Size;
|
|
575
575
|
}
|
|
576
576
|
|
|
577
577
|
export function Button({
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
578
|
+
className,
|
|
579
|
+
variant = "default",
|
|
580
|
+
size = "md",
|
|
581
|
+
...props
|
|
582
582
|
}: ButtonProps) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
583
|
+
return (
|
|
584
|
+
<button
|
|
585
|
+
className={cn(
|
|
586
|
+
"inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors disabled:opacity-50 disabled:pointer-events-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500",
|
|
587
|
+
variants[variant],
|
|
588
|
+
sizes[size],
|
|
589
|
+
className,
|
|
590
|
+
)}
|
|
591
|
+
{...props}
|
|
592
|
+
/>
|
|
593
|
+
);
|
|
594
594
|
}
|
|
595
595
|
`,
|
|
596
596
|
);
|
|
@@ -603,18 +603,18 @@ import { cn } from "./cn";
|
|
|
603
603
|
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
|
604
604
|
|
|
605
605
|
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
606
|
+
function Input({ className, ...props }, ref) {
|
|
607
|
+
return (
|
|
608
|
+
<input
|
|
609
|
+
ref={ref}
|
|
610
|
+
className={cn(
|
|
611
|
+
"flex h-9 w-full rounded-md border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 text-sm placeholder:text-neutral-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:opacity-50",
|
|
612
|
+
className,
|
|
613
|
+
)}
|
|
614
|
+
{...props}
|
|
615
|
+
/>
|
|
616
|
+
);
|
|
617
|
+
},
|
|
618
618
|
);
|
|
619
619
|
`,
|
|
620
620
|
);
|
|
@@ -625,34 +625,34 @@ write(
|
|
|
625
625
|
import { cn } from "./cn";
|
|
626
626
|
|
|
627
627
|
export function Card({
|
|
628
|
-
|
|
629
|
-
|
|
628
|
+
className,
|
|
629
|
+
...props
|
|
630
630
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
631
|
+
return (
|
|
632
|
+
<div
|
|
633
|
+
className={cn(
|
|
634
|
+
"rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900",
|
|
635
|
+
className,
|
|
636
|
+
)}
|
|
637
|
+
{...props}
|
|
638
|
+
/>
|
|
639
|
+
);
|
|
640
640
|
}
|
|
641
641
|
|
|
642
642
|
export function CardHeader({
|
|
643
|
-
|
|
644
|
-
|
|
643
|
+
className,
|
|
644
|
+
...props
|
|
645
645
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
646
|
+
return (
|
|
647
|
+
<div className={cn("p-5 border-b border-neutral-200 dark:border-neutral-800", className)} {...props} />
|
|
648
|
+
);
|
|
649
649
|
}
|
|
650
650
|
|
|
651
651
|
export function CardContent({
|
|
652
|
-
|
|
653
|
-
|
|
652
|
+
className,
|
|
653
|
+
...props
|
|
654
654
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
655
|
-
|
|
655
|
+
return <div className={cn("p-5", className)} {...props} />;
|
|
656
656
|
}
|
|
657
657
|
`,
|
|
658
658
|
);
|
|
@@ -749,22 +749,22 @@ write(
|
|
|
749
749
|
const PYLON_API_URL = process.env.PYLON_API_URL ?? "http://localhost:4321";
|
|
750
750
|
|
|
751
751
|
const config: NextConfig = {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
752
|
+
transpilePackages: [
|
|
753
|
+
"@${projectName}/ui",
|
|
754
|
+
"@pylonsync/sdk",
|
|
755
|
+
"@pylonsync/react",
|
|
756
|
+
"@pylonsync/next",
|
|
757
|
+
"@pylonsync/functions",
|
|
758
|
+
"@pylonsync/sync",
|
|
759
|
+
],
|
|
760
|
+
async rewrites() {
|
|
761
|
+
return [
|
|
762
|
+
{ source: "/api/fn/:path*", destination: \`\${PYLON_API_URL}/api/fn/:path*\` },
|
|
763
|
+
{ source: "/api/auth/:path*", destination: \`\${PYLON_API_URL}/api/auth/:path*\` },
|
|
764
|
+
{ source: "/api/sync/:path*", destination: \`\${PYLON_API_URL}/api/sync/:path*\` },
|
|
765
|
+
{ source: "/api/:path*", destination: \`\${PYLON_API_URL}/api/:path*\` },
|
|
766
|
+
];
|
|
767
|
+
},
|
|
768
768
|
};
|
|
769
769
|
|
|
770
770
|
export default config;
|
|
@@ -775,7 +775,7 @@ write(
|
|
|
775
775
|
"apps/web/postcss.config.mjs",
|
|
776
776
|
`/** Tailwind v4 PostCSS pipeline. */
|
|
777
777
|
export default {
|
|
778
|
-
|
|
778
|
+
plugins: { "@tailwindcss/postcss": {} },
|
|
779
779
|
};
|
|
780
780
|
`,
|
|
781
781
|
);
|
|
@@ -786,7 +786,7 @@ write(
|
|
|
786
786
|
@source "../../../../packages/ui/src/**/*.{ts,tsx}";
|
|
787
787
|
|
|
788
788
|
:root {
|
|
789
|
-
|
|
789
|
+
color-scheme: light dark;
|
|
790
790
|
}
|
|
791
791
|
|
|
792
792
|
html, body { height: 100%; }
|
|
@@ -800,22 +800,22 @@ write(
|
|
|
800
800
|
import "./globals.css";
|
|
801
801
|
|
|
802
802
|
export const metadata: Metadata = {
|
|
803
|
-
|
|
804
|
-
|
|
803
|
+
title: "${projectName}",
|
|
804
|
+
description: "Realtime app powered by Pylon",
|
|
805
805
|
};
|
|
806
806
|
|
|
807
807
|
export default function RootLayout({
|
|
808
|
-
|
|
808
|
+
children,
|
|
809
809
|
}: {
|
|
810
|
-
|
|
810
|
+
children: React.ReactNode;
|
|
811
811
|
}) {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
812
|
+
return (
|
|
813
|
+
<html lang="en">
|
|
814
|
+
<body className="antialiased min-h-screen bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100">
|
|
815
|
+
{children}
|
|
816
|
+
</body>
|
|
817
|
+
</html>
|
|
818
|
+
);
|
|
819
819
|
}
|
|
820
820
|
`,
|
|
821
821
|
);
|
|
@@ -834,7 +834,7 @@ write(
|
|
|
834
834
|
* deployment env can't silently break auth.
|
|
835
835
|
*/
|
|
836
836
|
export const pylon = createPylonServer({
|
|
837
|
-
|
|
837
|
+
cookieName: "${projectName}_session",
|
|
838
838
|
});
|
|
839
839
|
`,
|
|
840
840
|
);
|
|
@@ -850,41 +850,41 @@ import { TodoList } from "./components/TodoList";
|
|
|
850
850
|
export const dynamic = "force-dynamic";
|
|
851
851
|
|
|
852
852
|
type Todo = {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
853
|
+
id: string;
|
|
854
|
+
title: string;
|
|
855
|
+
done: boolean;
|
|
856
|
+
createdAt: string;
|
|
857
857
|
};
|
|
858
858
|
|
|
859
859
|
export default async function HomePage() {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
860
|
+
const todos = await pylon
|
|
861
|
+
.json<Todo[]>("/api/fn/listTodos", {
|
|
862
|
+
method: "POST",
|
|
863
|
+
body: "{}",
|
|
864
|
+
headers: { "Content-Type": "application/json" },
|
|
865
|
+
})
|
|
866
|
+
.catch(() => [] as Todo[]);
|
|
867
|
+
|
|
868
|
+
return (
|
|
869
|
+
<main className="mx-auto max-w-2xl px-6 py-12 space-y-8">
|
|
870
|
+
<header className="space-y-2">
|
|
871
|
+
<h1 className="text-3xl font-semibold tracking-tight">${projectName}</h1>
|
|
872
|
+
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
|
873
|
+
A Pylon-powered realtime app. Edit{" "}
|
|
874
|
+
<code className="font-mono text-xs">apps/api/schema.ts</code> to change
|
|
875
|
+
the data model,{" "}
|
|
876
|
+
<code className="font-mono text-xs">apps/api/functions/</code> to add
|
|
877
|
+
handlers, or{" "}
|
|
878
|
+
<code className="font-mono text-xs">
|
|
879
|
+
apps/web/src/app/components/TodoList.tsx
|
|
880
|
+
</code>{" "}
|
|
881
|
+
for the UI.
|
|
882
|
+
</p>
|
|
883
|
+
</header>
|
|
884
|
+
|
|
885
|
+
<TodoList initialTodos={todos} />
|
|
886
|
+
</main>
|
|
887
|
+
);
|
|
888
888
|
}
|
|
889
889
|
`,
|
|
890
890
|
);
|
|
@@ -897,29 +897,29 @@ import { useState, useTransition, useRef, useEffect } from "react";
|
|
|
897
897
|
import { Button } from "@${projectName}/ui";
|
|
898
898
|
import { Input } from "@${projectName}/ui";
|
|
899
899
|
import {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
900
|
+
DndContext,
|
|
901
|
+
closestCenter,
|
|
902
|
+
KeyboardSensor,
|
|
903
|
+
PointerSensor,
|
|
904
|
+
useSensor,
|
|
905
|
+
useSensors,
|
|
906
|
+
type DragEndEvent,
|
|
907
907
|
} from "@dnd-kit/core";
|
|
908
908
|
import {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
909
|
+
arrayMove,
|
|
910
|
+
SortableContext,
|
|
911
|
+
sortableKeyboardCoordinates,
|
|
912
|
+
useSortable,
|
|
913
|
+
verticalListSortingStrategy,
|
|
914
914
|
} from "@dnd-kit/sortable";
|
|
915
915
|
import { CSS } from "@dnd-kit/utilities";
|
|
916
916
|
|
|
917
917
|
type Todo = {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
918
|
+
id: string;
|
|
919
|
+
title: string;
|
|
920
|
+
done: boolean;
|
|
921
|
+
createdAt: string;
|
|
922
|
+
position?: number;
|
|
923
923
|
};
|
|
924
924
|
|
|
925
925
|
/**
|
|
@@ -929,265 +929,265 @@ type Todo = {
|
|
|
929
929
|
* the midpoint between its new neighbors and POST it to reorderTodo.
|
|
930
930
|
*/
|
|
931
931
|
export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
932
|
+
const [todos, setTodos] = useState(initialTodos);
|
|
933
|
+
const [title, setTitle] = useState("");
|
|
934
|
+
const [pending, startTransition] = useTransition();
|
|
935
|
+
const sensors = useSensors(
|
|
936
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
937
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
async function add() {
|
|
941
|
+
if (!title.trim()) return;
|
|
942
|
+
const newTitle = title;
|
|
943
|
+
setTitle("");
|
|
944
|
+
startTransition(async () => {
|
|
945
|
+
const res = await fetch("/api/fn/addTodo", {
|
|
946
|
+
method: "POST",
|
|
947
|
+
headers: { "Content-Type": "application/json" },
|
|
948
|
+
body: JSON.stringify({ title: newTitle }),
|
|
949
|
+
});
|
|
950
|
+
if (res.ok) {
|
|
951
|
+
const todo = (await res.json()) as Todo;
|
|
952
|
+
setTodos((prev) => [...prev, todo]);
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function toggle(t: Todo) {
|
|
958
|
+
const next = !t.done;
|
|
959
|
+
setTodos((prev) =>
|
|
960
|
+
prev.map((row) => (row.id === t.id ? { ...row, done: next } : row)),
|
|
961
|
+
);
|
|
962
|
+
startTransition(async () => {
|
|
963
|
+
const res = await fetch("/api/fn/toggleTodo", {
|
|
964
|
+
method: "POST",
|
|
965
|
+
headers: { "Content-Type": "application/json" },
|
|
966
|
+
body: JSON.stringify({ id: t.id, done: next }),
|
|
967
|
+
});
|
|
968
|
+
if (!res.ok) {
|
|
969
|
+
setTodos((prev) =>
|
|
970
|
+
prev.map((row) => (row.id === t.id ? { ...row, done: t.done } : row)),
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function remove(t: Todo) {
|
|
977
|
+
const snapshot = todos;
|
|
978
|
+
setTodos((prev) => prev.filter((row) => row.id !== t.id));
|
|
979
|
+
startTransition(async () => {
|
|
980
|
+
const res = await fetch("/api/fn/deleteTodo", {
|
|
981
|
+
method: "POST",
|
|
982
|
+
headers: { "Content-Type": "application/json" },
|
|
983
|
+
body: JSON.stringify({ id: t.id }),
|
|
984
|
+
});
|
|
985
|
+
if (!res.ok) setTodos(snapshot);
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function rename(t: Todo, newTitle: string) {
|
|
990
|
+
const trimmed = newTitle.trim();
|
|
991
|
+
if (!trimmed || trimmed === t.title) return;
|
|
992
|
+
setTodos((prev) =>
|
|
993
|
+
prev.map((row) => (row.id === t.id ? { ...row, title: trimmed } : row)),
|
|
994
|
+
);
|
|
995
|
+
startTransition(async () => {
|
|
996
|
+
const res = await fetch("/api/fn/editTodo", {
|
|
997
|
+
method: "POST",
|
|
998
|
+
headers: { "Content-Type": "application/json" },
|
|
999
|
+
body: JSON.stringify({ id: t.id, title: trimmed }),
|
|
1000
|
+
});
|
|
1001
|
+
if (!res.ok) {
|
|
1002
|
+
setTodos((prev) =>
|
|
1003
|
+
prev.map((row) => (row.id === t.id ? { ...row, title: t.title } : row)),
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function onDragEnd(e: DragEndEvent) {
|
|
1010
|
+
const { active, over } = e;
|
|
1011
|
+
if (!over || active.id === over.id) return;
|
|
1012
|
+
const oldIndex = todos.findIndex((t) => t.id === active.id);
|
|
1013
|
+
const newIndex = todos.findIndex((t) => t.id === over.id);
|
|
1014
|
+
if (oldIndex < 0 || newIndex < 0) return;
|
|
1015
|
+
const reordered = arrayMove(todos, oldIndex, newIndex);
|
|
1016
|
+
setTodos(reordered);
|
|
1017
|
+
const prev = reordered[newIndex - 1];
|
|
1018
|
+
const next = reordered[newIndex + 1];
|
|
1019
|
+
const prevPos = prev?.position ?? Date.parse(prev?.createdAt ?? "") ?? 0;
|
|
1020
|
+
const nextPos = next?.position ?? Date.parse(next?.createdAt ?? "") ?? 0;
|
|
1021
|
+
let position: number;
|
|
1022
|
+
if (prev && next) position = (prevPos + nextPos) / 2;
|
|
1023
|
+
else if (prev) position = prevPos + 1024;
|
|
1024
|
+
else if (next) position = nextPos - 1024;
|
|
1025
|
+
else position = 1024;
|
|
1026
|
+
const movedId = String(active.id);
|
|
1027
|
+
const snapshot = todos;
|
|
1028
|
+
startTransition(async () => {
|
|
1029
|
+
const res = await fetch("/api/fn/reorderTodo", {
|
|
1030
|
+
method: "POST",
|
|
1031
|
+
headers: { "Content-Type": "application/json" },
|
|
1032
|
+
body: JSON.stringify({ id: movedId, position }),
|
|
1033
|
+
});
|
|
1034
|
+
if (!res.ok) setTodos(snapshot);
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
return (
|
|
1039
|
+
<div className="space-y-4">
|
|
1040
|
+
<form
|
|
1041
|
+
onSubmit={(e) => {
|
|
1042
|
+
e.preventDefault();
|
|
1043
|
+
add();
|
|
1044
|
+
}}
|
|
1045
|
+
className="flex gap-2"
|
|
1046
|
+
>
|
|
1047
|
+
<Input
|
|
1048
|
+
value={title}
|
|
1049
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
1050
|
+
placeholder="What needs doing?"
|
|
1051
|
+
disabled={pending}
|
|
1052
|
+
className="flex-1"
|
|
1053
|
+
/>
|
|
1054
|
+
<Button
|
|
1055
|
+
type="submit"
|
|
1056
|
+
variant="primary"
|
|
1057
|
+
disabled={pending || !title.trim()}
|
|
1058
|
+
>
|
|
1059
|
+
Add
|
|
1060
|
+
</Button>
|
|
1061
|
+
</form>
|
|
1062
|
+
|
|
1063
|
+
{todos.length === 0 ? (
|
|
1064
|
+
<p className="text-sm text-neutral-500 dark:text-neutral-400 text-center py-8">
|
|
1065
|
+
No todos yet. Add one above.
|
|
1066
|
+
</p>
|
|
1067
|
+
) : (
|
|
1068
|
+
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
|
1069
|
+
<SortableContext items={todos.map((t) => t.id)} strategy={verticalListSortingStrategy}>
|
|
1070
|
+
<ul className="divide-y divide-neutral-200 dark:divide-neutral-800 rounded-md border border-neutral-200 dark:border-neutral-800">
|
|
1071
|
+
{todos.map((t) => (
|
|
1072
|
+
<SortableRow
|
|
1073
|
+
key={t.id}
|
|
1074
|
+
todo={t}
|
|
1075
|
+
pending={pending}
|
|
1076
|
+
onToggle={() => toggle(t)}
|
|
1077
|
+
onRemove={() => remove(t)}
|
|
1078
|
+
onRename={(next) => rename(t, next)}
|
|
1079
|
+
/>
|
|
1080
|
+
))}
|
|
1081
|
+
</ul>
|
|
1082
|
+
</SortableContext>
|
|
1083
|
+
</DndContext>
|
|
1084
|
+
)}
|
|
1085
|
+
</div>
|
|
1086
|
+
);
|
|
1087
1087
|
}
|
|
1088
1088
|
|
|
1089
1089
|
function SortableRow({ todo, pending, onToggle, onRemove, onRename }: {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1090
|
+
todo: Todo;
|
|
1091
|
+
pending: boolean;
|
|
1092
|
+
onToggle: () => void;
|
|
1093
|
+
onRemove: () => void;
|
|
1094
|
+
onRename: (next: string) => void;
|
|
1095
1095
|
}) {
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1096
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
|
|
1097
|
+
useSortable({ id: todo.id });
|
|
1098
|
+
const style = {
|
|
1099
|
+
transform: CSS.Transform.toString(transform),
|
|
1100
|
+
transition,
|
|
1101
|
+
opacity: isDragging ? 0.4 : 1,
|
|
1102
|
+
};
|
|
1103
|
+
const [editing, setEditing] = useState(false);
|
|
1104
|
+
const [draft, setDraft] = useState(todo.title);
|
|
1105
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
1106
|
+
|
|
1107
|
+
useEffect(() => {
|
|
1108
|
+
if (editing) {
|
|
1109
|
+
setDraft(todo.title);
|
|
1110
|
+
requestAnimationFrame(() => {
|
|
1111
|
+
inputRef.current?.focus();
|
|
1112
|
+
inputRef.current?.select();
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
}, [editing, todo.title]);
|
|
1116
|
+
|
|
1117
|
+
function commit() {
|
|
1118
|
+
setEditing(false);
|
|
1119
|
+
onRename(draft);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
return (
|
|
1123
|
+
<li
|
|
1124
|
+
ref={setNodeRef}
|
|
1125
|
+
style={style}
|
|
1126
|
+
className="flex items-center gap-3 px-4 py-3 text-sm group bg-white dark:bg-neutral-950"
|
|
1127
|
+
>
|
|
1128
|
+
<button
|
|
1129
|
+
type="button"
|
|
1130
|
+
{...attributes}
|
|
1131
|
+
{...listeners}
|
|
1132
|
+
className="cursor-grab active:cursor-grabbing text-neutral-300 hover:text-neutral-500 select-none touch-none"
|
|
1133
|
+
aria-label="Drag to reorder"
|
|
1134
|
+
tabIndex={-1}
|
|
1135
|
+
>
|
|
1136
|
+
⋮⋮
|
|
1137
|
+
</button>
|
|
1138
|
+
<input
|
|
1139
|
+
type="checkbox"
|
|
1140
|
+
checked={todo.done}
|
|
1141
|
+
onChange={onToggle}
|
|
1142
|
+
disabled={pending}
|
|
1143
|
+
className="size-4 cursor-pointer"
|
|
1144
|
+
aria-label={\\\`Mark "\${todo.title}" as \${todo.done ? "not done" : "done"}\\\`}
|
|
1145
|
+
/>
|
|
1146
|
+
{editing ? (
|
|
1147
|
+
<input
|
|
1148
|
+
ref={inputRef}
|
|
1149
|
+
value={draft}
|
|
1150
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
1151
|
+
onBlur={commit}
|
|
1152
|
+
onKeyDown={(e) => {
|
|
1153
|
+
if (e.key === "Enter") commit();
|
|
1154
|
+
else if (e.key === "Escape") {
|
|
1155
|
+
setEditing(false);
|
|
1156
|
+
setDraft(todo.title);
|
|
1157
|
+
}
|
|
1158
|
+
}}
|
|
1159
|
+
className="flex-1 bg-transparent border-b border-neutral-300 dark:border-neutral-700 outline-none text-sm"
|
|
1160
|
+
aria-label="Edit title"
|
|
1161
|
+
/>
|
|
1162
|
+
) : (
|
|
1163
|
+
<button
|
|
1164
|
+
type="button"
|
|
1165
|
+
onDoubleClick={() => setEditing(true)}
|
|
1166
|
+
className={\\\`flex-1 text-left \${todo.done ? "line-through text-neutral-400" : ""}\\\`}
|
|
1167
|
+
title="Double-click to edit"
|
|
1168
|
+
>
|
|
1169
|
+
{todo.title}
|
|
1170
|
+
</button>
|
|
1171
|
+
)}
|
|
1172
|
+
<button
|
|
1173
|
+
type="button"
|
|
1174
|
+
onClick={() => setEditing(true)}
|
|
1175
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity text-xs text-neutral-500 hover:text-neutral-800 dark:hover:text-neutral-200"
|
|
1176
|
+
aria-label={\\\`Edit "\${todo.title}"\\\`}
|
|
1177
|
+
>
|
|
1178
|
+
Edit
|
|
1179
|
+
</button>
|
|
1180
|
+
<button
|
|
1181
|
+
type="button"
|
|
1182
|
+
onClick={onRemove}
|
|
1183
|
+
disabled={pending}
|
|
1184
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity text-xs text-neutral-500 hover:text-red-500"
|
|
1185
|
+
aria-label={\\\`Delete "\${todo.title}"\\\`}
|
|
1186
|
+
>
|
|
1187
|
+
Delete
|
|
1188
|
+
</button>
|
|
1189
|
+
</li>
|
|
1190
|
+
);
|
|
1191
1191
|
}
|
|
1192
1192
|
`,
|
|
1193
1193
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/create-pylon",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.51",
|
|
4
4
|
"description": "Scaffold a new Pylon app — realtime backend + Next.js frontend in one command. Run via `npm create @pylonsync/pylon@latest`.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|