@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.
Files changed (127) hide show
  1. package/bin/create-pylon.js +98 -42
  2. package/package.json +1 -1
  3. package/templates/backend/b2b/apps/api/functions/archiveProject.ts +15 -0
  4. package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
  5. package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
  6. package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
  7. package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
  8. package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
  9. package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
  10. package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
  11. package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
  12. package/templates/backend/b2b/apps/api/package.json +20 -0
  13. package/templates/backend/b2b/apps/api/schema.ts +171 -0
  14. package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
  15. package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
  16. package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
  17. package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
  18. package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
  19. package/templates/backend/chat/apps/api/package.json +20 -0
  20. package/templates/backend/chat/apps/api/schema.ts +93 -0
  21. package/templates/backend/chat/apps/api/tsconfig.json +13 -0
  22. package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
  23. package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
  24. package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
  25. package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
  26. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
  27. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
  28. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
  29. package/templates/backend/consumer/apps/api/package.json +20 -0
  30. package/templates/backend/consumer/apps/api/schema.ts +130 -0
  31. package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
  32. package/templates/expo/chat/apps/expo/App.tsx +414 -0
  33. package/templates/expo/chat/apps/expo/app.json +25 -0
  34. package/templates/expo/chat/apps/expo/babel.config.js +6 -0
  35. package/templates/expo/chat/apps/expo/package.json +30 -0
  36. package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
  37. package/templates/expo/consumer/apps/expo/App.tsx +360 -0
  38. package/templates/expo/consumer/apps/expo/app.json +25 -0
  39. package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
  40. package/templates/expo/consumer/apps/expo/package.json +30 -0
  41. package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
  42. package/templates/ios/chat/apps/ios/Package.swift +34 -0
  43. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +120 -0
  44. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  45. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
  46. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +35 -0
  47. package/templates/ios/chat/apps/ios/project.yml +42 -0
  48. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  49. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  50. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  51. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  52. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +29 -0
  53. package/templates/ios/consumer/apps/ios/project.yml +42 -0
  54. package/templates/ios/todo/apps/ios/Package.swift +23 -0
  55. package/templates/mac/b2b/apps/mac/Package.swift +22 -0
  56. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
  57. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
  58. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  59. package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  60. package/templates/mac/b2b/apps/mac/project.yml +34 -0
  61. package/templates/mac/barebones/apps/mac/Package.swift +33 -0
  62. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
  63. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  64. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  65. package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  66. package/templates/mac/barebones/apps/mac/project.yml +34 -0
  67. package/templates/mac/chat/apps/mac/Package.swift +33 -0
  68. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +140 -0
  69. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  70. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
  71. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +37 -0
  72. package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  73. package/templates/mac/chat/apps/mac/project.yml +34 -0
  74. package/templates/mac/consumer/apps/mac/Package.swift +33 -0
  75. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  76. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  77. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  78. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  79. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +31 -0
  80. package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  81. package/templates/mac/consumer/apps/mac/project.yml +34 -0
  82. package/templates/mac/todo/apps/mac/Package.swift +33 -0
  83. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
  84. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
  85. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  86. package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  87. package/templates/mac/todo/apps/mac/project.yml +34 -0
  88. package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
  89. package/templates/web/b2b/apps/web/next.config.ts +24 -0
  90. package/templates/web/b2b/apps/web/package.json +29 -0
  91. package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
  92. package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
  93. package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
  94. package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
  95. package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
  96. package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
  97. package/templates/web/b2b/apps/web/tsconfig.json +26 -0
  98. package/templates/web/chat/apps/web/next-env.d.ts +2 -0
  99. package/templates/web/chat/apps/web/next.config.ts +24 -0
  100. package/templates/web/chat/apps/web/package.json +29 -0
  101. package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
  102. package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +250 -0
  103. package/templates/web/chat/apps/web/src/app/globals.css +6 -0
  104. package/templates/web/chat/apps/web/src/app/layout.tsx +21 -0
  105. package/templates/web/chat/apps/web/src/app/page.tsx +51 -0
  106. package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
  107. package/templates/web/chat/apps/web/tsconfig.json +26 -0
  108. package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
  109. package/templates/web/consumer/apps/web/next.config.ts +24 -0
  110. package/templates/web/consumer/apps/web/package.json +29 -0
  111. package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
  112. package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +295 -0
  113. package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
  114. package/templates/web/consumer/apps/web/src/app/layout.tsx +21 -0
  115. package/templates/web/consumer/apps/web/src/app/page.tsx +55 -0
  116. package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
  117. package/templates/web/consumer/apps/web/tsconfig.json +26 -0
  118. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Package.swift +0 -0
  119. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/ContentView.swift +0 -0
  120. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
  121. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
  122. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/project.yml +0 -0
  123. /package/templates/{mobile/todo/apps/mobile → ios/consumer/apps/ios}/Package.swift +0 -0
  124. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
  125. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/TodoListView.swift +0 -0
  126. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
  127. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/project.yml +0 -0
@@ -0,0 +1,244 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ /// macOS Todo list. List with inline checkbox + edit-on-double-click +
5
+ /// delete on hover. macOS doesn't ship `EditButton`, so reordering is
6
+ /// drag-and-drop via `.onMove(perform:)` which works in editable List.
7
+ struct TodoListView: View {
8
+ @EnvironmentObject var session: AppSession
9
+ @State private var todos: [Todo] = []
10
+ @State private var draftTitle: String = ""
11
+ @State private var loading = true
12
+ @State private var pending = false
13
+ @State private var errorMessage: String?
14
+ @State private var editingId: String?
15
+ @State private var editingDraft: String = ""
16
+ @State private var hoveredId: String?
17
+
18
+ var body: some View {
19
+ VStack(spacing: 0) {
20
+ HStack(spacing: 8) {
21
+ TextField("What needs doing?", text: $draftTitle)
22
+ .textFieldStyle(.roundedBorder)
23
+ .onSubmit { Task { await add() } }
24
+ Button("Add") { Task { await add() } }
25
+ .keyboardShortcut(.defaultAction)
26
+ .disabled(draftTitle.trimmingCharacters(in: .whitespaces).isEmpty || pending)
27
+ }
28
+ .padding(16)
29
+
30
+ Divider()
31
+
32
+ if loading {
33
+ ProgressView().padding()
34
+ Spacer()
35
+ } else if todos.isEmpty {
36
+ Spacer()
37
+ Text("No todos yet.")
38
+ .foregroundStyle(.secondary)
39
+ Spacer()
40
+ } else {
41
+ List {
42
+ ForEach(todos) { todo in
43
+ row(todo)
44
+ }
45
+ .onMove { source, destination in
46
+ Task { await reorder(from: source, to: destination) }
47
+ }
48
+ }
49
+ .listStyle(.inset)
50
+ }
51
+
52
+ if let errorMessage {
53
+ Divider()
54
+ Text(errorMessage)
55
+ .foregroundStyle(.red)
56
+ .font(.caption)
57
+ .padding(8)
58
+ }
59
+ }
60
+ .task { await load() }
61
+ .toolbar {
62
+ ToolbarItem(placement: .primaryAction) {
63
+ Button { Task { await load() } } label: {
64
+ Image(systemName: "arrow.clockwise")
65
+ }
66
+ .disabled(loading)
67
+ }
68
+ }
69
+ }
70
+
71
+ @ViewBuilder
72
+ private func row(_ todo: Todo) -> some View {
73
+ HStack(spacing: 10) {
74
+ Button { Task { await toggle(todo) } } label: {
75
+ Image(systemName: todo.done ? "checkmark.circle.fill" : "circle")
76
+ .foregroundStyle(todo.done ? .green : .secondary)
77
+ .imageScale(.large)
78
+ }
79
+ .buttonStyle(.plain)
80
+
81
+ if editingId == todo.id {
82
+ TextField("Title", text: $editingDraft)
83
+ .textFieldStyle(.roundedBorder)
84
+ .onSubmit { Task { await commitEdit(todo) } }
85
+ Button("Save") { Task { await commitEdit(todo) } }
86
+ .buttonStyle(.bordered)
87
+ Button("Cancel") {
88
+ editingId = nil
89
+ editingDraft = ""
90
+ }
91
+ .buttonStyle(.bordered)
92
+ } else {
93
+ Text(todo.title)
94
+ .strikethrough(todo.done, color: .secondary)
95
+ .foregroundStyle(todo.done ? .secondary : .primary)
96
+ .onTapGesture(count: 2) {
97
+ editingId = todo.id
98
+ editingDraft = todo.title
99
+ }
100
+ Spacer()
101
+ if hoveredId == todo.id {
102
+ Button("Edit") {
103
+ editingId = todo.id
104
+ editingDraft = todo.title
105
+ }
106
+ .buttonStyle(.borderless)
107
+ .font(.caption)
108
+ Button("Delete") { Task { await delete(todo) } }
109
+ .buttonStyle(.borderless)
110
+ .font(.caption)
111
+ .foregroundStyle(.red)
112
+ }
113
+ }
114
+ }
115
+ .padding(.vertical, 4)
116
+ .contentShape(Rectangle())
117
+ .onHover { hovering in
118
+ hoveredId = hovering ? todo.id : (hoveredId == todo.id ? nil : hoveredId)
119
+ }
120
+ }
121
+
122
+ // MARK: - Network
123
+
124
+ private func load() async {
125
+ loading = true
126
+ defer { loading = false }
127
+ do {
128
+ let rows: [Todo] = try await session.pylon.callFn("listTodos", args: EmptyArgs())
129
+ todos = rows
130
+ errorMessage = nil
131
+ } catch {
132
+ errorMessage = "Load failed: \(error.localizedDescription)"
133
+ }
134
+ }
135
+
136
+ private func add() async {
137
+ let trimmed = draftTitle.trimmingCharacters(in: .whitespaces)
138
+ guard !trimmed.isEmpty else { return }
139
+ pending = true
140
+ defer { pending = false }
141
+ do {
142
+ let todo: Todo = try await session.pylon.callFn(
143
+ "addTodo",
144
+ args: AddTodoArgs(title: trimmed),
145
+ )
146
+ todos.append(todo)
147
+ draftTitle = ""
148
+ } catch {
149
+ errorMessage = "Add failed: \(error.localizedDescription)"
150
+ }
151
+ }
152
+
153
+ private func toggle(_ todo: Todo) async {
154
+ let nextDone = !todo.done
155
+ if let i = todos.firstIndex(where: { $0.id == todo.id }) {
156
+ todos[i].done = nextDone
157
+ }
158
+ do {
159
+ let _: Todo = try await session.pylon.callFn(
160
+ "toggleTodo",
161
+ args: ToggleTodoArgs(id: todo.id, done: nextDone),
162
+ )
163
+ } catch {
164
+ if let i = todos.firstIndex(where: { $0.id == todo.id }) {
165
+ todos[i].done = todo.done
166
+ }
167
+ errorMessage = "Toggle failed: \(error.localizedDescription)"
168
+ }
169
+ }
170
+
171
+ private func commitEdit(_ todo: Todo) async {
172
+ let trimmed = editingDraft.trimmingCharacters(in: .whitespaces)
173
+ guard !trimmed.isEmpty, trimmed != todo.title else {
174
+ editingId = nil
175
+ editingDraft = ""
176
+ return
177
+ }
178
+ let originalTitle = todo.title
179
+ if let i = todos.firstIndex(where: { $0.id == todo.id }) {
180
+ todos[i].title = trimmed
181
+ }
182
+ editingId = nil
183
+ editingDraft = ""
184
+ do {
185
+ let _: Todo = try await session.pylon.callFn(
186
+ "editTodo",
187
+ args: EditTodoArgs(id: todo.id, title: trimmed),
188
+ )
189
+ } catch {
190
+ if let i = todos.firstIndex(where: { $0.id == todo.id }) {
191
+ todos[i].title = originalTitle
192
+ }
193
+ errorMessage = "Rename failed: \(error.localizedDescription)"
194
+ }
195
+ }
196
+
197
+ private func delete(_ todo: Todo) async {
198
+ let snapshot = todos
199
+ todos.removeAll { $0.id == todo.id }
200
+ do {
201
+ let _: Todo = try await session.pylon.callFn(
202
+ "deleteTodo",
203
+ args: DeleteTodoArgs(id: todo.id),
204
+ )
205
+ } catch {
206
+ todos = snapshot
207
+ errorMessage = "Delete failed: \(error.localizedDescription)"
208
+ }
209
+ }
210
+
211
+ private func reorder(from source: IndexSet, to destination: Int) async {
212
+ var moved = todos
213
+ moved.move(fromOffsets: source, toOffset: destination)
214
+ guard let movedIndex = source.first else { return }
215
+ let newIndex = destination > movedIndex ? destination - 1 : destination
216
+ guard newIndex < moved.count else { return }
217
+ let movedTodo = moved[newIndex]
218
+ let prev = newIndex > 0 ? moved[newIndex - 1] : nil
219
+ let next = newIndex + 1 < moved.count ? moved[newIndex + 1] : nil
220
+ let prevPos = prev?.position ?? 0
221
+ let nextPos = next?.position ?? 0
222
+ let position: Double
223
+ if prev != nil && next != nil {
224
+ position = (prevPos + nextPos) / 2
225
+ } else if prev != nil {
226
+ position = prevPos + 1024
227
+ } else if next != nil {
228
+ position = nextPos - 1024
229
+ } else {
230
+ position = 1024
231
+ }
232
+ let snapshot = todos
233
+ todos = moved
234
+ do {
235
+ let _: Todo = try await session.pylon.callFn(
236
+ "reorderTodo",
237
+ args: ReorderTodoArgs(id: movedTodo.id, position: position),
238
+ )
239
+ } catch {
240
+ todos = snapshot
241
+ errorMessage = "Reorder failed: \(error.localizedDescription)"
242
+ }
243
+ }
244
+ }
@@ -0,0 +1,30 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ @main
5
+ struct __APP_NAME_PASCAL__App: App {
6
+ @StateObject private var session = AppSession()
7
+
8
+ var body: some Scene {
9
+ Window("__APP_NAME__", id: "main") {
10
+ TodoListView()
11
+ .environmentObject(session)
12
+ .frame(minWidth: 480, minHeight: 360)
13
+ }
14
+ .windowResizability(.contentMinSize)
15
+ }
16
+ }
17
+
18
+ @MainActor
19
+ final class AppSession: ObservableObject {
20
+ let pylon: PylonClient
21
+
22
+ init() {
23
+ let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
24
+ ?? "http://localhost:4321"
25
+ guard let url = URL(string: baseURLString) else {
26
+ fatalError("Invalid PYLON_BASE_URL: \(baseURLString)")
27
+ }
28
+ self.pylon = PylonClient(baseURL: url, appName: "__APP_NAME_SNAKE__")
29
+ }
30
+ }
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <!-- App Sandbox + outgoing-network client. Without these the
6
+ scaffolded app can't reach localhost:4321 during dev or
7
+ a remote Pylon Cloud URL in production. -->
8
+ <key>com.apple.security.app-sandbox</key>
9
+ <true/>
10
+ <key>com.apple.security.network.client</key>
11
+ <true/>
12
+ </dict>
13
+ </plist>
@@ -0,0 +1,34 @@
1
+ name: __APP_NAME_PASCAL__
2
+ options:
3
+ bundleIdPrefix: com.example
4
+ deploymentTarget:
5
+ macOS: "13.0"
6
+
7
+ packages:
8
+ pylon:
9
+ url: https://github.com/pylonsync/pylon.git
10
+ from: "0.3.0"
11
+
12
+ targets:
13
+ __APP_NAME_PASCAL__:
14
+ type: application
15
+ platform: macOS
16
+ sources:
17
+ - path: Sources/__APP_NAME_PASCAL__
18
+ settings:
19
+ base:
20
+ GENERATE_INFOPLIST_FILE: YES
21
+ INFOPLIST_KEY_CFBundleDisplayName: __APP_NAME__
22
+ INFOPLIST_KEY_LSApplicationCategoryType: "public.app-category.developer-tools"
23
+ INFOPLIST_KEY_NSHumanReadableCopyright: ""
24
+ SWIFT_VERSION: "5.9"
25
+ ENABLE_PREVIEWS: YES
26
+ # The mac app talks to the local Pylon control plane during dev.
27
+ # App Sandbox needs the network client entitlement to reach
28
+ # localhost; we declare it via codesign entitlements at build time.
29
+ CODE_SIGN_ENTITLEMENTS: __APP_NAME_PASCAL__.entitlements
30
+ dependencies:
31
+ - package: pylon
32
+ product: PylonClient
33
+ - package: pylon
34
+ product: PylonSwiftUI
@@ -0,0 +1,2 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
@@ -0,0 +1,24 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const PYLON_API_URL = process.env.PYLON_API_URL ?? "http://localhost:4321";
4
+
5
+ const config: NextConfig = {
6
+ transpilePackages: [
7
+ "@__APP_NAME_KEBAB__/ui",
8
+ "@pylonsync/sdk",
9
+ "@pylonsync/react",
10
+ "@pylonsync/next",
11
+ "@pylonsync/functions",
12
+ "@pylonsync/sync",
13
+ ],
14
+ async rewrites() {
15
+ return [
16
+ { source: "/api/fn/:path*", destination: `${PYLON_API_URL}/api/fn/:path*` },
17
+ { source: "/api/auth/:path*", destination: `${PYLON_API_URL}/api/auth/:path*` },
18
+ { source: "/api/sync/:path*", destination: `${PYLON_API_URL}/api/sync/:path*` },
19
+ { source: "/api/:path*", destination: `${PYLON_API_URL}/api/:path*` },
20
+ ];
21
+ },
22
+ };
23
+
24
+ export default config;
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@__APP_NAME_KEBAB__/web",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "next dev --port 3000",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "lint": "next lint"
11
+ },
12
+ "dependencies": {
13
+ "@__APP_NAME_KEBAB__/ui": "__WORKSPACE_DEP__",
14
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
15
+ "@pylonsync/react": "^__PYLON_VERSION__",
16
+ "@pylonsync/next": "^__PYLON_VERSION__",
17
+ "next": "^16.0.0",
18
+ "react": "^19.0.0",
19
+ "react-dom": "^19.0.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.0.0",
23
+ "@types/react": "^19.0.0",
24
+ "@types/react-dom": "^19.0.0",
25
+ "@tailwindcss/postcss": "^4.0.0",
26
+ "tailwindcss": "^4.0.0",
27
+ "typescript": "^5.5.0"
28
+ }
29
+ }
@@ -0,0 +1,3 @@
1
+ export default {
2
+ plugins: { "@tailwindcss/postcss": {} },
3
+ };
@@ -0,0 +1,171 @@
1
+ "use client";
2
+
3
+ import { useState, useTransition } from "react";
4
+ import { Button, Input, Card, CardHeader, CardContent } from "@__APP_NAME_KEBAB__/ui";
5
+
6
+ type Org = {
7
+ id: string;
8
+ slug: string;
9
+ name: string;
10
+ role: string;
11
+ createdAt: string;
12
+ };
13
+
14
+ export function OrgPicker({ initialOrgs }: { initialOrgs: Org[] }) {
15
+ const [orgs, setOrgs] = useState(initialOrgs);
16
+ const [activeId, setActiveId] = useState<string | null>(
17
+ initialOrgs[0]?.id ?? null,
18
+ );
19
+ const [creating, setCreating] = useState(false);
20
+ const [name, setName] = useState("");
21
+ const [slug, setSlug] = useState("");
22
+ const [pending, startTransition] = useTransition();
23
+ const [error, setError] = useState<string | null>(null);
24
+
25
+ async function create(e: React.FormEvent) {
26
+ e.preventDefault();
27
+ setError(null);
28
+ startTransition(async () => {
29
+ const res = await fetch("/api/fn/createOrg", {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({ slug, name }),
33
+ });
34
+ if (res.ok) {
35
+ const org = (await res.json()) as Org;
36
+ setOrgs((prev) => [
37
+ { ...org, role: "owner" },
38
+ ...prev,
39
+ ]);
40
+ setActiveId(org.id);
41
+ setName("");
42
+ setSlug("");
43
+ setCreating(false);
44
+ } else {
45
+ const body = await res.json().catch(() => ({}));
46
+ setError(body?.message ?? "create failed");
47
+ }
48
+ });
49
+ }
50
+
51
+ const active = orgs.find((o) => o.id === activeId) ?? null;
52
+
53
+ return (
54
+ <div className="space-y-6">
55
+ <Card>
56
+ <CardHeader>
57
+ <div className="flex items-center justify-between">
58
+ <h2 className="text-sm font-medium">Your organizations</h2>
59
+ <Button
60
+ size="sm"
61
+ variant={creating ? "ghost" : "default"}
62
+ onClick={() => setCreating((v) => !v)}
63
+ >
64
+ {creating ? "Cancel" : "New org"}
65
+ </Button>
66
+ </div>
67
+ </CardHeader>
68
+ <CardContent>
69
+ {creating && (
70
+ <form onSubmit={create} className="grid grid-cols-2 gap-2 mb-4">
71
+ <Input
72
+ placeholder="Org name"
73
+ value={name}
74
+ onChange={(e) => setName(e.target.value)}
75
+ disabled={pending}
76
+ required
77
+ />
78
+ <Input
79
+ placeholder="acme-corp"
80
+ value={slug}
81
+ onChange={(e) => setSlug(e.target.value.toLowerCase())}
82
+ disabled={pending}
83
+ pattern="[a-z0-9][a-z0-9-]{1,30}[a-z0-9]"
84
+ required
85
+ />
86
+ <Button
87
+ type="submit"
88
+ variant="primary"
89
+ disabled={pending || !name.trim() || !slug.trim()}
90
+ className="col-span-2"
91
+ >
92
+ {pending ? "Creating…" : "Create org"}
93
+ </Button>
94
+ {error && (
95
+ <p className="text-xs text-red-500 col-span-2">{error}</p>
96
+ )}
97
+ </form>
98
+ )}
99
+
100
+ {orgs.length === 0 ? (
101
+ <p className="text-sm text-neutral-500 py-2">
102
+ You&apos;re not in any orgs yet. Create one above to get started.
103
+ </p>
104
+ ) : (
105
+ <ul className="divide-y divide-neutral-200 dark:divide-neutral-800 -mx-5">
106
+ {orgs.map((o) => (
107
+ <li
108
+ key={o.id}
109
+ className="flex items-center justify-between px-5 py-2.5 cursor-pointer hover:bg-neutral-50 dark:hover:bg-neutral-900"
110
+ data-active={o.id === activeId || undefined}
111
+ onClick={() => setActiveId(o.id)}
112
+ >
113
+ <div>
114
+ <div className="text-sm font-medium">{o.name}</div>
115
+ <div className="text-xs text-neutral-400 font-mono">
116
+ {o.slug}
117
+ </div>
118
+ </div>
119
+ <span
120
+ className={`text-xs font-mono uppercase tracking-wide px-2 py-0.5 rounded ${
121
+ o.role === "owner"
122
+ ? "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300"
123
+ : o.role === "admin"
124
+ ? "bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-300"
125
+ : "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400"
126
+ }`}
127
+ >
128
+ {o.role}
129
+ </span>
130
+ </li>
131
+ ))}
132
+ </ul>
133
+ )}
134
+ </CardContent>
135
+ </Card>
136
+
137
+ {active && <OrgDetail org={active} />}
138
+ </div>
139
+ );
140
+ }
141
+
142
+ function OrgDetail({ org }: { org: Org }) {
143
+ return (
144
+ <Card>
145
+ <CardHeader>
146
+ <div className="flex items-baseline justify-between">
147
+ <h2 className="text-sm font-medium">
148
+ {org.name}{" "}
149
+ <span className="text-xs text-neutral-400 font-mono ml-1">
150
+ /{org.slug}
151
+ </span>
152
+ </h2>
153
+ <span className="text-xs text-neutral-500">your role: {org.role}</span>
154
+ </div>
155
+ </CardHeader>
156
+ <CardContent className="space-y-4">
157
+ <p className="text-sm text-neutral-500">
158
+ Wire member management and project lists into this panel by calling
159
+ <code className="font-mono text-xs mx-1">/api/fn/orgMembers</code>
160
+ and
161
+ <code className="font-mono text-xs mx-1">/api/fn/orgProjects</code>
162
+ with this org&apos;s id. Both queries enforce membership; you only
163
+ see what your role allows.
164
+ </p>
165
+ <div className="text-xs font-mono text-neutral-400">
166
+ orgId: {org.id}
167
+ </div>
168
+ </CardContent>
169
+ </Card>
170
+ );
171
+ }
@@ -0,0 +1,6 @@
1
+ @import "tailwindcss";
2
+ @source "../../../../packages/ui/src/**/*.{ts,tsx}";
3
+
4
+ :root { color-scheme: light dark; }
5
+ html, body { height: 100%; }
6
+ body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; }
@@ -0,0 +1,21 @@
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "__APP_NAME__",
6
+ description: "Multi-tenant SaaS scaffold powered by Pylon",
7
+ };
8
+
9
+ export default function RootLayout({
10
+ children,
11
+ }: {
12
+ children: React.ReactNode;
13
+ }) {
14
+ return (
15
+ <html lang="en">
16
+ <body className="antialiased min-h-screen bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-100">
17
+ {children}
18
+ </body>
19
+ </html>
20
+ );
21
+ }
@@ -0,0 +1,39 @@
1
+ import { pylon } from "@/lib/pylon";
2
+ import { OrgPicker } from "./components/OrgPicker";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ type Org = {
7
+ id: string;
8
+ slug: string;
9
+ name: string;
10
+ role: string;
11
+ createdAt: string;
12
+ };
13
+
14
+ export default async function HomePage() {
15
+ const orgs = await pylon
16
+ .json<Org[]>("/api/fn/myOrgs", {
17
+ method: "POST",
18
+ body: "{}",
19
+ headers: { "Content-Type": "application/json" },
20
+ })
21
+ .catch(() => [] as Org[]);
22
+
23
+ return (
24
+ <main className="mx-auto max-w-3xl px-6 py-12 space-y-8">
25
+ <header className="space-y-2">
26
+ <h1 className="text-3xl font-semibold tracking-tight">__APP_NAME__</h1>
27
+ <p className="text-sm text-neutral-500 dark:text-neutral-400">
28
+ Multi-tenant SaaS scaffold. Pick an org to manage its members and
29
+ projects, or create a new one. Tenant isolation is enforced
30
+ server-side by Pylon row-level policies — clients can&apos;t leak
31
+ data across orgs even if they fabricate <code className="font-mono text-xs">orgId</code>{" "}
32
+ in a request.
33
+ </p>
34
+ </header>
35
+
36
+ <OrgPicker initialOrgs={orgs} />
37
+ </main>
38
+ );
39
+ }
@@ -0,0 +1,5 @@
1
+ import { createPylonServer } from "@pylonsync/next/server";
2
+
3
+ export const pylon = createPylonServer({
4
+ cookieName: "__APP_NAME_SNAKE___session",
5
+ });
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": { "@/*": ["./src/*"] }
18
+ },
19
+ "include": [
20
+ "next-env.d.ts",
21
+ "src/**/*.ts",
22
+ "src/**/*.tsx",
23
+ ".next/types/**/*.ts"
24
+ ],
25
+ "exclude": ["node_modules"]
26
+ }
@@ -0,0 +1,2 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
@@ -0,0 +1,24 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const PYLON_API_URL = process.env.PYLON_API_URL ?? "http://localhost:4321";
4
+
5
+ const config: NextConfig = {
6
+ transpilePackages: [
7
+ "@__APP_NAME_KEBAB__/ui",
8
+ "@pylonsync/sdk",
9
+ "@pylonsync/react",
10
+ "@pylonsync/next",
11
+ "@pylonsync/functions",
12
+ "@pylonsync/sync",
13
+ ],
14
+ async rewrites() {
15
+ return [
16
+ { source: "/api/fn/:path*", destination: `${PYLON_API_URL}/api/fn/:path*` },
17
+ { source: "/api/auth/:path*", destination: `${PYLON_API_URL}/api/auth/:path*` },
18
+ { source: "/api/sync/:path*", destination: `${PYLON_API_URL}/api/sync/:path*` },
19
+ { source: "/api/:path*", destination: `${PYLON_API_URL}/api/:path*` },
20
+ ];
21
+ },
22
+ };
23
+
24
+ export default config;