@pylonsync/create-pylon 0.3.53 → 0.3.55

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 (129) hide show
  1. package/bin/create-pylon.js +98 -42
  2. package/package.json +1 -1
  3. package/templates/backend/b2b/apps/api/functions/archiveProject.ts +15 -0
  4. package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
  5. package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
  6. package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
  7. package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
  8. package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
  9. package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
  10. package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
  11. package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
  12. package/templates/backend/b2b/apps/api/package.json +20 -0
  13. package/templates/backend/b2b/apps/api/schema.ts +171 -0
  14. package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
  15. package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
  16. package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
  17. package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
  18. package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
  19. package/templates/backend/chat/apps/api/package.json +20 -0
  20. package/templates/backend/chat/apps/api/schema.ts +93 -0
  21. package/templates/backend/chat/apps/api/tsconfig.json +13 -0
  22. package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
  23. package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
  24. package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
  25. package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
  26. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
  27. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
  28. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
  29. package/templates/backend/consumer/apps/api/package.json +20 -0
  30. package/templates/backend/consumer/apps/api/schema.ts +130 -0
  31. package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
  32. package/templates/expo/chat/apps/expo/App.tsx +384 -0
  33. package/templates/expo/chat/apps/expo/app.json +25 -0
  34. package/templates/expo/chat/apps/expo/babel.config.js +6 -0
  35. package/templates/expo/chat/apps/expo/package.json +30 -0
  36. package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
  37. package/templates/expo/consumer/apps/expo/App.tsx +392 -0
  38. package/templates/expo/consumer/apps/expo/app.json +25 -0
  39. package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
  40. package/templates/expo/consumer/apps/expo/package.json +30 -0
  41. package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
  42. package/templates/ios/chat/apps/ios/Package.swift +24 -0
  43. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +116 -0
  44. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  45. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
  46. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
  47. package/templates/ios/chat/apps/ios/project.yml +44 -0
  48. package/templates/ios/consumer/apps/ios/Package.swift +24 -0
  49. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
  50. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
  51. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
  52. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
  53. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +57 -0
  54. package/templates/ios/consumer/apps/ios/project.yml +44 -0
  55. package/templates/mac/b2b/apps/mac/Package.swift +22 -0
  56. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
  57. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
  58. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  59. package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  60. package/templates/mac/b2b/apps/mac/project.yml +34 -0
  61. package/templates/mac/barebones/apps/mac/Package.swift +33 -0
  62. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
  63. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  64. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  65. package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  66. package/templates/mac/barebones/apps/mac/project.yml +34 -0
  67. package/templates/mac/chat/apps/mac/Package.swift +34 -0
  68. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +143 -0
  69. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  70. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
  71. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
  72. package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  73. package/templates/mac/chat/apps/mac/project.yml +36 -0
  74. package/templates/mac/consumer/apps/mac/Package.swift +34 -0
  75. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
  76. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
  77. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
  78. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
  79. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +59 -0
  80. package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  81. package/templates/mac/consumer/apps/mac/project.yml +36 -0
  82. package/templates/mac/todo/apps/mac/Package.swift +33 -0
  83. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
  84. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
  85. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  86. package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  87. package/templates/mac/todo/apps/mac/project.yml +34 -0
  88. package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
  89. package/templates/web/b2b/apps/web/next.config.ts +24 -0
  90. package/templates/web/b2b/apps/web/package.json +29 -0
  91. package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
  92. package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
  93. package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
  94. package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
  95. package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
  96. package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
  97. package/templates/web/b2b/apps/web/tsconfig.json +26 -0
  98. package/templates/web/chat/apps/web/next-env.d.ts +2 -0
  99. package/templates/web/chat/apps/web/next.config.ts +24 -0
  100. package/templates/web/chat/apps/web/package.json +29 -0
  101. package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
  102. package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +231 -0
  103. package/templates/web/chat/apps/web/src/app/globals.css +6 -0
  104. package/templates/web/chat/apps/web/src/app/layout.tsx +22 -0
  105. package/templates/web/chat/apps/web/src/app/page.tsx +12 -0
  106. package/templates/web/chat/apps/web/src/app/providers.tsx +25 -0
  107. package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
  108. package/templates/web/chat/apps/web/tsconfig.json +26 -0
  109. package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
  110. package/templates/web/consumer/apps/web/next.config.ts +24 -0
  111. package/templates/web/consumer/apps/web/package.json +29 -0
  112. package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
  113. package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +315 -0
  114. package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
  115. package/templates/web/consumer/apps/web/src/app/layout.tsx +22 -0
  116. package/templates/web/consumer/apps/web/src/app/page.tsx +21 -0
  117. package/templates/web/consumer/apps/web/src/app/providers.tsx +25 -0
  118. package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
  119. package/templates/web/consumer/apps/web/tsconfig.json +26 -0
  120. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Package.swift +0 -0
  121. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/ContentView.swift +0 -0
  122. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
  123. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
  124. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/project.yml +0 -0
  125. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Package.swift +0 -0
  126. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
  127. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/TodoListView.swift +0 -0
  128. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
  129. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/project.yml +0 -0
@@ -0,0 +1,392 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import {
3
+ View,
4
+ Text,
5
+ TextInput,
6
+ Pressable,
7
+ FlatList,
8
+ ActivityIndicator,
9
+ StyleSheet,
10
+ Platform,
11
+ Alert,
12
+ SafeAreaView,
13
+ } from "react-native";
14
+ import { StatusBar } from "expo-status-bar";
15
+ import { init, db, callFn } from "@pylonsync/react-native";
16
+ import AsyncStorage from "@react-native-async-storage/async-storage";
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
+ const PROFILE_KEY = "__APP_NAME_SNAKE___profile_id";
23
+
24
+ type Profile = {
25
+ id: string;
26
+ userId: string;
27
+ handle: string;
28
+ displayName: string;
29
+ bio?: string | null;
30
+ createdAt: string;
31
+ };
32
+
33
+ type Post = {
34
+ id: string;
35
+ authorId: string;
36
+ body: string;
37
+ createdAt: string;
38
+ };
39
+
40
+ type Like = {
41
+ id: string;
42
+ postId: string;
43
+ profileId: string;
44
+ createdAt: string;
45
+ };
46
+
47
+ let initPromise: Promise<void> | null = null;
48
+ function ensureInit() {
49
+ if (!initPromise) {
50
+ initPromise = init({
51
+ baseUrl: PYLON_BASE_URL,
52
+ appName: "__APP_NAME_SNAKE__",
53
+ });
54
+ }
55
+ return initPromise;
56
+ }
57
+
58
+ export default function App() {
59
+ const [ready, setReady] = useState(false);
60
+ const [profileId, setProfileId] = useState<string | null>(null);
61
+ useEffect(() => {
62
+ (async () => {
63
+ await ensureInit();
64
+ const stored = await AsyncStorage.getItem(PROFILE_KEY);
65
+ setProfileId(stored);
66
+ setReady(true);
67
+ })();
68
+ }, []);
69
+ if (!ready) {
70
+ return (
71
+ <View style={[styles.screen, styles.center]}>
72
+ <ActivityIndicator />
73
+ </View>
74
+ );
75
+ }
76
+ return (
77
+ <Root
78
+ profileId={profileId}
79
+ onProfileChange={async (id) => {
80
+ if (id) await AsyncStorage.setItem(PROFILE_KEY, id);
81
+ else await AsyncStorage.removeItem(PROFILE_KEY);
82
+ setProfileId(id);
83
+ }}
84
+ />
85
+ );
86
+ }
87
+
88
+ function Root({
89
+ profileId,
90
+ onProfileChange,
91
+ }: {
92
+ profileId: string | null;
93
+ onProfileChange: (id: string | null) => void;
94
+ }) {
95
+ const { data: profiles = [] } = db.useQuery<Profile>("Profile", {});
96
+ const me = useMemo(
97
+ () => (profileId ? (profiles.find((p) => p.id === profileId) ?? null) : null),
98
+ [profileId, profiles],
99
+ );
100
+
101
+ if (!me) {
102
+ return (
103
+ <ProfileSetup
104
+ existingHandles={profiles.map((p) => p.handle)}
105
+ onSaved={(p) => onProfileChange(p.id)}
106
+ />
107
+ );
108
+ }
109
+ return <Feed me={me} profiles={profiles} />;
110
+ }
111
+
112
+ function Feed({ me, profiles }: { me: Profile; profiles: Profile[] }) {
113
+ const { data: posts = [] } = db.useQuery<Post>("Post", {
114
+ orderBy: { createdAt: "desc" },
115
+ limit: 100,
116
+ });
117
+ const { data: likes = [] } = db.useQuery<Like>("Like", {});
118
+
119
+ const profilesById = useMemo(() => {
120
+ const map = new Map<string, Profile>();
121
+ for (const p of profiles) map.set(p.id, p);
122
+ return map;
123
+ }, [profiles]);
124
+
125
+ const items = useMemo(
126
+ () =>
127
+ posts.map((post) => {
128
+ const author = profilesById.get(post.authorId) ?? null;
129
+ const postLikes = likes.filter((l) => l.postId === post.id);
130
+ const likedByMe = postLikes.some((l) => l.profileId === me.id);
131
+ return { post, author, likeCount: postLikes.length, likedByMe };
132
+ }),
133
+ [posts, profilesById, likes, me.id],
134
+ );
135
+
136
+ const [draft, setDraft] = useState("");
137
+ const [posting, setPosting] = useState(false);
138
+
139
+ async function post() {
140
+ const body = draft.trim();
141
+ if (!body) return;
142
+ setDraft("");
143
+ setPosting(true);
144
+ try {
145
+ await callFn("createPost", { body });
146
+ } catch (e) {
147
+ Alert.alert("Post failed", String(e));
148
+ setDraft(body);
149
+ } finally {
150
+ setPosting(false);
151
+ }
152
+ }
153
+
154
+ return (
155
+ <SafeAreaView style={styles.screen}>
156
+ <StatusBar style="auto" />
157
+ <View style={styles.headerRow}>
158
+ <View>
159
+ <Text style={styles.title}>__APP_NAME__</Text>
160
+ <Text style={styles.handle}>@{me.handle}</Text>
161
+ </View>
162
+ </View>
163
+
164
+ <View style={styles.composerCard}>
165
+ <TextInput
166
+ style={styles.composerInput}
167
+ placeholder="What's on your mind?"
168
+ value={draft}
169
+ onChangeText={setDraft}
170
+ multiline
171
+ maxLength={1000}
172
+ />
173
+ <View style={styles.composerFoot}>
174
+ <Text style={styles.counter}>{draft.length}/1000</Text>
175
+ <Pressable
176
+ onPress={post}
177
+ disabled={posting || !draft.trim()}
178
+ style={({ pressed }) => [
179
+ styles.buttonSmall,
180
+ (posting || !draft.trim()) && styles.buttonDisabled,
181
+ pressed && styles.buttonPressed,
182
+ ]}
183
+ >
184
+ <Text style={styles.buttonLabel}>{posting ? "Posting…" : "Post"}</Text>
185
+ </Pressable>
186
+ </View>
187
+ </View>
188
+
189
+ {items.length === 0 ? (
190
+ <Text style={styles.empty}>No posts yet.</Text>
191
+ ) : (
192
+ <FlatList
193
+ data={items}
194
+ keyExtractor={(it) => it.post.id}
195
+ contentContainerStyle={{ paddingTop: 8, paddingBottom: 32 }}
196
+ ItemSeparatorComponent={() => <View style={styles.separator} />}
197
+ renderItem={({ item }) => <Row item={item} myId={me.id} />}
198
+ />
199
+ )}
200
+ </SafeAreaView>
201
+ );
202
+ }
203
+
204
+ function Row({
205
+ item,
206
+ myId,
207
+ }: {
208
+ item: {
209
+ post: Post;
210
+ author: Profile | null;
211
+ likeCount: number;
212
+ likedByMe: boolean;
213
+ };
214
+ myId: string;
215
+ }) {
216
+ async function toggleLike() {
217
+ try {
218
+ await callFn("toggleLike", { postId: item.post.id });
219
+ } catch (e) {
220
+ Alert.alert("Like failed", String(e));
221
+ }
222
+ }
223
+ async function remove() {
224
+ try {
225
+ await callFn("deletePost", { id: item.post.id });
226
+ } catch (e) {
227
+ Alert.alert("Delete failed", String(e));
228
+ }
229
+ }
230
+ return (
231
+ <View style={styles.post}>
232
+ <View style={styles.postHead}>
233
+ <Text style={styles.postName}>
234
+ {item.author?.displayName ?? "Unknown"}
235
+ </Text>
236
+ <Text style={styles.postHandle}>@{item.author?.handle ?? "?"}</Text>
237
+ </View>
238
+ <Text style={styles.postBody}>{item.post.body}</Text>
239
+ <View style={styles.postFoot}>
240
+ <Pressable onPress={toggleLike}>
241
+ <Text
242
+ style={[styles.likeBtn, item.likedByMe && styles.likeBtnActive]}
243
+ >
244
+ {item.likedByMe ? "♥" : "♡"} {item.likeCount}
245
+ </Text>
246
+ </Pressable>
247
+ {item.author?.id === myId && (
248
+ <Pressable onPress={remove}>
249
+ <Text style={styles.deleteBtn}>Delete</Text>
250
+ </Pressable>
251
+ )}
252
+ </View>
253
+ </View>
254
+ );
255
+ }
256
+
257
+ function ProfileSetup({
258
+ existingHandles,
259
+ onSaved,
260
+ }: {
261
+ existingHandles: string[];
262
+ onSaved: (p: Profile) => void;
263
+ }) {
264
+ const [handle, setHandle] = useState("");
265
+ const [displayName, setDisplayName] = useState("");
266
+ const [bio, setBio] = useState("");
267
+ const [saving, setSaving] = useState(false);
268
+
269
+ async function save() {
270
+ const lower = handle.trim().toLowerCase();
271
+ if (existingHandles.includes(lower)) {
272
+ Alert.alert("Handle taken", `@${lower} is already in use.`);
273
+ return;
274
+ }
275
+ setSaving(true);
276
+ try {
277
+ const profile = (await callFn("upsertProfile", {
278
+ handle: lower,
279
+ displayName: displayName.trim(),
280
+ bio: bio.trim(),
281
+ })) as Profile;
282
+ onSaved(profile);
283
+ } catch (e) {
284
+ Alert.alert("Save failed", String(e));
285
+ } finally {
286
+ setSaving(false);
287
+ }
288
+ }
289
+
290
+ return (
291
+ <SafeAreaView style={styles.screen}>
292
+ <StatusBar style="auto" />
293
+ <View style={styles.content}>
294
+ <Text style={styles.title}>__APP_NAME__</Text>
295
+ <Text style={styles.subtitle}>Set up your profile</Text>
296
+ <TextInput
297
+ style={styles.input}
298
+ placeholder="handle (lowercase, 2–20)"
299
+ autoCapitalize="none"
300
+ autoCorrect={false}
301
+ value={handle}
302
+ onChangeText={setHandle}
303
+ />
304
+ <TextInput
305
+ style={styles.input}
306
+ placeholder="Display name"
307
+ value={displayName}
308
+ onChangeText={setDisplayName}
309
+ />
310
+ <TextInput
311
+ style={styles.input}
312
+ placeholder="Bio (optional)"
313
+ value={bio}
314
+ onChangeText={setBio}
315
+ />
316
+ <Pressable
317
+ onPress={save}
318
+ disabled={saving || !handle.trim() || !displayName.trim()}
319
+ style={({ pressed }) => [
320
+ styles.button,
321
+ (saving || !handle.trim() || !displayName.trim()) && styles.buttonDisabled,
322
+ pressed && styles.buttonPressed,
323
+ ]}
324
+ >
325
+ <Text style={styles.buttonLabel}>{saving ? "Saving…" : "Save"}</Text>
326
+ </Pressable>
327
+ </View>
328
+ </SafeAreaView>
329
+ );
330
+ }
331
+
332
+ const styles = StyleSheet.create({
333
+ screen: { flex: 1, paddingTop: 64, paddingHorizontal: 20, backgroundColor: "#fff" },
334
+ center: { alignItems: "center", justifyContent: "center" },
335
+ content: { padding: 20, gap: 12 },
336
+ title: { fontSize: 28, fontWeight: "600" },
337
+ subtitle: { color: "#666", marginBottom: 12 },
338
+ handle: { fontFamily: "Menlo", fontSize: 12, color: "#999" },
339
+ headerRow: { flexDirection: "row", justifyContent: "space-between", marginBottom: 16 },
340
+ input: {
341
+ borderWidth: 1,
342
+ borderColor: "#d4d4d8",
343
+ borderRadius: 6,
344
+ paddingHorizontal: 12,
345
+ paddingVertical: 8,
346
+ fontSize: 14,
347
+ marginBottom: 8,
348
+ },
349
+ composerCard: {
350
+ borderWidth: 1,
351
+ borderColor: "#e5e5e5",
352
+ borderRadius: 8,
353
+ padding: 12,
354
+ marginBottom: 12,
355
+ },
356
+ composerInput: { minHeight: 70, fontSize: 14, textAlignVertical: "top" },
357
+ composerFoot: {
358
+ flexDirection: "row",
359
+ justifyContent: "space-between",
360
+ alignItems: "center",
361
+ marginTop: 6,
362
+ },
363
+ counter: { color: "#999", fontSize: 12 },
364
+ button: {
365
+ backgroundColor: "#171717",
366
+ borderRadius: 6,
367
+ paddingHorizontal: 16,
368
+ paddingVertical: 12,
369
+ justifyContent: "center",
370
+ alignItems: "center",
371
+ },
372
+ buttonSmall: {
373
+ backgroundColor: "#171717",
374
+ borderRadius: 6,
375
+ paddingHorizontal: 12,
376
+ paddingVertical: 6,
377
+ },
378
+ buttonDisabled: { opacity: 0.5 },
379
+ buttonPressed: { opacity: 0.8 },
380
+ buttonLabel: { color: "#fff", fontWeight: "600", fontSize: 13 },
381
+ empty: { textAlign: "center", color: "#999", marginTop: 32 },
382
+ separator: { height: 1, backgroundColor: "#e5e5e5" },
383
+ post: { paddingVertical: 12 },
384
+ postHead: { flexDirection: "row", alignItems: "baseline", gap: 6 },
385
+ postName: { fontSize: 14, fontWeight: "500" },
386
+ postHandle: { fontFamily: "Menlo", fontSize: 12, color: "#999" },
387
+ postBody: { fontSize: 15, marginTop: 4, lineHeight: 20 },
388
+ postFoot: { flexDirection: "row", gap: 16, marginTop: 8 },
389
+ likeBtn: { fontSize: 13, color: "#666" },
390
+ likeBtnActive: { color: "#ec4899" },
391
+ deleteBtn: { fontSize: 13, color: "#ef4444" },
392
+ });
@@ -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
+ }
@@ -0,0 +1,24 @@
1
+ // swift-tools-version:5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "__APP_NAME_PASCAL__",
6
+ platforms: [
7
+ .iOS(.v16),
8
+ .macOS(.v13),
9
+ ],
10
+ dependencies: [
11
+ .package(url: "https://github.com/pylonsync/pylon.git", from: "0.3.0"),
12
+ ],
13
+ targets: [
14
+ .executableTarget(
15
+ name: "__APP_NAME_PASCAL__",
16
+ dependencies: [
17
+ .product(name: "PylonClient", package: "pylon"),
18
+ .product(name: "PylonSync", package: "pylon"),
19
+ .product(name: "PylonSwiftUI", package: "pylon"),
20
+ ],
21
+ path: "Sources/__APP_NAME_PASCAL__"
22
+ ),
23
+ ]
24
+ )
@@ -0,0 +1,116 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+ import PylonSync
4
+ import PylonSwiftUI
5
+
6
+ /// Two-pane chat: rooms list → room view. Subscribes to Room
7
+ /// inserts/deletes via PylonQuery — every new room created on any
8
+ /// device shows up here within ~ms over the WebSocket sync channel.
9
+ struct ChatRootView: View {
10
+ @EnvironmentObject var session: AppSession
11
+ let engine: SyncEngine
12
+ @StateObject private var rooms: PylonQuery<Room>
13
+ @State private var errorMessage: String?
14
+
15
+ init(engine: SyncEngine) {
16
+ self.engine = engine
17
+ _rooms = StateObject(
18
+ wrappedValue: PylonQuery<Room>(engine: engine, entity: "Room"),
19
+ )
20
+ }
21
+
22
+ var body: some View {
23
+ NavigationStack {
24
+ List {
25
+ Section("Your name") {
26
+ TextField("display name", text: Binding(
27
+ get: { session.authorName },
28
+ set: { session.setAuthorName($0) },
29
+ ))
30
+ .autocorrectionDisabled()
31
+ }
32
+ Section("Rooms") {
33
+ if rooms.rows.isEmpty {
34
+ Text("No rooms yet — create one below.")
35
+ .foregroundStyle(.secondary)
36
+ } else {
37
+ ForEach(sortedRooms) { r in
38
+ NavigationLink(value: r) {
39
+ VStack(alignment: .leading) {
40
+ Text(r.name)
41
+ Text("#\(r.slug)")
42
+ .font(.system(.caption, design: .monospaced))
43
+ .foregroundStyle(.secondary)
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ Section {
50
+ Button("Create room") {
51
+ Task { await createRoom() }
52
+ }
53
+ }
54
+ if let errorMessage {
55
+ Section {
56
+ Text(errorMessage)
57
+ .foregroundStyle(.red)
58
+ .font(.caption)
59
+ }
60
+ }
61
+ }
62
+ .navigationTitle("__APP_NAME__")
63
+ .navigationDestination(for: Room.self) { room in
64
+ RoomView(room: room, engine: engine)
65
+ }
66
+ }
67
+ }
68
+
69
+ private var sortedRooms: [Room] {
70
+ rooms.rows.sorted { $0.createdAt < $1.createdAt }
71
+ }
72
+
73
+ private func createRoom() async {
74
+ let name = await prompt(title: "Create room", message: "Room name?")
75
+ guard let name = name?.trimmingCharacters(in: .whitespaces),
76
+ !name.isEmpty
77
+ else { return }
78
+ let slug = name.lowercased()
79
+ .replacingOccurrences(of: "[^a-z0-9]+", with: "-", options: .regularExpression)
80
+ .trimmingCharacters(in: CharacterSet(charactersIn: "-"))
81
+ do {
82
+ // Server validates + writes via ctx.db.insert. The change_event
83
+ // flows back through the SyncEngine and PylonQuery picks up
84
+ // the new Room without us touching local state.
85
+ let _: Room = try await session.client.callFn(
86
+ "createRoom",
87
+ args: CreateRoomArgs(slug: slug, name: name),
88
+ )
89
+ } catch {
90
+ errorMessage = "Create failed: \(error.localizedDescription)"
91
+ }
92
+ }
93
+
94
+ @MainActor
95
+ private func prompt(title: String, message: String) async -> String? {
96
+ #if canImport(UIKit)
97
+ await withCheckedContinuation { (cont: CheckedContinuation<String?, Never>) in
98
+ let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
99
+ alert.addTextField()
100
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in cont.resume(returning: nil) })
101
+ alert.addAction(UIAlertAction(title: "Create", style: .default) { _ in cont.resume(returning: alert.textFields?.first?.text) })
102
+ UIApplication.shared.connectedScenes
103
+ .compactMap { ($0 as? UIWindowScene)?.windows.first }
104
+ .first?
105
+ .rootViewController?
106
+ .present(alert, animated: true)
107
+ }
108
+ #else
109
+ return nil
110
+ #endif
111
+ }
112
+ }
113
+
114
+ #if canImport(UIKit)
115
+ import UIKit
116
+ #endif
@@ -0,0 +1,26 @@
1
+ import Foundation
2
+
3
+ struct Room: Codable, Identifiable, Hashable {
4
+ let id: String
5
+ let slug: String
6
+ let name: String
7
+ let createdAt: String
8
+ }
9
+
10
+ struct Message: Codable, Identifiable, Hashable {
11
+ let id: String
12
+ let roomId: String
13
+ let authorId: String
14
+ let authorName: String
15
+ let body: String
16
+ let createdAt: String
17
+ }
18
+
19
+ struct CreateRoomArgs: Encodable { let slug: String; let name: String }
20
+ struct RoomMessagesArgs: Encodable { let roomId: String }
21
+ struct SendMessageArgs: Encodable {
22
+ let roomId: String
23
+ let body: String
24
+ let authorName: String
25
+ }
26
+ struct EmptyArgs: Encodable {}