@pylonsync/create-pylon 0.3.53 → 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 +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 +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/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/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/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/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 +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/{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/consumer/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,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>
|
|
@@ -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,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,104 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import PylonClient
|
|
3
|
+
|
|
4
|
+
/// macOS list + create form for Widget. Two-pane NavigationSplitView
|
|
5
|
+
/// with sidebar on the left and detail on the right — feels native on
|
|
6
|
+
/// macOS. Same backend as web/iOS/expo.
|
|
7
|
+
struct ContentView: View {
|
|
8
|
+
@EnvironmentObject var session: AppSession
|
|
9
|
+
@State private var widgets: [Widget] = []
|
|
10
|
+
@State private var selected: Widget.ID?
|
|
11
|
+
@State private var newName = ""
|
|
12
|
+
@State private var loading = true
|
|
13
|
+
@State private var creating = false
|
|
14
|
+
@State private var errorMessage: String?
|
|
15
|
+
|
|
16
|
+
var body: some View {
|
|
17
|
+
NavigationSplitView {
|
|
18
|
+
List(selection: $selected) {
|
|
19
|
+
if widgets.isEmpty && !loading {
|
|
20
|
+
Text("No widgets yet.")
|
|
21
|
+
.foregroundStyle(.secondary)
|
|
22
|
+
.font(.callout)
|
|
23
|
+
} else {
|
|
24
|
+
ForEach(widgets) { w in
|
|
25
|
+
HStack {
|
|
26
|
+
Text(w.name)
|
|
27
|
+
Spacer()
|
|
28
|
+
Text("\(w.count)")
|
|
29
|
+
.font(.system(.caption, design: .monospaced))
|
|
30
|
+
.foregroundStyle(.secondary)
|
|
31
|
+
}
|
|
32
|
+
.tag(w.id)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
.navigationTitle("__APP_NAME__")
|
|
37
|
+
.toolbar {
|
|
38
|
+
ToolbarItem(placement: .primaryAction) {
|
|
39
|
+
Button {
|
|
40
|
+
Task { await load() }
|
|
41
|
+
} label: {
|
|
42
|
+
Image(systemName: "arrow.clockwise")
|
|
43
|
+
}
|
|
44
|
+
.disabled(loading)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} detail: {
|
|
48
|
+
VStack(alignment: .leading, spacing: 18) {
|
|
49
|
+
Text("Create a widget")
|
|
50
|
+
.font(.title3)
|
|
51
|
+
.fontWeight(.semibold)
|
|
52
|
+
HStack {
|
|
53
|
+
TextField("Name…", text: $newName)
|
|
54
|
+
.textFieldStyle(.roundedBorder)
|
|
55
|
+
.onSubmit { Task { await create() } }
|
|
56
|
+
Button("Create") { Task { await create() } }
|
|
57
|
+
.keyboardShortcut(.defaultAction)
|
|
58
|
+
.disabled(newName.trimmingCharacters(in: .whitespaces).isEmpty || creating)
|
|
59
|
+
}
|
|
60
|
+
if let errorMessage {
|
|
61
|
+
Text(errorMessage)
|
|
62
|
+
.foregroundStyle(.red)
|
|
63
|
+
.font(.caption)
|
|
64
|
+
}
|
|
65
|
+
Spacer()
|
|
66
|
+
}
|
|
67
|
+
.padding(24)
|
|
68
|
+
}
|
|
69
|
+
.task { await load() }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private func load() async {
|
|
73
|
+
loading = true
|
|
74
|
+
defer { loading = false }
|
|
75
|
+
do {
|
|
76
|
+
let rows: [Widget] = try await session.pylon.callFn(
|
|
77
|
+
"listWidgets",
|
|
78
|
+
args: EmptyArgs(),
|
|
79
|
+
)
|
|
80
|
+
widgets = rows
|
|
81
|
+
errorMessage = nil
|
|
82
|
+
} catch {
|
|
83
|
+
errorMessage = "Load failed: \(error.localizedDescription)"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private func create() async {
|
|
88
|
+
let trimmed = newName.trimmingCharacters(in: .whitespaces)
|
|
89
|
+
guard !trimmed.isEmpty else { return }
|
|
90
|
+
creating = true
|
|
91
|
+
defer { creating = false }
|
|
92
|
+
do {
|
|
93
|
+
let widget: Widget = try await session.pylon.callFn(
|
|
94
|
+
"createWidget",
|
|
95
|
+
args: CreateWidgetArgs(name: trimmed),
|
|
96
|
+
)
|
|
97
|
+
widgets.insert(widget, at: 0)
|
|
98
|
+
newName = ""
|
|
99
|
+
errorMessage = nil
|
|
100
|
+
} catch {
|
|
101
|
+
errorMessage = "Create failed: \(error.localizedDescription)"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Mirrors `Widget` from `apps/api/schema.ts`. Regenerate from the
|
|
4
|
+
/// schema with `pylon codegen client schema.ts --target swift` for
|
|
5
|
+
/// production.
|
|
6
|
+
struct Widget: Codable, Identifiable, Hashable {
|
|
7
|
+
let id: String
|
|
8
|
+
let name: String
|
|
9
|
+
let count: Int
|
|
10
|
+
let createdAt: String
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
struct CreateWidgetArgs: Encodable {
|
|
14
|
+
let name: String
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
struct EmptyArgs: Encodable {}
|
package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift
ADDED
|
@@ -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
|
+
ContentView()
|
|
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,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,140 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import PylonClient
|
|
3
|
+
|
|
4
|
+
/// macOS chat: rooms in a sidebar, room view in the detail pane.
|
|
5
|
+
/// Polls the active room every 1.5s. Replace with PylonQuery<Message>
|
|
6
|
+
/// from PylonSwiftUI for realtime push.
|
|
7
|
+
struct ChatRootView: View {
|
|
8
|
+
@EnvironmentObject var session: AppSession
|
|
9
|
+
@State private var rooms: [Room] = []
|
|
10
|
+
@State private var selected: Room.ID?
|
|
11
|
+
@State private var showingCreate = false
|
|
12
|
+
@State private var draftSlug = ""
|
|
13
|
+
@State private var draftName = ""
|
|
14
|
+
@State private var loading = true
|
|
15
|
+
@State private var errorMessage: String?
|
|
16
|
+
|
|
17
|
+
var body: some View {
|
|
18
|
+
NavigationSplitView {
|
|
19
|
+
VStack(spacing: 0) {
|
|
20
|
+
List(selection: $selected) {
|
|
21
|
+
Section("Rooms") {
|
|
22
|
+
ForEach(rooms) { r in
|
|
23
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
24
|
+
Text(r.name)
|
|
25
|
+
Text("#\(r.slug)")
|
|
26
|
+
.font(.system(.caption, design: .monospaced))
|
|
27
|
+
.foregroundStyle(.secondary)
|
|
28
|
+
}
|
|
29
|
+
.tag(r.id)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
Divider()
|
|
34
|
+
HStack(spacing: 8) {
|
|
35
|
+
TextField("display name", text: Binding(
|
|
36
|
+
get: { session.authorName },
|
|
37
|
+
set: { session.setAuthorName($0) },
|
|
38
|
+
))
|
|
39
|
+
.textFieldStyle(.roundedBorder)
|
|
40
|
+
.font(.caption)
|
|
41
|
+
}
|
|
42
|
+
.padding(8)
|
|
43
|
+
}
|
|
44
|
+
.toolbar {
|
|
45
|
+
ToolbarItem(placement: .primaryAction) {
|
|
46
|
+
Button {
|
|
47
|
+
showingCreate.toggle()
|
|
48
|
+
} label: {
|
|
49
|
+
Image(systemName: "plus")
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
.sheet(isPresented: $showingCreate) {
|
|
54
|
+
createSheet
|
|
55
|
+
}
|
|
56
|
+
} detail: {
|
|
57
|
+
if let id = selected, let room = rooms.first(where: { $0.id == id }) {
|
|
58
|
+
RoomView(room: room)
|
|
59
|
+
} else {
|
|
60
|
+
placeholder
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
.task { await load() }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private var createSheet: some View {
|
|
67
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
68
|
+
Text("Create a room")
|
|
69
|
+
.font(.headline)
|
|
70
|
+
TextField("Name", text: $draftName)
|
|
71
|
+
.textFieldStyle(.roundedBorder)
|
|
72
|
+
TextField("Slug (e.g. general)", text: $draftSlug)
|
|
73
|
+
.textFieldStyle(.roundedBorder)
|
|
74
|
+
.autocorrectionDisabled()
|
|
75
|
+
HStack {
|
|
76
|
+
Spacer()
|
|
77
|
+
Button("Cancel") { showingCreate = false }
|
|
78
|
+
.buttonStyle(.bordered)
|
|
79
|
+
Button("Create") { Task { await create() } }
|
|
80
|
+
.keyboardShortcut(.defaultAction)
|
|
81
|
+
.disabled(draftName.trimmingCharacters(in: .whitespaces).isEmpty
|
|
82
|
+
|| draftSlug.trimmingCharacters(in: .whitespaces).isEmpty)
|
|
83
|
+
}
|
|
84
|
+
if let errorMessage {
|
|
85
|
+
Text(errorMessage).foregroundStyle(.red).font(.caption)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
.padding(20)
|
|
89
|
+
.frame(width: 360)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private var placeholder: some View {
|
|
93
|
+
VStack(spacing: 8) {
|
|
94
|
+
if loading {
|
|
95
|
+
ProgressView()
|
|
96
|
+
} else if rooms.isEmpty {
|
|
97
|
+
Text("No rooms yet.")
|
|
98
|
+
.foregroundStyle(.secondary)
|
|
99
|
+
Button("Create your first room") { showingCreate = true }
|
|
100
|
+
.buttonStyle(.bordered)
|
|
101
|
+
} else {
|
|
102
|
+
Text("Pick a room from the sidebar.")
|
|
103
|
+
.foregroundStyle(.secondary)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private func load() async {
|
|
110
|
+
loading = true
|
|
111
|
+
defer { loading = false }
|
|
112
|
+
do {
|
|
113
|
+
rooms = try await session.pylon.callFn("listRooms", args: EmptyArgs())
|
|
114
|
+
if selected == nil { selected = rooms.first?.id }
|
|
115
|
+
errorMessage = nil
|
|
116
|
+
} catch {
|
|
117
|
+
errorMessage = "Load failed: \(error.localizedDescription)"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private func create() async {
|
|
122
|
+
let name = draftName.trimmingCharacters(in: .whitespaces)
|
|
123
|
+
let slug = draftSlug.trimmingCharacters(in: .whitespaces).lowercased()
|
|
124
|
+
guard !name.isEmpty, !slug.isEmpty else { return }
|
|
125
|
+
do {
|
|
126
|
+
let room: Room = try await session.pylon.callFn(
|
|
127
|
+
"createRoom",
|
|
128
|
+
args: CreateRoomArgs(slug: slug, name: name),
|
|
129
|
+
)
|
|
130
|
+
rooms.append(room)
|
|
131
|
+
selected = room.id
|
|
132
|
+
showingCreate = false
|
|
133
|
+
draftName = ""
|
|
134
|
+
draftSlug = ""
|
|
135
|
+
errorMessage = nil
|
|
136
|
+
} catch {
|
|
137
|
+
errorMessage = "Create failed: \(error.localizedDescription)"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct Room: Codable, Identifiable, Hashable {
|
|
4
|
+
let id: String
|
|
5
|
+
let slug: String
|
|
6
|
+
let name: String
|
|
7
|
+
let createdAt: String
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
struct Message: Codable, Identifiable, Hashable {
|
|
11
|
+
let id: String
|
|
12
|
+
let roomId: String
|
|
13
|
+
let authorId: String
|
|
14
|
+
let authorName: String
|
|
15
|
+
let body: String
|
|
16
|
+
let createdAt: String
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
struct CreateRoomArgs: Encodable { let slug: String; let name: String }
|
|
20
|
+
struct RoomMessagesArgs: Encodable { let roomId: String }
|
|
21
|
+
struct SendMessageArgs: Encodable {
|
|
22
|
+
let roomId: String
|
|
23
|
+
let body: String
|
|
24
|
+
let authorName: String
|
|
25
|
+
}
|
|
26
|
+
struct EmptyArgs: Encodable {}
|