@pylonsync/create-pylon 0.3.50 → 0.3.53

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 (65) hide show
  1. package/bin/create-pylon.js +292 -1157
  2. package/package.json +4 -3
  3. package/templates/_root/.env.example +9 -0
  4. package/templates/_root/README.md +43 -0
  5. package/templates/backend/barebones/apps/api/functions/createWidget.ts +22 -0
  6. package/templates/backend/barebones/apps/api/functions/listWidgets.ts +18 -0
  7. package/templates/backend/barebones/apps/api/package.json +20 -0
  8. package/templates/backend/barebones/apps/api/schema.ts +61 -0
  9. package/templates/backend/barebones/apps/api/tsconfig.json +13 -0
  10. package/templates/backend/todo/apps/api/functions/addTodo.ts +27 -0
  11. package/templates/backend/todo/apps/api/functions/deleteTodo.ts +14 -0
  12. package/templates/backend/todo/apps/api/functions/editTodo.ts +16 -0
  13. package/templates/backend/todo/apps/api/functions/listTodos.ts +24 -0
  14. package/templates/backend/todo/apps/api/functions/reorderTodo.ts +14 -0
  15. package/templates/backend/todo/apps/api/functions/toggleTodo.ts +13 -0
  16. package/templates/backend/todo/apps/api/package.json +20 -0
  17. package/templates/backend/todo/apps/api/schema.ts +85 -0
  18. package/templates/backend/todo/apps/api/tsconfig.json +13 -0
  19. package/templates/expo/barebones/apps/expo/App.tsx +166 -0
  20. package/templates/expo/barebones/apps/expo/app.json +31 -0
  21. package/templates/expo/barebones/apps/expo/babel.config.js +6 -0
  22. package/templates/expo/barebones/apps/expo/package.json +30 -0
  23. package/templates/expo/barebones/apps/expo/tsconfig.json +16 -0
  24. package/templates/expo/todo/apps/expo/App.tsx +287 -0
  25. package/templates/expo/todo/apps/expo/app.json +25 -0
  26. package/templates/expo/todo/apps/expo/babel.config.js +6 -0
  27. package/templates/expo/todo/apps/expo/package.json +30 -0
  28. package/templates/expo/todo/apps/expo/tsconfig.json +16 -0
  29. package/templates/mobile/barebones/apps/mobile/Package.swift +34 -0
  30. package/templates/mobile/barebones/apps/mobile/Sources/__APP_NAME_PASCAL__/ContentView.swift +98 -0
  31. package/templates/mobile/barebones/apps/mobile/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  32. package/templates/mobile/barebones/apps/mobile/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -0
  33. package/templates/mobile/barebones/apps/mobile/project.yml +42 -0
  34. package/templates/mobile/todo/apps/mobile/Package.swift +23 -0
  35. package/templates/mobile/todo/apps/mobile/Sources/__APP_NAME_PASCAL__/Models.swift +18 -0
  36. package/templates/mobile/todo/apps/mobile/Sources/__APP_NAME_PASCAL__/TodoListView.swift +230 -0
  37. package/templates/mobile/todo/apps/mobile/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +28 -0
  38. package/templates/mobile/todo/apps/mobile/project.yml +32 -0
  39. package/templates/ui/packages/ui/package.json +26 -0
  40. package/templates/ui/packages/ui/src/button.tsx +44 -0
  41. package/templates/ui/packages/ui/src/card.tsx +39 -0
  42. package/templates/ui/packages/ui/src/cn.ts +12 -0
  43. package/templates/ui/packages/ui/src/index.ts +4 -0
  44. package/templates/ui/packages/ui/src/input.tsx +19 -0
  45. package/templates/ui/packages/ui/tsconfig.json +15 -0
  46. package/templates/web/barebones/apps/web/next-env.d.ts +2 -0
  47. package/templates/web/barebones/apps/web/next.config.ts +40 -0
  48. package/templates/web/barebones/apps/web/package.json +29 -0
  49. package/templates/web/barebones/apps/web/postcss.config.mjs +4 -0
  50. package/templates/web/barebones/apps/web/src/app/components/WidgetList.tsx +81 -0
  51. package/templates/web/barebones/apps/web/src/app/globals.css +9 -0
  52. package/templates/web/barebones/apps/web/src/app/layout.tsx +21 -0
  53. package/templates/web/barebones/apps/web/src/app/page.tsx +43 -0
  54. package/templates/web/barebones/apps/web/src/lib/pylon.ts +14 -0
  55. package/templates/web/barebones/apps/web/tsconfig.json +26 -0
  56. package/templates/web/todo/apps/web/next-env.d.ts +2 -0
  57. package/templates/web/todo/apps/web/next.config.ts +24 -0
  58. package/templates/web/todo/apps/web/package.json +32 -0
  59. package/templates/web/todo/apps/web/postcss.config.mjs +3 -0
  60. package/templates/web/todo/apps/web/src/app/components/TodoList.tsx +310 -0
  61. package/templates/web/todo/apps/web/src/app/globals.css +6 -0
  62. package/templates/web/todo/apps/web/src/app/layout.tsx +21 -0
  63. package/templates/web/todo/apps/web/src/app/page.tsx +36 -0
  64. package/templates/web/todo/apps/web/src/lib/pylon.ts +5 -0
  65. package/templates/web/todo/apps/web/tsconfig.json +26 -0
@@ -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,26 @@
1
+ {
2
+ "name": "@__APP_NAME_KEBAB__/ui",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./button": "./src/button.tsx",
11
+ "./input": "./src/input.tsx",
12
+ "./card": "./src/card.tsx",
13
+ "./cn": "./src/cn.ts"
14
+ },
15
+ "dependencies": {
16
+ "clsx": "^2.1.0",
17
+ "tailwind-merge": "^2.5.0"
18
+ },
19
+ "peerDependencies": {
20
+ "react": "^19.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/react": "^19.0.0",
24
+ "typescript": "^5.5.0"
25
+ }
26
+ }
@@ -0,0 +1,44 @@
1
+ import * as React from "react";
2
+ import { cn } from "./cn";
3
+
4
+ type Variant = "default" | "primary" | "ghost";
5
+ type Size = "sm" | "md";
6
+
7
+ const variants: Record<Variant, string> = {
8
+ default:
9
+ "bg-neutral-100 hover:bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:text-neutral-100",
10
+ primary:
11
+ "bg-neutral-900 hover:bg-neutral-800 text-white dark:bg-white dark:hover:bg-neutral-200 dark:text-neutral-900",
12
+ ghost:
13
+ "bg-transparent hover:bg-neutral-100 text-neutral-700 dark:hover:bg-neutral-800 dark:text-neutral-300",
14
+ };
15
+
16
+ const sizes: Record<Size, string> = {
17
+ sm: "h-8 px-3 text-[13px]",
18
+ md: "h-9 px-4 text-sm",
19
+ };
20
+
21
+ export interface ButtonProps
22
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
23
+ variant?: Variant;
24
+ size?: Size;
25
+ }
26
+
27
+ export function Button({
28
+ className,
29
+ variant = "default",
30
+ size = "md",
31
+ ...props
32
+ }: ButtonProps) {
33
+ return (
34
+ <button
35
+ className={cn(
36
+ "inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors disabled:opacity-50 disabled:pointer-events-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500",
37
+ variants[variant],
38
+ sizes[size],
39
+ className,
40
+ )}
41
+ {...props}
42
+ />
43
+ );
44
+ }
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+ import { cn } from "./cn";
3
+
4
+ export function Card({
5
+ className,
6
+ ...props
7
+ }: React.HTMLAttributes<HTMLDivElement>) {
8
+ return (
9
+ <div
10
+ className={cn(
11
+ "rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900",
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ export function CardHeader({
20
+ className,
21
+ ...props
22
+ }: React.HTMLAttributes<HTMLDivElement>) {
23
+ return (
24
+ <div
25
+ className={cn(
26
+ "p-5 border-b border-neutral-200 dark:border-neutral-800",
27
+ className,
28
+ )}
29
+ {...props}
30
+ />
31
+ );
32
+ }
33
+
34
+ export function CardContent({
35
+ className,
36
+ ...props
37
+ }: React.HTMLAttributes<HTMLDivElement>) {
38
+ return <div className={cn("p-5", className)} {...props} />;
39
+ }
@@ -0,0 +1,12 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ /**
5
+ * Tailwind-aware class merger. Last-class-wins semantics so a
6
+ * caller's `className` reliably overrides a default in a UI
7
+ * primitive (e.g. <Button className="bg-red-500"> beats the
8
+ * primitive's bg-neutral-900 base).
9
+ */
10
+ export function cn(...inputs: ClassValue[]): string {
11
+ return twMerge(clsx(inputs));
12
+ }
@@ -0,0 +1,4 @@
1
+ export { cn } from "./cn";
2
+ export { Button, type ButtonProps } from "./button";
3
+ export { Input, type InputProps } from "./input";
4
+ export { Card, CardHeader, CardContent } from "./card";
@@ -0,0 +1,19 @@
1
+ import * as React from "react";
2
+ import { cn } from "./cn";
3
+
4
+ export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
5
+
6
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
7
+ function Input({ className, ...props }, ref) {
8
+ return (
9
+ <input
10
+ ref={ref}
11
+ className={cn(
12
+ "flex h-9 w-full rounded-md border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 text-sm placeholder:text-neutral-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:opacity-50",
13
+ className,
14
+ )}
15
+ {...props}
16
+ />
17
+ );
18
+ },
19
+ );
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "esnext"],
5
+ "jsx": "preserve",
6
+ "module": "ESNext",
7
+ "moduleResolution": "Bundler",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "esModuleInterop": true,
12
+ "allowSyntheticDefaultImports": true
13
+ },
14
+ "include": ["src/**/*.ts", "src/**/*.tsx"]
15
+ }
@@ -0,0 +1,2 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
@@ -0,0 +1,40 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ /**
4
+ * Pylon's typed client + functions packages re-export across the
5
+ * server/client boundary AND the workspace UI package ships TSX.
6
+ * `transpilePackages` makes Next bundle them cleanly.
7
+ *
8
+ * `rewrites` proxies every Pylon-owned path (`/api/fn/*`,
9
+ * `/api/auth/*`, `/api/sync/*`, …) to the Pylon binary running on
10
+ * `PYLON_API_URL` (default http://localhost:4321). Without this,
11
+ * Next.js sees `/api/fn/createWidget` as a missing route and 404s
12
+ * before the request reaches Pylon.
13
+ *
14
+ * In production set `PYLON_API_URL` to wherever you've deployed the
15
+ * Pylon binary (Fly, Render, Railway, your own box). The browser
16
+ * still hits same-origin paths under your Next deployment, and Next
17
+ * forwards them server-side — no CORS, no extra DNS.
18
+ */
19
+ const PYLON_API_URL = process.env.PYLON_API_URL ?? "http://localhost:4321";
20
+
21
+ const config: NextConfig = {
22
+ transpilePackages: [
23
+ "@__APP_NAME_KEBAB__/ui",
24
+ "@pylonsync/sdk",
25
+ "@pylonsync/react",
26
+ "@pylonsync/next",
27
+ "@pylonsync/functions",
28
+ "@pylonsync/sync",
29
+ ],
30
+ async rewrites() {
31
+ return [
32
+ { source: "/api/fn/:path*", destination: `${PYLON_API_URL}/api/fn/:path*` },
33
+ { source: "/api/auth/:path*", destination: `${PYLON_API_URL}/api/auth/:path*` },
34
+ { source: "/api/sync/:path*", destination: `${PYLON_API_URL}/api/sync/:path*` },
35
+ { source: "/api/:path*", destination: `${PYLON_API_URL}/api/:path*` },
36
+ ];
37
+ },
38
+ };
39
+
40
+ 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,4 @@
1
+ /** Tailwind v4 PostCSS pipeline. */
2
+ export default {
3
+ plugins: { "@tailwindcss/postcss": {} },
4
+ };
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { useState, useTransition } from "react";
4
+ import { Button, Input } from "@__APP_NAME_KEBAB__/ui";
5
+
6
+ type Widget = {
7
+ id: string;
8
+ name: string;
9
+ count: number;
10
+ createdAt: string;
11
+ };
12
+
13
+ export function WidgetList({ initialWidgets }: { initialWidgets: Widget[] }) {
14
+ const [widgets, setWidgets] = useState(initialWidgets);
15
+ const [name, setName] = useState("");
16
+ const [pending, startTransition] = useTransition();
17
+
18
+ async function add() {
19
+ const trimmed = name.trim();
20
+ if (!trimmed) return;
21
+ setName("");
22
+ startTransition(async () => {
23
+ const res = await fetch("/api/fn/createWidget", {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/json" },
26
+ body: JSON.stringify({ name: trimmed }),
27
+ });
28
+ if (res.ok) {
29
+ const widget = (await res.json()) as Widget;
30
+ setWidgets((prev) => [widget, ...prev]);
31
+ }
32
+ });
33
+ }
34
+
35
+ return (
36
+ <div className="space-y-4">
37
+ <form
38
+ onSubmit={(e) => {
39
+ e.preventDefault();
40
+ add();
41
+ }}
42
+ className="flex gap-2"
43
+ >
44
+ <Input
45
+ value={name}
46
+ onChange={(e) => setName(e.target.value)}
47
+ placeholder="Name a widget…"
48
+ disabled={pending}
49
+ className="flex-1"
50
+ />
51
+ <Button
52
+ type="submit"
53
+ variant="primary"
54
+ disabled={pending || !name.trim()}
55
+ >
56
+ Create
57
+ </Button>
58
+ </form>
59
+
60
+ {widgets.length === 0 ? (
61
+ <p className="text-sm text-neutral-500 dark:text-neutral-400 text-center py-8">
62
+ No widgets yet. Create one above.
63
+ </p>
64
+ ) : (
65
+ <ul className="divide-y divide-neutral-200 dark:divide-neutral-800 rounded-md border border-neutral-200 dark:border-neutral-800">
66
+ {widgets.map((w) => (
67
+ <li
68
+ key={w.id}
69
+ className="flex items-center justify-between gap-3 px-4 py-3 text-sm bg-white dark:bg-neutral-950"
70
+ >
71
+ <span className="font-medium">{w.name}</span>
72
+ <span className="font-mono text-xs text-neutral-400">
73
+ count: {w.count}
74
+ </span>
75
+ </li>
76
+ ))}
77
+ </ul>
78
+ )}
79
+ </div>
80
+ );
81
+ }
@@ -0,0 +1,9 @@
1
+ @import "tailwindcss";
2
+ @source "../../../../packages/ui/src/**/*.{ts,tsx}";
3
+
4
+ :root {
5
+ color-scheme: light dark;
6
+ }
7
+
8
+ html, body { height: 100%; }
9
+ 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: "Realtime app 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
+ }