@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.
- package/bin/create-pylon.js +98 -42
- package/package.json +1 -1
- package/templates/backend/b2b/apps/api/functions/archiveProject.ts +15 -0
- package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
- package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
- package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
- package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
- package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
- package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
- package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
- package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
- package/templates/backend/b2b/apps/api/package.json +20 -0
- package/templates/backend/b2b/apps/api/schema.ts +171 -0
- package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
- package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
- package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
- package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
- package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
- package/templates/backend/chat/apps/api/package.json +20 -0
- package/templates/backend/chat/apps/api/schema.ts +93 -0
- package/templates/backend/chat/apps/api/tsconfig.json +13 -0
- package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
- package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
- package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
- package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
- package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
- package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
- package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
- package/templates/backend/consumer/apps/api/package.json +20 -0
- package/templates/backend/consumer/apps/api/schema.ts +130 -0
- package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
- package/templates/expo/chat/apps/expo/App.tsx +384 -0
- package/templates/expo/chat/apps/expo/app.json +25 -0
- package/templates/expo/chat/apps/expo/babel.config.js +6 -0
- package/templates/expo/chat/apps/expo/package.json +30 -0
- package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
- package/templates/expo/consumer/apps/expo/App.tsx +392 -0
- package/templates/expo/consumer/apps/expo/app.json +25 -0
- package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
- package/templates/expo/consumer/apps/expo/package.json +30 -0
- package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
- package/templates/ios/chat/apps/ios/Package.swift +24 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +116 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
- package/templates/ios/chat/apps/ios/project.yml +44 -0
- package/templates/ios/consumer/apps/ios/Package.swift +24 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +57 -0
- package/templates/ios/consumer/apps/ios/project.yml +44 -0
- package/templates/mac/b2b/apps/mac/Package.swift +22 -0
- package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
- package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
- package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
- package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/b2b/apps/mac/project.yml +34 -0
- package/templates/mac/barebones/apps/mac/Package.swift +33 -0
- package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
- package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
- package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
- package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/barebones/apps/mac/project.yml +34 -0
- package/templates/mac/chat/apps/mac/Package.swift +34 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +143 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
- package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/chat/apps/mac/project.yml +36 -0
- package/templates/mac/consumer/apps/mac/Package.swift +34 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +59 -0
- package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/consumer/apps/mac/project.yml +36 -0
- package/templates/mac/todo/apps/mac/Package.swift +33 -0
- package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
- package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
- package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
- package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/todo/apps/mac/project.yml +34 -0
- package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
- package/templates/web/b2b/apps/web/next.config.ts +24 -0
- package/templates/web/b2b/apps/web/package.json +29 -0
- package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
- package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
- package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
- package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
- package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/b2b/apps/web/tsconfig.json +26 -0
- package/templates/web/chat/apps/web/next-env.d.ts +2 -0
- package/templates/web/chat/apps/web/next.config.ts +24 -0
- package/templates/web/chat/apps/web/package.json +29 -0
- package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
- package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +231 -0
- package/templates/web/chat/apps/web/src/app/globals.css +6 -0
- package/templates/web/chat/apps/web/src/app/layout.tsx +22 -0
- package/templates/web/chat/apps/web/src/app/page.tsx +12 -0
- package/templates/web/chat/apps/web/src/app/providers.tsx +25 -0
- package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/chat/apps/web/tsconfig.json +26 -0
- package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
- package/templates/web/consumer/apps/web/next.config.ts +24 -0
- package/templates/web/consumer/apps/web/package.json +29 -0
- package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
- package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +315 -0
- package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
- package/templates/web/consumer/apps/web/src/app/layout.tsx +22 -0
- package/templates/web/consumer/apps/web/src/app/page.tsx +21 -0
- package/templates/web/consumer/apps/web/src/app/providers.tsx +25 -0
- package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/consumer/apps/web/tsconfig.json +26 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Package.swift +0 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/ContentView.swift +0 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/project.yml +0 -0
- /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Package.swift +0 -0
- /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
- /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/TodoListView.swift +0 -0
- /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
- /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
|
+
}
|
package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift
ADDED
|
@@ -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,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
|
+
}
|