@topogram/template-todo 0.1.30

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 (85) hide show
  1. package/README.md +91 -0
  2. package/implementation/README.md +9 -0
  3. package/implementation/backend/reference.js +206 -0
  4. package/implementation/backend/repository-reference.js +74 -0
  5. package/implementation/backend/repository-renderers.js +442 -0
  6. package/implementation/index.js +53 -0
  7. package/implementation/runtime/check-renderers.js +215 -0
  8. package/implementation/runtime/checks.js +120 -0
  9. package/implementation/runtime/reference.js +92 -0
  10. package/implementation/web/reference.js +51 -0
  11. package/implementation/web/renderers.js +1223 -0
  12. package/implementation/web/screens-reference.js +15 -0
  13. package/package.json +31 -0
  14. package/topogram/actors/actor-user.tg +6 -0
  15. package/topogram/capabilities/cap-complete-task.tg +12 -0
  16. package/topogram/capabilities/cap-create-project.tg +11 -0
  17. package/topogram/capabilities/cap-create-task.tg +14 -0
  18. package/topogram/capabilities/cap-create-user.tg +11 -0
  19. package/topogram/capabilities/cap-delete-task.tg +12 -0
  20. package/topogram/capabilities/cap-download-task-export.tg +10 -0
  21. package/topogram/capabilities/cap-export-tasks.tg +11 -0
  22. package/topogram/capabilities/cap-get-project.tg +11 -0
  23. package/topogram/capabilities/cap-get-task-export-job.tg +11 -0
  24. package/topogram/capabilities/cap-get-task.tg +11 -0
  25. package/topogram/capabilities/cap-get-user.tg +11 -0
  26. package/topogram/capabilities/cap-list-projects.tg +11 -0
  27. package/topogram/capabilities/cap-list-tasks.tg +11 -0
  28. package/topogram/capabilities/cap-list-users.tg +11 -0
  29. package/topogram/capabilities/cap-update-project.tg +12 -0
  30. package/topogram/capabilities/cap-update-task.tg +12 -0
  31. package/topogram/capabilities/cap-update-user.tg +12 -0
  32. package/topogram/components/component-ui-task-board.tg +33 -0
  33. package/topogram/components/component-ui-task-calendar.tg +30 -0
  34. package/topogram/components/component-ui-task-summary.tg +23 -0
  35. package/topogram/components/component-ui-task-table.tg +34 -0
  36. package/topogram/decisions/decision-task-ownership.tg +9 -0
  37. package/topogram/docs/glossary/user.md +22 -0
  38. package/topogram/docs/journeys/task-creation-and-ownership.md +57 -0
  39. package/topogram/entities/entity-project.tg +28 -0
  40. package/topogram/entities/entity-task.tg +38 -0
  41. package/topogram/entities/entity-user.tg +24 -0
  42. package/topogram/enums/enum-export-job-status.tg +3 -0
  43. package/topogram/enums/enum-project-status.tg +3 -0
  44. package/topogram/enums/enum-task-priority.tg +3 -0
  45. package/topogram/enums/enum-task-status.tg +3 -0
  46. package/topogram/operations/operation-task-creation-monitoring.tg +10 -0
  47. package/topogram/projections/proj-api.tg +177 -0
  48. package/topogram/projections/proj-db-postgres.tg +55 -0
  49. package/topogram/projections/proj-db-sqlite.tg +47 -0
  50. package/topogram/projections/proj-ui-shared.tg +133 -0
  51. package/topogram/projections/proj-ui-web-react.tg +92 -0
  52. package/topogram/projections/proj-ui-web.tg +92 -0
  53. package/topogram/rules/rule-no-task-creation-in-archived-project.tg +10 -0
  54. package/topogram/rules/rule-only-active-users-may-own-tasks.tg +10 -0
  55. package/topogram/shapes/shape-input-complete-task.tg +11 -0
  56. package/topogram/shapes/shape-input-create-project.tg +6 -0
  57. package/topogram/shapes/shape-input-create-task.tg +6 -0
  58. package/topogram/shapes/shape-input-create-user.tg +6 -0
  59. package/topogram/shapes/shape-input-delete-task.tg +10 -0
  60. package/topogram/shapes/shape-input-export-tasks.tg +13 -0
  61. package/topogram/shapes/shape-input-get-project.tg +10 -0
  62. package/topogram/shapes/shape-input-get-task-export-job.tg +10 -0
  63. package/topogram/shapes/shape-input-get-task.tg +10 -0
  64. package/topogram/shapes/shape-input-get-user.tg +10 -0
  65. package/topogram/shapes/shape-input-list-projects.tg +11 -0
  66. package/topogram/shapes/shape-input-list-tasks.tg +14 -0
  67. package/topogram/shapes/shape-input-list-users.tg +11 -0
  68. package/topogram/shapes/shape-input-update-project.tg +14 -0
  69. package/topogram/shapes/shape-input-update-task.tg +16 -0
  70. package/topogram/shapes/shape-input-update-user.tg +13 -0
  71. package/topogram/shapes/shape-output-project-card.tg +6 -0
  72. package/topogram/shapes/shape-output-project-detail.tg +6 -0
  73. package/topogram/shapes/shape-output-task-card.tg +19 -0
  74. package/topogram/shapes/shape-output-task-detail.tg +6 -0
  75. package/topogram/shapes/shape-output-task-export-callback.tg +14 -0
  76. package/topogram/shapes/shape-output-task-export-job.tg +13 -0
  77. package/topogram/shapes/shape-output-task-export-status.tg +17 -0
  78. package/topogram/shapes/shape-output-user-card.tg +6 -0
  79. package/topogram/shapes/shape-output-user-detail.tg +6 -0
  80. package/topogram/terms/term-user.tg +5 -0
  81. package/topogram/verifications/verification-create-task-policy.tg +15 -0
  82. package/topogram/verifications/verification-runtime-smoke.tg +16 -0
  83. package/topogram/verifications/verification-task-runtime-flow.tg +31 -0
  84. package/topogram-template.json +11 -0
  85. package/topogram.project.json +53 -0
@@ -0,0 +1,1223 @@
1
+ import { renderSvelteKitRedirectingAction } from "@topogram/cli/src/generator/surfaces/web/sveltekit-actions.js";
2
+ import {
3
+ renderSvelteKitComponentRegion
4
+ } from "@topogram/cli/template-helpers/sveltekit.js";
5
+ import { TODO_WEB_SCREEN_REFERENCE } from "./screens-reference.js";
6
+
7
+ export function renderTodoHomePage({
8
+ useTypescript,
9
+ demoPrimaryEnvVar,
10
+ screens,
11
+ projectionName,
12
+ homeDescription,
13
+ webReference
14
+ }) {
15
+ return `<script${useTypescript ? ' lang="ts"' : ""}>
16
+ import { ${demoPrimaryEnvVar} as DEMO_TASK_ID } from "$env/static/public";
17
+
18
+ const screens = ${JSON.stringify(screens, null, 2)};
19
+ const demoTaskRoute = DEMO_TASK_ID ? \`/tasks/\${DEMO_TASK_ID}\` : null;
20
+ </script>
21
+
22
+ <main>
23
+ <div class="stack">
24
+ <section class="card hero">
25
+ <div>
26
+ <h1>${projectionName}</h1>
27
+ <p>${homeDescription}</p>
28
+ </div>
29
+ <div class="button-row">
30
+ <a class="button-link" href="${webReference.nav.browseRoute}">${webReference.nav.browseLabel}</a>
31
+ <a class="button-link secondary" href="${webReference.nav.createRoute}">${webReference.nav.createLabel}</a>
32
+ {#if demoTaskRoute}
33
+ <a class="button-link secondary" href={demoTaskRoute}>${webReference.home.demoTaskLabel}</a>
34
+ {/if}
35
+ </div>
36
+ </section>
37
+
38
+ <section class="grid two">
39
+ {#each screens as screen}
40
+ <article class="card">
41
+ <h2>{screen.title}</h2>
42
+ {#if screen.navigable}
43
+ <p><a href={screen.route}>Open screen</a></p>
44
+ {:else if screen.route}
45
+ <p class="muted">${webReference.home.dynamicRouteText}</p>
46
+ <small class="route-hint">{screen.route}</small>
47
+ {:else}
48
+ <p class="muted">${webReference.home.noRouteText}</p>
49
+ {/if}
50
+ </article>
51
+ {/each}
52
+ </section>
53
+ </div>
54
+ </main>
55
+ `;
56
+ }
57
+
58
+ export function renderTodoTaskRoutes({
59
+ useTypescript,
60
+ contract,
61
+ taskList,
62
+ taskDetail,
63
+ taskCreate,
64
+ taskEdit,
65
+ taskExports,
66
+ taskListLookups,
67
+ taskCreateLookups,
68
+ taskEditLookups,
69
+ projectEnvVar,
70
+ ownerEnvVar,
71
+ webReference,
72
+ prettyScreenKind
73
+ }) {
74
+ const files = {};
75
+ const editTaskVisibility = taskDetail.visibility?.find((entry) => entry.capability?.id === "cap_update_task") || null;
76
+ const completeTaskVisibility = taskDetail.visibility?.find((entry) => entry.capability?.id === "cap_complete_task") || null;
77
+ const deleteTaskVisibility = taskDetail.visibility?.find((entry) => entry.capability?.id === "cap_delete_task") || null;
78
+ const taskListHeroComponents = renderSvelteKitComponentRegion(taskList, "hero", {
79
+ componentContracts: contract.components,
80
+ itemsExpression: "data.result.items",
81
+ useTypescript
82
+ });
83
+ const taskListResultsComponents = renderSvelteKitComponentRegion(taskList, "results", {
84
+ componentContracts: contract.components,
85
+ itemsExpression: "data.result.items",
86
+ useTypescript
87
+ });
88
+ const taskListDefaultResults = `<ul class="task-list">
89
+ {#each data.result.items as task}
90
+ <li>
91
+ <div class="task-meta">
92
+ <a href={'/tasks/' + task.id}><strong>{task.title}</strong></a>
93
+ {#if task.description}<span class="muted">{task.description}</span>{/if}
94
+ <span class="muted">Priority: {task.priority ?? "medium"}</span>
95
+ </div>
96
+ <div class="button-row">
97
+ <span class="badge">{task.priority ?? "medium"}</span>
98
+ <span class="badge">{task.status}</span>
99
+ </div>
100
+ </li>
101
+ {/each}
102
+ </ul>`;
103
+
104
+ files["tasks/+page.server.ts"] = `import { redirect, fail } from "@sveltejs/kit";
105
+ import type { Actions } from "./$types";
106
+ import { exportTasks } from "$lib/api/client";
107
+
108
+ export const actions: Actions = {
109
+ ${renderSvelteKitRedirectingAction({
110
+ actionName: "export",
111
+ signature: "{ request, fetch }",
112
+ prelude: `const form = await request.formData();
113
+ const payload = {
114
+ project_id: String(form.get("project_id") || "") || undefined,
115
+ owner_id: String(form.get("owner_id") || "") || undefined,
116
+ status: String(form.get("status") || "") || undefined
117
+ };
118
+
119
+ let job;`,
120
+ tryStatement: "job = await exportTasks(fetch, payload);",
121
+ catchReturn:
122
+ 'return fail(400, { exportError: error instanceof Error ? error.message : "Unable to start export", exportValues: payload });',
123
+ successStatement: "throw redirect(303, `/task-exports/${job.job_id}`);"
124
+ })}
125
+ };
126
+ `;
127
+
128
+ files["tasks/+page.ts"] = `import type { PageLoad } from "./$types";
129
+ import { listTasks } from "$lib/api/client";
130
+ import { listLookupOptions } from "$lib/api/lookups";
131
+
132
+ export const load: PageLoad = async ({ fetch, url }) => {
133
+ const limit = url.searchParams.get("limit");
134
+ const [result, projectOptions, ownerOptions] = await Promise.all([
135
+ listTasks(fetch, {
136
+ project_id: url.searchParams.get("project_id") ?? undefined,
137
+ owner_id: url.searchParams.get("owner_id") ?? undefined,
138
+ status: url.searchParams.get("status") ?? undefined,
139
+ after: url.searchParams.get("after") ?? undefined,
140
+ limit: limit ? Number(limit) : undefined
141
+ }),
142
+ ${taskListLookups.project_id?.route ? `listLookupOptions(fetch, "${taskListLookups.project_id.route}")` : "Promise.resolve([])"},
143
+ ${taskListLookups.owner_id?.route ? `listLookupOptions(fetch, "${taskListLookups.owner_id.route}")` : "Promise.resolve([])"}
144
+ ]);
145
+ return {
146
+ screen: ${JSON.stringify({ id: taskList.id, title: taskList.title, collection: taskList.collection, web: taskList.web }, null, 2)},
147
+ filters: {
148
+ project_id: url.searchParams.get("project_id") ?? "",
149
+ owner_id: url.searchParams.get("owner_id") ?? "",
150
+ status: url.searchParams.get("status") ?? "",
151
+ limit: limit ?? ""
152
+ },
153
+ lookups: {
154
+ project_id: projectOptions,
155
+ owner_id: ownerOptions
156
+ },
157
+ result
158
+ };
159
+ };
160
+ `;
161
+
162
+ files["tasks/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
163
+ export let data;
164
+ export let form;
165
+
166
+ const buildNextHref = () => {
167
+ if (!data.result.next_cursor) return null;
168
+ const params = new URLSearchParams();
169
+ if (data.filters.project_id) params.set("project_id", data.filters.project_id);
170
+ if (data.filters.owner_id) params.set("owner_id", data.filters.owner_id);
171
+ if (data.filters.status) params.set("status", data.filters.status);
172
+ if (data.filters.limit) params.set("limit", String(data.filters.limit));
173
+ params.set("after", data.result.next_cursor);
174
+ return \`/tasks?\${params.toString()}\`;
175
+ };
176
+
177
+ const nextHref = buildNextHref();
178
+ </script>
179
+
180
+ <main>
181
+ <div class="stack">
182
+ <section class="card">
183
+ <div class="button-row" style="justify-content: space-between;">
184
+ <div>
185
+ <h1>${taskList.title || taskList.id}</h1>
186
+ <p>This ${prettyScreenKind(taskList.kind)} screen was generated from \`${taskList.id}\`.</p>
187
+ </div>
188
+ <a class="button-link" href="/tasks/new">Create Task</a>
189
+ </div>
190
+ ${taskListHeroComponents ? `\n ${taskListHeroComponents}\n` : ""}
191
+
192
+ <form class="filters" method="GET">
193
+ <label>
194
+ Project
195
+ <select name="project_id">
196
+ <option value="">${taskListLookups.project_id?.emptyLabel || "All projects"}</option>
197
+ {#each data.lookups.project_id as option}
198
+ <option value={option.value} selected={option.value === (data.filters.project_id ?? "")}>{option.label}</option>
199
+ {/each}
200
+ </select>
201
+ </label>
202
+ <label>
203
+ Owner
204
+ <select name="owner_id">
205
+ <option value="">${taskListLookups.owner_id?.emptyLabel || "All owners"}</option>
206
+ {#each data.lookups.owner_id as option}
207
+ <option value={option.value} selected={option.value === (data.filters.owner_id ?? "")}>{option.label}</option>
208
+ {/each}
209
+ </select>
210
+ </label>
211
+ <label>
212
+ Status
213
+ <input name="status" value={data.filters.status ?? ""} />
214
+ </label>
215
+ <label>
216
+ Limit
217
+ <input name="limit" type="number" min="1" value={data.filters.limit ?? ""} />
218
+ </label>
219
+ <div class="button-row">
220
+ <button type="submit">Apply Filters</button>
221
+ <a class="button-link secondary" href="/tasks">Reset</a>
222
+ </div>
223
+ </form>
224
+
225
+ <form method="POST" action="?/export">
226
+ <input type="hidden" name="project_id" value={data.filters.project_id ?? ""} />
227
+ <input type="hidden" name="owner_id" value={data.filters.owner_id ?? ""} />
228
+ <input type="hidden" name="status" value={data.filters.status ?? ""} />
229
+ <div class="button-row">
230
+ <button type="submit">Start Export</button>
231
+ {#if form?.exportError}<span class="muted">{form.exportError}</span>{/if}
232
+ </div>
233
+ </form>
234
+
235
+ {#if data.result.items.length === 0}
236
+ <div class="empty-state">
237
+ <p><strong>${taskList.emptyState?.title || "No items"}</strong></p>
238
+ <p class="muted">${taskList.emptyState?.body || ""}</p>
239
+ </div>
240
+ {:else}
241
+ <p class="muted">Showing {data.result.items.length} task{data.result.items.length === 1 ? "" : "s"}.</p>
242
+ ${taskListResultsComponents || taskListDefaultResults}
243
+ {#if nextHref}
244
+ <p><a class="button-link secondary" href={nextHref}>Next Page</a></p>
245
+ {/if}
246
+ {/if}
247
+ </section>
248
+ </div>
249
+ </main>
250
+ `;
251
+
252
+ files["tasks/[id]/+page.server.ts"] = `import { randomUUID } from "node:crypto";
253
+ import { redirect, fail } from "@sveltejs/kit";
254
+ import type { Actions } from "./$types";
255
+ import { completeTask, deleteTask } from "$lib/api/client";
256
+
257
+ export const actions: Actions = {
258
+ ${renderSvelteKitRedirectingAction({
259
+ actionName: "complete",
260
+ signature: "{ request, fetch, params }",
261
+ prelude: `const form = await request.formData();
262
+ const updated_at = String(form.get("updated_at") || "");
263
+ const completed_at = String(form.get("completed_at") || "") || new Date().toISOString();
264
+ if (!updated_at) {
265
+ return fail(400, { actionError: "updated_at is required to complete this task." });
266
+ }`,
267
+ tryStatement: `await completeTask(fetch, params.id, { completed_at }, {
268
+ headers: {
269
+ "If-Match": updated_at,
270
+ "Idempotency-Key": randomUUID()
271
+ }
272
+ });`,
273
+ catchReturn: 'return fail(400, { actionError: error instanceof Error ? error.message : "Unable to complete task" });',
274
+ successStatement: "throw redirect(303, `/tasks/${params.id}`);"
275
+ })},
276
+ ${renderSvelteKitRedirectingAction({
277
+ actionName: "delete",
278
+ signature: "{ request, fetch, params }",
279
+ prelude: `const form = await request.formData();
280
+ const updated_at = String(form.get("updated_at") || "");
281
+ if (!updated_at) {
282
+ return fail(400, { actionError: "updated_at is required to delete this task." });
283
+ }`,
284
+ tryStatement: `await deleteTask(fetch, params.id, {
285
+ headers: {
286
+ "If-Match": updated_at
287
+ }
288
+ });`,
289
+ catchReturn: 'return fail(400, { actionError: error instanceof Error ? error.message : "Unable to delete task" });',
290
+ successStatement: 'throw redirect(303, "/tasks");'
291
+ })}
292
+ };
293
+ `;
294
+
295
+ files["tasks/[id]/+page.ts"] = `import type { PageLoad } from "./$types";
296
+ import { getTask } from "$lib/api/client";
297
+
298
+ export const load: PageLoad = async ({ fetch, params, url }) => {
299
+ return {
300
+ screen: ${JSON.stringify({ id: taskDetail.id, title: taskDetail.title, web: taskDetail.web }, null, 2)},
301
+ task: await getTask(fetch, params.id),
302
+ visibilityDebug: {
303
+ userId: url.searchParams.get("topogram_auth_user_id") ?? "",
304
+ permissions: url.searchParams.get("topogram_auth_permissions") ?? "",
305
+ isAdmin: url.searchParams.get("topogram_auth_admin") ?? ""
306
+ }
307
+ };
308
+ };
309
+ `;
310
+
311
+ files["tasks/[id]/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
312
+ import { canShowAction } from "$lib/auth/visibility";
313
+
314
+ export let data;
315
+ export let form;
316
+
317
+ const editTaskVisibility = ${JSON.stringify(editTaskVisibility, null, 2)};
318
+ const completeTaskVisibility = ${JSON.stringify(completeTaskVisibility, null, 2)};
319
+ const deleteTaskVisibility = ${JSON.stringify(deleteTaskVisibility, null, 2)};
320
+
321
+ $: canEditTask = canShowAction(editTaskVisibility, data?.task, data?.visibilityDebug);
322
+ $: canCompleteTask = canShowAction(completeTaskVisibility, data?.task, data?.visibilityDebug);
323
+ $: canDeleteTask = canShowAction(deleteTaskVisibility, data?.task, data?.visibilityDebug);
324
+ </script>
325
+
326
+ <main>
327
+ <div class="stack">
328
+ <section class="card">
329
+ <div class="button-row" style="justify-content: space-between;">
330
+ <div>
331
+ <h1>{data.task.title}</h1>
332
+ <p>This ${prettyScreenKind(taskDetail.kind)} screen was generated from \`${taskDetail.id}\`.</p>
333
+ </div>
334
+ <span class="badge">{data.task.status}</span>
335
+ </div>
336
+
337
+ {#if data.task.description}
338
+ <p>{data.task.description}</p>
339
+ {:else}
340
+ <p class="muted">No description was provided for this task.</p>
341
+ {/if}
342
+
343
+ {#if form?.actionError}
344
+ <p><strong>{form.actionError}</strong></p>
345
+ {/if}
346
+
347
+ <dl class="definition-list">
348
+ <dt>Task ID</dt><dd>{data.task.id}</dd>
349
+ <dt>Project</dt><dd>{data.task.project_id}</dd>
350
+ <dt>Owner</dt><dd>{data.task.owner_id ?? "Unassigned"}</dd>
351
+ <dt>Priority</dt><dd>{data.task.priority ?? "medium"}</dd>
352
+ <dt>Created</dt><dd>{data.task.created_at}</dd>
353
+ <dt>Updated</dt><dd>{data.task.updated_at}</dd>
354
+ </dl>
355
+
356
+ <div class="button-row">
357
+ <a class="button-link secondary" href="/tasks">Back to Tasks</a>
358
+ {#if canEditTask}
359
+ <a class="button-link" href={"/tasks/" + data.task.id + "/edit"}>Edit Task</a>
360
+ {/if}
361
+ </div>
362
+
363
+ <div class="button-row">
364
+ {#if canCompleteTask}
365
+ <form method="POST" action="?/complete">
366
+ <input type="hidden" name="updated_at" value={data.task.updated_at} />
367
+ <button type="submit">Complete Task</button>
368
+ </form>
369
+ {/if}
370
+ {#if canDeleteTask}
371
+ <form method="POST" action="?/delete">
372
+ <input type="hidden" name="updated_at" value={data.task.updated_at} />
373
+ <button type="submit">Archive Task</button>
374
+ </form>
375
+ {/if}
376
+ </div>
377
+ </section>
378
+ </div>
379
+ </main>
380
+ `;
381
+
382
+ files["tasks/new/+page.ts"] = `import type { PageLoad } from "./$types";
383
+ import { listLookupOptions } from "$lib/api/lookups";
384
+
385
+ export const load: PageLoad = async ({ fetch }) => {
386
+ const [projectOptions, ownerOptions] = await Promise.all([
387
+ ${taskCreateLookups.project_id?.route ? `listLookupOptions(fetch, "${taskCreateLookups.project_id.route}")` : "Promise.resolve([])"},
388
+ ${taskCreateLookups.owner_id?.route ? `listLookupOptions(fetch, "${taskCreateLookups.owner_id.route}")` : "Promise.resolve([])"}
389
+ ]);
390
+
391
+ return {
392
+ lookups: {
393
+ project_id: projectOptions,
394
+ owner_id: ownerOptions
395
+ }
396
+ };
397
+ };
398
+ `;
399
+
400
+ files["tasks/new/+page.server.ts"] = `import { randomUUID } from "node:crypto";
401
+ import { redirect, fail } from "@sveltejs/kit";
402
+ import type { Actions } from "./$types";
403
+ import { createTask } from "$lib/api/client";
404
+
405
+ export const actions: Actions = {
406
+ ${renderSvelteKitRedirectingAction({
407
+ actionName: "default",
408
+ signature: "{ request, fetch }",
409
+ prelude: `const form = await request.formData();
410
+ const payload = {
411
+ title: String(form.get("title") || ""),
412
+ description: String(form.get("description") || "") || undefined,
413
+ priority: String(form.get("priority") || "") || undefined,
414
+ owner_id: String(form.get("owner_id") || "") || undefined,
415
+ project_id: String(form.get("project_id") || ""),
416
+ due_at: String(form.get("due_at") || "") || undefined
417
+ };
418
+
419
+ if (!payload.title || !payload.project_id) {
420
+ return fail(400, { error: "Title and project are required.", values: payload });
421
+ }
422
+
423
+ let created;`,
424
+ tryStatement: `created = await createTask(fetch, payload, {
425
+ headers: {
426
+ "Idempotency-Key": randomUUID()
427
+ }
428
+ });`,
429
+ catchReturn:
430
+ 'return fail(400, { error: error instanceof Error ? error.message : "Unable to create task", values: payload });',
431
+ successStatement: "throw redirect(303, `/tasks/${created.id}`);"
432
+ })}
433
+ };
434
+ `;
435
+
436
+ files["tasks/new/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
437
+ import { ${projectEnvVar} as DEMO_PROJECT_ID, ${ownerEnvVar} as DEMO_USER_ID } from "$env/static/public";
438
+
439
+ export let data;
440
+ export let form;
441
+
442
+ const values = {
443
+ title: form?.values?.title ?? "",
444
+ description: form?.values?.description ?? "",
445
+ priority: form?.values?.priority ?? "medium",
446
+ owner_id: form?.values?.owner_id ?? DEMO_USER_ID ?? "",
447
+ project_id: form?.values?.project_id ?? DEMO_PROJECT_ID ?? "",
448
+ due_at: form?.values?.due_at ?? ""
449
+ };
450
+ </script>
451
+
452
+ <main>
453
+ <div class="stack">
454
+ <section class="card">
455
+ <h1>${taskCreate.title || taskCreate.id}</h1>
456
+ <p>This ${prettyScreenKind(taskCreate.kind)} screen was generated from \`${taskCreate.id}\`.</p>
457
+ <p class="muted">${webReference.createPrimary.helperText}</p>
458
+ {#if form?.error}<p><strong>{form.error}</strong></p>{/if}
459
+ <form class="stack" method="POST">
460
+ <label>Title <input name="title" required value={values.title} /></label>
461
+ <label>Description <textarea name="description">{values.description}</textarea></label>
462
+ <label>
463
+ Priority
464
+ <select name="priority">
465
+ <option value="low" selected={values.priority === "low"}>low</option>
466
+ <option value="medium" selected={values.priority === "medium"}>medium</option>
467
+ <option value="high" selected={values.priority === "high"}>high</option>
468
+ </select>
469
+ </label>
470
+ <label>
471
+ Owner
472
+ <select name="owner_id">
473
+ <option value="">${taskCreateLookups.owner_id?.emptyLabel || "Unassigned"}</option>
474
+ {#each data.lookups.owner_id as option}
475
+ <option value={option.value} selected={option.value === values.owner_id}>{option.label}</option>
476
+ {/each}
477
+ </select>
478
+ </label>
479
+ <label>
480
+ Project
481
+ <select name="project_id" required>
482
+ <option value="">${webReference.createPrimary.projectPlaceholder}</option>
483
+ {#each data.lookups.project_id as option}
484
+ <option value={option.value} selected={option.value === values.project_id}>{option.label}</option>
485
+ {/each}
486
+ </select>
487
+ </label>
488
+ <label>Due At <input name="due_at" type="datetime-local" value={values.due_at} /></label>
489
+ <div class="button-row">
490
+ <button type="submit">${webReference.createPrimary.submitLabel}</button>
491
+ <a class="button-link secondary" href="/tasks">${webReference.createPrimary.cancelLabel}</a>
492
+ </div>
493
+ </form>
494
+ </section>
495
+ </div>
496
+ </main>
497
+ `;
498
+
499
+ files["tasks/[id]/edit/+page.ts"] = `import type { PageLoad } from "./$types";
500
+ import { getTask } from "$lib/api/client";
501
+ import { listLookupOptions } from "$lib/api/lookups";
502
+
503
+ export const load: PageLoad = async ({ fetch, params }) => {
504
+ const [task, ownerOptions] = await Promise.all([
505
+ getTask(fetch, params.id),
506
+ ${taskEditLookups.owner_id?.route ? `listLookupOptions(fetch, "${taskEditLookups.owner_id.route}")` : "Promise.resolve([])"}
507
+ ]);
508
+ return {
509
+ screen: ${JSON.stringify({ id: taskEdit.id, title: taskEdit.title, web: taskEdit.web }, null, 2)},
510
+ task,
511
+ lookups: {
512
+ owner_id: ownerOptions
513
+ },
514
+ values: {
515
+ title: task.title ?? "",
516
+ description: task.description ?? "",
517
+ priority: task.priority ?? "medium",
518
+ owner_id: task.owner_id ?? "",
519
+ due_at: task.due_at ? String(task.due_at).slice(0, 16) : "",
520
+ status: task.status ?? ""
521
+ }
522
+ };
523
+ };
524
+ `;
525
+
526
+ files["tasks/[id]/edit/+page.server.ts"] = `import { redirect, fail } from "@sveltejs/kit";
527
+ import type { Actions } from "./$types";
528
+ import { updateTask } from "$lib/api/client";
529
+
530
+ export const actions: Actions = {
531
+ ${renderSvelteKitRedirectingAction({
532
+ actionName: "default",
533
+ signature: "{ request, fetch, params }",
534
+ prelude: `const form = await request.formData();
535
+ const updated_at = String(form.get("updated_at") || "");
536
+ const payload = {
537
+ title: String(form.get("title") || "") || undefined,
538
+ description: String(form.get("description") || "") || undefined,
539
+ priority: String(form.get("priority") || "") || undefined,
540
+ owner_id: String(form.get("owner_id") || "") || undefined,
541
+ due_at: String(form.get("due_at") || "") || undefined,
542
+ status: String(form.get("status") || "") || undefined
543
+ };
544
+
545
+ if (!updated_at) {
546
+ return fail(400, { error: "updated_at is required to update this task.", values: payload });
547
+ }`,
548
+ tryStatement: `await updateTask(fetch, params.id, payload, {
549
+ headers: {
550
+ "If-Match": updated_at
551
+ }
552
+ });`,
553
+ catchReturn:
554
+ 'return fail(400, { error: error instanceof Error ? error.message : "Unable to update task", values: payload });',
555
+ successStatement: "throw redirect(303, `/tasks/${params.id}`);"
556
+ })}
557
+ };
558
+ `;
559
+
560
+ files["tasks/[id]/edit/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
561
+ export let data;
562
+ export let form;
563
+
564
+ const values = form?.values ?? data.values;
565
+ </script>
566
+
567
+ <main>
568
+ <div class="stack">
569
+ <section class="card">
570
+ <h1>${taskEdit.title || "Edit Task"}</h1>
571
+ <p>Update the mutable fields for <strong>{data.task.title}</strong>.</p>
572
+ {#if form?.error}<p><strong>{form.error}</strong></p>{/if}
573
+ <form class="stack" method="POST">
574
+ <input type="hidden" name="updated_at" value={data.task.updated_at} />
575
+ <label>Title <input name="title" value={values.title ?? ""} /></label>
576
+ <label>Description <textarea name="description">{values.description ?? ""}</textarea></label>
577
+ <label>
578
+ Priority
579
+ <select name="priority">
580
+ <option value="low" selected={(values.priority ?? "") === "low"}>low</option>
581
+ <option value="medium" selected={(values.priority ?? "") === "medium"}>medium</option>
582
+ <option value="high" selected={(values.priority ?? "") === "high"}>high</option>
583
+ </select>
584
+ </label>
585
+ <label>
586
+ Owner
587
+ <select name="owner_id">
588
+ <option value="">${taskEditLookups.owner_id?.emptyLabel || "Unassigned"}</option>
589
+ {#each data.lookups.owner_id as option}
590
+ <option value={option.value} selected={option.value === (values.owner_id ?? "")}>{option.label}</option>
591
+ {/each}
592
+ </select>
593
+ </label>
594
+ <label>Due At <input name="due_at" type="datetime-local" value={values.due_at ?? ""} /></label>
595
+ <label>
596
+ Status
597
+ <select name="status">
598
+ <option value="">Keep current ({data.task.status})</option>
599
+ <option value="draft">draft</option>
600
+ <option value="active">active</option>
601
+ <option value="completed">completed</option>
602
+ <option value="archived">archived</option>
603
+ </select>
604
+ </label>
605
+ <div class="button-row">
606
+ <button type="submit">Save Changes</button>
607
+ <a class="button-link secondary" href={"/tasks/" + data.task.id}>Cancel</a>
608
+ </div>
609
+ </form>
610
+ </section>
611
+ </div>
612
+ </main>
613
+ `;
614
+
615
+ files["task-exports/[job_id]/+page.ts"] = `import type { PageLoad } from "./$types";
616
+ import { getTaskExportJob } from "$lib/api/client";
617
+
618
+ export const load: PageLoad = async ({ fetch, params }) => {
619
+ try {
620
+ return {
621
+ screen: ${JSON.stringify({ id: taskExports.id, title: taskExports.title, web: taskExports.web }, null, 2)},
622
+ job: await getTaskExportJob(fetch, params.job_id),
623
+ notFound: false
624
+ };
625
+ } catch (error) {
626
+ if ((error as { status?: number }).status === 404) {
627
+ return {
628
+ screen: ${JSON.stringify({ id: taskExports.id, title: taskExports.title, web: taskExports.web }, null, 2)},
629
+ job: null,
630
+ notFound: true
631
+ };
632
+ }
633
+ throw error;
634
+ }
635
+ };
636
+ `;
637
+
638
+ files["task-exports/[job_id]/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
639
+ export let data;
640
+ </script>
641
+
642
+ <main>
643
+ <div class="stack">
644
+ <section class="card">
645
+ <h1>${taskExports.title || taskExports.id}</h1>
646
+ <p>This ${prettyScreenKind(taskExports.kind)} screen was generated from \`${taskExports.id}\`.</p>
647
+ {#if data.notFound}
648
+ <p>No export job was found for this id yet.</p>
649
+ <p class="muted">Start an export from a future generated action or revisit this page with a valid job id.</p>
650
+ {:else}
651
+ <dl class="definition-list">
652
+ <dt>Status</dt><dd><span class="badge">{data.job.status}</span></dd>
653
+ <dt>Submitted</dt><dd>{data.job.submitted_at}</dd>
654
+ {#if data.job.completed_at}<dt>Completed</dt><dd>{data.job.completed_at}</dd>{/if}
655
+ {#if data.job.error_message}<dt>Error</dt><dd>{data.job.error_message}</dd>{/if}
656
+ </dl>
657
+ {#if data.job.download_url}
658
+ <p><a class="button-link" href={data.job.download_url}>Download Export</a></p>
659
+ {/if}
660
+ {/if}
661
+ </section>
662
+ </div>
663
+ </main>
664
+ `;
665
+
666
+ const projectList = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.projectListScreenId);
667
+ const projectDetail = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.projectDetailScreenId);
668
+ const projectCreate = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.projectCreateScreenId);
669
+ const projectEdit = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.projectEditScreenId);
670
+ const userList = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.userListScreenId);
671
+ const userDetail = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.userDetailScreenId);
672
+ const userCreate = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.userCreateScreenId);
673
+ const userEdit = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.userEditScreenId);
674
+
675
+ if (projectList && projectDetail && projectCreate && projectEdit) {
676
+ files["projects/+page.ts"] = `import type { PageLoad } from "./$types";
677
+ import { requestCapability } from "$lib/api/client";
678
+
679
+ export const load: PageLoad = async ({ fetch, url }) => {
680
+ const limit = url.searchParams.get("limit");
681
+ return {
682
+ screen: ${JSON.stringify({ id: projectList.id, title: projectList.title, collection: projectList.collection, web: projectList.web }, null, 2)},
683
+ filters: {
684
+ limit: limit ?? ""
685
+ },
686
+ result: await requestCapability(fetch, "cap_list_projects", {
687
+ after: url.searchParams.get("after") ?? undefined,
688
+ limit: limit ? Number(limit) : undefined
689
+ })
690
+ };
691
+ };
692
+ `;
693
+
694
+ files["projects/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
695
+ export let data;
696
+
697
+ const buildNextHref = () => {
698
+ if (!data.result.next_cursor) return null;
699
+ const params = new URLSearchParams();
700
+ if (data.filters.limit) params.set("limit", String(data.filters.limit));
701
+ params.set("after", data.result.next_cursor);
702
+ return \`/projects?\${params.toString()}\`;
703
+ };
704
+
705
+ const nextHref = buildNextHref();
706
+ </script>
707
+
708
+ <main>
709
+ <div class="stack">
710
+ <section class="card">
711
+ <div class="button-row" style="justify-content: space-between;">
712
+ <div>
713
+ <h1>${projectList.title || projectList.id}</h1>
714
+ <p>This ${prettyScreenKind(projectList.kind)} screen was generated from \`${projectList.id}\`.</p>
715
+ </div>
716
+ <a class="button-link" href="/projects/new">Create Project</a>
717
+ </div>
718
+
719
+ {#if data.result.items.length === 0}
720
+ <div class="empty-state">
721
+ <p><strong>${projectList.emptyState?.title || "No projects yet"}</strong></p>
722
+ <p class="muted">${projectList.emptyState?.body || ""}</p>
723
+ </div>
724
+ {:else}
725
+ <ul class="task-list resource-list">
726
+ {#each data.result.items as project}
727
+ <li>
728
+ <div class="task-meta resource-meta">
729
+ <a href={'/projects/' + project.id}><strong>{project.name}</strong></a>
730
+ {#if project.description}<span class="muted">{project.description}</span>{/if}
731
+ <span class="muted">Owner: {project.owner_id || "Unassigned"}</span>
732
+ </div>
733
+ <span class="badge">{project.status}</span>
734
+ </li>
735
+ {/each}
736
+ </ul>
737
+ {#if nextHref}
738
+ <p><a class="button-link secondary" href={nextHref}>Next Page</a></p>
739
+ {/if}
740
+ {/if}
741
+ </section>
742
+ </div>
743
+ </main>
744
+ `;
745
+
746
+ files["projects/[id]/+page.ts"] = `import type { PageLoad } from "./$types";
747
+ import { requestCapability } from "$lib/api/client";
748
+
749
+ export const load: PageLoad = async ({ fetch, params }) => {
750
+ return {
751
+ screen: ${JSON.stringify({ id: projectDetail.id, title: projectDetail.title, web: projectDetail.web }, null, 2)},
752
+ project: await requestCapability(fetch, "cap_get_project", { project_id: params.id })
753
+ };
754
+ };
755
+ `;
756
+
757
+ files["projects/[id]/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
758
+ export let data;
759
+ </script>
760
+
761
+ <main>
762
+ <div class="stack">
763
+ <section class="card">
764
+ <div class="button-row" style="justify-content: space-between;">
765
+ <div>
766
+ <h1>{data.project.name}</h1>
767
+ <p>This ${prettyScreenKind(projectDetail.kind)} screen was generated from \`${projectDetail.id}\`.</p>
768
+ </div>
769
+ <span class="badge">{data.project.status}</span>
770
+ </div>
771
+
772
+ {#if data.project.description}
773
+ <p>{data.project.description}</p>
774
+ {:else}
775
+ <p class="muted">No description was provided for this project.</p>
776
+ {/if}
777
+
778
+ <dl class="definition-list">
779
+ <dt>Project ID</dt><dd>{data.project.id}</dd>
780
+ <dt>Status</dt><dd>{data.project.status}</dd>
781
+ <dt>Owner</dt><dd>{data.project.owner_id || "Unassigned"}</dd>
782
+ <dt>Created</dt><dd>{data.project.created_at}</dd>
783
+ </dl>
784
+
785
+ <div class="button-row">
786
+ <a class="button-link secondary" href="/projects">Back to Projects</a>
787
+ <a class="button-link" href={"/projects/" + data.project.id + "/edit"}>Edit Project</a>
788
+ </div>
789
+ </section>
790
+ </div>
791
+ </main>
792
+ `;
793
+
794
+ files["projects/new/+page.ts"] = `import type { PageLoad } from "./$types";
795
+ import { listLookupOptions } from "$lib/api/lookups";
796
+
797
+ export const load: PageLoad = async ({ fetch }) => {
798
+ return {
799
+ lookups: {
800
+ owner_id: await listLookupOptions(fetch, "/lookups/users")
801
+ }
802
+ };
803
+ };
804
+ `;
805
+
806
+ files["projects/new/+page.server.ts"] = `import { redirect, fail } from "@sveltejs/kit";
807
+ import type { Actions } from "./$types";
808
+ import { requestCapability } from "$lib/api/client";
809
+
810
+ export const actions: Actions = {
811
+ ${renderSvelteKitRedirectingAction({
812
+ actionName: "default",
813
+ signature: "{ request, fetch }",
814
+ prelude: `const form = await request.formData();
815
+ const payload = {
816
+ name: String(form.get("name") || ""),
817
+ description: String(form.get("description") || "") || undefined,
818
+ status: String(form.get("status") || "") || "active",
819
+ owner_id: String(form.get("owner_id") || "") || undefined
820
+ };
821
+
822
+ if (!payload.name) {
823
+ return fail(400, { error: "Name is required.", values: payload });
824
+ }
825
+
826
+ let created;`,
827
+ tryStatement: `created = await requestCapability(fetch, "cap_create_project", payload);`,
828
+ catchReturn:
829
+ 'return fail(400, { error: error instanceof Error ? error.message : "Unable to create project", values: payload });',
830
+ successStatement: "throw redirect(303, `/projects/${created.id}`);"
831
+ })}
832
+ };
833
+ `;
834
+
835
+ files["projects/new/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
836
+ export let data;
837
+ export let form;
838
+
839
+ const values = {
840
+ name: form?.values?.name ?? "",
841
+ description: form?.values?.description ?? "",
842
+ status: form?.values?.status ?? "active",
843
+ owner_id: form?.values?.owner_id ?? ""
844
+ };
845
+ </script>
846
+
847
+ <main>
848
+ <div class="stack">
849
+ <section class="card">
850
+ <h1>${projectCreate.title || projectCreate.id}</h1>
851
+ <p>This ${prettyScreenKind(projectCreate.kind)} screen was generated from \`${projectCreate.id}\`.</p>
852
+ {#if form?.error}<p><strong>{form.error}</strong></p>{/if}
853
+ <form class="stack" method="POST">
854
+ <label>Name <input name="name" required value={values.name} /></label>
855
+ <label>Description <textarea name="description">{values.description}</textarea></label>
856
+ <label>
857
+ Status
858
+ <select name="status">
859
+ <option value="active" selected={values.status === "active"}>active</option>
860
+ <option value="archived" selected={values.status === "archived"}>archived</option>
861
+ </select>
862
+ </label>
863
+ <label>
864
+ Owner
865
+ <select name="owner_id">
866
+ <option value="">Unassigned</option>
867
+ {#each data.lookups.owner_id as option}
868
+ <option value={option.value} selected={option.value === (values.owner_id ?? "")}>{option.label}</option>
869
+ {/each}
870
+ </select>
871
+ </label>
872
+ <div class="button-row">
873
+ <button type="submit">Create Project</button>
874
+ <a class="button-link secondary" href="/projects">Cancel</a>
875
+ </div>
876
+ </form>
877
+ </section>
878
+ </div>
879
+ </main>
880
+ `;
881
+
882
+ files["projects/[id]/edit/+page.ts"] = `import type { PageLoad } from "./$types";
883
+ import { requestCapability } from "$lib/api/client";
884
+ import { listLookupOptions } from "$lib/api/lookups";
885
+
886
+ export const load: PageLoad = async ({ fetch, params }) => {
887
+ const [project, ownerOptions] = await Promise.all([
888
+ requestCapability(fetch, "cap_get_project", { project_id: params.id }),
889
+ listLookupOptions(fetch, "/lookups/users")
890
+ ]);
891
+ return {
892
+ screen: ${JSON.stringify({ id: projectEdit.id, title: projectEdit.title, web: projectEdit.web }, null, 2)},
893
+ project,
894
+ lookups: {
895
+ owner_id: ownerOptions
896
+ },
897
+ values: {
898
+ name: project.name ?? "",
899
+ description: project.description ?? "",
900
+ status: project.status ?? "active",
901
+ owner_id: project.owner_id ?? ""
902
+ }
903
+ };
904
+ };
905
+ `;
906
+
907
+ files["projects/[id]/edit/+page.server.ts"] = `import { redirect, fail } from "@sveltejs/kit";
908
+ import type { Actions } from "./$types";
909
+ import { requestCapability } from "$lib/api/client";
910
+
911
+ export const actions: Actions = {
912
+ ${renderSvelteKitRedirectingAction({
913
+ actionName: "default",
914
+ signature: "{ request, fetch, params }",
915
+ prelude: `const form = await request.formData();
916
+ const payload = {
917
+ name: String(form.get("name") || "") || undefined,
918
+ description: String(form.get("description") || "") || undefined,
919
+ status: String(form.get("status") || "") || undefined,
920
+ owner_id: String(form.get("owner_id") || "") || undefined
921
+ };`,
922
+ tryStatement: `await requestCapability(fetch, "cap_update_project", { project_id: params.id, ...payload });`,
923
+ catchReturn:
924
+ 'return fail(400, { error: error instanceof Error ? error.message : "Unable to update project", values: payload });',
925
+ successStatement: "throw redirect(303, `/projects/${params.id}`);"
926
+ })}
927
+ };
928
+ `;
929
+
930
+ files["projects/[id]/edit/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
931
+ export let data;
932
+ export let form;
933
+
934
+ const values = form?.values ?? data.values;
935
+ </script>
936
+
937
+ <main>
938
+ <div class="stack">
939
+ <section class="card">
940
+ <h1>${projectEdit.title || "Edit Project"}</h1>
941
+ <p>Update the mutable fields for <strong>{data.project.name}</strong>.</p>
942
+ {#if form?.error}<p><strong>{form.error}</strong></p>{/if}
943
+ <form class="stack" method="POST">
944
+ <label>Name <input name="name" value={values.name ?? ""} /></label>
945
+ <label>Description <textarea name="description">{values.description ?? ""}</textarea></label>
946
+ <label>
947
+ Status
948
+ <select name="status">
949
+ <option value="active" selected={(values.status ?? data.project.status) === "active"}>active</option>
950
+ <option value="archived" selected={(values.status ?? data.project.status) === "archived"}>archived</option>
951
+ </select>
952
+ </label>
953
+ <label>
954
+ Owner
955
+ <select name="owner_id">
956
+ <option value="">Unassigned</option>
957
+ {#each data.lookups.owner_id as option}
958
+ <option value={option.value} selected={option.value === (values.owner_id ?? "")}>{option.label}</option>
959
+ {/each}
960
+ </select>
961
+ </label>
962
+ <div class="button-row">
963
+ <button type="submit">Save Changes</button>
964
+ <a class="button-link secondary" href={"/projects/" + data.project.id}>Cancel</a>
965
+ </div>
966
+ </form>
967
+ </section>
968
+ </div>
969
+ </main>
970
+ `;
971
+ }
972
+
973
+ if (userList && userDetail && userCreate && userEdit) {
974
+ files["users/+page.ts"] = `import type { PageLoad } from "./$types";
975
+ import { requestCapability } from "$lib/api/client";
976
+
977
+ export const load: PageLoad = async ({ fetch, url }) => {
978
+ const limit = url.searchParams.get("limit");
979
+ return {
980
+ screen: ${JSON.stringify({ id: userList.id, title: userList.title, collection: userList.collection, web: userList.web }, null, 2)},
981
+ filters: {
982
+ limit: limit ?? ""
983
+ },
984
+ result: await requestCapability(fetch, "cap_list_users", {
985
+ after: url.searchParams.get("after") ?? undefined,
986
+ limit: limit ? Number(limit) : undefined
987
+ })
988
+ };
989
+ };
990
+ `;
991
+
992
+ files["users/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
993
+ export let data;
994
+
995
+ const buildNextHref = () => {
996
+ if (!data.result.next_cursor) return null;
997
+ const params = new URLSearchParams();
998
+ if (data.filters.limit) params.set("limit", String(data.filters.limit));
999
+ params.set("after", data.result.next_cursor);
1000
+ return \`/users?\${params.toString()}\`;
1001
+ };
1002
+
1003
+ const nextHref = buildNextHref();
1004
+ </script>
1005
+
1006
+ <main>
1007
+ <div class="stack">
1008
+ <section class="card">
1009
+ <div class="button-row" style="justify-content: space-between;">
1010
+ <div>
1011
+ <h1>${userList.title || userList.id}</h1>
1012
+ <p>This ${prettyScreenKind(userList.kind)} screen was generated from \`${userList.id}\`.</p>
1013
+ </div>
1014
+ <a class="button-link" href="/users/new">Create User</a>
1015
+ </div>
1016
+
1017
+ {#if data.result.items.length === 0}
1018
+ <div class="empty-state">
1019
+ <p><strong>${userList.emptyState?.title || "No users yet"}</strong></p>
1020
+ <p class="muted">${userList.emptyState?.body || ""}</p>
1021
+ </div>
1022
+ {:else}
1023
+ <ul class="task-list resource-list">
1024
+ {#each data.result.items as user}
1025
+ <li>
1026
+ <div class="task-meta resource-meta">
1027
+ <a href={'/users/' + user.id}><strong>{user.display_name}</strong></a>
1028
+ <span class="muted">{user.email}</span>
1029
+ </div>
1030
+ <span class="badge">{user.is_active ? "active" : "inactive"}</span>
1031
+ </li>
1032
+ {/each}
1033
+ </ul>
1034
+ {#if nextHref}
1035
+ <p><a class="button-link secondary" href={nextHref}>Next Page</a></p>
1036
+ {/if}
1037
+ {/if}
1038
+ </section>
1039
+ </div>
1040
+ </main>
1041
+ `;
1042
+
1043
+ files["users/[id]/+page.ts"] = `import type { PageLoad } from "./$types";
1044
+ import { requestCapability } from "$lib/api/client";
1045
+
1046
+ export const load: PageLoad = async ({ fetch, params }) => {
1047
+ return {
1048
+ screen: ${JSON.stringify({ id: userDetail.id, title: userDetail.title, web: userDetail.web }, null, 2)},
1049
+ user: await requestCapability(fetch, "cap_get_user", { user_id: params.id })
1050
+ };
1051
+ };
1052
+ `;
1053
+
1054
+ files["users/[id]/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
1055
+ export let data;
1056
+ </script>
1057
+
1058
+ <main>
1059
+ <div class="stack">
1060
+ <section class="card">
1061
+ <div class="button-row" style="justify-content: space-between;">
1062
+ <div>
1063
+ <h1>{data.user.display_name}</h1>
1064
+ <p>This ${prettyScreenKind(userDetail.kind)} screen was generated from \`${userDetail.id}\`.</p>
1065
+ </div>
1066
+ <span class="badge">{data.user.is_active ? "active" : "inactive"}</span>
1067
+ </div>
1068
+
1069
+ <dl class="definition-list">
1070
+ <dt>User ID</dt><dd>{data.user.id}</dd>
1071
+ <dt>Email</dt><dd>{data.user.email}</dd>
1072
+ <dt>Display Name</dt><dd>{data.user.display_name}</dd>
1073
+ <dt>Created</dt><dd>{data.user.created_at}</dd>
1074
+ </dl>
1075
+
1076
+ <div class="button-row">
1077
+ <a class="button-link secondary" href="/users">Back to Users</a>
1078
+ <a class="button-link" href={"/users/" + data.user.id + "/edit"}>Edit User</a>
1079
+ </div>
1080
+ </section>
1081
+ </div>
1082
+ </main>
1083
+ `;
1084
+
1085
+ files["users/new/+page.server.ts"] = `import { redirect, fail } from "@sveltejs/kit";
1086
+ import type { Actions } from "./$types";
1087
+ import { requestCapability } from "$lib/api/client";
1088
+
1089
+ export const actions: Actions = {
1090
+ ${renderSvelteKitRedirectingAction({
1091
+ actionName: "default",
1092
+ signature: "{ request, fetch }",
1093
+ prelude: `const form = await request.formData();
1094
+ const payload = {
1095
+ email: String(form.get("email") || ""),
1096
+ display_name: String(form.get("display_name") || ""),
1097
+ is_active: form.get("is_active") === "true"
1098
+ };
1099
+
1100
+ if (!payload.email || !payload.display_name) {
1101
+ return fail(400, { error: "Email and display name are required.", values: payload });
1102
+ }
1103
+
1104
+ let created;`,
1105
+ tryStatement: `created = await requestCapability(fetch, "cap_create_user", payload);`,
1106
+ catchReturn:
1107
+ 'return fail(400, { error: error instanceof Error ? error.message : "Unable to create user", values: payload });',
1108
+ successStatement: "throw redirect(303, `/users/${created.id}`);"
1109
+ })}
1110
+ };
1111
+ `;
1112
+
1113
+ files["users/new/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
1114
+ export let form;
1115
+
1116
+ const values = {
1117
+ email: form?.values?.email ?? "",
1118
+ display_name: form?.values?.display_name ?? "",
1119
+ is_active: form?.values?.is_active ?? true
1120
+ };
1121
+ </script>
1122
+
1123
+ <main>
1124
+ <div class="stack">
1125
+ <section class="card">
1126
+ <h1>${userCreate.title || userCreate.id}</h1>
1127
+ <p>This ${prettyScreenKind(userCreate.kind)} screen was generated from \`${userCreate.id}\`.</p>
1128
+ {#if form?.error}<p><strong>{form.error}</strong></p>{/if}
1129
+ <form class="stack" method="POST">
1130
+ <label>Email <input name="email" type="email" required value={values.email} /></label>
1131
+ <label>Display Name <input name="display_name" required value={values.display_name} /></label>
1132
+ <label>
1133
+ Active
1134
+ <select name="is_active">
1135
+ <option value="true" selected={values.is_active === true}>active</option>
1136
+ <option value="false" selected={values.is_active === false}>inactive</option>
1137
+ </select>
1138
+ </label>
1139
+ <div class="button-row">
1140
+ <button type="submit">Create User</button>
1141
+ <a class="button-link secondary" href="/users">Cancel</a>
1142
+ </div>
1143
+ </form>
1144
+ </section>
1145
+ </div>
1146
+ </main>
1147
+ `;
1148
+
1149
+ files["users/[id]/edit/+page.ts"] = `import type { PageLoad } from "./$types";
1150
+ import { requestCapability } from "$lib/api/client";
1151
+
1152
+ export const load: PageLoad = async ({ fetch, params }) => {
1153
+ const user = await requestCapability(fetch, "cap_get_user", { user_id: params.id });
1154
+ return {
1155
+ screen: ${JSON.stringify({ id: userEdit.id, title: userEdit.title, web: userEdit.web }, null, 2)},
1156
+ user,
1157
+ values: {
1158
+ email: user.email ?? "",
1159
+ display_name: user.display_name ?? "",
1160
+ is_active: user.is_active ?? true
1161
+ }
1162
+ };
1163
+ };
1164
+ `;
1165
+
1166
+ files["users/[id]/edit/+page.server.ts"] = `import { redirect, fail } from "@sveltejs/kit";
1167
+ import type { Actions } from "./$types";
1168
+ import { requestCapability } from "$lib/api/client";
1169
+
1170
+ export const actions: Actions = {
1171
+ ${renderSvelteKitRedirectingAction({
1172
+ actionName: "default",
1173
+ signature: "{ request, fetch, params }",
1174
+ prelude: `const form = await request.formData();
1175
+ const payload = {
1176
+ email: String(form.get("email") || "") || undefined,
1177
+ display_name: String(form.get("display_name") || "") || undefined,
1178
+ is_active: form.get("is_active") === "true"
1179
+ };`,
1180
+ tryStatement: `await requestCapability(fetch, "cap_update_user", { user_id: params.id, ...payload });`,
1181
+ catchReturn:
1182
+ 'return fail(400, { error: error instanceof Error ? error.message : "Unable to update user", values: payload });',
1183
+ successStatement: "throw redirect(303, `/users/${params.id}`);"
1184
+ })}
1185
+ };
1186
+ `;
1187
+
1188
+ files["users/[id]/edit/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
1189
+ export let data;
1190
+ export let form;
1191
+
1192
+ const values = form?.values ?? data.values;
1193
+ </script>
1194
+
1195
+ <main>
1196
+ <div class="stack">
1197
+ <section class="card">
1198
+ <h1>${userEdit.title || "Edit User"}</h1>
1199
+ <p>Update the mutable fields for <strong>{data.user.display_name}</strong>.</p>
1200
+ {#if form?.error}<p><strong>{form.error}</strong></p>{/if}
1201
+ <form class="stack" method="POST">
1202
+ <label>Email <input name="email" type="email" value={values.email ?? ""} /></label>
1203
+ <label>Display Name <input name="display_name" value={values.display_name ?? ""} /></label>
1204
+ <label>
1205
+ Active
1206
+ <select name="is_active">
1207
+ <option value="true" selected={(values.is_active ?? data.user.is_active) === true}>active</option>
1208
+ <option value="false" selected={(values.is_active ?? data.user.is_active) === false}>inactive</option>
1209
+ </select>
1210
+ </label>
1211
+ <div class="button-row">
1212
+ <button type="submit">Save Changes</button>
1213
+ <a class="button-link secondary" href={"/users/" + data.user.id}>Cancel</a>
1214
+ </div>
1215
+ </form>
1216
+ </section>
1217
+ </div>
1218
+ </main>
1219
+ `;
1220
+ }
1221
+
1222
+ return files;
1223
+ }