@pylonsync/create-pylon 0.3.54 → 0.3.57

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 (46) hide show
  1. package/bin/create-pylon.js +28 -51
  2. package/package.json +1 -1
  3. package/templates/_root/turbo.json +18 -0
  4. package/templates/backend/b2b/apps/api/package.json +3 -3
  5. package/templates/backend/barebones/apps/api/package.json +3 -3
  6. package/templates/backend/chat/apps/api/package.json +3 -3
  7. package/templates/backend/consumer/apps/api/package.json +3 -3
  8. package/templates/backend/todo/apps/api/package.json +3 -3
  9. package/templates/expo/barebones/apps/expo/package.json +1 -0
  10. package/templates/expo/chat/apps/expo/App.tsx +126 -156
  11. package/templates/expo/chat/apps/expo/package.json +1 -0
  12. package/templates/expo/consumer/apps/expo/App.tsx +211 -179
  13. package/templates/expo/consumer/apps/expo/package.json +1 -0
  14. package/templates/expo/todo/apps/expo/package.json +1 -0
  15. package/templates/ios/chat/apps/ios/Package.swift +1 -11
  16. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +26 -30
  17. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +35 -36
  18. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +27 -4
  19. package/templates/ios/chat/apps/ios/project.yml +2 -0
  20. package/templates/ios/consumer/apps/ios/Package.swift +1 -0
  21. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +81 -52
  22. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +6 -14
  23. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +14 -6
  24. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -15
  25. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +33 -5
  26. package/templates/ios/consumer/apps/ios/project.yml +2 -0
  27. package/templates/mac/chat/apps/mac/Package.swift +1 -0
  28. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +30 -27
  29. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +35 -36
  30. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +26 -5
  31. package/templates/mac/chat/apps/mac/project.yml +2 -0
  32. package/templates/mac/consumer/apps/mac/Package.swift +1 -0
  33. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +81 -52
  34. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +6 -14
  35. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +14 -6
  36. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -15
  37. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -6
  38. package/templates/mac/consumer/apps/mac/project.yml +2 -0
  39. package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +52 -71
  40. package/templates/web/chat/apps/web/src/app/layout.tsx +3 -2
  41. package/templates/web/chat/apps/web/src/app/page.tsx +5 -44
  42. package/templates/web/chat/apps/web/src/app/providers.tsx +25 -0
  43. package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +139 -119
  44. package/templates/web/consumer/apps/web/src/app/layout.tsx +3 -2
  45. package/templates/web/consumer/apps/web/src/app/page.tsx +8 -42
  46. package/templates/web/consumer/apps/web/src/app/providers.tsx +25 -0
@@ -340,28 +340,37 @@ walkAndSubstitute(root);
340
340
  // picked (each PM exposes "run X in workspace Y" differently).
341
341
  // ---------------------------------------------------------------------------
342
342
 
343
- const wsScripts = pmScripts(flags.pm);
344
- const devScripts = {};
345
- // API runs always every frontend connects to it.
346
- devScripts["dev:api"] = wsScripts.devApi;
347
- if (platforms.includes("web")) devScripts["dev:web"] = wsScripts.devWeb;
348
- if (platforms.includes("expo")) devScripts["dev:expo"] = wsScripts.devExpo;
343
+ // Turborepo orchestrates the workspace. `turbo dev` runs the `dev`
344
+ // task in every package that defines one (apps/api always; apps/web,
345
+ // apps/expo when scaffolded). Native targets (ios, mac) aren't
346
+ // `turbo dev`-shaped Xcode / `swift run` block — so they get
347
+ // dedicated escape-hatch scripts instead. turbo.json ships in
348
+ // _root/, so it's already in the project.
349
+ const helperScripts = {};
349
350
  if (platforms.includes("ios")) {
350
- // `xcodegen generate` materializes the .xcodeproj from project.yml,
351
- // then it's an Xcode-driven flow — no `bun run dev` semantics.
352
- devScripts["dev:ios"] =
351
+ helperScripts["dev:ios"] =
353
352
  "echo 'cd apps/ios && xcodegen generate && open *.xcodeproj (or: swift run for a quick macOS preview)'";
354
353
  }
355
354
  if (platforms.includes("mac")) {
356
- devScripts["dev:mac"] =
355
+ helperScripts["dev:mac"] =
357
356
  "echo 'cd apps/mac && swift run (or: xcodegen generate && open *.xcodeproj)'";
358
357
  }
359
358
 
360
- const parallelDevs = Object.keys(devScripts);
359
+ // Turbo 2.x refuses to run without packageManager set. Pick a recent-
360
+ // stable for whichever PM the user picked. npm doesn't enforce this
361
+ // field but turbo still expects it to be present.
362
+ const PACKAGE_MANAGERS = {
363
+ bun: "bun@1.2.19",
364
+ pnpm: "pnpm@9.12.0",
365
+ yarn: "yarn@4.5.0",
366
+ npm: "npm@10.9.0",
367
+ };
368
+
361
369
  const rootPkg = {
362
370
  name: APP_NAME_KEBAB,
363
371
  private: true,
364
372
  type: "module",
373
+ packageManager: PACKAGE_MANAGERS[flags.pm],
365
374
  workspaces: ["apps/*", "packages/*"].filter((p) => {
366
375
  // Only declare packages/* as a workspace if we actually scaffolded
367
376
  // packages/ui — otherwise the empty match warns on bun install.
@@ -369,14 +378,15 @@ const rootPkg = {
369
378
  return true;
370
379
  }),
371
380
  scripts: {
372
- dev:
373
- parallelDevs.length > 1
374
- ? `npm-run-all --parallel ${parallelDevs.join(" ")}`
375
- : wsScripts.devApi,
376
- ...devScripts,
377
- build: wsScripts.build,
381
+ dev: "turbo dev",
382
+ build: "turbo build",
383
+ check: "turbo check",
384
+ lint: "turbo lint",
385
+ ...helperScripts,
386
+ },
387
+ devDependencies: {
388
+ turbo: "^2.3.0",
378
389
  },
379
- devDependencies: parallelDevs.length > 1 ? { "npm-run-all": "^4.1.5" } : {},
380
390
  };
381
391
  writeFileSync(
382
392
  join(root, "package.json"),
@@ -457,36 +467,3 @@ function detectPackageManager() {
457
467
  return null;
458
468
  }
459
469
 
460
- function pmScripts(pm) {
461
- switch (pm) {
462
- case "bun":
463
- return {
464
- devApi: "bun run --filter './apps/api' dev",
465
- devWeb: "bun run --filter './apps/web' dev",
466
- devExpo: "bun run --filter './apps/expo' start",
467
- build: "bun run --filter '*' build",
468
- };
469
- case "pnpm":
470
- return {
471
- devApi: "pnpm --filter './apps/api' run dev",
472
- devWeb: "pnpm --filter './apps/web' run dev",
473
- devExpo: "pnpm --filter './apps/expo' run start",
474
- build: "pnpm --filter '*' run build",
475
- };
476
- case "yarn":
477
- return {
478
- devApi: `yarn workspace @${APP_NAME_KEBAB}/api run dev`,
479
- devWeb: `yarn workspace @${APP_NAME_KEBAB}/web run dev`,
480
- devExpo: `yarn workspace @${APP_NAME_KEBAB}/expo run start`,
481
- build: "yarn workspaces foreach -A run build",
482
- };
483
- case "npm":
484
- default:
485
- return {
486
- devApi: "npm --workspace apps/api run dev",
487
- devWeb: "npm --workspace apps/web run dev",
488
- devExpo: "npm --workspace apps/expo run start",
489
- build: "npm --workspaces run build --if-present",
490
- };
491
- }
492
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.3.54",
3
+ "version": "0.3.57",
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,18 @@
1
+ {
2
+ "$schema": "https://turbo.build/schema.json",
3
+ "ui": "stream",
4
+ "tasks": {
5
+ "dev": {
6
+ "cache": false,
7
+ "persistent": true
8
+ },
9
+ "build": {
10
+ "dependsOn": ["^build"],
11
+ "outputs": [".next/**", "!.next/cache/**", "dist/**"]
12
+ },
13
+ "check": {
14
+ "dependsOn": ["^build"]
15
+ },
16
+ "lint": {}
17
+ }
18
+ }
@@ -4,9 +4,9 @@
4
4
  "private": true,
5
5
  "type": "module",
6
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",
7
+ "dev": "pylon dev",
8
+ "build": "pylon build",
9
+ "schema:push": "pylon schema push --sqlite dev.db",
10
10
  "schema:inspect": "pylon schema inspect --sqlite dev.db"
11
11
  },
12
12
  "dependencies": {
@@ -4,9 +4,9 @@
4
4
  "private": true,
5
5
  "type": "module",
6
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",
7
+ "dev": "pylon dev",
8
+ "build": "pylon build",
9
+ "schema:push": "pylon schema push --sqlite dev.db",
10
10
  "schema:inspect": "pylon schema inspect --sqlite dev.db"
11
11
  },
12
12
  "dependencies": {
@@ -4,9 +4,9 @@
4
4
  "private": true,
5
5
  "type": "module",
6
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",
7
+ "dev": "pylon dev",
8
+ "build": "pylon build",
9
+ "schema:push": "pylon schema push --sqlite dev.db",
10
10
  "schema:inspect": "pylon schema inspect --sqlite dev.db"
11
11
  },
12
12
  "dependencies": {
@@ -4,9 +4,9 @@
4
4
  "private": true,
5
5
  "type": "module",
6
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",
7
+ "dev": "pylon dev",
8
+ "build": "pylon build",
9
+ "schema:push": "pylon schema push --sqlite dev.db",
10
10
  "schema:inspect": "pylon schema inspect --sqlite dev.db"
11
11
  },
12
12
  "dependencies": {
@@ -4,9 +4,9 @@
4
4
  "private": true,
5
5
  "type": "module",
6
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",
7
+ "dev": "pylon dev",
8
+ "build": "pylon build",
9
+ "schema:push": "pylon schema push --sqlite dev.db",
10
10
  "schema:inspect": "pylon schema inspect --sqlite dev.db"
11
11
  },
12
12
  "dependencies": {
@@ -5,6 +5,7 @@
5
5
  "main": "node_modules/expo/AppEntry.js",
6
6
  "scripts": {
7
7
  "start": "expo start",
8
+ "dev": "expo start",
8
9
  "android": "expo start --android",
9
10
  "ios": "expo start --ios",
10
11
  "web": "expo start --web"
@@ -13,7 +13,7 @@ import {
13
13
  SafeAreaView,
14
14
  } from "react-native";
15
15
  import { StatusBar } from "expo-status-bar";
16
- import { init, callFn } from "@pylonsync/react-native";
16
+ import { init, db, callFn } from "@pylonsync/react-native";
17
17
 
18
18
  const PYLON_BASE_URL =
19
19
  process.env.EXPO_PUBLIC_PYLON_BASE_URL ??
@@ -48,23 +48,9 @@ function ensureInit() {
48
48
 
49
49
  export default function App() {
50
50
  const [ready, setReady] = useState(false);
51
- const [rooms, setRooms] = useState<Room[]>([]);
52
- const [active, setActive] = useState<Room | null>(null);
53
- const [authorName, setAuthorName] = useState("anonymous");
54
-
55
51
  useEffect(() => {
56
- ensureInit().then(async () => {
57
- try {
58
- const list = await callFn<Room[]>("listRooms", {});
59
- setRooms(list);
60
- setActive(list[0] ?? null);
61
- } catch (e) {
62
- Alert.alert("Load failed", String(e));
63
- }
64
- setReady(true);
65
- });
52
+ ensureInit().then(() => setReady(true));
66
53
  }, []);
67
-
68
54
  if (!ready) {
69
55
  return (
70
56
  <View style={[styles.screen, styles.center]}>
@@ -72,159 +58,61 @@ export default function App() {
72
58
  </View>
73
59
  );
74
60
  }
75
-
76
- if (!active) {
77
- return (
78
- <RoomCreate
79
- authorName={authorName}
80
- onAuthorNameChange={setAuthorName}
81
- onCreated={(r) => {
82
- setRooms((prev) => [...prev, r]);
83
- setActive(r);
84
- }}
85
- />
86
- );
87
- }
88
-
89
- return (
90
- <RoomView
91
- room={active}
92
- rooms={rooms}
93
- onSwitch={setActive}
94
- authorName={authorName}
95
- onAuthorNameChange={setAuthorName}
96
- onCreate={(r) => {
97
- setRooms((prev) => [...prev, r]);
98
- setActive(r);
99
- }}
100
- />
101
- );
61
+ return <Chat />;
102
62
  }
103
63
 
104
- function RoomCreate({
105
- authorName,
106
- onAuthorNameChange,
107
- onCreated,
108
- }: {
109
- authorName: string;
110
- onAuthorNameChange: (s: string) => void;
111
- onCreated: (r: Room) => void;
112
- }) {
113
- const [name, setName] = useState("General");
114
- const [slug, setSlug] = useState("general");
115
- const [creating, setCreating] = useState(false);
64
+ function Chat() {
65
+ // Live subscriptions — sync engine pushes diffs over WebSocket.
66
+ // New rooms / new messages from any other device or device update
67
+ // re-render this component without polling.
68
+ const { data: rooms = [] } = db.useQuery<Room>("Room", {
69
+ orderBy: { createdAt: "asc" },
70
+ });
71
+ const [activeId, setActiveId] = useState<string | null>(null);
72
+ useEffect(() => {
73
+ if (!activeId && rooms.length > 0) setActiveId(rooms[0].id);
74
+ }, [rooms, activeId]);
116
75
 
117
- async function create() {
118
- setCreating(true);
119
- try {
120
- const r = await callFn<Room>("createRoom", {
121
- slug: slug.toLowerCase(),
122
- name,
123
- });
124
- onCreated(r);
125
- } catch (e) {
126
- Alert.alert("Create failed", String(e));
127
- } finally {
128
- setCreating(false);
129
- }
130
- }
76
+ const active = rooms.find((r) => r.id === activeId) ?? null;
131
77
 
132
- return (
133
- <SafeAreaView style={styles.screen}>
134
- <StatusBar style="auto" />
135
- <View style={styles.content}>
136
- <Text style={styles.title}>__APP_NAME__</Text>
137
- <Text style={styles.subtitle}>No rooms yet — create one.</Text>
138
- <TextInput
139
- style={styles.input}
140
- placeholder="Your display name"
141
- value={authorName}
142
- onChangeText={onAuthorNameChange}
143
- />
144
- <TextInput
145
- style={styles.input}
146
- placeholder="Room name"
147
- value={name}
148
- onChangeText={setName}
149
- />
150
- <TextInput
151
- style={styles.input}
152
- placeholder="Slug"
153
- value={slug}
154
- onChangeText={(s) => setSlug(s.toLowerCase())}
155
- autoCapitalize="none"
156
- autoCorrect={false}
157
- />
158
- <Pressable
159
- onPress={create}
160
- disabled={creating || !name.trim() || !slug.trim()}
161
- style={({ pressed }) => [
162
- styles.button,
163
- (creating || !name.trim() || !slug.trim()) && styles.buttonDisabled,
164
- pressed && styles.buttonPressed,
165
- ]}
166
- >
167
- <Text style={styles.buttonLabel}>
168
- {creating ? "Creating…" : "Create room"}
169
- </Text>
170
- </Pressable>
171
- </View>
172
- </SafeAreaView>
173
- );
174
- }
78
+ const { data: messages = [] } = db.useQuery<Message>("Message", {
79
+ where: activeId ? { roomId: activeId } : undefined,
80
+ orderBy: { createdAt: "asc" },
81
+ limit: 200,
82
+ });
175
83
 
176
- function RoomView({
177
- room,
178
- rooms,
179
- onSwitch,
180
- authorName,
181
- onAuthorNameChange,
182
- onCreate,
183
- }: {
184
- room: Room;
185
- rooms: Room[];
186
- onSwitch: (r: Room) => void;
187
- authorName: string;
188
- onAuthorNameChange: (s: string) => void;
189
- onCreate: (r: Room) => void;
190
- }) {
191
- const [messages, setMessages] = useState<Message[]>([]);
192
84
  const [draft, setDraft] = useState("");
193
85
  const [sending, setSending] = useState(false);
86
+ const [authorName, setAuthorName] = useState("anonymous");
194
87
  const listRef = useRef<FlatList<Message>>(null);
195
88
 
196
- useEffect(() => {
197
- void load();
198
- const t = setInterval(load, 1500);
199
- return () => clearInterval(t);
200
- async function load() {
201
- try {
202
- const m = await callFn<Message[]>("roomMessages", { roomId: room.id });
203
- setMessages(m);
204
- } catch {
205
- // ignore — will retry on next tick
206
- }
207
- }
208
- }, [room.id]);
209
-
210
89
  useEffect(() => {
211
90
  if (messages.length > 0) {
212
91
  listRef.current?.scrollToEnd({ animated: true });
213
92
  }
214
- }, [messages.length]);
93
+ }, [messages.length, activeId]);
94
+
95
+ if (!active) {
96
+ return (
97
+ <RoomCreate
98
+ authorName={authorName}
99
+ onAuthorNameChange={setAuthorName}
100
+ onCreated={(r) => setActiveId(r.id)}
101
+ />
102
+ );
103
+ }
215
104
 
216
105
  async function send() {
217
106
  const body = draft.trim();
218
107
  if (!body) return;
219
- setSending(true);
220
108
  setDraft("");
109
+ setSending(true);
221
110
  try {
222
- const msg = await callFn<Message>("sendMessage", {
223
- roomId: room.id,
111
+ await callFn("sendMessage", {
112
+ roomId: active.id,
224
113
  body,
225
114
  authorName,
226
115
  });
227
- setMessages((prev) => [...prev, msg]);
228
116
  } catch (e) {
229
117
  setDraft(body);
230
118
  Alert.alert("Send failed", String(e));
@@ -241,8 +129,8 @@ function RoomView({
241
129
  .replace(/[^a-z0-9]+/g, "-")
242
130
  .replace(/^-|-$/g, "");
243
131
  try {
244
- const r = await callFn<Room>("createRoom", { slug, name });
245
- onCreate(r);
132
+ const r = (await callFn("createRoom", { slug, name })) as Room;
133
+ setActiveId(r.id);
246
134
  } catch (e) {
247
135
  Alert.alert("Create failed", String(e));
248
136
  }
@@ -258,15 +146,14 @@ function RoomView({
258
146
  >
259
147
  <View style={styles.header}>
260
148
  <View>
261
- <Text style={styles.title}>{room.name}</Text>
262
- <Text style={styles.handle}>#{room.slug}</Text>
149
+ <Text style={styles.title}>{active.name}</Text>
150
+ <Text style={styles.handle}>#{active.slug}</Text>
263
151
  </View>
264
152
  {rooms.length > 1 && (
265
153
  <Pressable
266
154
  onPress={() => {
267
- // Cycle to next room simplest "switch room" UX without a sidebar.
268
- const idx = rooms.findIndex((r) => r.id === room.id);
269
- onSwitch(rooms[(idx + 1) % rooms.length]);
155
+ const idx = rooms.findIndex((r) => r.id === active.id);
156
+ setActiveId(rooms[(idx + 1) % rooms.length].id);
270
157
  }}
271
158
  >
272
159
  <Text style={styles.switchBtn}>Next room →</Text>
@@ -279,7 +166,7 @@ function RoomView({
279
166
  <TextInput
280
167
  style={styles.nameinput}
281
168
  value={authorName}
282
- onChangeText={onAuthorNameChange}
169
+ onChangeText={setAuthorName}
283
170
  autoCapitalize="none"
284
171
  />
285
172
  </View>
@@ -313,7 +200,7 @@ function RoomView({
313
200
  style={styles.composerInput}
314
201
  value={draft}
315
202
  onChangeText={setDraft}
316
- placeholder={`Message ${room.name}…`}
203
+ placeholder={`Message ${active.name}…`}
317
204
  multiline
318
205
  editable={!sending}
319
206
  />
@@ -329,11 +216,87 @@ function RoomView({
329
216
  <Text style={styles.buttonLabel}>Send</Text>
330
217
  </Pressable>
331
218
  </View>
219
+
220
+ <Pressable onPress={createRoom} style={styles.newRoomBar}>
221
+ <Text style={styles.newRoomLabel}>+ New room</Text>
222
+ </Pressable>
332
223
  </KeyboardAvoidingView>
333
224
  </SafeAreaView>
334
225
  );
335
226
  }
336
227
 
228
+ function RoomCreate({
229
+ authorName,
230
+ onAuthorNameChange,
231
+ onCreated,
232
+ }: {
233
+ authorName: string;
234
+ onAuthorNameChange: (s: string) => void;
235
+ onCreated: (r: Room) => void;
236
+ }) {
237
+ const [name, setName] = useState("General");
238
+ const [slug, setSlug] = useState("general");
239
+ const [creating, setCreating] = useState(false);
240
+
241
+ async function create() {
242
+ setCreating(true);
243
+ try {
244
+ const r = (await callFn("createRoom", {
245
+ slug: slug.toLowerCase(),
246
+ name,
247
+ })) as Room;
248
+ onCreated(r);
249
+ } catch (e) {
250
+ Alert.alert("Create failed", String(e));
251
+ } finally {
252
+ setCreating(false);
253
+ }
254
+ }
255
+
256
+ return (
257
+ <SafeAreaView style={styles.screen}>
258
+ <StatusBar style="auto" />
259
+ <View style={styles.content}>
260
+ <Text style={styles.title}>__APP_NAME__</Text>
261
+ <Text style={styles.subtitle}>No rooms yet — create one.</Text>
262
+ <TextInput
263
+ style={styles.input}
264
+ placeholder="Your display name"
265
+ value={authorName}
266
+ onChangeText={onAuthorNameChange}
267
+ />
268
+ <TextInput
269
+ style={styles.input}
270
+ placeholder="Room name"
271
+ value={name}
272
+ onChangeText={setName}
273
+ />
274
+ <TextInput
275
+ style={styles.input}
276
+ placeholder="Slug"
277
+ value={slug}
278
+ onChangeText={(s) => setSlug(s.toLowerCase())}
279
+ autoCapitalize="none"
280
+ autoCorrect={false}
281
+ />
282
+ <Pressable
283
+ onPress={create}
284
+ disabled={creating || !name.trim() || !slug.trim()}
285
+ style={({ pressed }) => [
286
+ styles.button,
287
+ (creating || !name.trim() || !slug.trim()) && styles.buttonDisabled,
288
+ pressed && styles.buttonPressed,
289
+ ]}
290
+ >
291
+ <Text style={styles.buttonLabel}>
292
+ {creating ? "Creating…" : "Create room"}
293
+ </Text>
294
+ </Pressable>
295
+ </View>
296
+ </SafeAreaView>
297
+ );
298
+ }
299
+
337
300
  const styles = StyleSheet.create({
338
301
  screen: { flex: 1, backgroundColor: "#fff" },
339
302
  flex: { flex: 1 },
@@ -411,4 +374,11 @@ const styles = StyleSheet.create({
411
374
  paddingVertical: 8,
412
375
  fontSize: 14,
413
376
  },
377
+ newRoomBar: {
378
+ paddingVertical: 10,
379
+ alignItems: "center",
380
+ borderTopWidth: 1,
381
+ borderColor: "#e5e5e5",
382
+ },
383
+ newRoomLabel: { color: "#3b82f6", fontSize: 13 },
414
384
  });
@@ -5,6 +5,7 @@
5
5
  "main": "node_modules/expo/AppEntry.js",
6
6
  "scripts": {
7
7
  "start": "expo start",
8
+ "dev": "expo start",
8
9
  "android": "expo start --android",
9
10
  "ios": "expo start --ios",
10
11
  "web": "expo start --web"