@pylonsync/create-pylon 0.3.50 → 0.3.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-pylon.js +292 -1157
- package/package.json +4 -3
- package/templates/_root/.env.example +9 -0
- package/templates/_root/README.md +43 -0
- package/templates/backend/barebones/apps/api/functions/createWidget.ts +22 -0
- package/templates/backend/barebones/apps/api/functions/listWidgets.ts +18 -0
- package/templates/backend/barebones/apps/api/package.json +20 -0
- package/templates/backend/barebones/apps/api/schema.ts +61 -0
- package/templates/backend/barebones/apps/api/tsconfig.json +13 -0
- package/templates/backend/todo/apps/api/functions/addTodo.ts +27 -0
- package/templates/backend/todo/apps/api/functions/deleteTodo.ts +14 -0
- package/templates/backend/todo/apps/api/functions/editTodo.ts +16 -0
- package/templates/backend/todo/apps/api/functions/listTodos.ts +24 -0
- package/templates/backend/todo/apps/api/functions/reorderTodo.ts +14 -0
- package/templates/backend/todo/apps/api/functions/toggleTodo.ts +13 -0
- package/templates/backend/todo/apps/api/package.json +20 -0
- package/templates/backend/todo/apps/api/schema.ts +85 -0
- package/templates/backend/todo/apps/api/tsconfig.json +13 -0
- package/templates/expo/barebones/apps/expo/App.tsx +166 -0
- package/templates/expo/barebones/apps/expo/app.json +31 -0
- package/templates/expo/barebones/apps/expo/babel.config.js +6 -0
- package/templates/expo/barebones/apps/expo/package.json +30 -0
- package/templates/expo/barebones/apps/expo/tsconfig.json +16 -0
- package/templates/expo/todo/apps/expo/App.tsx +287 -0
- package/templates/expo/todo/apps/expo/app.json +25 -0
- package/templates/expo/todo/apps/expo/babel.config.js +6 -0
- package/templates/expo/todo/apps/expo/package.json +30 -0
- package/templates/expo/todo/apps/expo/tsconfig.json +16 -0
- package/templates/mobile/barebones/apps/mobile/Package.swift +34 -0
- package/templates/mobile/barebones/apps/mobile/Sources/__APP_NAME_PASCAL__/ContentView.swift +98 -0
- package/templates/mobile/barebones/apps/mobile/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
- package/templates/mobile/barebones/apps/mobile/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -0
- package/templates/mobile/barebones/apps/mobile/project.yml +42 -0
- package/templates/mobile/todo/apps/mobile/Package.swift +23 -0
- package/templates/mobile/todo/apps/mobile/Sources/__APP_NAME_PASCAL__/Models.swift +18 -0
- package/templates/mobile/todo/apps/mobile/Sources/__APP_NAME_PASCAL__/TodoListView.swift +230 -0
- package/templates/mobile/todo/apps/mobile/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +28 -0
- package/templates/mobile/todo/apps/mobile/project.yml +32 -0
- package/templates/ui/packages/ui/package.json +26 -0
- package/templates/ui/packages/ui/src/button.tsx +44 -0
- package/templates/ui/packages/ui/src/card.tsx +39 -0
- package/templates/ui/packages/ui/src/cn.ts +12 -0
- package/templates/ui/packages/ui/src/index.ts +4 -0
- package/templates/ui/packages/ui/src/input.tsx +19 -0
- package/templates/ui/packages/ui/tsconfig.json +15 -0
- package/templates/web/barebones/apps/web/next-env.d.ts +2 -0
- package/templates/web/barebones/apps/web/next.config.ts +40 -0
- package/templates/web/barebones/apps/web/package.json +29 -0
- package/templates/web/barebones/apps/web/postcss.config.mjs +4 -0
- package/templates/web/barebones/apps/web/src/app/components/WidgetList.tsx +81 -0
- package/templates/web/barebones/apps/web/src/app/globals.css +9 -0
- package/templates/web/barebones/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/barebones/apps/web/src/app/page.tsx +43 -0
- package/templates/web/barebones/apps/web/src/lib/pylon.ts +14 -0
- package/templates/web/barebones/apps/web/tsconfig.json +26 -0
- package/templates/web/todo/apps/web/next-env.d.ts +2 -0
- package/templates/web/todo/apps/web/next.config.ts +24 -0
- package/templates/web/todo/apps/web/package.json +32 -0
- package/templates/web/todo/apps/web/postcss.config.mjs +3 -0
- package/templates/web/todo/apps/web/src/app/components/TodoList.tsx +310 -0
- package/templates/web/todo/apps/web/src/app/globals.css +6 -0
- package/templates/web/todo/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/todo/apps/web/src/app/page.tsx +36 -0
- package/templates/web/todo/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/todo/apps/web/tsconfig.json +26 -0
|
@@ -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,287 @@
|
|
|
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, db, 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 Todo = {
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
done: boolean;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
position?: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let initPromise: Promise<void> | null = null;
|
|
29
|
+
function ensureInit() {
|
|
30
|
+
if (!initPromise) {
|
|
31
|
+
initPromise = init({
|
|
32
|
+
baseUrl: PYLON_BASE_URL,
|
|
33
|
+
appName: "__APP_NAME_SNAKE__",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return initPromise;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function App() {
|
|
40
|
+
const [ready, setReady] = useState(false);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
ensureInit().then(() => setReady(true));
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
if (!ready) {
|
|
46
|
+
return (
|
|
47
|
+
<View style={[styles.screen, styles.center]}>
|
|
48
|
+
<ActivityIndicator />
|
|
49
|
+
</View>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return <TodoApp />;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function TodoApp() {
|
|
56
|
+
const { data: todos, loading } = db.useQuery<Todo>("Todo", {});
|
|
57
|
+
const [draft, setDraft] = useState("");
|
|
58
|
+
const [busy, setBusy] = useState(false);
|
|
59
|
+
const [editingId, setEditingId] = useState<string | null>(null);
|
|
60
|
+
const [editingDraft, setEditingDraft] = useState("");
|
|
61
|
+
|
|
62
|
+
async function add() {
|
|
63
|
+
const trimmed = draft.trim();
|
|
64
|
+
if (!trimmed) return;
|
|
65
|
+
setBusy(true);
|
|
66
|
+
try {
|
|
67
|
+
await callFn("addTodo", { title: trimmed });
|
|
68
|
+
setDraft("");
|
|
69
|
+
} catch (e) {
|
|
70
|
+
Alert.alert("Add failed", String(e));
|
|
71
|
+
} finally {
|
|
72
|
+
setBusy(false);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function toggle(t: Todo) {
|
|
77
|
+
try {
|
|
78
|
+
await callFn("toggleTodo", { id: t.id, done: !t.done });
|
|
79
|
+
} catch (e) {
|
|
80
|
+
Alert.alert("Toggle failed", String(e));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function remove(t: Todo) {
|
|
85
|
+
try {
|
|
86
|
+
await callFn("deleteTodo", { id: t.id });
|
|
87
|
+
} catch (e) {
|
|
88
|
+
Alert.alert("Delete failed", String(e));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function commitEdit(t: Todo) {
|
|
93
|
+
const trimmed = editingDraft.trim();
|
|
94
|
+
if (!trimmed || trimmed === t.title) {
|
|
95
|
+
setEditingId(null);
|
|
96
|
+
setEditingDraft("");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await callFn("editTodo", { id: t.id, title: trimmed });
|
|
101
|
+
} catch (e) {
|
|
102
|
+
Alert.alert("Rename failed", String(e));
|
|
103
|
+
} finally {
|
|
104
|
+
setEditingId(null);
|
|
105
|
+
setEditingDraft("");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const sorted = (todos ?? [])
|
|
110
|
+
.slice()
|
|
111
|
+
.sort((a, b) => {
|
|
112
|
+
const ap = typeof a.position === "number" ? a.position : Date.parse(a.createdAt) || 0;
|
|
113
|
+
const bp = typeof b.position === "number" ? b.position : Date.parse(b.createdAt) || 0;
|
|
114
|
+
return ap - bp;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<View style={styles.screen}>
|
|
119
|
+
<StatusBar style="auto" />
|
|
120
|
+
<Text style={styles.title}>__APP_NAME__</Text>
|
|
121
|
+
<Text style={styles.subtitle}>
|
|
122
|
+
Tap to toggle. Long-press to edit. Swipe-style buttons for delete.
|
|
123
|
+
</Text>
|
|
124
|
+
|
|
125
|
+
<View style={styles.row}>
|
|
126
|
+
<TextInput
|
|
127
|
+
style={styles.input}
|
|
128
|
+
placeholder="What needs doing?"
|
|
129
|
+
value={draft}
|
|
130
|
+
onChangeText={setDraft}
|
|
131
|
+
onSubmitEditing={add}
|
|
132
|
+
editable={!busy}
|
|
133
|
+
autoCorrect={false}
|
|
134
|
+
/>
|
|
135
|
+
<Pressable
|
|
136
|
+
onPress={add}
|
|
137
|
+
disabled={busy || !draft.trim()}
|
|
138
|
+
style={({ pressed }) => [
|
|
139
|
+
styles.button,
|
|
140
|
+
(busy || !draft.trim()) && styles.buttonDisabled,
|
|
141
|
+
pressed && styles.buttonPressed,
|
|
142
|
+
]}
|
|
143
|
+
>
|
|
144
|
+
<Text style={styles.buttonLabel}>Add</Text>
|
|
145
|
+
</Pressable>
|
|
146
|
+
</View>
|
|
147
|
+
|
|
148
|
+
{loading ? (
|
|
149
|
+
<ActivityIndicator style={{ marginTop: 24 }} />
|
|
150
|
+
) : sorted.length === 0 ? (
|
|
151
|
+
<Text style={styles.empty}>No todos yet.</Text>
|
|
152
|
+
) : (
|
|
153
|
+
<FlatList
|
|
154
|
+
data={sorted}
|
|
155
|
+
keyExtractor={(t) => t.id}
|
|
156
|
+
contentContainerStyle={{ paddingTop: 16 }}
|
|
157
|
+
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
|
158
|
+
renderItem={({ item }) => (
|
|
159
|
+
<Row
|
|
160
|
+
todo={item}
|
|
161
|
+
editing={editingId === item.id}
|
|
162
|
+
editingDraft={editingDraft}
|
|
163
|
+
onEditingDraftChange={setEditingDraft}
|
|
164
|
+
onStartEdit={() => {
|
|
165
|
+
setEditingId(item.id);
|
|
166
|
+
setEditingDraft(item.title);
|
|
167
|
+
}}
|
|
168
|
+
onCommitEdit={() => commitEdit(item)}
|
|
169
|
+
onCancelEdit={() => {
|
|
170
|
+
setEditingId(null);
|
|
171
|
+
setEditingDraft("");
|
|
172
|
+
}}
|
|
173
|
+
onToggle={() => toggle(item)}
|
|
174
|
+
onDelete={() => remove(item)}
|
|
175
|
+
/>
|
|
176
|
+
)}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
</View>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function Row({
|
|
184
|
+
todo,
|
|
185
|
+
editing,
|
|
186
|
+
editingDraft,
|
|
187
|
+
onEditingDraftChange,
|
|
188
|
+
onStartEdit,
|
|
189
|
+
onCommitEdit,
|
|
190
|
+
onCancelEdit,
|
|
191
|
+
onToggle,
|
|
192
|
+
onDelete,
|
|
193
|
+
}: {
|
|
194
|
+
todo: Todo;
|
|
195
|
+
editing: boolean;
|
|
196
|
+
editingDraft: string;
|
|
197
|
+
onEditingDraftChange: (v: string) => void;
|
|
198
|
+
onStartEdit: () => void;
|
|
199
|
+
onCommitEdit: () => void;
|
|
200
|
+
onCancelEdit: () => void;
|
|
201
|
+
onToggle: () => void;
|
|
202
|
+
onDelete: () => void;
|
|
203
|
+
}) {
|
|
204
|
+
return (
|
|
205
|
+
<View style={styles.item}>
|
|
206
|
+
<Pressable onPress={onToggle} hitSlop={8}>
|
|
207
|
+
<Text style={[styles.checkbox, todo.done && styles.checkboxDone]}>
|
|
208
|
+
{todo.done ? "☑" : "☐"}
|
|
209
|
+
</Text>
|
|
210
|
+
</Pressable>
|
|
211
|
+
{editing ? (
|
|
212
|
+
<TextInput
|
|
213
|
+
style={[styles.itemText, styles.itemEditInput]}
|
|
214
|
+
value={editingDraft}
|
|
215
|
+
onChangeText={onEditingDraftChange}
|
|
216
|
+
onSubmitEditing={onCommitEdit}
|
|
217
|
+
onBlur={onCommitEdit}
|
|
218
|
+
autoFocus
|
|
219
|
+
blurOnSubmit
|
|
220
|
+
/>
|
|
221
|
+
) : (
|
|
222
|
+
<Pressable onLongPress={onStartEdit} style={{ flex: 1 }}>
|
|
223
|
+
<Text
|
|
224
|
+
style={[styles.itemText, todo.done && styles.itemTextDone]}
|
|
225
|
+
numberOfLines={1}
|
|
226
|
+
>
|
|
227
|
+
{todo.title}
|
|
228
|
+
</Text>
|
|
229
|
+
</Pressable>
|
|
230
|
+
)}
|
|
231
|
+
{editing ? (
|
|
232
|
+
<Pressable onPress={onCancelEdit} hitSlop={8}>
|
|
233
|
+
<Text style={styles.actionLabel}>Cancel</Text>
|
|
234
|
+
</Pressable>
|
|
235
|
+
) : (
|
|
236
|
+
<Pressable onPress={onDelete} hitSlop={8}>
|
|
237
|
+
<Text style={[styles.actionLabel, styles.actionDelete]}>Delete</Text>
|
|
238
|
+
</Pressable>
|
|
239
|
+
)}
|
|
240
|
+
</View>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const styles = StyleSheet.create({
|
|
245
|
+
screen: { flex: 1, paddingTop: 64, paddingHorizontal: 20, backgroundColor: "#fff" },
|
|
246
|
+
center: { alignItems: "center", justifyContent: "center" },
|
|
247
|
+
title: { fontSize: 28, fontWeight: "600" },
|
|
248
|
+
subtitle: { color: "#666", marginTop: 4, marginBottom: 20 },
|
|
249
|
+
row: { flexDirection: "row", gap: 8 },
|
|
250
|
+
input: {
|
|
251
|
+
flex: 1,
|
|
252
|
+
borderWidth: 1,
|
|
253
|
+
borderColor: "#d4d4d8",
|
|
254
|
+
borderRadius: 6,
|
|
255
|
+
paddingHorizontal: 12,
|
|
256
|
+
paddingVertical: 8,
|
|
257
|
+
fontSize: 14,
|
|
258
|
+
},
|
|
259
|
+
button: {
|
|
260
|
+
backgroundColor: "#171717",
|
|
261
|
+
borderRadius: 6,
|
|
262
|
+
paddingHorizontal: 16,
|
|
263
|
+
justifyContent: "center",
|
|
264
|
+
},
|
|
265
|
+
buttonDisabled: { opacity: 0.5 },
|
|
266
|
+
buttonPressed: { opacity: 0.8 },
|
|
267
|
+
buttonLabel: { color: "#fff", fontWeight: "600" },
|
|
268
|
+
empty: { textAlign: "center", color: "#999", marginTop: 32 },
|
|
269
|
+
item: {
|
|
270
|
+
flexDirection: "row",
|
|
271
|
+
alignItems: "center",
|
|
272
|
+
gap: 12,
|
|
273
|
+
paddingVertical: 12,
|
|
274
|
+
},
|
|
275
|
+
checkbox: { fontSize: 22, color: "#9ca3af" },
|
|
276
|
+
checkboxDone: { color: "#16a34a" },
|
|
277
|
+
itemText: { flex: 1, fontSize: 15 },
|
|
278
|
+
itemTextDone: { color: "#9ca3af", textDecorationLine: "line-through" },
|
|
279
|
+
itemEditInput: {
|
|
280
|
+
borderBottomWidth: 1,
|
|
281
|
+
borderColor: "#d4d4d8",
|
|
282
|
+
paddingVertical: 4,
|
|
283
|
+
},
|
|
284
|
+
actionLabel: { fontSize: 13, color: "#6b7280" },
|
|
285
|
+
actionDelete: { color: "#ef4444" },
|
|
286
|
+
separator: { height: 1, backgroundColor: "#e5e5e5" },
|
|
287
|
+
});
|
|
@@ -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,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,98 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import PylonClient
|
|
3
|
+
|
|
4
|
+
/// Lists every Widget and lets the user create a new one. Uses
|
|
5
|
+
/// PylonClient's HTTP API directly — no offline mirror, no realtime
|
|
6
|
+
/// subscription. Plenty for a barebones starter; upgrade to
|
|
7
|
+
/// `PylonQuery` from `PylonSwiftUI` when you need live updates.
|
|
8
|
+
struct ContentView: View {
|
|
9
|
+
@EnvironmentObject var session: AppSession
|
|
10
|
+
|
|
11
|
+
@State private var widgets: [Widget] = []
|
|
12
|
+
@State private var newName: String = ""
|
|
13
|
+
@State private var loading = true
|
|
14
|
+
@State private var creating = false
|
|
15
|
+
@State private var errorMessage: String?
|
|
16
|
+
|
|
17
|
+
var body: some View {
|
|
18
|
+
NavigationStack {
|
|
19
|
+
List {
|
|
20
|
+
Section("Create") {
|
|
21
|
+
HStack {
|
|
22
|
+
TextField("Name a widget…", text: $newName)
|
|
23
|
+
.textFieldStyle(.roundedBorder)
|
|
24
|
+
.autocorrectionDisabled()
|
|
25
|
+
Button("Add") { Task { await create() } }
|
|
26
|
+
.buttonStyle(.borderedProminent)
|
|
27
|
+
.disabled(newName.trimmingCharacters(in: .whitespaces).isEmpty || creating)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Section("Widgets") {
|
|
32
|
+
if loading {
|
|
33
|
+
ProgressView()
|
|
34
|
+
} else if widgets.isEmpty {
|
|
35
|
+
Text("No widgets yet.")
|
|
36
|
+
.foregroundStyle(.secondary)
|
|
37
|
+
} else {
|
|
38
|
+
ForEach(widgets) { w in
|
|
39
|
+
HStack {
|
|
40
|
+
Text(w.name).font(.body)
|
|
41
|
+
Spacer()
|
|
42
|
+
Text("count: \(w.count)")
|
|
43
|
+
.font(.system(.caption, design: .monospaced))
|
|
44
|
+
.foregroundStyle(.secondary)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if let errorMessage {
|
|
51
|
+
Section {
|
|
52
|
+
Text(errorMessage)
|
|
53
|
+
.foregroundStyle(.red)
|
|
54
|
+
.font(.caption)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
.navigationTitle("__APP_NAME__")
|
|
59
|
+
.task { await load() }
|
|
60
|
+
.refreshable { await load() }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private func load() async {
|
|
65
|
+
loading = true
|
|
66
|
+
defer { loading = false }
|
|
67
|
+
do {
|
|
68
|
+
let rows: [Widget] = try await session.pylon.callFn(
|
|
69
|
+
"listWidgets",
|
|
70
|
+
args: EmptyArgs(),
|
|
71
|
+
)
|
|
72
|
+
widgets = rows
|
|
73
|
+
errorMessage = nil
|
|
74
|
+
} catch {
|
|
75
|
+
errorMessage = "Load failed: \(error.localizedDescription)"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private func create() async {
|
|
80
|
+
let trimmed = newName.trimmingCharacters(in: .whitespaces)
|
|
81
|
+
guard !trimmed.isEmpty else { return }
|
|
82
|
+
creating = true
|
|
83
|
+
defer { creating = false }
|
|
84
|
+
do {
|
|
85
|
+
let widget: Widget = try await session.pylon.callFn(
|
|
86
|
+
"createWidget",
|
|
87
|
+
args: CreateWidgetArgs(name: trimmed),
|
|
88
|
+
)
|
|
89
|
+
widgets.insert(widget, at: 0)
|
|
90
|
+
newName = ""
|
|
91
|
+
errorMessage = nil
|
|
92
|
+
} catch {
|
|
93
|
+
errorMessage = "Create failed: \(error.localizedDescription)"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private struct EmptyArgs: Encodable {}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Mirrors `Widget` from `apps/api/schema.ts`. Kept hand-written here
|
|
4
|
+
/// for clarity in the scaffold; for production, run
|
|
5
|
+
/// `pylon codegen client schema.ts --target swift --out Models.swift`
|
|
6
|
+
/// from `apps/api/` to regenerate this file from the schema.
|
|
7
|
+
struct Widget: Codable, Identifiable, Hashable {
|
|
8
|
+
let id: String
|
|
9
|
+
let name: String
|
|
10
|
+
let count: Int
|
|
11
|
+
let createdAt: String
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Argument shape for `createWidget`.
|
|
15
|
+
struct CreateWidgetArgs: Encodable {
|
|
16
|
+
let name: String
|
|
17
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import PylonClient
|
|
3
|
+
|
|
4
|
+
/// SwiftUI entry point. Bootstraps a single shared PylonClient pointed
|
|
5
|
+
/// at the local Pylon control plane (`pylon dev`'s default port). For
|
|
6
|
+
/// production, override `PYLON_BASE_URL` via the build environment or
|
|
7
|
+
/// edit the URL inline.
|
|
8
|
+
@main
|
|
9
|
+
struct __APP_NAME_PASCAL__App: App {
|
|
10
|
+
@StateObject private var session = AppSession()
|
|
11
|
+
|
|
12
|
+
var body: some Scene {
|
|
13
|
+
WindowGroup {
|
|
14
|
+
ContentView()
|
|
15
|
+
.environmentObject(session)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Shared app state. The PylonClient is held here so every view in
|
|
21
|
+
/// the tree gets the same instance via `@EnvironmentObject`.
|
|
22
|
+
@MainActor
|
|
23
|
+
final class AppSession: ObservableObject {
|
|
24
|
+
let pylon: PylonClient
|
|
25
|
+
|
|
26
|
+
init() {
|
|
27
|
+
let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
|
|
28
|
+
?? "http://localhost:4321"
|
|
29
|
+
guard let url = URL(string: baseURLString) else {
|
|
30
|
+
fatalError("Invalid PYLON_BASE_URL: \(baseURLString)")
|
|
31
|
+
}
|
|
32
|
+
self.pylon = PylonClient(baseURL: url, appName: "__APP_NAME_SNAKE__")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# xcodegen spec for the iOS build of __APP_NAME__.
|
|
2
|
+
#
|
|
3
|
+
# Materialize the Xcode project once with:
|
|
4
|
+
# brew install xcodegen
|
|
5
|
+
# xcodegen generate
|
|
6
|
+
# Then open __APP_NAME_PASCAL__.xcodeproj.
|
|
7
|
+
|
|
8
|
+
name: __APP_NAME_PASCAL__
|
|
9
|
+
options:
|
|
10
|
+
bundleIdPrefix: com.example
|
|
11
|
+
deploymentTarget:
|
|
12
|
+
iOS: "16.0"
|
|
13
|
+
|
|
14
|
+
packages:
|
|
15
|
+
pylon:
|
|
16
|
+
url: https://github.com/pylonsync/pylon.git
|
|
17
|
+
from: "0.3.0"
|
|
18
|
+
|
|
19
|
+
targets:
|
|
20
|
+
__APP_NAME_PASCAL__:
|
|
21
|
+
type: application
|
|
22
|
+
platform: iOS
|
|
23
|
+
sources:
|
|
24
|
+
- path: Sources/__APP_NAME_PASCAL__
|
|
25
|
+
settings:
|
|
26
|
+
base:
|
|
27
|
+
GENERATE_INFOPLIST_FILE: YES
|
|
28
|
+
INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES
|
|
29
|
+
INFOPLIST_KEY_UILaunchScreen_Generation: YES
|
|
30
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
|
|
31
|
+
TARGETED_DEVICE_FAMILY: "1,2"
|
|
32
|
+
SWIFT_VERSION: "5.9"
|
|
33
|
+
ENABLE_PREVIEWS: YES
|
|
34
|
+
# __APP_NAME__ talks to a Pylon backend on localhost during dev.
|
|
35
|
+
# The simulator can reach localhost directly; on a physical device
|
|
36
|
+
# set PYLON_BASE_URL to your machine's LAN IP.
|
|
37
|
+
INFOPLIST_KEY_NSAppTransportSecurity: "{NSAllowsLocalNetworking = YES;}"
|
|
38
|
+
dependencies:
|
|
39
|
+
- package: pylon
|
|
40
|
+
product: PylonClient
|
|
41
|
+
- package: pylon
|
|
42
|
+
product: PylonSwiftUI
|
|
@@ -0,0 +1,23 @@
|
|
|
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: "PylonSwiftUI", package: "pylon"),
|
|
19
|
+
],
|
|
20
|
+
path: "Sources/__APP_NAME_PASCAL__"
|
|
21
|
+
),
|
|
22
|
+
]
|
|
23
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Mirrors `Todo` from `apps/api/schema.ts`. For production, regenerate
|
|
4
|
+
/// from the schema with `pylon codegen client schema.ts --target swift`.
|
|
5
|
+
struct Todo: Codable, Identifiable, Hashable {
|
|
6
|
+
let id: String
|
|
7
|
+
var title: String
|
|
8
|
+
var done: Bool
|
|
9
|
+
let createdAt: String
|
|
10
|
+
var position: Double?
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
struct AddTodoArgs: Encodable { let title: String }
|
|
14
|
+
struct ToggleTodoArgs: Encodable { let id: String; let done: Bool }
|
|
15
|
+
struct EditTodoArgs: Encodable { let id: String; let title: String }
|
|
16
|
+
struct DeleteTodoArgs: Encodable { let id: String }
|
|
17
|
+
struct ReorderTodoArgs: Encodable { let id: String; let position: Double }
|
|
18
|
+
struct EmptyArgs: Encodable {}
|