@pylonsync/create-pylon 0.3.53 → 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 (127) 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 +414 -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 +360 -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 +34 -0
  43. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +120 -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 +137 -0
  46. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +35 -0
  47. package/templates/ios/chat/apps/ios/project.yml +42 -0
  48. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  49. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  50. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  51. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  52. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +29 -0
  53. package/templates/ios/consumer/apps/ios/project.yml +42 -0
  54. package/templates/ios/todo/apps/ios/Package.swift +23 -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 +33 -0
  68. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +140 -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 +137 -0
  71. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +37 -0
  72. package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  73. package/templates/mac/chat/apps/mac/project.yml +34 -0
  74. package/templates/mac/consumer/apps/mac/Package.swift +33 -0
  75. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  76. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  77. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  78. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  79. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +31 -0
  80. package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  81. package/templates/mac/consumer/apps/mac/project.yml +34 -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 +250 -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 +21 -0
  105. package/templates/web/chat/apps/web/src/app/page.tsx +51 -0
  106. package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
  107. package/templates/web/chat/apps/web/tsconfig.json +26 -0
  108. package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
  109. package/templates/web/consumer/apps/web/next.config.ts +24 -0
  110. package/templates/web/consumer/apps/web/package.json +29 -0
  111. package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
  112. package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +295 -0
  113. package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
  114. package/templates/web/consumer/apps/web/src/app/layout.tsx +21 -0
  115. package/templates/web/consumer/apps/web/src/app/page.tsx +55 -0
  116. package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
  117. package/templates/web/consumer/apps/web/tsconfig.json +26 -0
  118. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Package.swift +0 -0
  119. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/ContentView.swift +0 -0
  120. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
  121. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
  122. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/project.yml +0 -0
  123. /package/templates/{mobile/todo/apps/mobile → ios/consumer/apps/ios}/Package.swift +0 -0
  124. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
  125. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/TodoListView.swift +0 -0
  126. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
  127. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/project.yml +0 -0
@@ -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,360 @@
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
+ Alert,
12
+ } from "react-native";
13
+ import { StatusBar } from "expo-status-bar";
14
+ import { init, callFn } from "@pylonsync/react-native";
15
+
16
+ const PYLON_BASE_URL =
17
+ process.env.EXPO_PUBLIC_PYLON_BASE_URL ??
18
+ (Platform.OS === "android" ? "http://10.0.2.2:4321" : "http://localhost:4321");
19
+
20
+ type Profile = {
21
+ id: string;
22
+ userId: string;
23
+ handle: string;
24
+ displayName: string;
25
+ bio?: string | null;
26
+ createdAt: string;
27
+ };
28
+
29
+ type FeedItem = {
30
+ id: string;
31
+ body: string;
32
+ createdAt: string;
33
+ author: { id: string; handle: string; displayName: string } | null;
34
+ likeCount: number;
35
+ likedByMe: boolean;
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 [me, setMe] = useState<Profile | null>(null);
52
+
53
+ useEffect(() => {
54
+ ensureInit().then(async () => {
55
+ try {
56
+ const profile = await callFn<Profile | null>("myProfile", {});
57
+ setMe(profile);
58
+ } catch {
59
+ setMe(null);
60
+ }
61
+ setReady(true);
62
+ });
63
+ }, []);
64
+
65
+ if (!ready) {
66
+ return (
67
+ <View style={[styles.screen, styles.center]}>
68
+ <ActivityIndicator />
69
+ </View>
70
+ );
71
+ }
72
+ if (!me) {
73
+ return <ProfileSetup onSaved={setMe} />;
74
+ }
75
+ return <Feed me={me} />;
76
+ }
77
+
78
+ function ProfileSetup({ onSaved }: { onSaved: (p: Profile) => void }) {
79
+ const [handle, setHandle] = useState("");
80
+ const [displayName, setDisplayName] = useState("");
81
+ const [bio, setBio] = useState("");
82
+ const [saving, setSaving] = useState(false);
83
+
84
+ async function save() {
85
+ setSaving(true);
86
+ try {
87
+ const p = await callFn<Profile>("upsertProfile", {
88
+ handle: handle.trim().toLowerCase(),
89
+ displayName: displayName.trim(),
90
+ bio: bio.trim(),
91
+ });
92
+ onSaved(p);
93
+ } catch (e) {
94
+ Alert.alert("Save failed", String(e));
95
+ } finally {
96
+ setSaving(false);
97
+ }
98
+ }
99
+
100
+ return (
101
+ <View style={styles.screen}>
102
+ <StatusBar style="auto" />
103
+ <Text style={styles.title}>__APP_NAME__</Text>
104
+ <Text style={styles.subtitle}>Set up your profile</Text>
105
+ <TextInput
106
+ style={styles.input}
107
+ placeholder="handle (lowercase, 2–20)"
108
+ autoCapitalize="none"
109
+ autoCorrect={false}
110
+ value={handle}
111
+ onChangeText={setHandle}
112
+ />
113
+ <TextInput
114
+ style={styles.input}
115
+ placeholder="Display name"
116
+ value={displayName}
117
+ onChangeText={setDisplayName}
118
+ />
119
+ <TextInput
120
+ style={styles.input}
121
+ placeholder="Bio (optional)"
122
+ value={bio}
123
+ onChangeText={setBio}
124
+ />
125
+ <Pressable
126
+ onPress={save}
127
+ disabled={saving || !handle.trim() || !displayName.trim()}
128
+ style={({ pressed }) => [
129
+ styles.button,
130
+ (saving || !handle.trim() || !displayName.trim()) && styles.buttonDisabled,
131
+ pressed && styles.buttonPressed,
132
+ ]}
133
+ >
134
+ <Text style={styles.buttonLabel}>{saving ? "Saving…" : "Save"}</Text>
135
+ </Pressable>
136
+ </View>
137
+ );
138
+ }
139
+
140
+ function Feed({ me }: { me: Profile }) {
141
+ const [feed, setFeed] = useState<FeedItem[]>([]);
142
+ const [loading, setLoading] = useState(true);
143
+ const [draft, setDraft] = useState("");
144
+ const [posting, setPosting] = useState(false);
145
+
146
+ useEffect(() => {
147
+ void load();
148
+ }, []);
149
+
150
+ async function load() {
151
+ setLoading(true);
152
+ try {
153
+ setFeed(await callFn<FeedItem[]>("feed", {}));
154
+ } catch (e) {
155
+ Alert.alert("Load failed", String(e));
156
+ } finally {
157
+ setLoading(false);
158
+ }
159
+ }
160
+
161
+ async function post() {
162
+ const body = draft.trim();
163
+ if (!body) return;
164
+ setPosting(true);
165
+ try {
166
+ const item = await callFn<FeedItem>("createPost", { body });
167
+ setFeed((prev) => [item, ...prev]);
168
+ setDraft("");
169
+ } catch (e) {
170
+ Alert.alert("Post failed", String(e));
171
+ } finally {
172
+ setPosting(false);
173
+ }
174
+ }
175
+
176
+ async function toggleLike(item: FeedItem) {
177
+ setFeed((prev) =>
178
+ prev.map((p) =>
179
+ p.id === item.id
180
+ ? {
181
+ ...p,
182
+ likedByMe: !p.likedByMe,
183
+ likeCount: p.likeCount + (p.likedByMe ? -1 : 1),
184
+ }
185
+ : p,
186
+ ),
187
+ );
188
+ try {
189
+ const result = await callFn<{ liked: boolean; likeCount: number }>(
190
+ "toggleLike",
191
+ { postId: item.id },
192
+ );
193
+ setFeed((prev) =>
194
+ prev.map((p) =>
195
+ p.id === item.id
196
+ ? { ...p, likedByMe: result.liked, likeCount: result.likeCount }
197
+ : p,
198
+ ),
199
+ );
200
+ } catch {
201
+ setFeed((prev) =>
202
+ prev.map((p) =>
203
+ p.id === item.id
204
+ ? { ...p, likedByMe: item.likedByMe, likeCount: item.likeCount }
205
+ : p,
206
+ ),
207
+ );
208
+ }
209
+ }
210
+
211
+ async function remove(item: FeedItem) {
212
+ setFeed((prev) => prev.filter((p) => p.id !== item.id));
213
+ try {
214
+ await callFn("deletePost", { id: item.id });
215
+ } catch {
216
+ setFeed((prev) => [...prev, item]);
217
+ }
218
+ }
219
+
220
+ return (
221
+ <View style={styles.screen}>
222
+ <StatusBar style="auto" />
223
+ <View style={styles.headerRow}>
224
+ <View>
225
+ <Text style={styles.title}>__APP_NAME__</Text>
226
+ <Text style={styles.handle}>@{me.handle}</Text>
227
+ </View>
228
+ </View>
229
+
230
+ <View style={styles.composerCard}>
231
+ <TextInput
232
+ style={styles.composerInput}
233
+ placeholder="What's on your mind?"
234
+ value={draft}
235
+ onChangeText={setDraft}
236
+ multiline
237
+ maxLength={1000}
238
+ />
239
+ <View style={styles.composerFoot}>
240
+ <Text style={styles.counter}>{draft.length}/1000</Text>
241
+ <Pressable
242
+ onPress={post}
243
+ disabled={posting || !draft.trim()}
244
+ style={({ pressed }) => [
245
+ styles.buttonSmall,
246
+ (posting || !draft.trim()) && styles.buttonDisabled,
247
+ pressed && styles.buttonPressed,
248
+ ]}
249
+ >
250
+ <Text style={styles.buttonLabel}>{posting ? "Posting…" : "Post"}</Text>
251
+ </Pressable>
252
+ </View>
253
+ </View>
254
+
255
+ {loading ? (
256
+ <ActivityIndicator style={{ marginTop: 24 }} />
257
+ ) : feed.length === 0 ? (
258
+ <Text style={styles.empty}>No posts yet.</Text>
259
+ ) : (
260
+ <FlatList
261
+ data={feed}
262
+ keyExtractor={(p) => p.id}
263
+ contentContainerStyle={{ paddingTop: 8, paddingBottom: 32 }}
264
+ ItemSeparatorComponent={() => <View style={styles.separator} />}
265
+ renderItem={({ item }) => (
266
+ <View style={styles.post}>
267
+ <View style={styles.postHead}>
268
+ <Text style={styles.postName}>
269
+ {item.author?.displayName ?? "Unknown"}
270
+ </Text>
271
+ <Text style={styles.postHandle}>
272
+ @{item.author?.handle ?? "?"}
273
+ </Text>
274
+ </View>
275
+ <Text style={styles.postBody}>{item.body}</Text>
276
+ <View style={styles.postFoot}>
277
+ <Pressable onPress={() => toggleLike(item)}>
278
+ <Text
279
+ style={[
280
+ styles.likeBtn,
281
+ item.likedByMe && styles.likeBtnActive,
282
+ ]}
283
+ >
284
+ {item.likedByMe ? "♥" : "♡"} {item.likeCount}
285
+ </Text>
286
+ </Pressable>
287
+ {item.author?.id === me.id && (
288
+ <Pressable onPress={() => remove(item)}>
289
+ <Text style={styles.deleteBtn}>Delete</Text>
290
+ </Pressable>
291
+ )}
292
+ </View>
293
+ </View>
294
+ )}
295
+ />
296
+ )}
297
+ </View>
298
+ );
299
+ }
300
+
301
+ const styles = StyleSheet.create({
302
+ screen: { flex: 1, paddingTop: 64, paddingHorizontal: 20, backgroundColor: "#fff" },
303
+ center: { alignItems: "center", justifyContent: "center" },
304
+ title: { fontSize: 28, fontWeight: "600" },
305
+ subtitle: { color: "#666", marginTop: 4, marginBottom: 20 },
306
+ handle: { fontFamily: "Menlo", fontSize: 12, color: "#999" },
307
+ headerRow: { flexDirection: "row", justifyContent: "space-between", marginBottom: 16 },
308
+ input: {
309
+ borderWidth: 1,
310
+ borderColor: "#d4d4d8",
311
+ borderRadius: 6,
312
+ paddingHorizontal: 12,
313
+ paddingVertical: 8,
314
+ fontSize: 14,
315
+ marginBottom: 8,
316
+ },
317
+ composerCard: {
318
+ borderWidth: 1,
319
+ borderColor: "#e5e5e5",
320
+ borderRadius: 8,
321
+ padding: 12,
322
+ marginBottom: 12,
323
+ },
324
+ composerInput: { minHeight: 70, fontSize: 14, textAlignVertical: "top" },
325
+ composerFoot: {
326
+ flexDirection: "row",
327
+ justifyContent: "space-between",
328
+ alignItems: "center",
329
+ marginTop: 6,
330
+ },
331
+ counter: { color: "#999", fontSize: 12 },
332
+ button: {
333
+ backgroundColor: "#171717",
334
+ borderRadius: 6,
335
+ paddingHorizontal: 16,
336
+ paddingVertical: 12,
337
+ justifyContent: "center",
338
+ alignItems: "center",
339
+ },
340
+ buttonSmall: {
341
+ backgroundColor: "#171717",
342
+ borderRadius: 6,
343
+ paddingHorizontal: 12,
344
+ paddingVertical: 6,
345
+ },
346
+ buttonDisabled: { opacity: 0.5 },
347
+ buttonPressed: { opacity: 0.8 },
348
+ buttonLabel: { color: "#fff", fontWeight: "600", fontSize: 13 },
349
+ empty: { textAlign: "center", color: "#999", marginTop: 32 },
350
+ separator: { height: 1, backgroundColor: "#e5e5e5" },
351
+ post: { paddingVertical: 12 },
352
+ postHead: { flexDirection: "row", alignItems: "baseline", gap: 6 },
353
+ postName: { fontSize: 14, fontWeight: "500" },
354
+ postHandle: { fontFamily: "Menlo", fontSize: 12, color: "#999" },
355
+ postBody: { fontSize: 15, marginTop: 4, lineHeight: 20 },
356
+ postFoot: { flexDirection: "row", gap: 16, marginTop: 8 },
357
+ likeBtn: { fontSize: 13, color: "#666" },
358
+ likeBtnActive: { color: "#ec4899" },
359
+ deleteBtn: { fontSize: 13, color: "#ef4444" },
360
+ });
@@ -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,34 @@
1
+ // swift-tools-version:5.9
2
+ import PackageDescription
3
+
4
+ // SwiftPM package for the __APP_NAME__ mobile app.
5
+ //
6
+ // The executable target runs on macOS via `swift run`. For an iOS
7
+ // build, generate an Xcode project from `project.yml`:
8
+ //
9
+ // brew install xcodegen
10
+ // xcodegen generate
11
+ // open __APP_NAME_PASCAL__.xcodeproj
12
+ //
13
+ // The Xcode project pulls the same Sources/__APP_NAME_PASCAL__/ tree
14
+ // as `swift build`, so iOS + macOS share one source set.
15
+ let package = Package(
16
+ name: "__APP_NAME_PASCAL__",
17
+ platforms: [
18
+ .iOS(.v16),
19
+ .macOS(.v13),
20
+ ],
21
+ dependencies: [
22
+ .package(url: "https://github.com/pylonsync/pylon.git", from: "0.3.0"),
23
+ ],
24
+ targets: [
25
+ .executableTarget(
26
+ name: "__APP_NAME_PASCAL__",
27
+ dependencies: [
28
+ .product(name: "PylonClient", package: "pylon"),
29
+ .product(name: "PylonSwiftUI", package: "pylon"),
30
+ ],
31
+ path: "Sources/__APP_NAME_PASCAL__"
32
+ ),
33
+ ]
34
+ )
@@ -0,0 +1,120 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ /// Two-pane chat: rooms list → room view. Polls the active room
5
+ /// every 1.5s. For realtime, swap `pollMessages()` out for a
6
+ /// PylonQuery<Message> from PylonSwiftUI subscribed by roomId.
7
+ struct ChatRootView: View {
8
+ @EnvironmentObject var session: AppSession
9
+ @State private var rooms: [Room] = []
10
+ @State private var loadingRooms = true
11
+ @State private var errorMessage: String?
12
+
13
+ var body: some View {
14
+ NavigationStack {
15
+ List {
16
+ Section("Your name") {
17
+ TextField("display name", text: Binding(
18
+ get: { session.authorName },
19
+ set: { session.setAuthorName($0) },
20
+ ))
21
+ .autocorrectionDisabled()
22
+ }
23
+ Section("Rooms") {
24
+ if loadingRooms {
25
+ ProgressView()
26
+ } else if rooms.isEmpty {
27
+ Text("No rooms yet. Create one below.")
28
+ .foregroundStyle(.secondary)
29
+ } else {
30
+ ForEach(rooms) { r in
31
+ NavigationLink(value: r) {
32
+ VStack(alignment: .leading) {
33
+ Text(r.name)
34
+ Text("#\(r.slug)")
35
+ .font(.system(.caption, design: .monospaced))
36
+ .foregroundStyle(.secondary)
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ Section {
43
+ Button("Create room") {
44
+ Task { await createRoom() }
45
+ }
46
+ }
47
+ if let errorMessage {
48
+ Section {
49
+ Text(errorMessage)
50
+ .foregroundStyle(.red)
51
+ .font(.caption)
52
+ }
53
+ }
54
+ }
55
+ .navigationTitle("__APP_NAME__")
56
+ .navigationDestination(for: Room.self) { room in
57
+ RoomView(room: room)
58
+ }
59
+ .task { await loadRooms() }
60
+ .refreshable { await loadRooms() }
61
+ }
62
+ }
63
+
64
+ private func loadRooms() async {
65
+ loadingRooms = true
66
+ defer { loadingRooms = false }
67
+ do {
68
+ rooms = try await session.pylon.callFn("listRooms", args: EmptyArgs())
69
+ errorMessage = nil
70
+ } catch {
71
+ errorMessage = "Load failed: \(error.localizedDescription)"
72
+ }
73
+ }
74
+
75
+ private func createRoom() async {
76
+ let alert = await prompt(title: "Create room", message: "Room name?")
77
+ guard let name = alert?.trimmingCharacters(in: .whitespaces),
78
+ !name.isEmpty
79
+ else { return }
80
+ let slug = name.lowercased()
81
+ .replacingOccurrences(of: "[^a-z0-9]+", with: "-", options: .regularExpression)
82
+ .trimmingCharacters(in: CharacterSet(charactersIn: "-"))
83
+ do {
84
+ let room: Room = try await session.pylon.callFn(
85
+ "createRoom",
86
+ args: CreateRoomArgs(slug: slug, name: name),
87
+ )
88
+ rooms.append(room)
89
+ } catch {
90
+ errorMessage = "Create failed: \(error.localizedDescription)"
91
+ }
92
+ }
93
+
94
+ @MainActor
95
+ private func prompt(title: String, message: String) async -> String? {
96
+ // Minimal SwiftUI prompt — for production replace with an
97
+ // in-tree alert + TextField sheet. The scaffold uses a tiny
98
+ // UIKit detour on iOS so the demo works without a custom
99
+ // modal implementation.
100
+ #if canImport(UIKit)
101
+ await withCheckedContinuation { (cont: CheckedContinuation<String?, Never>) in
102
+ let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
103
+ alert.addTextField()
104
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in cont.resume(returning: nil) })
105
+ alert.addAction(UIAlertAction(title: "Create", style: .default) { _ in cont.resume(returning: alert.textFields?.first?.text) })
106
+ UIApplication.shared.connectedScenes
107
+ .compactMap { ($0 as? UIWindowScene)?.windows.first }
108
+ .first?
109
+ .rootViewController?
110
+ .present(alert, animated: true)
111
+ }
112
+ #else
113
+ return nil
114
+ #endif
115
+ }
116
+ }
117
+
118
+ #if canImport(UIKit)
119
+ import UIKit
120
+ #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 {}