@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,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 {}
@@ -0,0 +1,137 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ struct RoomView: View {
5
+ @EnvironmentObject var session: AppSession
6
+ let room: Room
7
+ @State private var messages: [Message] = []
8
+ @State private var draft = ""
9
+ @State private var sending = false
10
+ @State private var errorMessage: String?
11
+ @State private var pollTimer: Task<Void, Never>?
12
+
13
+ var body: some View {
14
+ VStack(spacing: 0) {
15
+ ScrollViewReader { proxy in
16
+ ScrollView {
17
+ LazyVStack(alignment: .leading, spacing: 12) {
18
+ ForEach(messages) { msg in
19
+ messageRow(msg).id(msg.id)
20
+ }
21
+ if messages.isEmpty {
22
+ Text("No messages yet. Say hi.")
23
+ .foregroundStyle(.secondary)
24
+ .padding(.top, 32)
25
+ .frame(maxWidth: .infinity)
26
+ }
27
+ }
28
+ .padding(16)
29
+ }
30
+ .onChange(of: messages.count) { _, _ in
31
+ if let last = messages.last {
32
+ withAnimation { proxy.scrollTo(last.id, anchor: .bottom) }
33
+ }
34
+ }
35
+ }
36
+
37
+ if let errorMessage {
38
+ Text(errorMessage)
39
+ .foregroundStyle(.red)
40
+ .font(.caption)
41
+ .padding(8)
42
+ }
43
+
44
+ HStack {
45
+ TextField("Message #\(room.slug)…", text: $draft, axis: .vertical)
46
+ .textFieldStyle(.roundedBorder)
47
+ .lineLimit(1...4)
48
+ .onSubmit { Task { await send() } }
49
+ Button("Send") { Task { await send() } }
50
+ .keyboardShortcut(.defaultAction)
51
+ .disabled(draft.trimmingCharacters(in: .whitespaces).isEmpty || sending)
52
+ }
53
+ .padding(12)
54
+ .background(.thinMaterial)
55
+ }
56
+ .navigationTitle(room.name)
57
+ #if os(iOS)
58
+ .navigationBarTitleDisplayMode(.inline)
59
+ #endif
60
+ .task {
61
+ await loadMessages()
62
+ startPolling()
63
+ }
64
+ .onDisappear { pollTimer?.cancel() }
65
+ }
66
+
67
+ @ViewBuilder
68
+ private func messageRow(_ msg: Message) -> some View {
69
+ VStack(alignment: .leading, spacing: 2) {
70
+ HStack(alignment: .firstTextBaseline) {
71
+ Text(msg.authorName).font(.subheadline.weight(.medium))
72
+ Text(formatTime(msg.createdAt))
73
+ .font(.caption2)
74
+ .foregroundStyle(.tertiary)
75
+ }
76
+ Text(msg.body)
77
+ .font(.body)
78
+ .fixedSize(horizontal: false, vertical: true)
79
+ }
80
+ }
81
+
82
+ private func formatTime(_ iso: String) -> String {
83
+ let formatter = ISO8601DateFormatter()
84
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
85
+ guard let date = formatter.date(from: iso) ?? ISO8601DateFormatter().date(from: iso) else {
86
+ return ""
87
+ }
88
+ let display = DateFormatter()
89
+ display.timeStyle = .short
90
+ return display.string(from: date)
91
+ }
92
+
93
+ private func loadMessages() async {
94
+ do {
95
+ messages = try await session.pylon.callFn(
96
+ "roomMessages",
97
+ args: RoomMessagesArgs(roomId: room.id),
98
+ )
99
+ errorMessage = nil
100
+ } catch {
101
+ errorMessage = "Load failed: \(error.localizedDescription)"
102
+ }
103
+ }
104
+
105
+ private func startPolling() {
106
+ pollTimer?.cancel()
107
+ pollTimer = Task {
108
+ while !Task.isCancelled {
109
+ try? await Task.sleep(nanoseconds: 1_500_000_000)
110
+ if Task.isCancelled { break }
111
+ await loadMessages()
112
+ }
113
+ }
114
+ }
115
+
116
+ private func send() async {
117
+ let body = draft.trimmingCharacters(in: .whitespaces)
118
+ guard !body.isEmpty else { return }
119
+ sending = true
120
+ defer { sending = false }
121
+ do {
122
+ let msg: Message = try await session.pylon.callFn(
123
+ "sendMessage",
124
+ args: SendMessageArgs(
125
+ roomId: room.id,
126
+ body: body,
127
+ authorName: session.authorName,
128
+ ),
129
+ )
130
+ messages.append(msg)
131
+ draft = ""
132
+ errorMessage = nil
133
+ } catch {
134
+ errorMessage = "Send failed: \(error.localizedDescription)"
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,35 @@
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
+ ChatRootView()
11
+ .environmentObject(session)
12
+ }
13
+ }
14
+ }
15
+
16
+ @MainActor
17
+ final class AppSession: ObservableObject {
18
+ let pylon: PylonClient
19
+ @Published var authorName: String
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
+ self.authorName = UserDefaults.standard.string(forKey: "authorName") ?? "anonymous"
29
+ }
30
+
31
+ func setAuthorName(_ name: String) {
32
+ authorName = name
33
+ UserDefaults.standard.set(name, forKey: "authorName")
34
+ }
35
+ }
@@ -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,170 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ struct FeedView: View {
5
+ @EnvironmentObject var session: AppSession
6
+ @State private var feed: [FeedItem] = []
7
+ @State private var loading = true
8
+ @State private var posting = false
9
+ @State private var draft = ""
10
+ @State private var errorMessage: String?
11
+
12
+ var body: some View {
13
+ NavigationStack {
14
+ List {
15
+ Section("Post") {
16
+ VStack(alignment: .leading, spacing: 8) {
17
+ TextEditor(text: $draft)
18
+ .frame(minHeight: 80)
19
+ .font(.body)
20
+ HStack {
21
+ Text("\(draft.count)/1000")
22
+ .font(.caption)
23
+ .foregroundStyle(.secondary)
24
+ Spacer()
25
+ Button(posting ? "Posting…" : "Post") {
26
+ Task { await post() }
27
+ }
28
+ .disabled(posting || draft.trimmingCharacters(in: .whitespaces).isEmpty)
29
+ }
30
+ }
31
+ }
32
+
33
+ Section("Feed") {
34
+ if loading {
35
+ ProgressView()
36
+ } else if feed.isEmpty {
37
+ Text("No posts yet.")
38
+ .foregroundStyle(.secondary)
39
+ } else {
40
+ ForEach(feed) { item in
41
+ row(item)
42
+ }
43
+ }
44
+ }
45
+
46
+ if let errorMessage {
47
+ Section {
48
+ Text(errorMessage)
49
+ .foregroundStyle(.red)
50
+ .font(.caption)
51
+ }
52
+ }
53
+ }
54
+ .navigationTitle("__APP_NAME__")
55
+ .task { await load() }
56
+ .refreshable { await load() }
57
+ }
58
+ }
59
+
60
+ @ViewBuilder
61
+ private func row(_ item: FeedItem) -> some View {
62
+ VStack(alignment: .leading, spacing: 6) {
63
+ HStack(alignment: .firstTextBaseline) {
64
+ Text(item.author?.displayName ?? "Unknown")
65
+ .font(.subheadline.weight(.medium))
66
+ Text("@\(item.author?.handle ?? "?")")
67
+ .font(.system(.caption, design: .monospaced))
68
+ .foregroundStyle(.secondary)
69
+ Spacer()
70
+ Text(item.createdAt)
71
+ .font(.caption2)
72
+ .foregroundStyle(.tertiary)
73
+ }
74
+ Text(item.body)
75
+ .font(.body)
76
+ .fixedSize(horizontal: false, vertical: true)
77
+ HStack {
78
+ Button {
79
+ Task { await toggleLike(item) }
80
+ } label: {
81
+ HStack(spacing: 4) {
82
+ Image(systemName: item.likedByMe ? "heart.fill" : "heart")
83
+ Text("\(item.likeCount)")
84
+ }
85
+ .font(.caption)
86
+ .foregroundStyle(item.likedByMe ? .pink : .secondary)
87
+ }
88
+ .buttonStyle(.plain)
89
+
90
+ if item.author?.id == session.me?.id {
91
+ Spacer()
92
+ Button("Delete", role: .destructive) {
93
+ Task { await delete(item) }
94
+ }
95
+ .font(.caption)
96
+ }
97
+ }
98
+ }
99
+ .padding(.vertical, 4)
100
+ }
101
+
102
+ // MARK: - Network
103
+
104
+ private func load() async {
105
+ loading = true
106
+ defer { loading = false }
107
+ do {
108
+ feed = try await session.pylon.callFn("feed", args: EmptyArgs())
109
+ errorMessage = nil
110
+ } catch {
111
+ errorMessage = "Load failed: \(error.localizedDescription)"
112
+ }
113
+ }
114
+
115
+ private func post() async {
116
+ let body = draft.trimmingCharacters(in: .whitespaces)
117
+ guard !body.isEmpty else { return }
118
+ posting = true
119
+ defer { posting = false }
120
+ do {
121
+ let item: FeedItem = try await session.pylon.callFn(
122
+ "createPost",
123
+ args: CreatePostArgs(body: body),
124
+ )
125
+ feed.insert(item, at: 0)
126
+ draft = ""
127
+ } catch {
128
+ errorMessage = "Post failed: \(error.localizedDescription)"
129
+ }
130
+ }
131
+
132
+ private func toggleLike(_ item: FeedItem) async {
133
+ // Optimistic
134
+ if let i = feed.firstIndex(where: { $0.id == item.id }) {
135
+ feed[i].likedByMe.toggle()
136
+ feed[i].likeCount += feed[i].likedByMe ? 1 : -1
137
+ }
138
+ do {
139
+ let result: ToggleLikeResult = try await session.pylon.callFn(
140
+ "toggleLike",
141
+ args: ToggleLikeArgs(postId: item.id),
142
+ )
143
+ if let i = feed.firstIndex(where: { $0.id == item.id }) {
144
+ feed[i].likedByMe = result.liked
145
+ feed[i].likeCount = result.likeCount
146
+ }
147
+ } catch {
148
+ // Revert
149
+ if let i = feed.firstIndex(where: { $0.id == item.id }) {
150
+ feed[i].likedByMe = item.likedByMe
151
+ feed[i].likeCount = item.likeCount
152
+ }
153
+ errorMessage = "Like failed: \(error.localizedDescription)"
154
+ }
155
+ }
156
+
157
+ private func delete(_ item: FeedItem) async {
158
+ let snapshot = feed
159
+ feed.removeAll { $0.id == item.id }
160
+ do {
161
+ let _: FeedItem = try await session.pylon.callFn(
162
+ "deletePost",
163
+ args: DeletePostArgs(id: item.id),
164
+ )
165
+ } catch {
166
+ feed = snapshot
167
+ errorMessage = "Delete failed: \(error.localizedDescription)"
168
+ }
169
+ }
170
+ }
@@ -0,0 +1,42 @@
1
+ import Foundation
2
+
3
+ struct Profile: Codable, Identifiable, Hashable {
4
+ let id: String
5
+ let userId: String
6
+ let handle: String
7
+ let displayName: String
8
+ let bio: String?
9
+ let createdAt: String
10
+ }
11
+
12
+ struct FeedItem: Codable, Identifiable, Hashable {
13
+ let id: String
14
+ let body: String
15
+ let createdAt: String
16
+ let author: AuthorCard?
17
+ var likeCount: Int
18
+ var likedByMe: Bool
19
+ }
20
+
21
+ struct AuthorCard: Codable, Hashable {
22
+ let id: String
23
+ let handle: String
24
+ let displayName: String
25
+ }
26
+
27
+ struct UpsertProfileArgs: Encodable {
28
+ let handle: String
29
+ let displayName: String
30
+ let bio: String
31
+ }
32
+
33
+ struct CreatePostArgs: Encodable { let body: String }
34
+ struct DeletePostArgs: Encodable { let id: String }
35
+ struct ToggleLikeArgs: Encodable { let postId: String }
36
+
37
+ struct ToggleLikeResult: Codable {
38
+ let liked: Bool
39
+ let likeCount: Int
40
+ }
41
+
42
+ struct EmptyArgs: Encodable {}
@@ -0,0 +1,60 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ struct ProfileSetupView: View {
5
+ @EnvironmentObject var session: AppSession
6
+ @State private var handle = ""
7
+ @State private var displayName = ""
8
+ @State private var bio = ""
9
+ @State private var saving = false
10
+ @State private var errorMessage: String?
11
+
12
+ var body: some View {
13
+ NavigationStack {
14
+ Form {
15
+ Section("Set up your profile") {
16
+ TextField("handle (lowercase, 2–20)", text: $handle)
17
+ .autocorrectionDisabled()
18
+ .textInputAutocapitalization(.never)
19
+ TextField("Display name", text: $displayName)
20
+ TextField("Bio (optional)", text: $bio)
21
+ }
22
+
23
+ if let errorMessage {
24
+ Section {
25
+ Text(errorMessage)
26
+ .foregroundStyle(.red)
27
+ .font(.caption)
28
+ }
29
+ }
30
+
31
+ Section {
32
+ Button(saving ? "Saving…" : "Save") {
33
+ Task { await save() }
34
+ }
35
+ .disabled(saving || handle.trimmingCharacters(in: .whitespaces).isEmpty
36
+ || displayName.trimmingCharacters(in: .whitespaces).isEmpty)
37
+ }
38
+ }
39
+ .navigationTitle("__APP_NAME__")
40
+ }
41
+ }
42
+
43
+ private func save() async {
44
+ saving = true
45
+ defer { saving = false }
46
+ do {
47
+ let profile: Profile = try await session.pylon.callFn(
48
+ "upsertProfile",
49
+ args: UpsertProfileArgs(
50
+ handle: handle.trimmingCharacters(in: .whitespaces).lowercased(),
51
+ displayName: displayName.trimmingCharacters(in: .whitespaces),
52
+ bio: bio.trimmingCharacters(in: .whitespaces),
53
+ ),
54
+ )
55
+ session.me = profile
56
+ } catch {
57
+ errorMessage = "Save failed: \(error.localizedDescription)"
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,30 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ struct RootView: View {
5
+ @EnvironmentObject var session: AppSession
6
+ @State private var loading = true
7
+
8
+ var body: some View {
9
+ Group {
10
+ if loading {
11
+ ProgressView()
12
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
13
+ } else if session.me == nil {
14
+ ProfileSetupView()
15
+ } else {
16
+ FeedView()
17
+ }
18
+ }
19
+ .task { await load() }
20
+ }
21
+
22
+ private func load() async {
23
+ do {
24
+ session.me = try await session.pylon.callFn("myProfile", args: EmptyArgs())
25
+ } catch {
26
+ session.me = nil
27
+ }
28
+ loading = false
29
+ }
30
+ }