@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.
Files changed (180) hide show
  1. package/bin/create-pylon.js +347 -1156
  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/b2b/apps/api/functions/archiveProject.ts +15 -0
  6. package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
  7. package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
  8. package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
  9. package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
  10. package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
  11. package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
  12. package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
  13. package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
  14. package/templates/backend/b2b/apps/api/package.json +20 -0
  15. package/templates/backend/b2b/apps/api/schema.ts +171 -0
  16. package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
  17. package/templates/backend/barebones/apps/api/functions/createWidget.ts +22 -0
  18. package/templates/backend/barebones/apps/api/functions/listWidgets.ts +18 -0
  19. package/templates/backend/barebones/apps/api/package.json +20 -0
  20. package/templates/backend/barebones/apps/api/schema.ts +61 -0
  21. package/templates/backend/barebones/apps/api/tsconfig.json +13 -0
  22. package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
  23. package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
  24. package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
  25. package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
  26. package/templates/backend/chat/apps/api/package.json +20 -0
  27. package/templates/backend/chat/apps/api/schema.ts +93 -0
  28. package/templates/backend/chat/apps/api/tsconfig.json +13 -0
  29. package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
  30. package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
  31. package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
  32. package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
  33. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
  34. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
  35. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
  36. package/templates/backend/consumer/apps/api/package.json +20 -0
  37. package/templates/backend/consumer/apps/api/schema.ts +130 -0
  38. package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
  39. package/templates/backend/todo/apps/api/functions/addTodo.ts +27 -0
  40. package/templates/backend/todo/apps/api/functions/deleteTodo.ts +14 -0
  41. package/templates/backend/todo/apps/api/functions/editTodo.ts +16 -0
  42. package/templates/backend/todo/apps/api/functions/listTodos.ts +24 -0
  43. package/templates/backend/todo/apps/api/functions/reorderTodo.ts +14 -0
  44. package/templates/backend/todo/apps/api/functions/toggleTodo.ts +13 -0
  45. package/templates/backend/todo/apps/api/package.json +20 -0
  46. package/templates/backend/todo/apps/api/schema.ts +85 -0
  47. package/templates/backend/todo/apps/api/tsconfig.json +13 -0
  48. package/templates/expo/barebones/apps/expo/App.tsx +166 -0
  49. package/templates/expo/barebones/apps/expo/app.json +31 -0
  50. package/templates/expo/barebones/apps/expo/babel.config.js +6 -0
  51. package/templates/expo/barebones/apps/expo/package.json +30 -0
  52. package/templates/expo/barebones/apps/expo/tsconfig.json +16 -0
  53. package/templates/expo/chat/apps/expo/App.tsx +414 -0
  54. package/templates/expo/chat/apps/expo/app.json +25 -0
  55. package/templates/expo/chat/apps/expo/babel.config.js +6 -0
  56. package/templates/expo/chat/apps/expo/package.json +30 -0
  57. package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
  58. package/templates/expo/consumer/apps/expo/App.tsx +360 -0
  59. package/templates/expo/consumer/apps/expo/app.json +25 -0
  60. package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
  61. package/templates/expo/consumer/apps/expo/package.json +30 -0
  62. package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
  63. package/templates/expo/todo/apps/expo/App.tsx +287 -0
  64. package/templates/expo/todo/apps/expo/app.json +25 -0
  65. package/templates/expo/todo/apps/expo/babel.config.js +6 -0
  66. package/templates/expo/todo/apps/expo/package.json +30 -0
  67. package/templates/expo/todo/apps/expo/tsconfig.json +16 -0
  68. package/templates/ios/barebones/apps/ios/Package.swift +34 -0
  69. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/ContentView.swift +98 -0
  70. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  71. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -0
  72. package/templates/ios/barebones/apps/ios/project.yml +42 -0
  73. package/templates/ios/chat/apps/ios/Package.swift +34 -0
  74. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +120 -0
  75. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  76. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
  77. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +35 -0
  78. package/templates/ios/chat/apps/ios/project.yml +42 -0
  79. package/templates/ios/consumer/apps/ios/Package.swift +23 -0
  80. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  81. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  82. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  83. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  84. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +29 -0
  85. package/templates/ios/consumer/apps/ios/project.yml +42 -0
  86. package/templates/ios/todo/apps/ios/Package.swift +23 -0
  87. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +18 -0
  88. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/TodoListView.swift +230 -0
  89. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +28 -0
  90. package/templates/ios/todo/apps/ios/project.yml +32 -0
  91. package/templates/mac/b2b/apps/mac/Package.swift +22 -0
  92. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
  93. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
  94. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  95. package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  96. package/templates/mac/b2b/apps/mac/project.yml +34 -0
  97. package/templates/mac/barebones/apps/mac/Package.swift +33 -0
  98. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
  99. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  100. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  101. package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  102. package/templates/mac/barebones/apps/mac/project.yml +34 -0
  103. package/templates/mac/chat/apps/mac/Package.swift +33 -0
  104. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +140 -0
  105. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  106. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
  107. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +37 -0
  108. package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  109. package/templates/mac/chat/apps/mac/project.yml +34 -0
  110. package/templates/mac/consumer/apps/mac/Package.swift +33 -0
  111. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  112. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  113. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  114. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  115. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +31 -0
  116. package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  117. package/templates/mac/consumer/apps/mac/project.yml +34 -0
  118. package/templates/mac/todo/apps/mac/Package.swift +33 -0
  119. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
  120. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
  121. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  122. package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  123. package/templates/mac/todo/apps/mac/project.yml +34 -0
  124. package/templates/ui/packages/ui/package.json +26 -0
  125. package/templates/ui/packages/ui/src/button.tsx +44 -0
  126. package/templates/ui/packages/ui/src/card.tsx +39 -0
  127. package/templates/ui/packages/ui/src/cn.ts +12 -0
  128. package/templates/ui/packages/ui/src/index.ts +4 -0
  129. package/templates/ui/packages/ui/src/input.tsx +19 -0
  130. package/templates/ui/packages/ui/tsconfig.json +15 -0
  131. package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
  132. package/templates/web/b2b/apps/web/next.config.ts +24 -0
  133. package/templates/web/b2b/apps/web/package.json +29 -0
  134. package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
  135. package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
  136. package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
  137. package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
  138. package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
  139. package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
  140. package/templates/web/b2b/apps/web/tsconfig.json +26 -0
  141. package/templates/web/barebones/apps/web/next-env.d.ts +2 -0
  142. package/templates/web/barebones/apps/web/next.config.ts +40 -0
  143. package/templates/web/barebones/apps/web/package.json +29 -0
  144. package/templates/web/barebones/apps/web/postcss.config.mjs +4 -0
  145. package/templates/web/barebones/apps/web/src/app/components/WidgetList.tsx +81 -0
  146. package/templates/web/barebones/apps/web/src/app/globals.css +9 -0
  147. package/templates/web/barebones/apps/web/src/app/layout.tsx +21 -0
  148. package/templates/web/barebones/apps/web/src/app/page.tsx +43 -0
  149. package/templates/web/barebones/apps/web/src/lib/pylon.ts +14 -0
  150. package/templates/web/barebones/apps/web/tsconfig.json +26 -0
  151. package/templates/web/chat/apps/web/next-env.d.ts +2 -0
  152. package/templates/web/chat/apps/web/next.config.ts +24 -0
  153. package/templates/web/chat/apps/web/package.json +29 -0
  154. package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
  155. package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +250 -0
  156. package/templates/web/chat/apps/web/src/app/globals.css +6 -0
  157. package/templates/web/chat/apps/web/src/app/layout.tsx +21 -0
  158. package/templates/web/chat/apps/web/src/app/page.tsx +51 -0
  159. package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
  160. package/templates/web/chat/apps/web/tsconfig.json +26 -0
  161. package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
  162. package/templates/web/consumer/apps/web/next.config.ts +24 -0
  163. package/templates/web/consumer/apps/web/package.json +29 -0
  164. package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
  165. package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +295 -0
  166. package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
  167. package/templates/web/consumer/apps/web/src/app/layout.tsx +21 -0
  168. package/templates/web/consumer/apps/web/src/app/page.tsx +55 -0
  169. package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
  170. package/templates/web/consumer/apps/web/tsconfig.json +26 -0
  171. package/templates/web/todo/apps/web/next-env.d.ts +2 -0
  172. package/templates/web/todo/apps/web/next.config.ts +24 -0
  173. package/templates/web/todo/apps/web/package.json +32 -0
  174. package/templates/web/todo/apps/web/postcss.config.mjs +3 -0
  175. package/templates/web/todo/apps/web/src/app/components/TodoList.tsx +310 -0
  176. package/templates/web/todo/apps/web/src/app/globals.css +6 -0
  177. package/templates/web/todo/apps/web/src/app/layout.tsx +21 -0
  178. package/templates/web/todo/apps/web/src/app/page.tsx +36 -0
  179. package/templates/web/todo/apps/web/src/lib/pylon.ts +5 -0
  180. package/templates/web/todo/apps/web/tsconfig.json +26 -0
@@ -0,0 +1,310 @@
1
+ "use client";
2
+
3
+ import { useState, useTransition, useRef, useEffect } from "react";
4
+ import { Button, Input } from "@__APP_NAME_KEBAB__/ui";
5
+ import {
6
+ DndContext,
7
+ closestCenter,
8
+ KeyboardSensor,
9
+ PointerSensor,
10
+ useSensor,
11
+ useSensors,
12
+ type DragEndEvent,
13
+ } from "@dnd-kit/core";
14
+ import {
15
+ arrayMove,
16
+ SortableContext,
17
+ sortableKeyboardCoordinates,
18
+ useSortable,
19
+ verticalListSortingStrategy,
20
+ } from "@dnd-kit/sortable";
21
+ import { CSS } from "@dnd-kit/utilities";
22
+
23
+ type Todo = {
24
+ id: string;
25
+ title: string;
26
+ done: boolean;
27
+ createdAt: string;
28
+ position?: number;
29
+ };
30
+
31
+ export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
32
+ const [todos, setTodos] = useState(initialTodos);
33
+ const [title, setTitle] = useState("");
34
+ const [pending, startTransition] = useTransition();
35
+ const sensors = useSensors(
36
+ useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
37
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
38
+ );
39
+
40
+ async function add() {
41
+ if (!title.trim()) return;
42
+ const newTitle = title;
43
+ setTitle("");
44
+ startTransition(async () => {
45
+ const res = await fetch("/api/fn/addTodo", {
46
+ method: "POST",
47
+ headers: { "Content-Type": "application/json" },
48
+ body: JSON.stringify({ title: newTitle }),
49
+ });
50
+ if (res.ok) {
51
+ const todo = (await res.json()) as Todo;
52
+ setTodos((prev) => [...prev, todo]);
53
+ }
54
+ });
55
+ }
56
+
57
+ async function toggle(t: Todo) {
58
+ const next = !t.done;
59
+ setTodos((prev) =>
60
+ prev.map((row) => (row.id === t.id ? { ...row, done: next } : row)),
61
+ );
62
+ startTransition(async () => {
63
+ const res = await fetch("/api/fn/toggleTodo", {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/json" },
66
+ body: JSON.stringify({ id: t.id, done: next }),
67
+ });
68
+ if (!res.ok) {
69
+ setTodos((prev) =>
70
+ prev.map((row) => (row.id === t.id ? { ...row, done: t.done } : row)),
71
+ );
72
+ }
73
+ });
74
+ }
75
+
76
+ async function remove(t: Todo) {
77
+ const snapshot = todos;
78
+ setTodos((prev) => prev.filter((row) => row.id !== t.id));
79
+ startTransition(async () => {
80
+ const res = await fetch("/api/fn/deleteTodo", {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/json" },
83
+ body: JSON.stringify({ id: t.id }),
84
+ });
85
+ if (!res.ok) setTodos(snapshot);
86
+ });
87
+ }
88
+
89
+ async function rename(t: Todo, newTitle: string) {
90
+ const trimmed = newTitle.trim();
91
+ if (!trimmed || trimmed === t.title) return;
92
+ setTodos((prev) =>
93
+ prev.map((row) => (row.id === t.id ? { ...row, title: trimmed } : row)),
94
+ );
95
+ startTransition(async () => {
96
+ const res = await fetch("/api/fn/editTodo", {
97
+ method: "POST",
98
+ headers: { "Content-Type": "application/json" },
99
+ body: JSON.stringify({ id: t.id, title: trimmed }),
100
+ });
101
+ if (!res.ok) {
102
+ setTodos((prev) =>
103
+ prev.map((row) => (row.id === t.id ? { ...row, title: t.title } : row)),
104
+ );
105
+ }
106
+ });
107
+ }
108
+
109
+ function onDragEnd(e: DragEndEvent) {
110
+ const { active, over } = e;
111
+ if (!over || active.id === over.id) return;
112
+ const oldIndex = todos.findIndex((t) => t.id === active.id);
113
+ const newIndex = todos.findIndex((t) => t.id === over.id);
114
+ if (oldIndex < 0 || newIndex < 0) return;
115
+ const reordered = arrayMove(todos, oldIndex, newIndex);
116
+ setTodos(reordered);
117
+ const prev = reordered[newIndex - 1];
118
+ const next = reordered[newIndex + 1];
119
+ const prevPos = prev?.position ?? Date.parse(prev?.createdAt ?? "") ?? 0;
120
+ const nextPos = next?.position ?? Date.parse(next?.createdAt ?? "") ?? 0;
121
+ let position: number;
122
+ if (prev && next) position = (prevPos + nextPos) / 2;
123
+ else if (prev) position = prevPos + 1024;
124
+ else if (next) position = nextPos - 1024;
125
+ else position = 1024;
126
+ const movedId = String(active.id);
127
+ const snapshot = todos;
128
+ startTransition(async () => {
129
+ const res = await fetch("/api/fn/reorderTodo", {
130
+ method: "POST",
131
+ headers: { "Content-Type": "application/json" },
132
+ body: JSON.stringify({ id: movedId, position }),
133
+ });
134
+ if (!res.ok) setTodos(snapshot);
135
+ });
136
+ }
137
+
138
+ return (
139
+ <div className="space-y-4">
140
+ <form
141
+ onSubmit={(e) => {
142
+ e.preventDefault();
143
+ add();
144
+ }}
145
+ className="flex gap-2"
146
+ >
147
+ <Input
148
+ value={title}
149
+ onChange={(e) => setTitle(e.target.value)}
150
+ placeholder="What needs doing?"
151
+ disabled={pending}
152
+ className="flex-1"
153
+ />
154
+ <Button
155
+ type="submit"
156
+ variant="primary"
157
+ disabled={pending || !title.trim()}
158
+ >
159
+ Add
160
+ </Button>
161
+ </form>
162
+
163
+ {todos.length === 0 ? (
164
+ <p className="text-sm text-neutral-500 dark:text-neutral-400 text-center py-8">
165
+ No todos yet. Add one above.
166
+ </p>
167
+ ) : (
168
+ <DndContext
169
+ sensors={sensors}
170
+ collisionDetection={closestCenter}
171
+ onDragEnd={onDragEnd}
172
+ >
173
+ <SortableContext
174
+ items={todos.map((t) => t.id)}
175
+ strategy={verticalListSortingStrategy}
176
+ >
177
+ <ul className="divide-y divide-neutral-200 dark:divide-neutral-800 rounded-md border border-neutral-200 dark:border-neutral-800">
178
+ {todos.map((t) => (
179
+ <SortableRow
180
+ key={t.id}
181
+ todo={t}
182
+ pending={pending}
183
+ onToggle={() => toggle(t)}
184
+ onRemove={() => remove(t)}
185
+ onRename={(next) => rename(t, next)}
186
+ />
187
+ ))}
188
+ </ul>
189
+ </SortableContext>
190
+ </DndContext>
191
+ )}
192
+ </div>
193
+ );
194
+ }
195
+
196
+ function SortableRow({
197
+ todo,
198
+ pending,
199
+ onToggle,
200
+ onRemove,
201
+ onRename,
202
+ }: {
203
+ todo: Todo;
204
+ pending: boolean;
205
+ onToggle: () => void;
206
+ onRemove: () => void;
207
+ onRename: (next: string) => void;
208
+ }) {
209
+ const {
210
+ attributes,
211
+ listeners,
212
+ setNodeRef,
213
+ transform,
214
+ transition,
215
+ isDragging,
216
+ } = useSortable({ id: todo.id });
217
+ const style = {
218
+ transform: CSS.Transform.toString(transform),
219
+ transition,
220
+ opacity: isDragging ? 0.4 : 1,
221
+ };
222
+ const [editing, setEditing] = useState(false);
223
+ const [draft, setDraft] = useState(todo.title);
224
+ const inputRef = useRef<HTMLInputElement>(null);
225
+
226
+ useEffect(() => {
227
+ if (editing) {
228
+ setDraft(todo.title);
229
+ requestAnimationFrame(() => {
230
+ inputRef.current?.focus();
231
+ inputRef.current?.select();
232
+ });
233
+ }
234
+ }, [editing, todo.title]);
235
+
236
+ function commit() {
237
+ setEditing(false);
238
+ onRename(draft);
239
+ }
240
+
241
+ return (
242
+ <li
243
+ ref={setNodeRef}
244
+ style={style}
245
+ className="flex items-center gap-3 px-4 py-3 text-sm group bg-white dark:bg-neutral-950"
246
+ >
247
+ <button
248
+ type="button"
249
+ {...attributes}
250
+ {...listeners}
251
+ className="cursor-grab active:cursor-grabbing text-neutral-300 hover:text-neutral-500 select-none touch-none"
252
+ aria-label="Drag to reorder"
253
+ tabIndex={-1}
254
+ >
255
+ ⋮⋮
256
+ </button>
257
+ <input
258
+ type="checkbox"
259
+ checked={todo.done}
260
+ onChange={onToggle}
261
+ disabled={pending}
262
+ className="size-4 cursor-pointer"
263
+ aria-label={`Mark "${todo.title}" as ${todo.done ? "not done" : "done"}`}
264
+ />
265
+ {editing ? (
266
+ <input
267
+ ref={inputRef}
268
+ value={draft}
269
+ onChange={(e) => setDraft(e.target.value)}
270
+ onBlur={commit}
271
+ onKeyDown={(e) => {
272
+ if (e.key === "Enter") commit();
273
+ else if (e.key === "Escape") {
274
+ setEditing(false);
275
+ setDraft(todo.title);
276
+ }
277
+ }}
278
+ className="flex-1 bg-transparent border-b border-neutral-300 dark:border-neutral-700 outline-none text-sm"
279
+ aria-label="Edit title"
280
+ />
281
+ ) : (
282
+ <button
283
+ type="button"
284
+ onDoubleClick={() => setEditing(true)}
285
+ className={`flex-1 text-left ${todo.done ? "line-through text-neutral-400" : ""}`}
286
+ title="Double-click to edit"
287
+ >
288
+ {todo.title}
289
+ </button>
290
+ )}
291
+ <button
292
+ type="button"
293
+ onClick={() => setEditing(true)}
294
+ className="opacity-0 group-hover:opacity-100 transition-opacity text-xs text-neutral-500 hover:text-neutral-800 dark:hover:text-neutral-200"
295
+ aria-label={`Edit "${todo.title}"`}
296
+ >
297
+ Edit
298
+ </button>
299
+ <button
300
+ type="button"
301
+ onClick={onRemove}
302
+ disabled={pending}
303
+ className="opacity-0 group-hover:opacity-100 transition-opacity text-xs text-neutral-500 hover:text-red-500"
304
+ aria-label={`Delete "${todo.title}"`}
305
+ >
306
+ Delete
307
+ </button>
308
+ </li>
309
+ );
310
+ }
@@ -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: "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
+ }
@@ -0,0 +1,36 @@
1
+ import { pylon } from "@/lib/pylon";
2
+ import { TodoList } from "./components/TodoList";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ type Todo = {
7
+ id: string;
8
+ title: string;
9
+ done: boolean;
10
+ createdAt: string;
11
+ position?: number;
12
+ };
13
+
14
+ export default async function HomePage() {
15
+ const todos = await pylon
16
+ .json<Todo[]>("/api/fn/listTodos", {
17
+ method: "POST",
18
+ body: "{}",
19
+ headers: { "Content-Type": "application/json" },
20
+ })
21
+ .catch(() => [] as Todo[]);
22
+
23
+ return (
24
+ <main className="mx-auto max-w-2xl 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
+ Drag rows to reorder, double-click a title to edit, hover for
29
+ delete. All mutations are optimistic with revert-on-failure.
30
+ </p>
31
+ </header>
32
+
33
+ <TodoList initialTodos={todos} />
34
+ </main>
35
+ );
36
+ }
@@ -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
+ }