@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,29 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ @main
5
+ struct __APP_NAME_PASCAL__App: App {
6
+ @StateObject private var session = AppSession()
7
+
8
+ var body: some Scene {
9
+ WindowGroup {
10
+ RootView()
11
+ .environmentObject(session)
12
+ }
13
+ }
14
+ }
15
+
16
+ @MainActor
17
+ final class AppSession: ObservableObject {
18
+ let pylon: PylonClient
19
+ @Published var me: Profile?
20
+
21
+ init() {
22
+ let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
23
+ ?? "http://localhost:4321"
24
+ guard let url = URL(string: baseURLString) else {
25
+ fatalError("Invalid PYLON_BASE_URL: \(baseURLString)")
26
+ }
27
+ self.pylon = PylonClient(baseURL: url, appName: "__APP_NAME_SNAKE__")
28
+ }
29
+ }
@@ -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 {}
@@ -0,0 +1,230 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ /// Live Todo list with add, toggle, edit, drag-reorder, and delete.
5
+ /// Uses PylonClient's HTTP API directly. Drop-in upgrade path:
6
+ /// swap `load()` for a `PylonQuery<Todo>` ObservableObject from
7
+ /// PylonSwiftUI to get realtime updates without polling.
8
+ struct TodoListView: View {
9
+ @EnvironmentObject var session: AppSession
10
+
11
+ @State private var todos: [Todo] = []
12
+ @State private var draftTitle: String = ""
13
+ @State private var loading = true
14
+ @State private var pending = false
15
+ @State private var errorMessage: String?
16
+ @State private var editingId: String?
17
+ @State private var editingDraft: String = ""
18
+
19
+ var body: some View {
20
+ NavigationStack {
21
+ List {
22
+ Section("Add") {
23
+ HStack {
24
+ TextField("What needs doing?", text: $draftTitle)
25
+ .textFieldStyle(.roundedBorder)
26
+ .onSubmit { Task { await add() } }
27
+ Button("Add") { Task { await add() } }
28
+ .buttonStyle(.borderedProminent)
29
+ .disabled(draftTitle.trimmingCharacters(in: .whitespaces).isEmpty || pending)
30
+ }
31
+ }
32
+
33
+ Section("Todos") {
34
+ if loading {
35
+ ProgressView()
36
+ } else if todos.isEmpty {
37
+ Text("No todos yet.").foregroundStyle(.secondary)
38
+ } else {
39
+ ForEach(todos) { todo in
40
+ row(todo)
41
+ }
42
+ .onMove { source, destination in
43
+ Task { await reorder(from: source, to: destination) }
44
+ }
45
+ .onDelete { offsets in
46
+ Task { await delete(at: offsets) }
47
+ }
48
+ }
49
+ }
50
+
51
+ if let errorMessage {
52
+ Section {
53
+ Text(errorMessage)
54
+ .foregroundStyle(.red)
55
+ .font(.caption)
56
+ }
57
+ }
58
+ }
59
+ .navigationTitle("__APP_NAME__")
60
+ .toolbar { EditButton() }
61
+ .task { await load() }
62
+ .refreshable { await load() }
63
+ }
64
+ }
65
+
66
+ @ViewBuilder
67
+ private func row(_ todo: Todo) -> some View {
68
+ HStack(spacing: 12) {
69
+ Button {
70
+ Task { await toggle(todo) }
71
+ } label: {
72
+ Image(systemName: todo.done ? "checkmark.circle.fill" : "circle")
73
+ .foregroundStyle(todo.done ? .green : .secondary)
74
+ }
75
+ .buttonStyle(.plain)
76
+
77
+ if editingId == todo.id {
78
+ TextField("Title", text: $editingDraft)
79
+ .textFieldStyle(.roundedBorder)
80
+ .onSubmit { Task { await commitEdit(todo) } }
81
+ Button("Save") { Task { await commitEdit(todo) } }
82
+ .buttonStyle(.bordered)
83
+ Button("Cancel") {
84
+ editingId = nil
85
+ editingDraft = ""
86
+ }
87
+ .buttonStyle(.bordered)
88
+ } else {
89
+ Text(todo.title)
90
+ .strikethrough(todo.done, color: .secondary)
91
+ .foregroundStyle(todo.done ? .secondary : .primary)
92
+ Spacer()
93
+ Button("Edit") {
94
+ editingId = todo.id
95
+ editingDraft = todo.title
96
+ }
97
+ .buttonStyle(.borderless)
98
+ .font(.caption)
99
+ }
100
+ }
101
+ }
102
+
103
+ // MARK: - Network
104
+
105
+ private func load() async {
106
+ loading = true
107
+ defer { loading = false }
108
+ do {
109
+ let rows: [Todo] = try await session.pylon.callFn("listTodos", args: EmptyArgs())
110
+ todos = rows
111
+ errorMessage = nil
112
+ } catch {
113
+ errorMessage = "Load failed: \(error.localizedDescription)"
114
+ }
115
+ }
116
+
117
+ private func add() async {
118
+ let trimmed = draftTitle.trimmingCharacters(in: .whitespaces)
119
+ guard !trimmed.isEmpty else { return }
120
+ pending = true
121
+ defer { pending = false }
122
+ do {
123
+ let todo: Todo = try await session.pylon.callFn(
124
+ "addTodo",
125
+ args: AddTodoArgs(title: trimmed),
126
+ )
127
+ todos.append(todo)
128
+ draftTitle = ""
129
+ } catch {
130
+ errorMessage = "Add failed: \(error.localizedDescription)"
131
+ }
132
+ }
133
+
134
+ private func toggle(_ todo: Todo) async {
135
+ let nextDone = !todo.done
136
+ // Optimistic update
137
+ if let i = todos.firstIndex(where: { $0.id == todo.id }) {
138
+ todos[i].done = nextDone
139
+ }
140
+ do {
141
+ let _: Todo = try await session.pylon.callFn(
142
+ "toggleTodo",
143
+ args: ToggleTodoArgs(id: todo.id, done: nextDone),
144
+ )
145
+ } catch {
146
+ // Revert
147
+ if let i = todos.firstIndex(where: { $0.id == todo.id }) {
148
+ todos[i].done = todo.done
149
+ }
150
+ errorMessage = "Toggle failed: \(error.localizedDescription)"
151
+ }
152
+ }
153
+
154
+ private func commitEdit(_ todo: Todo) async {
155
+ let trimmed = editingDraft.trimmingCharacters(in: .whitespaces)
156
+ guard !trimmed.isEmpty, trimmed != todo.title else {
157
+ editingId = nil
158
+ editingDraft = ""
159
+ return
160
+ }
161
+ let originalTitle = todo.title
162
+ if let i = todos.firstIndex(where: { $0.id == todo.id }) {
163
+ todos[i].title = trimmed
164
+ }
165
+ editingId = nil
166
+ editingDraft = ""
167
+ do {
168
+ let _: Todo = try await session.pylon.callFn(
169
+ "editTodo",
170
+ args: EditTodoArgs(id: todo.id, title: trimmed),
171
+ )
172
+ } catch {
173
+ if let i = todos.firstIndex(where: { $0.id == todo.id }) {
174
+ todos[i].title = originalTitle
175
+ }
176
+ errorMessage = "Rename failed: \(error.localizedDescription)"
177
+ }
178
+ }
179
+
180
+ private func delete(at offsets: IndexSet) async {
181
+ let removed = offsets.map { todos[$0] }
182
+ todos.remove(atOffsets: offsets)
183
+ for todo in removed {
184
+ do {
185
+ let _: Todo = try await session.pylon.callFn(
186
+ "deleteTodo",
187
+ args: DeleteTodoArgs(id: todo.id),
188
+ )
189
+ } catch {
190
+ todos.append(todo)
191
+ errorMessage = "Delete failed: \(error.localizedDescription)"
192
+ }
193
+ }
194
+ }
195
+
196
+ private func reorder(from source: IndexSet, to destination: Int) async {
197
+ var moved = todos
198
+ moved.move(fromOffsets: source, toOffset: destination)
199
+ guard let movedIndex = source.first else { return }
200
+ // Compute the moved row's new index after the move
201
+ let newIndex = destination > movedIndex ? destination - 1 : destination
202
+ guard newIndex < moved.count else { return }
203
+ let movedTodo = moved[newIndex]
204
+ let prev = newIndex > 0 ? moved[newIndex - 1] : nil
205
+ let next = newIndex + 1 < moved.count ? moved[newIndex + 1] : nil
206
+ let prevPos = prev?.position ?? 0
207
+ let nextPos = next?.position ?? 0
208
+ let position: Double
209
+ if prev != nil && next != nil {
210
+ position = (prevPos + nextPos) / 2
211
+ } else if prev != nil {
212
+ position = prevPos + 1024
213
+ } else if next != nil {
214
+ position = nextPos - 1024
215
+ } else {
216
+ position = 1024
217
+ }
218
+ let snapshot = todos
219
+ todos = moved
220
+ do {
221
+ let _: Todo = try await session.pylon.callFn(
222
+ "reorderTodo",
223
+ args: ReorderTodoArgs(id: movedTodo.id, position: position),
224
+ )
225
+ } catch {
226
+ todos = snapshot
227
+ errorMessage = "Reorder failed: \(error.localizedDescription)"
228
+ }
229
+ }
230
+ }
@@ -0,0 +1,28 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ @main
5
+ struct __APP_NAME_PASCAL__App: App {
6
+ @StateObject private var session = AppSession()
7
+
8
+ var body: some Scene {
9
+ WindowGroup {
10
+ TodoListView()
11
+ .environmentObject(session)
12
+ }
13
+ }
14
+ }
15
+
16
+ @MainActor
17
+ final class AppSession: ObservableObject {
18
+ let pylon: PylonClient
19
+
20
+ init() {
21
+ let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
22
+ ?? "http://localhost:4321"
23
+ guard let url = URL(string: baseURLString) else {
24
+ fatalError("Invalid PYLON_BASE_URL: \(baseURLString)")
25
+ }
26
+ self.pylon = PylonClient(baseURL: url, appName: "__APP_NAME_SNAKE__")
27
+ }
28
+ }
@@ -0,0 +1,32 @@
1
+ name: __APP_NAME_PASCAL__
2
+ options:
3
+ bundleIdPrefix: com.example
4
+ deploymentTarget:
5
+ iOS: "16.0"
6
+
7
+ packages:
8
+ pylon:
9
+ url: https://github.com/pylonsync/pylon.git
10
+ from: "0.3.0"
11
+
12
+ targets:
13
+ __APP_NAME_PASCAL__:
14
+ type: application
15
+ platform: iOS
16
+ sources:
17
+ - path: Sources/__APP_NAME_PASCAL__
18
+ settings:
19
+ base:
20
+ GENERATE_INFOPLIST_FILE: YES
21
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES
22
+ INFOPLIST_KEY_UILaunchScreen_Generation: YES
23
+ INFOPLIST_KEY_UISupportedInterfaceOrientations: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"
24
+ TARGETED_DEVICE_FAMILY: "1,2"
25
+ SWIFT_VERSION: "5.9"
26
+ ENABLE_PREVIEWS: YES
27
+ INFOPLIST_KEY_NSAppTransportSecurity: "{NSAllowsLocalNetworking = YES;}"
28
+ dependencies:
29
+ - package: pylon
30
+ product: PylonClient
31
+ - package: pylon
32
+ product: PylonSwiftUI
@@ -0,0 +1,22 @@
1
+ // swift-tools-version:5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "__APP_NAME_PASCAL__",
6
+ platforms: [
7
+ .macOS(.v13),
8
+ ],
9
+ dependencies: [
10
+ .package(url: "https://github.com/pylonsync/pylon.git", from: "0.3.0"),
11
+ ],
12
+ targets: [
13
+ .executableTarget(
14
+ name: "__APP_NAME_PASCAL__",
15
+ dependencies: [
16
+ .product(name: "PylonClient", package: "pylon"),
17
+ .product(name: "PylonSwiftUI", package: "pylon"),
18
+ ],
19
+ path: "Sources/__APP_NAME_PASCAL__"
20
+ ),
21
+ ]
22
+ )
@@ -0,0 +1,15 @@
1
+ import Foundation
2
+
3
+ /// Mirrors the `myOrgs` query response shape — Org row + the
4
+ /// caller's role. For production, codegen from schema.ts with
5
+ /// `pylon codegen client --target swift`.
6
+ struct Org: Codable, Identifiable, Hashable {
7
+ let id: String
8
+ let slug: String
9
+ let name: String
10
+ let role: String
11
+ let createdAt: String
12
+ }
13
+
14
+ struct CreateOrgArgs: Encodable { let slug: String; let name: String }
15
+ struct EmptyArgs: Encodable {}
@@ -0,0 +1,178 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ /// macOS org picker — sidebar with every Org the user belongs to,
5
+ /// detail panel for the active one. Mirrors the web `OrgPicker`
6
+ /// component; the same `myOrgs` query backs both.
7
+ struct OrgPickerView: View {
8
+ @EnvironmentObject var session: AppSession
9
+ @State private var orgs: [Org] = []
10
+ @State private var selected: Org.ID?
11
+ @State private var loading = true
12
+ @State private var creating = false
13
+ @State private var draftName = ""
14
+ @State private var draftSlug = ""
15
+ @State private var pending = false
16
+ @State private var errorMessage: String?
17
+
18
+ var body: some View {
19
+ NavigationSplitView {
20
+ List(selection: $selected) {
21
+ Section {
22
+ ForEach(orgs) { o in
23
+ HStack {
24
+ VStack(alignment: .leading, spacing: 2) {
25
+ Text(o.name).font(.body)
26
+ Text(o.slug)
27
+ .font(.system(.caption, design: .monospaced))
28
+ .foregroundStyle(.secondary)
29
+ }
30
+ Spacer()
31
+ Text(o.role.uppercased())
32
+ .font(.system(.caption, design: .monospaced))
33
+ .foregroundStyle(roleColor(o.role))
34
+ }
35
+ .tag(o.id)
36
+ }
37
+ } header: {
38
+ Text("Your organizations")
39
+ }
40
+ }
41
+ .toolbar {
42
+ ToolbarItem(placement: .primaryAction) {
43
+ Button {
44
+ creating.toggle()
45
+ } label: {
46
+ Image(systemName: "plus")
47
+ }
48
+ }
49
+ }
50
+ } detail: {
51
+ if creating {
52
+ createForm
53
+ } else if let id = selected, let org = orgs.first(where: { $0.id == id }) {
54
+ detail(org: org)
55
+ } else {
56
+ placeholder
57
+ }
58
+ }
59
+ .task { await load() }
60
+ }
61
+
62
+ private var createForm: some View {
63
+ VStack(alignment: .leading, spacing: 16) {
64
+ Text("Create an organization")
65
+ .font(.title3)
66
+ .fontWeight(.semibold)
67
+ VStack(alignment: .leading, spacing: 8) {
68
+ TextField("Org name", text: $draftName)
69
+ .textFieldStyle(.roundedBorder)
70
+ TextField("Slug (e.g. acme-corp)", text: $draftSlug)
71
+ .textFieldStyle(.roundedBorder)
72
+ .autocorrectionDisabled()
73
+ }
74
+ HStack {
75
+ Button("Cancel") {
76
+ creating = false
77
+ draftName = ""
78
+ draftSlug = ""
79
+ errorMessage = nil
80
+ }
81
+ .buttonStyle(.bordered)
82
+ Button("Create") { Task { await create() } }
83
+ .keyboardShortcut(.defaultAction)
84
+ .disabled(draftName.trimmingCharacters(in: .whitespaces).isEmpty
85
+ || draftSlug.trimmingCharacters(in: .whitespaces).isEmpty
86
+ || pending)
87
+ }
88
+ if let errorMessage {
89
+ Text(errorMessage)
90
+ .foregroundStyle(.red)
91
+ .font(.caption)
92
+ }
93
+ Spacer()
94
+ }
95
+ .padding(24)
96
+ }
97
+
98
+ @ViewBuilder
99
+ private func detail(org: Org) -> some View {
100
+ VStack(alignment: .leading, spacing: 16) {
101
+ Text(org.name).font(.title2).fontWeight(.semibold)
102
+ Text(org.slug)
103
+ .font(.system(.body, design: .monospaced))
104
+ .foregroundStyle(.secondary)
105
+ Divider()
106
+ Text("Your role: \(org.role)")
107
+ .foregroundStyle(.secondary)
108
+ .font(.callout)
109
+ Text("orgId: \(org.id)")
110
+ .foregroundStyle(.tertiary)
111
+ .font(.system(.caption, design: .monospaced))
112
+ Spacer()
113
+ Text("Wire member management + project lists into this panel by calling /api/fn/orgMembers and /api/fn/orgProjects with this orgId. Membership policies enforce tenant isolation server-side.")
114
+ .foregroundStyle(.secondary)
115
+ .font(.caption)
116
+ .fixedSize(horizontal: false, vertical: true)
117
+ }
118
+ .padding(24)
119
+ }
120
+
121
+ private var placeholder: some View {
122
+ VStack(spacing: 8) {
123
+ if loading {
124
+ ProgressView()
125
+ } else if orgs.isEmpty {
126
+ Text("You're not in any orgs yet.")
127
+ Button("Create your first org") { creating = true }
128
+ .buttonStyle(.bordered)
129
+ } else {
130
+ Text("Pick an org from the sidebar.")
131
+ .foregroundStyle(.secondary)
132
+ }
133
+ }
134
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
135
+ }
136
+
137
+ private func roleColor(_ role: String) -> Color {
138
+ switch role {
139
+ case "owner": return .blue
140
+ case "admin": return .orange
141
+ default: return .secondary
142
+ }
143
+ }
144
+
145
+ private func load() async {
146
+ loading = true
147
+ defer { loading = false }
148
+ do {
149
+ orgs = try await session.pylon.callFn("myOrgs", args: EmptyArgs())
150
+ if selected == nil { selected = orgs.first?.id }
151
+ errorMessage = nil
152
+ } catch {
153
+ errorMessage = "Load failed: \(error.localizedDescription)"
154
+ }
155
+ }
156
+
157
+ private func create() async {
158
+ let name = draftName.trimmingCharacters(in: .whitespaces)
159
+ let slug = draftSlug.trimmingCharacters(in: .whitespaces).lowercased()
160
+ guard !name.isEmpty, !slug.isEmpty else { return }
161
+ pending = true
162
+ defer { pending = false }
163
+ do {
164
+ let org: Org = try await session.pylon.callFn(
165
+ "createOrg",
166
+ args: CreateOrgArgs(slug: slug, name: name),
167
+ )
168
+ orgs.insert(org, at: 0)
169
+ selected = org.id
170
+ creating = false
171
+ draftName = ""
172
+ draftSlug = ""
173
+ errorMessage = nil
174
+ } catch {
175
+ errorMessage = "Create failed: \(error.localizedDescription)"
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,30 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ @main
5
+ struct __APP_NAME_PASCAL__App: App {
6
+ @StateObject private var session = AppSession()
7
+
8
+ var body: some Scene {
9
+ Window("__APP_NAME__", id: "main") {
10
+ OrgPickerView()
11
+ .environmentObject(session)
12
+ .frame(minWidth: 640, minHeight: 480)
13
+ }
14
+ .windowResizability(.contentMinSize)
15
+ }
16
+ }
17
+
18
+ @MainActor
19
+ final class AppSession: ObservableObject {
20
+ let pylon: PylonClient
21
+
22
+ init() {
23
+ let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
24
+ ?? "http://localhost:4321"
25
+ guard let url = URL(string: baseURLString) else {
26
+ fatalError("Invalid PYLON_BASE_URL: \(baseURLString)")
27
+ }
28
+ self.pylon = PylonClient(baseURL: url, appName: "__APP_NAME_SNAKE__")
29
+ }
30
+ }
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <!-- App Sandbox + outgoing-network client. Without these the
6
+ scaffolded app can't reach localhost:4321 during dev or
7
+ a remote Pylon Cloud URL in production. -->
8
+ <key>com.apple.security.app-sandbox</key>
9
+ <true/>
10
+ <key>com.apple.security.network.client</key>
11
+ <true/>
12
+ </dict>
13
+ </plist>