@pylonsync/create-pylon 0.3.51 → 0.3.54

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 (180) hide show
  1. package/bin/create-pylon.js +347 -1156
  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/b2b/apps/api/functions/archiveProject.ts +15 -0
  6. package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
  7. package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
  8. package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
  9. package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
  10. package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
  11. package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
  12. package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
  13. package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
  14. package/templates/backend/b2b/apps/api/package.json +20 -0
  15. package/templates/backend/b2b/apps/api/schema.ts +171 -0
  16. package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
  17. package/templates/backend/barebones/apps/api/functions/createWidget.ts +22 -0
  18. package/templates/backend/barebones/apps/api/functions/listWidgets.ts +18 -0
  19. package/templates/backend/barebones/apps/api/package.json +20 -0
  20. package/templates/backend/barebones/apps/api/schema.ts +61 -0
  21. package/templates/backend/barebones/apps/api/tsconfig.json +13 -0
  22. package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
  23. package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
  24. package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
  25. package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
  26. package/templates/backend/chat/apps/api/package.json +20 -0
  27. package/templates/backend/chat/apps/api/schema.ts +93 -0
  28. package/templates/backend/chat/apps/api/tsconfig.json +13 -0
  29. package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
  30. package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
  31. package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
  32. package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
  33. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
  34. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
  35. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
  36. package/templates/backend/consumer/apps/api/package.json +20 -0
  37. package/templates/backend/consumer/apps/api/schema.ts +130 -0
  38. package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
  39. package/templates/backend/todo/apps/api/functions/addTodo.ts +27 -0
  40. package/templates/backend/todo/apps/api/functions/deleteTodo.ts +14 -0
  41. package/templates/backend/todo/apps/api/functions/editTodo.ts +16 -0
  42. package/templates/backend/todo/apps/api/functions/listTodos.ts +24 -0
  43. package/templates/backend/todo/apps/api/functions/reorderTodo.ts +14 -0
  44. package/templates/backend/todo/apps/api/functions/toggleTodo.ts +13 -0
  45. package/templates/backend/todo/apps/api/package.json +20 -0
  46. package/templates/backend/todo/apps/api/schema.ts +85 -0
  47. package/templates/backend/todo/apps/api/tsconfig.json +13 -0
  48. package/templates/expo/barebones/apps/expo/App.tsx +166 -0
  49. package/templates/expo/barebones/apps/expo/app.json +31 -0
  50. package/templates/expo/barebones/apps/expo/babel.config.js +6 -0
  51. package/templates/expo/barebones/apps/expo/package.json +30 -0
  52. package/templates/expo/barebones/apps/expo/tsconfig.json +16 -0
  53. package/templates/expo/chat/apps/expo/App.tsx +414 -0
  54. package/templates/expo/chat/apps/expo/app.json +25 -0
  55. package/templates/expo/chat/apps/expo/babel.config.js +6 -0
  56. package/templates/expo/chat/apps/expo/package.json +30 -0
  57. package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
  58. package/templates/expo/consumer/apps/expo/App.tsx +360 -0
  59. package/templates/expo/consumer/apps/expo/app.json +25 -0
  60. package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
  61. package/templates/expo/consumer/apps/expo/package.json +30 -0
  62. package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
  63. package/templates/expo/todo/apps/expo/App.tsx +287 -0
  64. package/templates/expo/todo/apps/expo/app.json +25 -0
  65. package/templates/expo/todo/apps/expo/babel.config.js +6 -0
  66. package/templates/expo/todo/apps/expo/package.json +30 -0
  67. package/templates/expo/todo/apps/expo/tsconfig.json +16 -0
  68. package/templates/ios/barebones/apps/ios/Package.swift +34 -0
  69. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/ContentView.swift +98 -0
  70. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  71. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -0
  72. package/templates/ios/barebones/apps/ios/project.yml +42 -0
  73. package/templates/ios/chat/apps/ios/Package.swift +34 -0
  74. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +120 -0
  75. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  76. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
  77. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +35 -0
  78. package/templates/ios/chat/apps/ios/project.yml +42 -0
  79. package/templates/ios/consumer/apps/ios/Package.swift +23 -0
  80. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  81. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  82. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  83. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  84. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +29 -0
  85. package/templates/ios/consumer/apps/ios/project.yml +42 -0
  86. package/templates/ios/todo/apps/ios/Package.swift +23 -0
  87. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +18 -0
  88. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/TodoListView.swift +230 -0
  89. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +28 -0
  90. package/templates/ios/todo/apps/ios/project.yml +32 -0
  91. package/templates/mac/b2b/apps/mac/Package.swift +22 -0
  92. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
  93. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
  94. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  95. package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  96. package/templates/mac/b2b/apps/mac/project.yml +34 -0
  97. package/templates/mac/barebones/apps/mac/Package.swift +33 -0
  98. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
  99. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  100. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  101. package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  102. package/templates/mac/barebones/apps/mac/project.yml +34 -0
  103. package/templates/mac/chat/apps/mac/Package.swift +33 -0
  104. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +140 -0
  105. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  106. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
  107. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +37 -0
  108. package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  109. package/templates/mac/chat/apps/mac/project.yml +34 -0
  110. package/templates/mac/consumer/apps/mac/Package.swift +33 -0
  111. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  112. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  113. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  114. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  115. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +31 -0
  116. package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  117. package/templates/mac/consumer/apps/mac/project.yml +34 -0
  118. package/templates/mac/todo/apps/mac/Package.swift +33 -0
  119. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
  120. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
  121. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  122. package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  123. package/templates/mac/todo/apps/mac/project.yml +34 -0
  124. package/templates/ui/packages/ui/package.json +26 -0
  125. package/templates/ui/packages/ui/src/button.tsx +44 -0
  126. package/templates/ui/packages/ui/src/card.tsx +39 -0
  127. package/templates/ui/packages/ui/src/cn.ts +12 -0
  128. package/templates/ui/packages/ui/src/index.ts +4 -0
  129. package/templates/ui/packages/ui/src/input.tsx +19 -0
  130. package/templates/ui/packages/ui/tsconfig.json +15 -0
  131. package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
  132. package/templates/web/b2b/apps/web/next.config.ts +24 -0
  133. package/templates/web/b2b/apps/web/package.json +29 -0
  134. package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
  135. package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
  136. package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
  137. package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
  138. package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
  139. package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
  140. package/templates/web/b2b/apps/web/tsconfig.json +26 -0
  141. package/templates/web/barebones/apps/web/next-env.d.ts +2 -0
  142. package/templates/web/barebones/apps/web/next.config.ts +40 -0
  143. package/templates/web/barebones/apps/web/package.json +29 -0
  144. package/templates/web/barebones/apps/web/postcss.config.mjs +4 -0
  145. package/templates/web/barebones/apps/web/src/app/components/WidgetList.tsx +81 -0
  146. package/templates/web/barebones/apps/web/src/app/globals.css +9 -0
  147. package/templates/web/barebones/apps/web/src/app/layout.tsx +21 -0
  148. package/templates/web/barebones/apps/web/src/app/page.tsx +43 -0
  149. package/templates/web/barebones/apps/web/src/lib/pylon.ts +14 -0
  150. package/templates/web/barebones/apps/web/tsconfig.json +26 -0
  151. package/templates/web/chat/apps/web/next-env.d.ts +2 -0
  152. package/templates/web/chat/apps/web/next.config.ts +24 -0
  153. package/templates/web/chat/apps/web/package.json +29 -0
  154. package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
  155. package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +250 -0
  156. package/templates/web/chat/apps/web/src/app/globals.css +6 -0
  157. package/templates/web/chat/apps/web/src/app/layout.tsx +21 -0
  158. package/templates/web/chat/apps/web/src/app/page.tsx +51 -0
  159. package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
  160. package/templates/web/chat/apps/web/tsconfig.json +26 -0
  161. package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
  162. package/templates/web/consumer/apps/web/next.config.ts +24 -0
  163. package/templates/web/consumer/apps/web/package.json +29 -0
  164. package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
  165. package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +295 -0
  166. package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
  167. package/templates/web/consumer/apps/web/src/app/layout.tsx +21 -0
  168. package/templates/web/consumer/apps/web/src/app/page.tsx +55 -0
  169. package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
  170. package/templates/web/consumer/apps/web/tsconfig.json +26 -0
  171. package/templates/web/todo/apps/web/next-env.d.ts +2 -0
  172. package/templates/web/todo/apps/web/next.config.ts +24 -0
  173. package/templates/web/todo/apps/web/package.json +32 -0
  174. package/templates/web/todo/apps/web/postcss.config.mjs +3 -0
  175. package/templates/web/todo/apps/web/src/app/components/TodoList.tsx +310 -0
  176. package/templates/web/todo/apps/web/src/app/globals.css +6 -0
  177. package/templates/web/todo/apps/web/src/app/layout.tsx +21 -0
  178. package/templates/web/todo/apps/web/src/app/page.tsx +36 -0
  179. package/templates/web/todo/apps/web/src/lib/pylon.ts +5 -0
  180. package/templates/web/todo/apps/web/tsconfig.json +26 -0
@@ -0,0 +1,414 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import {
3
+ View,
4
+ Text,
5
+ TextInput,
6
+ Pressable,
7
+ FlatList,
8
+ ActivityIndicator,
9
+ StyleSheet,
10
+ Platform,
11
+ Alert,
12
+ KeyboardAvoidingView,
13
+ SafeAreaView,
14
+ } from "react-native";
15
+ import { StatusBar } from "expo-status-bar";
16
+ import { init, callFn } from "@pylonsync/react-native";
17
+
18
+ const PYLON_BASE_URL =
19
+ process.env.EXPO_PUBLIC_PYLON_BASE_URL ??
20
+ (Platform.OS === "android" ? "http://10.0.2.2:4321" : "http://localhost:4321");
21
+
22
+ type Room = {
23
+ id: string;
24
+ slug: string;
25
+ name: string;
26
+ createdAt: string;
27
+ };
28
+
29
+ type Message = {
30
+ id: string;
31
+ roomId: string;
32
+ authorId: string;
33
+ authorName: string;
34
+ body: string;
35
+ createdAt: string;
36
+ };
37
+
38
+ let initPromise: Promise<void> | null = null;
39
+ function ensureInit() {
40
+ if (!initPromise) {
41
+ initPromise = init({
42
+ baseUrl: PYLON_BASE_URL,
43
+ appName: "__APP_NAME_SNAKE__",
44
+ });
45
+ }
46
+ return initPromise;
47
+ }
48
+
49
+ export default function App() {
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
+ 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
+ });
66
+ }, []);
67
+
68
+ if (!ready) {
69
+ return (
70
+ <View style={[styles.screen, styles.center]}>
71
+ <ActivityIndicator />
72
+ </View>
73
+ );
74
+ }
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
+ );
102
+ }
103
+
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);
116
+
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
+ }
131
+
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
+ }
175
+
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
+ const [draft, setDraft] = useState("");
193
+ const [sending, setSending] = useState(false);
194
+ const listRef = useRef<FlatList<Message>>(null);
195
+
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
+ useEffect(() => {
211
+ if (messages.length > 0) {
212
+ listRef.current?.scrollToEnd({ animated: true });
213
+ }
214
+ }, [messages.length]);
215
+
216
+ async function send() {
217
+ const body = draft.trim();
218
+ if (!body) return;
219
+ setSending(true);
220
+ setDraft("");
221
+ try {
222
+ const msg = await callFn<Message>("sendMessage", {
223
+ roomId: room.id,
224
+ body,
225
+ authorName,
226
+ });
227
+ setMessages((prev) => [...prev, msg]);
228
+ } catch (e) {
229
+ setDraft(body);
230
+ Alert.alert("Send failed", String(e));
231
+ } finally {
232
+ setSending(false);
233
+ }
234
+ }
235
+
236
+ async function createRoom() {
237
+ const name = window?.prompt?.("Room name?") ?? null;
238
+ if (!name) return;
239
+ const slug = name
240
+ .toLowerCase()
241
+ .replace(/[^a-z0-9]+/g, "-")
242
+ .replace(/^-|-$/g, "");
243
+ try {
244
+ const r = await callFn<Room>("createRoom", { slug, name });
245
+ onCreate(r);
246
+ } catch (e) {
247
+ Alert.alert("Create failed", String(e));
248
+ }
249
+ }
250
+
251
+ return (
252
+ <SafeAreaView style={styles.screen}>
253
+ <StatusBar style="auto" />
254
+ <KeyboardAvoidingView
255
+ style={styles.flex}
256
+ behavior={Platform.OS === "ios" ? "padding" : undefined}
257
+ keyboardVerticalOffset={64}
258
+ >
259
+ <View style={styles.header}>
260
+ <View>
261
+ <Text style={styles.title}>{room.name}</Text>
262
+ <Text style={styles.handle}>#{room.slug}</Text>
263
+ </View>
264
+ {rooms.length > 1 && (
265
+ <Pressable
266
+ 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]);
270
+ }}
271
+ >
272
+ <Text style={styles.switchBtn}>Next room →</Text>
273
+ </Pressable>
274
+ )}
275
+ </View>
276
+
277
+ <View style={styles.namebar}>
278
+ <Text style={styles.namelabel}>You:</Text>
279
+ <TextInput
280
+ style={styles.nameinput}
281
+ value={authorName}
282
+ onChangeText={onAuthorNameChange}
283
+ autoCapitalize="none"
284
+ />
285
+ </View>
286
+
287
+ <FlatList
288
+ ref={listRef}
289
+ data={messages}
290
+ keyExtractor={(m) => m.id}
291
+ contentContainerStyle={{ padding: 16 }}
292
+ ListEmptyComponent={() => (
293
+ <Text style={styles.empty}>No messages yet. Say hi.</Text>
294
+ )}
295
+ renderItem={({ item }) => (
296
+ <View style={styles.msg}>
297
+ <View style={styles.msgHead}>
298
+ <Text style={styles.msgName}>{item.authorName}</Text>
299
+ <Text style={styles.msgTime}>
300
+ {new Date(item.createdAt).toLocaleTimeString(undefined, {
301
+ hour: "numeric",
302
+ minute: "2-digit",
303
+ })}
304
+ </Text>
305
+ </View>
306
+ <Text style={styles.msgBody}>{item.body}</Text>
307
+ </View>
308
+ )}
309
+ />
310
+
311
+ <View style={styles.composer}>
312
+ <TextInput
313
+ style={styles.composerInput}
314
+ value={draft}
315
+ onChangeText={setDraft}
316
+ placeholder={`Message ${room.name}…`}
317
+ multiline
318
+ editable={!sending}
319
+ />
320
+ <Pressable
321
+ onPress={send}
322
+ disabled={sending || !draft.trim()}
323
+ style={({ pressed }) => [
324
+ styles.button,
325
+ (sending || !draft.trim()) && styles.buttonDisabled,
326
+ pressed && styles.buttonPressed,
327
+ ]}
328
+ >
329
+ <Text style={styles.buttonLabel}>Send</Text>
330
+ </Pressable>
331
+ </View>
332
+ </KeyboardAvoidingView>
333
+ </SafeAreaView>
334
+ );
335
+ }
336
+
337
+ const styles = StyleSheet.create({
338
+ screen: { flex: 1, backgroundColor: "#fff" },
339
+ flex: { flex: 1 },
340
+ center: { alignItems: "center", justifyContent: "center" },
341
+ content: { padding: 20, gap: 12 },
342
+ title: { fontSize: 24, fontWeight: "600" },
343
+ subtitle: { color: "#666" },
344
+ handle: { fontFamily: "Menlo", fontSize: 12, color: "#999" },
345
+ header: {
346
+ paddingHorizontal: 20,
347
+ paddingTop: 12,
348
+ paddingBottom: 8,
349
+ flexDirection: "row",
350
+ justifyContent: "space-between",
351
+ alignItems: "flex-end",
352
+ borderBottomWidth: 1,
353
+ borderColor: "#e5e5e5",
354
+ },
355
+ switchBtn: { color: "#3b82f6", fontSize: 13 },
356
+ namebar: {
357
+ flexDirection: "row",
358
+ alignItems: "center",
359
+ gap: 6,
360
+ paddingHorizontal: 20,
361
+ paddingVertical: 6,
362
+ backgroundColor: "#fafafa",
363
+ },
364
+ namelabel: { fontSize: 12, color: "#666" },
365
+ nameinput: {
366
+ flex: 1,
367
+ borderBottomWidth: 1,
368
+ borderColor: "#d4d4d8",
369
+ fontSize: 13,
370
+ paddingVertical: 2,
371
+ },
372
+ input: {
373
+ borderWidth: 1,
374
+ borderColor: "#d4d4d8",
375
+ borderRadius: 6,
376
+ paddingHorizontal: 12,
377
+ paddingVertical: 8,
378
+ fontSize: 14,
379
+ },
380
+ button: {
381
+ backgroundColor: "#171717",
382
+ borderRadius: 6,
383
+ paddingHorizontal: 16,
384
+ paddingVertical: 12,
385
+ alignItems: "center",
386
+ },
387
+ buttonDisabled: { opacity: 0.5 },
388
+ buttonPressed: { opacity: 0.8 },
389
+ buttonLabel: { color: "#fff", fontWeight: "600", fontSize: 13 },
390
+ empty: { textAlign: "center", color: "#999", marginTop: 32 },
391
+ msg: { marginBottom: 16 },
392
+ msgHead: { flexDirection: "row", alignItems: "baseline", gap: 6 },
393
+ msgName: { fontSize: 13, fontWeight: "500" },
394
+ msgTime: { fontSize: 11, color: "#999" },
395
+ msgBody: { fontSize: 14, marginTop: 2 },
396
+ composer: {
397
+ flexDirection: "row",
398
+ gap: 8,
399
+ padding: 12,
400
+ borderTopWidth: 1,
401
+ borderColor: "#e5e5e5",
402
+ },
403
+ composerInput: {
404
+ flex: 1,
405
+ minHeight: 36,
406
+ maxHeight: 120,
407
+ borderWidth: 1,
408
+ borderColor: "#d4d4d8",
409
+ borderRadius: 6,
410
+ paddingHorizontal: 12,
411
+ paddingVertical: 8,
412
+ fontSize: 14,
413
+ },
414
+ });
@@ -0,0 +1,25 @@
1
+ {
2
+ "expo": {
3
+ "name": "__APP_NAME__",
4
+ "slug": "__APP_NAME_KEBAB__",
5
+ "version": "0.0.1",
6
+ "orientation": "portrait",
7
+ "userInterfaceStyle": "automatic",
8
+ "ios": {
9
+ "supportsTablet": true,
10
+ "bundleIdentifier": "com.example.__APP_NAME_SNAKE__",
11
+ "infoPlist": {
12
+ "NSAppTransportSecurity": {
13
+ "NSAllowsLocalNetworking": true
14
+ }
15
+ }
16
+ },
17
+ "android": {
18
+ "package": "com.example.__APP_NAME_SNAKE__",
19
+ "usesCleartextTraffic": true
20
+ },
21
+ "web": {
22
+ "bundler": "metro"
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = function (api) {
2
+ api.cache(true);
3
+ return {
4
+ presets: ["babel-preset-expo"],
5
+ };
6
+ };
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@__APP_NAME_KEBAB__/expo",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "main": "node_modules/expo/AppEntry.js",
6
+ "scripts": {
7
+ "start": "expo start",
8
+ "android": "expo start --android",
9
+ "ios": "expo start --ios",
10
+ "web": "expo start --web"
11
+ },
12
+ "dependencies": {
13
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
14
+ "@pylonsync/react": "^__PYLON_VERSION__",
15
+ "@pylonsync/react-native": "^__PYLON_VERSION__",
16
+ "@pylonsync/sync": "^__PYLON_VERSION__",
17
+ "@react-native-async-storage/async-storage": "1.23.1",
18
+ "@react-native-community/netinfo": "11.3.1",
19
+ "expo": "~51.0.0",
20
+ "expo-status-bar": "~1.12.1",
21
+ "react": "19.0.0",
22
+ "react-dom": "19.0.0",
23
+ "react-native": "0.74.5"
24
+ },
25
+ "devDependencies": {
26
+ "@babel/core": "^7.20.0",
27
+ "@types/react": "^19.0.0",
28
+ "typescript": "^5.5.0"
29
+ }
30
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "esnext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["esnext", "dom"],
7
+ "jsx": "react-native",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "isolatedModules": true,
12
+ "resolveJsonModule": true,
13
+ "noEmit": true
14
+ },
15
+ "include": ["App.tsx", "src/**/*.ts", "src/**/*.tsx"]
16
+ }