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