@pylonsync/create-pylon 0.3.50 → 0.3.53

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 (65) hide show
  1. package/bin/create-pylon.js +292 -1157
  2. package/package.json +4 -3
  3. package/templates/_root/.env.example +9 -0
  4. package/templates/_root/README.md +43 -0
  5. package/templates/backend/barebones/apps/api/functions/createWidget.ts +22 -0
  6. package/templates/backend/barebones/apps/api/functions/listWidgets.ts +18 -0
  7. package/templates/backend/barebones/apps/api/package.json +20 -0
  8. package/templates/backend/barebones/apps/api/schema.ts +61 -0
  9. package/templates/backend/barebones/apps/api/tsconfig.json +13 -0
  10. package/templates/backend/todo/apps/api/functions/addTodo.ts +27 -0
  11. package/templates/backend/todo/apps/api/functions/deleteTodo.ts +14 -0
  12. package/templates/backend/todo/apps/api/functions/editTodo.ts +16 -0
  13. package/templates/backend/todo/apps/api/functions/listTodos.ts +24 -0
  14. package/templates/backend/todo/apps/api/functions/reorderTodo.ts +14 -0
  15. package/templates/backend/todo/apps/api/functions/toggleTodo.ts +13 -0
  16. package/templates/backend/todo/apps/api/package.json +20 -0
  17. package/templates/backend/todo/apps/api/schema.ts +85 -0
  18. package/templates/backend/todo/apps/api/tsconfig.json +13 -0
  19. package/templates/expo/barebones/apps/expo/App.tsx +166 -0
  20. package/templates/expo/barebones/apps/expo/app.json +31 -0
  21. package/templates/expo/barebones/apps/expo/babel.config.js +6 -0
  22. package/templates/expo/barebones/apps/expo/package.json +30 -0
  23. package/templates/expo/barebones/apps/expo/tsconfig.json +16 -0
  24. package/templates/expo/todo/apps/expo/App.tsx +287 -0
  25. package/templates/expo/todo/apps/expo/app.json +25 -0
  26. package/templates/expo/todo/apps/expo/babel.config.js +6 -0
  27. package/templates/expo/todo/apps/expo/package.json +30 -0
  28. package/templates/expo/todo/apps/expo/tsconfig.json +16 -0
  29. package/templates/mobile/barebones/apps/mobile/Package.swift +34 -0
  30. package/templates/mobile/barebones/apps/mobile/Sources/__APP_NAME_PASCAL__/ContentView.swift +98 -0
  31. package/templates/mobile/barebones/apps/mobile/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  32. package/templates/mobile/barebones/apps/mobile/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -0
  33. package/templates/mobile/barebones/apps/mobile/project.yml +42 -0
  34. package/templates/mobile/todo/apps/mobile/Package.swift +23 -0
  35. package/templates/mobile/todo/apps/mobile/Sources/__APP_NAME_PASCAL__/Models.swift +18 -0
  36. package/templates/mobile/todo/apps/mobile/Sources/__APP_NAME_PASCAL__/TodoListView.swift +230 -0
  37. package/templates/mobile/todo/apps/mobile/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +28 -0
  38. package/templates/mobile/todo/apps/mobile/project.yml +32 -0
  39. package/templates/ui/packages/ui/package.json +26 -0
  40. package/templates/ui/packages/ui/src/button.tsx +44 -0
  41. package/templates/ui/packages/ui/src/card.tsx +39 -0
  42. package/templates/ui/packages/ui/src/cn.ts +12 -0
  43. package/templates/ui/packages/ui/src/index.ts +4 -0
  44. package/templates/ui/packages/ui/src/input.tsx +19 -0
  45. package/templates/ui/packages/ui/tsconfig.json +15 -0
  46. package/templates/web/barebones/apps/web/next-env.d.ts +2 -0
  47. package/templates/web/barebones/apps/web/next.config.ts +40 -0
  48. package/templates/web/barebones/apps/web/package.json +29 -0
  49. package/templates/web/barebones/apps/web/postcss.config.mjs +4 -0
  50. package/templates/web/barebones/apps/web/src/app/components/WidgetList.tsx +81 -0
  51. package/templates/web/barebones/apps/web/src/app/globals.css +9 -0
  52. package/templates/web/barebones/apps/web/src/app/layout.tsx +21 -0
  53. package/templates/web/barebones/apps/web/src/app/page.tsx +43 -0
  54. package/templates/web/barebones/apps/web/src/lib/pylon.ts +14 -0
  55. package/templates/web/barebones/apps/web/tsconfig.json +26 -0
  56. package/templates/web/todo/apps/web/next-env.d.ts +2 -0
  57. package/templates/web/todo/apps/web/next.config.ts +24 -0
  58. package/templates/web/todo/apps/web/package.json +32 -0
  59. package/templates/web/todo/apps/web/postcss.config.mjs +3 -0
  60. package/templates/web/todo/apps/web/src/app/components/TodoList.tsx +310 -0
  61. package/templates/web/todo/apps/web/src/app/globals.css +6 -0
  62. package/templates/web/todo/apps/web/src/app/layout.tsx +21 -0
  63. package/templates/web/todo/apps/web/src/app/page.tsx +36 -0
  64. package/templates/web/todo/apps/web/src/lib/pylon.ts +5 -0
  65. package/templates/web/todo/apps/web/tsconfig.json +26 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.3.50",
4
- "description": "Scaffold a new Pylon app — realtime backend + Next.js frontend in one command. Run via `npm create @pylonsync/pylon@latest`.",
3
+ "version": "0.3.53",
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"
7
7
  },
@@ -10,7 +10,8 @@
10
10
  "create-pylon": "./bin/create-pylon.js"
11
11
  },
12
12
  "files": [
13
- "bin"
13
+ "bin",
14
+ "templates"
14
15
  ],
15
16
  "engines": {
16
17
  "node": ">=18"
@@ -0,0 +1,9 @@
1
+ # Backend port the Pylon control plane listens on.
2
+ PYLON_PORT=4321
3
+
4
+ # Where the Next.js dev server can reach the control plane.
5
+ PYLON_TARGET=http://localhost:4321
6
+
7
+ # Cookie name the auth helpers look for.
8
+ # Pattern: `${app_name}_session` from the Pylon manifest.
9
+ PYLON_COOKIE_NAME=__APP_NAME_SNAKE___session
@@ -0,0 +1,43 @@
1
+ # __APP_NAME__
2
+
3
+ Scaffolded by [@pylonsync/create-pylon](https://npmjs.com/@pylonsync/create-pylon).
4
+
5
+ ## Layout
6
+
7
+ ```
8
+ apps/
9
+ api/ Pylon backend — schema, policies, function handlers
10
+ web/ Next.js frontend (if you picked --platforms web)
11
+ mobile/ Swift / SwiftUI app (if you picked --platforms mobile)
12
+ expo/ Expo + React Native app (if you picked --platforms expo)
13
+
14
+ packages/
15
+ ui/ Shared shadcn-style React primitives (web only)
16
+ ```
17
+
18
+ ## Getting started
19
+
20
+ ```sh
21
+ # Install
22
+ bun install # or pnpm install / yarn / npm install
23
+
24
+ # Run everything (the API + every frontend you picked)
25
+ bun run dev
26
+ ```
27
+
28
+ - **api** on http://localhost:4321 — Pylon control plane
29
+ - **web** on http://localhost:3000 — Next.js (if scaffolded)
30
+ - **expo** runs Metro on a separate port (if scaffolded)
31
+ - **mobile** lives in `apps/mobile/` — open in Xcode or run `swift run`
32
+
33
+ ## What to do next
34
+
35
+ - Edit `apps/api/schema.ts` to add entities + policies.
36
+ - Drop handlers into `apps/api/functions/` — auto-discovered by name.
37
+ - For web: components in `apps/web/src/app/components/`.
38
+ - For mobile: SwiftUI views in `apps/mobile/Sources/__APP_NAME_PASCAL__/`.
39
+ - For expo: screens in `apps/expo/src/screens/`.
40
+
41
+ ## Docs
42
+
43
+ [pylonsync.com/docs](https://pylonsync.com/docs)
@@ -0,0 +1,22 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Insert a new Widget. Initializes count to 0 and stamps the time.
5
+ * Returns the inserted row so the client can render it without a
6
+ * follow-up read.
7
+ */
8
+ export default mutation({
9
+ args: { name: v.string() },
10
+ async handler(ctx, args: { name: string }) {
11
+ const trimmed = args.name.trim();
12
+ if (!trimmed) {
13
+ throw ctx.error("EMPTY_NAME", "name cannot be empty");
14
+ }
15
+ const id = await ctx.db.insert("Widget", {
16
+ name: trimmed,
17
+ count: 0,
18
+ createdAt: new Date().toISOString(),
19
+ });
20
+ return await ctx.db.get("Widget", id);
21
+ },
22
+ });
@@ -0,0 +1,18 @@
1
+ import { query } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Live query — every Widget, newest first. Subscribed via the React
5
+ * `useQuery` / Swift `PylonQuery` / RN `useQuery` hooks; rerenders
6
+ * whenever a row is inserted, updated, or deleted.
7
+ */
8
+ export default query({
9
+ args: {},
10
+ async handler(ctx) {
11
+ const rows = await ctx.db.query("Widget", {});
12
+ return [...rows].sort((a: any, b: any) => {
13
+ const ad = Date.parse(a.createdAt) || 0;
14
+ const bd = Date.parse(b.createdAt) || 0;
15
+ return bd - ad;
16
+ });
17
+ },
18
+ });
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@__APP_NAME_KEBAB__/api",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "pylon dev schema.ts --port 4321",
8
+ "build": "pylon codegen schema.ts --out pylon.manifest.json && pylon codegen client pylon.manifest.json --out pylon.client.ts",
9
+ "schema:push": "pylon schema push pylon.manifest.json --sqlite dev.db",
10
+ "schema:inspect": "pylon schema inspect --sqlite dev.db"
11
+ },
12
+ "dependencies": {
13
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
14
+ "@pylonsync/functions": "^__PYLON_VERSION__"
15
+ },
16
+ "devDependencies": {
17
+ "@pylonsync/cli": "^__PYLON_VERSION__",
18
+ "typescript": "^5.5.0"
19
+ }
20
+ }
@@ -0,0 +1,61 @@
1
+ import {
2
+ entity,
3
+ field,
4
+ query,
5
+ action,
6
+ policy,
7
+ buildManifest,
8
+ } from "@pylonsync/sdk";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Schema — one entity, two operations. The smallest thing that
12
+ // demonstrates the loop: declare → handler → live query.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const Widget = entity("Widget", {
16
+ name: field.string(),
17
+ count: field.int(),
18
+ createdAt: field.datetime(),
19
+ });
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Function declarations — names only. Implementations live under
23
+ // functions/<name>.ts and are auto-discovered by the runtime.
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const listWidgets = query("listWidgets");
27
+
28
+ const createWidget = action("createWidget", {
29
+ input: [{ name: "name", type: "string" }],
30
+ });
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Policies — wide-open by default. Tighten for production.
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const widgetPolicy = policy({
37
+ name: "widget_open",
38
+ entity: "Widget",
39
+ allowRead: "true",
40
+ allowInsert: "true",
41
+ allowUpdate: "true",
42
+ allowDelete: "true",
43
+ });
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Manifest — pylon codegen reads this and emits pylon.manifest.json.
47
+ // pylon dev / pylon codegen run `bun run schema.ts` and read the
48
+ // manifest off stdout — every entry file ends with this console.log.
49
+ // ---------------------------------------------------------------------------
50
+
51
+ const manifest = buildManifest({
52
+ name: "__APP_NAME_SNAKE__",
53
+ version: "0.0.1",
54
+ entities: [Widget],
55
+ queries: [listWidgets],
56
+ actions: [createWidget],
57
+ policies: [widgetPolicy],
58
+ routes: [],
59
+ });
60
+
61
+ console.log(JSON.stringify(manifest));
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "allowSyntheticDefaultImports": true
11
+ },
12
+ "include": ["schema.ts", "functions/**/*.ts"]
13
+ }
@@ -0,0 +1,27 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Insert a new Todo. Seeds `position` to (max + 1024) so new rows
5
+ * land at the end of the drag-reorder list; the 1024 step leaves
6
+ * room for inserts-between without needing global renumber.
7
+ */
8
+ export default mutation({
9
+ args: { title: v.string() },
10
+ async handler(ctx, args: { title: string }) {
11
+ const existing = await ctx.db.query("Todo", {});
12
+ const maxPos = existing.reduce((acc: number, row: any) => {
13
+ const p =
14
+ typeof row.position === "number"
15
+ ? row.position
16
+ : Date.parse(row.createdAt) || 0;
17
+ return p > acc ? p : acc;
18
+ }, 0);
19
+ const id = await ctx.db.insert("Todo", {
20
+ title: args.title,
21
+ done: false,
22
+ createdAt: new Date().toISOString(),
23
+ position: maxPos + 1024,
24
+ });
25
+ return await ctx.db.get("Todo", id);
26
+ },
27
+ });
@@ -0,0 +1,14 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Remove a Todo row. Returns the row as it existed pre-delete so
5
+ * the client can show a "todo removed" toast or animate it out.
6
+ */
7
+ export default mutation({
8
+ args: { id: v.id("Todo") },
9
+ async handler(ctx, args: { id: string }) {
10
+ const snapshot = await ctx.db.get("Todo", args.id);
11
+ await ctx.db.delete("Todo", args.id);
12
+ return snapshot;
13
+ },
14
+ });
@@ -0,0 +1,16 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Rename a Todo. Trims whitespace; rejects empty titles.
5
+ */
6
+ export default mutation({
7
+ args: { id: v.id("Todo"), title: v.string() },
8
+ async handler(ctx, args: { id: string; title: string }) {
9
+ const trimmed = args.title.trim();
10
+ if (!trimmed) {
11
+ throw ctx.error("EMPTY_TITLE", "title cannot be empty");
12
+ }
13
+ await ctx.db.update("Todo", args.id, { title: trimmed });
14
+ return await ctx.db.get("Todo", args.id);
15
+ },
16
+ });
@@ -0,0 +1,24 @@
1
+ import { query } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Live query — every Todo, in user-controlled drag-reorder position.
5
+ * Rows without a `position` (legacy data) get sorted by createdAt as
6
+ * a fallback so the list stays deterministic.
7
+ */
8
+ export default query({
9
+ args: {},
10
+ async handler(ctx) {
11
+ const rows = await ctx.db.query("Todo", {});
12
+ return [...rows].sort((a: any, b: any) => {
13
+ const ap =
14
+ typeof a.position === "number"
15
+ ? a.position
16
+ : Date.parse(a.createdAt) || 0;
17
+ const bp =
18
+ typeof b.position === "number"
19
+ ? b.position
20
+ : Date.parse(b.createdAt) || 0;
21
+ return ap - bp;
22
+ });
23
+ },
24
+ });
@@ -0,0 +1,14 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Drag-reorder. Frontend computes `position` as the midpoint of the
5
+ * drop target's neighbors; we just write it. Floats give us ~52 inserts
6
+ * between any two rows before precision matters.
7
+ */
8
+ export default mutation({
9
+ args: { id: v.id("Todo"), position: v.number() },
10
+ async handler(ctx, args: { id: string; position: number }) {
11
+ await ctx.db.update("Todo", args.id, { position: args.position });
12
+ return await ctx.db.get("Todo", args.id);
13
+ },
14
+ });
@@ -0,0 +1,13 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Flip the `done` flag on a Todo. Mutation, not action — needs
5
+ * `ctx.db.update` which is only on writable ctx variants.
6
+ */
7
+ export default mutation({
8
+ args: { id: v.id("Todo"), done: v.bool() },
9
+ async handler(ctx, args: { id: string; done: boolean }) {
10
+ await ctx.db.update("Todo", args.id, { done: args.done });
11
+ return await ctx.db.get("Todo", args.id);
12
+ },
13
+ });
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@__APP_NAME_KEBAB__/api",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "pylon dev schema.ts --port 4321",
8
+ "build": "pylon codegen schema.ts --out pylon.manifest.json && pylon codegen client pylon.manifest.json --out pylon.client.ts",
9
+ "schema:push": "pylon schema push pylon.manifest.json --sqlite dev.db",
10
+ "schema:inspect": "pylon schema inspect --sqlite dev.db"
11
+ },
12
+ "dependencies": {
13
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
14
+ "@pylonsync/functions": "^__PYLON_VERSION__"
15
+ },
16
+ "devDependencies": {
17
+ "@pylonsync/cli": "^__PYLON_VERSION__",
18
+ "typescript": "^5.5.0"
19
+ }
20
+ }
@@ -0,0 +1,85 @@
1
+ import {
2
+ entity,
3
+ field,
4
+ query,
5
+ action,
6
+ policy,
7
+ buildManifest,
8
+ } from "@pylonsync/sdk";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Schema
12
+ // ---------------------------------------------------------------------------
13
+
14
+ const Todo = entity("Todo", {
15
+ title: field.string(),
16
+ done: field.bool(),
17
+ createdAt: field.datetime(),
18
+ // Float position so drag-reorder can insert between two existing
19
+ // rows without renumbering the whole list. Frontend computes
20
+ // (prev.position + next.position) / 2 on drop. Optional for
21
+ // backwards compat with legacy rows.
22
+ position: field.float().optional(),
23
+ });
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Function declarations — names only. Implementations live under
27
+ // functions/<name>.ts and are auto-discovered by the runtime.
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const listTodos = query("listTodos");
31
+
32
+ const addTodo = action("addTodo", {
33
+ input: [{ name: "title", type: "string" }],
34
+ });
35
+
36
+ const toggleTodo = action("toggleTodo", {
37
+ input: [{ name: "id", type: "id(Todo)" }, { name: "done", type: "bool" }],
38
+ });
39
+
40
+ const deleteTodo = action("deleteTodo", {
41
+ input: [{ name: "id", type: "id(Todo)" }],
42
+ });
43
+
44
+ const editTodo = action("editTodo", {
45
+ input: [
46
+ { name: "id", type: "id(Todo)" },
47
+ { name: "title", type: "string" },
48
+ ],
49
+ });
50
+
51
+ const reorderTodo = action("reorderTodo", {
52
+ input: [
53
+ { name: "id", type: "id(Todo)" },
54
+ { name: "position", type: "float" },
55
+ ],
56
+ });
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Policies — wide-open by default. Tighten for production.
60
+ // ---------------------------------------------------------------------------
61
+
62
+ const todoPolicy = policy({
63
+ name: "todo_open",
64
+ entity: "Todo",
65
+ allowRead: "true",
66
+ allowInsert: "true",
67
+ allowUpdate: "true",
68
+ allowDelete: "true",
69
+ });
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Manifest
73
+ // ---------------------------------------------------------------------------
74
+
75
+ const manifest = buildManifest({
76
+ name: "__APP_NAME_SNAKE__",
77
+ version: "0.0.1",
78
+ entities: [Todo],
79
+ queries: [listTodos],
80
+ actions: [addTodo, toggleTodo, deleteTodo, editTodo, reorderTodo],
81
+ policies: [todoPolicy],
82
+ routes: [],
83
+ });
84
+
85
+ console.log(JSON.stringify(manifest));
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "allowSyntheticDefaultImports": true
11
+ },
12
+ "include": ["schema.ts", "functions/**/*.ts"]
13
+ }
@@ -0,0 +1,166 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ View,
4
+ Text,
5
+ TextInput,
6
+ Pressable,
7
+ FlatList,
8
+ ActivityIndicator,
9
+ StyleSheet,
10
+ Platform,
11
+ } from "react-native";
12
+ import { StatusBar } from "expo-status-bar";
13
+ import { init, db, callFn } from "@pylonsync/react-native";
14
+
15
+ // In dev, point at the Pylon control plane on your machine. The iOS
16
+ // simulator can reach `localhost`; an Android emulator uses `10.0.2.2`;
17
+ // a physical device needs your LAN IP. Override via `PYLON_BASE_URL`
18
+ // in app.json `extra` or via an `.env`.
19
+ const PYLON_BASE_URL =
20
+ process.env.EXPO_PUBLIC_PYLON_BASE_URL ??
21
+ (Platform.OS === "android" ? "http://10.0.2.2:4321" : "http://localhost:4321");
22
+
23
+ type Widget = {
24
+ id: string;
25
+ name: string;
26
+ count: number;
27
+ createdAt: string;
28
+ };
29
+
30
+ let initPromise: Promise<void> | null = null;
31
+ function ensureInit() {
32
+ if (!initPromise) {
33
+ initPromise = init({
34
+ baseUrl: PYLON_BASE_URL,
35
+ appName: "__APP_NAME_SNAKE__",
36
+ });
37
+ }
38
+ return initPromise;
39
+ }
40
+
41
+ export default function App() {
42
+ const [ready, setReady] = useState(false);
43
+ useEffect(() => {
44
+ ensureInit().then(() => setReady(true));
45
+ }, []);
46
+
47
+ if (!ready) {
48
+ return (
49
+ <View style={[styles.screen, styles.center]}>
50
+ <ActivityIndicator />
51
+ </View>
52
+ );
53
+ }
54
+ return <Home />;
55
+ }
56
+
57
+ function Home() {
58
+ const { data: widgets, loading } = db.useQuery<Widget>("Widget", {});
59
+ const [name, setName] = useState("");
60
+ const [creating, setCreating] = useState(false);
61
+
62
+ async function create() {
63
+ const trimmed = name.trim();
64
+ if (!trimmed) return;
65
+ setCreating(true);
66
+ try {
67
+ await callFn("createWidget", { name: trimmed });
68
+ setName("");
69
+ } finally {
70
+ setCreating(false);
71
+ }
72
+ }
73
+
74
+ return (
75
+ <View style={styles.screen}>
76
+ <StatusBar style="auto" />
77
+ <Text style={styles.title}>__APP_NAME__</Text>
78
+ <Text style={styles.subtitle}>
79
+ Pylon backend → Expo + React Native, live-updating list.
80
+ </Text>
81
+
82
+ <View style={styles.row}>
83
+ <TextInput
84
+ style={styles.input}
85
+ placeholder="Name a widget…"
86
+ value={name}
87
+ onChangeText={setName}
88
+ editable={!creating}
89
+ onSubmitEditing={create}
90
+ autoCorrect={false}
91
+ />
92
+ <Pressable
93
+ onPress={create}
94
+ disabled={creating || !name.trim()}
95
+ style={({ pressed }) => [
96
+ styles.button,
97
+ (creating || !name.trim()) && styles.buttonDisabled,
98
+ pressed && styles.buttonPressed,
99
+ ]}
100
+ >
101
+ <Text style={styles.buttonLabel}>Create</Text>
102
+ </Pressable>
103
+ </View>
104
+
105
+ {loading ? (
106
+ <ActivityIndicator style={{ marginTop: 24 }} />
107
+ ) : (widgets ?? []).length === 0 ? (
108
+ <Text style={styles.empty}>No widgets yet. Create one above.</Text>
109
+ ) : (
110
+ <FlatList
111
+ data={widgets ?? []}
112
+ keyExtractor={(w) => w.id}
113
+ contentContainerStyle={{ paddingTop: 16 }}
114
+ ItemSeparatorComponent={() => <View style={styles.separator} />}
115
+ renderItem={({ item }) => (
116
+ <View style={styles.item}>
117
+ <Text style={styles.itemName}>{item.name}</Text>
118
+ <Text style={styles.itemCount}>count: {item.count}</Text>
119
+ </View>
120
+ )}
121
+ />
122
+ )}
123
+ </View>
124
+ );
125
+ }
126
+
127
+ const styles = StyleSheet.create({
128
+ screen: {
129
+ flex: 1,
130
+ paddingTop: 64,
131
+ paddingHorizontal: 20,
132
+ backgroundColor: "#fff",
133
+ },
134
+ center: { alignItems: "center", justifyContent: "center" },
135
+ title: { fontSize: 28, fontWeight: "600" },
136
+ subtitle: { color: "#666", marginTop: 4, marginBottom: 20 },
137
+ row: { flexDirection: "row", gap: 8 },
138
+ input: {
139
+ flex: 1,
140
+ borderWidth: 1,
141
+ borderColor: "#d4d4d8",
142
+ borderRadius: 6,
143
+ paddingHorizontal: 12,
144
+ paddingVertical: 8,
145
+ fontSize: 14,
146
+ },
147
+ button: {
148
+ backgroundColor: "#171717",
149
+ borderRadius: 6,
150
+ paddingHorizontal: 16,
151
+ justifyContent: "center",
152
+ },
153
+ buttonDisabled: { opacity: 0.5 },
154
+ buttonPressed: { opacity: 0.8 },
155
+ buttonLabel: { color: "#fff", fontWeight: "600" },
156
+ empty: { textAlign: "center", color: "#999", marginTop: 32 },
157
+ item: {
158
+ flexDirection: "row",
159
+ alignItems: "center",
160
+ justifyContent: "space-between",
161
+ paddingVertical: 12,
162
+ },
163
+ itemName: { fontSize: 15, fontWeight: "500" },
164
+ itemCount: { fontSize: 12, fontFamily: "Menlo", color: "#999" },
165
+ separator: { height: 1, backgroundColor: "#e5e5e5" },
166
+ });
@@ -0,0 +1,31 @@
1
+ {
2
+ "expo": {
3
+ "name": "__APP_NAME__",
4
+ "slug": "__APP_NAME_KEBAB__",
5
+ "version": "0.0.1",
6
+ "orientation": "portrait",
7
+ "icon": "./assets/icon.png",
8
+ "userInterfaceStyle": "automatic",
9
+ "splash": {
10
+ "image": "./assets/splash.png",
11
+ "resizeMode": "contain",
12
+ "backgroundColor": "#ffffff"
13
+ },
14
+ "ios": {
15
+ "supportsTablet": true,
16
+ "bundleIdentifier": "com.example.__APP_NAME_SNAKE__",
17
+ "infoPlist": {
18
+ "NSAppTransportSecurity": {
19
+ "NSAllowsLocalNetworking": true
20
+ }
21
+ }
22
+ },
23
+ "android": {
24
+ "package": "com.example.__APP_NAME_SNAKE__",
25
+ "usesCleartextTraffic": true
26
+ },
27
+ "web": {
28
+ "bundler": "metro"
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = function (api) {
2
+ api.cache(true);
3
+ return {
4
+ presets: ["babel-preset-expo"],
5
+ };
6
+ };