@pylonsync/create-pylon 0.3.53 → 0.3.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/bin/create-pylon.js +98 -42
  2. package/package.json +1 -1
  3. package/templates/backend/b2b/apps/api/functions/archiveProject.ts +15 -0
  4. package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
  5. package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
  6. package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
  7. package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
  8. package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
  9. package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
  10. package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
  11. package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
  12. package/templates/backend/b2b/apps/api/package.json +20 -0
  13. package/templates/backend/b2b/apps/api/schema.ts +171 -0
  14. package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
  15. package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
  16. package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
  17. package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
  18. package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
  19. package/templates/backend/chat/apps/api/package.json +20 -0
  20. package/templates/backend/chat/apps/api/schema.ts +93 -0
  21. package/templates/backend/chat/apps/api/tsconfig.json +13 -0
  22. package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
  23. package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
  24. package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
  25. package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
  26. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
  27. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
  28. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
  29. package/templates/backend/consumer/apps/api/package.json +20 -0
  30. package/templates/backend/consumer/apps/api/schema.ts +130 -0
  31. package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
  32. package/templates/expo/chat/apps/expo/App.tsx +384 -0
  33. package/templates/expo/chat/apps/expo/app.json +25 -0
  34. package/templates/expo/chat/apps/expo/babel.config.js +6 -0
  35. package/templates/expo/chat/apps/expo/package.json +30 -0
  36. package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
  37. package/templates/expo/consumer/apps/expo/App.tsx +392 -0
  38. package/templates/expo/consumer/apps/expo/app.json +25 -0
  39. package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
  40. package/templates/expo/consumer/apps/expo/package.json +30 -0
  41. package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
  42. package/templates/ios/chat/apps/ios/Package.swift +24 -0
  43. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +116 -0
  44. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  45. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
  46. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
  47. package/templates/ios/chat/apps/ios/project.yml +44 -0
  48. package/templates/ios/consumer/apps/ios/Package.swift +24 -0
  49. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
  50. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
  51. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
  52. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
  53. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +57 -0
  54. package/templates/ios/consumer/apps/ios/project.yml +44 -0
  55. package/templates/mac/b2b/apps/mac/Package.swift +22 -0
  56. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
  57. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
  58. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  59. package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  60. package/templates/mac/b2b/apps/mac/project.yml +34 -0
  61. package/templates/mac/barebones/apps/mac/Package.swift +33 -0
  62. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
  63. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  64. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  65. package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  66. package/templates/mac/barebones/apps/mac/project.yml +34 -0
  67. package/templates/mac/chat/apps/mac/Package.swift +34 -0
  68. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +143 -0
  69. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  70. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
  71. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
  72. package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  73. package/templates/mac/chat/apps/mac/project.yml +36 -0
  74. package/templates/mac/consumer/apps/mac/Package.swift +34 -0
  75. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
  76. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
  77. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
  78. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
  79. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +59 -0
  80. package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  81. package/templates/mac/consumer/apps/mac/project.yml +36 -0
  82. package/templates/mac/todo/apps/mac/Package.swift +33 -0
  83. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
  84. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
  85. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  86. package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  87. package/templates/mac/todo/apps/mac/project.yml +34 -0
  88. package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
  89. package/templates/web/b2b/apps/web/next.config.ts +24 -0
  90. package/templates/web/b2b/apps/web/package.json +29 -0
  91. package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
  92. package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
  93. package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
  94. package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
  95. package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
  96. package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
  97. package/templates/web/b2b/apps/web/tsconfig.json +26 -0
  98. package/templates/web/chat/apps/web/next-env.d.ts +2 -0
  99. package/templates/web/chat/apps/web/next.config.ts +24 -0
  100. package/templates/web/chat/apps/web/package.json +29 -0
  101. package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
  102. package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +231 -0
  103. package/templates/web/chat/apps/web/src/app/globals.css +6 -0
  104. package/templates/web/chat/apps/web/src/app/layout.tsx +22 -0
  105. package/templates/web/chat/apps/web/src/app/page.tsx +12 -0
  106. package/templates/web/chat/apps/web/src/app/providers.tsx +25 -0
  107. package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
  108. package/templates/web/chat/apps/web/tsconfig.json +26 -0
  109. package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
  110. package/templates/web/consumer/apps/web/next.config.ts +24 -0
  111. package/templates/web/consumer/apps/web/package.json +29 -0
  112. package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
  113. package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +315 -0
  114. package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
  115. package/templates/web/consumer/apps/web/src/app/layout.tsx +22 -0
  116. package/templates/web/consumer/apps/web/src/app/page.tsx +21 -0
  117. package/templates/web/consumer/apps/web/src/app/providers.tsx +25 -0
  118. package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
  119. package/templates/web/consumer/apps/web/tsconfig.json +26 -0
  120. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Package.swift +0 -0
  121. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/ContentView.swift +0 -0
  122. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
  123. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
  124. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/project.yml +0 -0
  125. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Package.swift +0 -0
  126. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
  127. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/TodoListView.swift +0 -0
  128. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
  129. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/project.yml +0 -0
@@ -0,0 +1,68 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+ import PylonSync
4
+
5
+ struct ProfileSetupView: View {
6
+ @EnvironmentObject var session: AppSession
7
+ let engine: SyncEngine
8
+ let existingHandles: [String]
9
+
10
+ @State private var handle = ""
11
+ @State private var displayName = ""
12
+ @State private var bio = ""
13
+ @State private var saving = false
14
+ @State private var errorMessage: String?
15
+
16
+ var body: some View {
17
+ NavigationStack {
18
+ Form {
19
+ Section("Set up your profile") {
20
+ TextField("handle (lowercase, 2–20)", text: $handle)
21
+ .autocorrectionDisabled()
22
+ .textInputAutocapitalization(.never)
23
+ TextField("Display name", text: $displayName)
24
+ TextField("Bio (optional)", text: $bio)
25
+ }
26
+ if let errorMessage {
27
+ Section {
28
+ Text(errorMessage)
29
+ .foregroundStyle(.red)
30
+ .font(.caption)
31
+ }
32
+ }
33
+ Section {
34
+ Button(saving ? "Saving…" : "Save") {
35
+ Task { await save() }
36
+ }
37
+ .disabled(saving
38
+ || handle.trimmingCharacters(in: .whitespaces).isEmpty
39
+ || displayName.trimmingCharacters(in: .whitespaces).isEmpty)
40
+ }
41
+ }
42
+ .navigationTitle("__APP_NAME__")
43
+ }
44
+ }
45
+
46
+ private func save() async {
47
+ saving = true
48
+ defer { saving = false }
49
+ let lower = handle.trimmingCharacters(in: .whitespaces).lowercased()
50
+ if existingHandles.contains(lower) {
51
+ errorMessage = "@\(lower) is taken"
52
+ return
53
+ }
54
+ do {
55
+ let profile: Profile = try await session.client.callFn(
56
+ "upsertProfile",
57
+ args: UpsertProfileArgs(
58
+ handle: lower,
59
+ displayName: displayName.trimmingCharacters(in: .whitespaces),
60
+ bio: bio.trimmingCharacters(in: .whitespaces),
61
+ ),
62
+ )
63
+ session.setMyProfileId(profile.id)
64
+ } catch {
65
+ errorMessage = "Save failed: \(error.localizedDescription)"
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,40 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+ import PylonSync
4
+ import PylonSwiftUI
5
+
6
+ /// Watches the live Profile collection. If we have a saved profileId
7
+ /// AND a Profile row in the local store with that id, render the
8
+ /// feed; otherwise, profile setup. The profile row appears in the
9
+ /// store via the SyncEngine, so a fresh device sees its own profile
10
+ /// pop in moments after `upsertProfile` returns.
11
+ struct RootView: View {
12
+ @EnvironmentObject var session: AppSession
13
+ let engine: SyncEngine
14
+ @StateObject private var profiles: PylonQuery<Profile>
15
+
16
+ init(engine: SyncEngine) {
17
+ self.engine = engine
18
+ _profiles = StateObject(
19
+ wrappedValue: PylonQuery<Profile>(engine: engine, entity: "Profile"),
20
+ )
21
+ }
22
+
23
+ var body: some View {
24
+ Group {
25
+ if let me = currentProfile {
26
+ FeedView(engine: engine, me: me, profiles: profiles.rows)
27
+ } else {
28
+ ProfileSetupView(
29
+ engine: engine,
30
+ existingHandles: profiles.rows.map { $0.handle },
31
+ )
32
+ }
33
+ }
34
+ }
35
+
36
+ private var currentProfile: Profile? {
37
+ guard let id = session.myProfileId else { return nil }
38
+ return profiles.rows.first { $0.id == id }
39
+ }
40
+ }
@@ -0,0 +1,59 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+ import PylonSync
4
+
5
+ @main
6
+ struct __APP_NAME_PASCAL__App: App {
7
+ @StateObject private var session = AppSession()
8
+
9
+ var body: some Scene {
10
+ Window("__APP_NAME__", id: "main") {
11
+ Group {
12
+ if let engine = session.engine {
13
+ RootView(engine: engine)
14
+ .environmentObject(session)
15
+ .frame(minWidth: 480, minHeight: 600)
16
+ } else {
17
+ ProgressView()
18
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
19
+ }
20
+ }
21
+ .task { await session.bootIfNeeded() }
22
+ }
23
+ .windowResizability(.contentMinSize)
24
+ }
25
+ }
26
+
27
+ @MainActor
28
+ final class AppSession: ObservableObject {
29
+ let client: PylonClient
30
+ @Published private(set) var engine: SyncEngine?
31
+ @Published var myProfileId: String?
32
+
33
+ init() {
34
+ let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
35
+ ?? "http://localhost:4321"
36
+ guard let url = URL(string: baseURLString) else {
37
+ fatalError("Invalid PYLON_BASE_URL: \(baseURLString)")
38
+ }
39
+ self.client = PylonClient(baseURL: url, appName: "__APP_NAME_SNAKE__")
40
+ self.myProfileId = UserDefaults.standard.string(forKey: "myProfileId")
41
+ }
42
+
43
+ func bootIfNeeded() async {
44
+ guard engine == nil else { return }
45
+ let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
46
+ ?? "http://localhost:4321"
47
+ guard let url = URL(string: baseURLString) else { return }
48
+ let config = SyncEngineConfig(baseURL: url, appName: "__APP_NAME_SNAKE__")
49
+ let engine = await SyncEngine(config: config, client: client)
50
+ await engine.start()
51
+ self.engine = engine
52
+ }
53
+
54
+ func setMyProfileId(_ id: String?) {
55
+ myProfileId = id
56
+ if let id { UserDefaults.standard.set(id, forKey: "myProfileId") }
57
+ else { UserDefaults.standard.removeObject(forKey: "myProfileId") }
58
+ }
59
+ }
@@ -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>
@@ -0,0 +1,36 @@
1
+ name: __APP_NAME_PASCAL__
2
+ options:
3
+ bundleIdPrefix: com.example
4
+ deploymentTarget:
5
+ macOS: "13.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: macOS
16
+ sources:
17
+ - path: Sources/__APP_NAME_PASCAL__
18
+ settings:
19
+ base:
20
+ GENERATE_INFOPLIST_FILE: YES
21
+ INFOPLIST_KEY_CFBundleDisplayName: __APP_NAME__
22
+ INFOPLIST_KEY_LSApplicationCategoryType: "public.app-category.developer-tools"
23
+ INFOPLIST_KEY_NSHumanReadableCopyright: ""
24
+ SWIFT_VERSION: "5.9"
25
+ ENABLE_PREVIEWS: YES
26
+ # The mac app talks to the local Pylon control plane during dev.
27
+ # App Sandbox needs the network client entitlement to reach
28
+ # localhost; we declare it via codesign entitlements at build time.
29
+ CODE_SIGN_ENTITLEMENTS: __APP_NAME_PASCAL__.entitlements
30
+ dependencies:
31
+ - package: pylon
32
+ product: PylonClient
33
+ - package: pylon
34
+ product: PylonSync
35
+ - package: pylon
36
+ product: PylonSwiftUI
@@ -0,0 +1,33 @@
1
+ // swift-tools-version:5.9
2
+ import PackageDescription
3
+
4
+ // SwiftPM package for the __APP_NAME__ macOS app.
5
+ //
6
+ // The executable target runs locally with `swift run`. For a proper
7
+ // signed `.app` bundle:
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 SwiftPM and Xcode share one source set.
15
+ let package = Package(
16
+ name: "__APP_NAME_PASCAL__",
17
+ platforms: [
18
+ .macOS(.v13),
19
+ ],
20
+ dependencies: [
21
+ .package(url: "https://github.com/pylonsync/pylon.git", from: "0.3.0"),
22
+ ],
23
+ targets: [
24
+ .executableTarget(
25
+ name: "__APP_NAME_PASCAL__",
26
+ dependencies: [
27
+ .product(name: "PylonClient", package: "pylon"),
28
+ .product(name: "PylonSwiftUI", package: "pylon"),
29
+ ],
30
+ path: "Sources/__APP_NAME_PASCAL__"
31
+ ),
32
+ ]
33
+ )
@@ -0,0 +1,19 @@
1
+ import Foundation
2
+
3
+ /// Mirrors `Todo` from `apps/api/schema.ts`. Regenerate from the
4
+ /// schema with `pylon codegen client schema.ts --target swift` for
5
+ /// production.
6
+ struct Todo: Codable, Identifiable, Hashable {
7
+ let id: String
8
+ var title: String
9
+ var done: Bool
10
+ let createdAt: String
11
+ var position: Double?
12
+ }
13
+
14
+ struct AddTodoArgs: Encodable { let title: String }
15
+ struct ToggleTodoArgs: Encodable { let id: String; let done: Bool }
16
+ struct EditTodoArgs: Encodable { let id: String; let title: String }
17
+ struct DeleteTodoArgs: Encodable { let id: String }
18
+ struct ReorderTodoArgs: Encodable { let id: String; let position: Double }
19
+ struct EmptyArgs: Encodable {}
@@ -0,0 +1,244 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ /// macOS Todo list. List with inline checkbox + edit-on-double-click +
5
+ /// delete on hover. macOS doesn't ship `EditButton`, so reordering is
6
+ /// drag-and-drop via `.onMove(perform:)` which works in editable List.
7
+ struct TodoListView: View {
8
+ @EnvironmentObject var session: AppSession
9
+ @State private var todos: [Todo] = []
10
+ @State private var draftTitle: String = ""
11
+ @State private var loading = true
12
+ @State private var pending = false
13
+ @State private var errorMessage: String?
14
+ @State private var editingId: String?
15
+ @State private var editingDraft: String = ""
16
+ @State private var hoveredId: String?
17
+
18
+ var body: some View {
19
+ VStack(spacing: 0) {
20
+ HStack(spacing: 8) {
21
+ TextField("What needs doing?", text: $draftTitle)
22
+ .textFieldStyle(.roundedBorder)
23
+ .onSubmit { Task { await add() } }
24
+ Button("Add") { Task { await add() } }
25
+ .keyboardShortcut(.defaultAction)
26
+ .disabled(draftTitle.trimmingCharacters(in: .whitespaces).isEmpty || pending)
27
+ }
28
+ .padding(16)
29
+
30
+ Divider()
31
+
32
+ if loading {
33
+ ProgressView().padding()
34
+ Spacer()
35
+ } else if todos.isEmpty {
36
+ Spacer()
37
+ Text("No todos yet.")
38
+ .foregroundStyle(.secondary)
39
+ Spacer()
40
+ } else {
41
+ List {
42
+ ForEach(todos) { todo in
43
+ row(todo)
44
+ }
45
+ .onMove { source, destination in
46
+ Task { await reorder(from: source, to: destination) }
47
+ }
48
+ }
49
+ .listStyle(.inset)
50
+ }
51
+
52
+ if let errorMessage {
53
+ Divider()
54
+ Text(errorMessage)
55
+ .foregroundStyle(.red)
56
+ .font(.caption)
57
+ .padding(8)
58
+ }
59
+ }
60
+ .task { await load() }
61
+ .toolbar {
62
+ ToolbarItem(placement: .primaryAction) {
63
+ Button { Task { await load() } } label: {
64
+ Image(systemName: "arrow.clockwise")
65
+ }
66
+ .disabled(loading)
67
+ }
68
+ }
69
+ }
70
+
71
+ @ViewBuilder
72
+ private func row(_ todo: Todo) -> some View {
73
+ HStack(spacing: 10) {
74
+ Button { Task { await toggle(todo) } } label: {
75
+ Image(systemName: todo.done ? "checkmark.circle.fill" : "circle")
76
+ .foregroundStyle(todo.done ? .green : .secondary)
77
+ .imageScale(.large)
78
+ }
79
+ .buttonStyle(.plain)
80
+
81
+ if editingId == todo.id {
82
+ TextField("Title", text: $editingDraft)
83
+ .textFieldStyle(.roundedBorder)
84
+ .onSubmit { Task { await commitEdit(todo) } }
85
+ Button("Save") { Task { await commitEdit(todo) } }
86
+ .buttonStyle(.bordered)
87
+ Button("Cancel") {
88
+ editingId = nil
89
+ editingDraft = ""
90
+ }
91
+ .buttonStyle(.bordered)
92
+ } else {
93
+ Text(todo.title)
94
+ .strikethrough(todo.done, color: .secondary)
95
+ .foregroundStyle(todo.done ? .secondary : .primary)
96
+ .onTapGesture(count: 2) {
97
+ editingId = todo.id
98
+ editingDraft = todo.title
99
+ }
100
+ Spacer()
101
+ if hoveredId == todo.id {
102
+ Button("Edit") {
103
+ editingId = todo.id
104
+ editingDraft = todo.title
105
+ }
106
+ .buttonStyle(.borderless)
107
+ .font(.caption)
108
+ Button("Delete") { Task { await delete(todo) } }
109
+ .buttonStyle(.borderless)
110
+ .font(.caption)
111
+ .foregroundStyle(.red)
112
+ }
113
+ }
114
+ }
115
+ .padding(.vertical, 4)
116
+ .contentShape(Rectangle())
117
+ .onHover { hovering in
118
+ hoveredId = hovering ? todo.id : (hoveredId == todo.id ? nil : hoveredId)
119
+ }
120
+ }
121
+
122
+ // MARK: - Network
123
+
124
+ private func load() async {
125
+ loading = true
126
+ defer { loading = false }
127
+ do {
128
+ let rows: [Todo] = try await session.pylon.callFn("listTodos", args: EmptyArgs())
129
+ todos = rows
130
+ errorMessage = nil
131
+ } catch {
132
+ errorMessage = "Load failed: \(error.localizedDescription)"
133
+ }
134
+ }
135
+
136
+ private func add() async {
137
+ let trimmed = draftTitle.trimmingCharacters(in: .whitespaces)
138
+ guard !trimmed.isEmpty else { return }
139
+ pending = true
140
+ defer { pending = false }
141
+ do {
142
+ let todo: Todo = try await session.pylon.callFn(
143
+ "addTodo",
144
+ args: AddTodoArgs(title: trimmed),
145
+ )
146
+ todos.append(todo)
147
+ draftTitle = ""
148
+ } catch {
149
+ errorMessage = "Add failed: \(error.localizedDescription)"
150
+ }
151
+ }
152
+
153
+ private func toggle(_ todo: Todo) async {
154
+ let nextDone = !todo.done
155
+ if let i = todos.firstIndex(where: { $0.id == todo.id }) {
156
+ todos[i].done = nextDone
157
+ }
158
+ do {
159
+ let _: Todo = try await session.pylon.callFn(
160
+ "toggleTodo",
161
+ args: ToggleTodoArgs(id: todo.id, done: nextDone),
162
+ )
163
+ } catch {
164
+ if let i = todos.firstIndex(where: { $0.id == todo.id }) {
165
+ todos[i].done = todo.done
166
+ }
167
+ errorMessage = "Toggle failed: \(error.localizedDescription)"
168
+ }
169
+ }
170
+
171
+ private func commitEdit(_ todo: Todo) async {
172
+ let trimmed = editingDraft.trimmingCharacters(in: .whitespaces)
173
+ guard !trimmed.isEmpty, trimmed != todo.title else {
174
+ editingId = nil
175
+ editingDraft = ""
176
+ return
177
+ }
178
+ let originalTitle = todo.title
179
+ if let i = todos.firstIndex(where: { $0.id == todo.id }) {
180
+ todos[i].title = trimmed
181
+ }
182
+ editingId = nil
183
+ editingDraft = ""
184
+ do {
185
+ let _: Todo = try await session.pylon.callFn(
186
+ "editTodo",
187
+ args: EditTodoArgs(id: todo.id, title: trimmed),
188
+ )
189
+ } catch {
190
+ if let i = todos.firstIndex(where: { $0.id == todo.id }) {
191
+ todos[i].title = originalTitle
192
+ }
193
+ errorMessage = "Rename failed: \(error.localizedDescription)"
194
+ }
195
+ }
196
+
197
+ private func delete(_ todo: Todo) async {
198
+ let snapshot = todos
199
+ todos.removeAll { $0.id == todo.id }
200
+ do {
201
+ let _: Todo = try await session.pylon.callFn(
202
+ "deleteTodo",
203
+ args: DeleteTodoArgs(id: todo.id),
204
+ )
205
+ } catch {
206
+ todos = snapshot
207
+ errorMessage = "Delete failed: \(error.localizedDescription)"
208
+ }
209
+ }
210
+
211
+ private func reorder(from source: IndexSet, to destination: Int) async {
212
+ var moved = todos
213
+ moved.move(fromOffsets: source, toOffset: destination)
214
+ guard let movedIndex = source.first else { return }
215
+ let newIndex = destination > movedIndex ? destination - 1 : destination
216
+ guard newIndex < moved.count else { return }
217
+ let movedTodo = moved[newIndex]
218
+ let prev = newIndex > 0 ? moved[newIndex - 1] : nil
219
+ let next = newIndex + 1 < moved.count ? moved[newIndex + 1] : nil
220
+ let prevPos = prev?.position ?? 0
221
+ let nextPos = next?.position ?? 0
222
+ let position: Double
223
+ if prev != nil && next != nil {
224
+ position = (prevPos + nextPos) / 2
225
+ } else if prev != nil {
226
+ position = prevPos + 1024
227
+ } else if next != nil {
228
+ position = nextPos - 1024
229
+ } else {
230
+ position = 1024
231
+ }
232
+ let snapshot = todos
233
+ todos = moved
234
+ do {
235
+ let _: Todo = try await session.pylon.callFn(
236
+ "reorderTodo",
237
+ args: ReorderTodoArgs(id: movedTodo.id, position: position),
238
+ )
239
+ } catch {
240
+ todos = snapshot
241
+ errorMessage = "Reorder failed: \(error.localizedDescription)"
242
+ }
243
+ }
244
+ }
@@ -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
+ TodoListView()
11
+ .environmentObject(session)
12
+ .frame(minWidth: 480, minHeight: 360)
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>
@@ -0,0 +1,34 @@
1
+ name: __APP_NAME_PASCAL__
2
+ options:
3
+ bundleIdPrefix: com.example
4
+ deploymentTarget:
5
+ macOS: "13.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: macOS
16
+ sources:
17
+ - path: Sources/__APP_NAME_PASCAL__
18
+ settings:
19
+ base:
20
+ GENERATE_INFOPLIST_FILE: YES
21
+ INFOPLIST_KEY_CFBundleDisplayName: __APP_NAME__
22
+ INFOPLIST_KEY_LSApplicationCategoryType: "public.app-category.developer-tools"
23
+ INFOPLIST_KEY_NSHumanReadableCopyright: ""
24
+ SWIFT_VERSION: "5.9"
25
+ ENABLE_PREVIEWS: YES
26
+ # The mac app talks to the local Pylon control plane during dev.
27
+ # App Sandbox needs the network client entitlement to reach
28
+ # localhost; we declare it via codesign entitlements at build time.
29
+ CODE_SIGN_ENTITLEMENTS: __APP_NAME_PASCAL__.entitlements
30
+ dependencies:
31
+ - package: pylon
32
+ product: PylonClient
33
+ - package: pylon
34
+ product: PylonSwiftUI
@@ -0,0 +1,2 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
@@ -0,0 +1,24 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const PYLON_API_URL = process.env.PYLON_API_URL ?? "http://localhost:4321";
4
+
5
+ const config: NextConfig = {
6
+ transpilePackages: [
7
+ "@__APP_NAME_KEBAB__/ui",
8
+ "@pylonsync/sdk",
9
+ "@pylonsync/react",
10
+ "@pylonsync/next",
11
+ "@pylonsync/functions",
12
+ "@pylonsync/sync",
13
+ ],
14
+ async rewrites() {
15
+ return [
16
+ { source: "/api/fn/:path*", destination: `${PYLON_API_URL}/api/fn/:path*` },
17
+ { source: "/api/auth/:path*", destination: `${PYLON_API_URL}/api/auth/:path*` },
18
+ { source: "/api/sync/:path*", destination: `${PYLON_API_URL}/api/sync/:path*` },
19
+ { source: "/api/:path*", destination: `${PYLON_API_URL}/api/:path*` },
20
+ ];
21
+ },
22
+ };
23
+
24
+ export default config;
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@__APP_NAME_KEBAB__/web",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "next dev --port 3000",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "lint": "next lint"
11
+ },
12
+ "dependencies": {
13
+ "@__APP_NAME_KEBAB__/ui": "__WORKSPACE_DEP__",
14
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
15
+ "@pylonsync/react": "^__PYLON_VERSION__",
16
+ "@pylonsync/next": "^__PYLON_VERSION__",
17
+ "next": "^16.0.0",
18
+ "react": "^19.0.0",
19
+ "react-dom": "^19.0.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.0.0",
23
+ "@types/react": "^19.0.0",
24
+ "@types/react-dom": "^19.0.0",
25
+ "@tailwindcss/postcss": "^4.0.0",
26
+ "tailwindcss": "^4.0.0",
27
+ "typescript": "^5.5.0"
28
+ }
29
+ }
@@ -0,0 +1,3 @@
1
+ export default {
2
+ plugins: { "@tailwindcss/postcss": {} },
3
+ };