@pyreon/create-zero 0.14.0 → 0.16.0

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/README.md +85 -22
  2. package/bin/create-pyreon-app.js +2 -0
  3. package/lib/index.js +1254 -191
  4. package/package.json +5 -2
  5. package/templates/{default → app}/src/routes/_layout.tsx +5 -2
  6. package/templates/{default → app}/src/routes/posts/[id].tsx +14 -0
  7. package/templates/blog/.mcp.json +8 -0
  8. package/templates/blog/CLAUDE.md +59 -0
  9. package/templates/blog/index.html +18 -0
  10. package/templates/blog/public/favicon.svg +4 -0
  11. package/templates/blog/src/content/posts/static-vs-ssr.tsx +54 -0
  12. package/templates/blog/src/content/posts/welcome.tsx +70 -0
  13. package/templates/blog/src/content/posts/why-signals.tsx +57 -0
  14. package/templates/blog/src/entry-client.ts +5 -0
  15. package/templates/blog/src/global.css +292 -0
  16. package/templates/blog/src/lib/posts.ts +45 -0
  17. package/templates/blog/src/routes/_layout.tsx +40 -0
  18. package/templates/blog/src/routes/about.tsx +28 -0
  19. package/templates/blog/src/routes/api/rss.ts +55 -0
  20. package/templates/blog/src/routes/blog/[slug].tsx +73 -0
  21. package/templates/blog/src/routes/blog/index.tsx +43 -0
  22. package/templates/blog/src/routes/index.tsx +52 -0
  23. package/templates/blog/tsconfig.json +16 -0
  24. package/templates/dashboard/.mcp.json +8 -0
  25. package/templates/dashboard/CLAUDE.md +50 -0
  26. package/templates/dashboard/index.html +16 -0
  27. package/templates/dashboard/public/favicon.svg +4 -0
  28. package/templates/dashboard/src/entry-client.ts +5 -0
  29. package/templates/dashboard/src/global.css +451 -0
  30. package/templates/dashboard/src/lib/auth.ts +106 -0
  31. package/templates/dashboard/src/lib/db.ts +118 -0
  32. package/templates/dashboard/src/routes/_layout.tsx +28 -0
  33. package/templates/dashboard/src/routes/api/signout.ts +15 -0
  34. package/templates/dashboard/src/routes/app/_layout.tsx +76 -0
  35. package/templates/dashboard/src/routes/app/dashboard.tsx +92 -0
  36. package/templates/dashboard/src/routes/app/invoices/[id].tsx +214 -0
  37. package/templates/dashboard/src/routes/app/invoices/index.tsx +61 -0
  38. package/templates/dashboard/src/routes/app/settings/account.tsx +31 -0
  39. package/templates/dashboard/src/routes/app/settings/billing.tsx +28 -0
  40. package/templates/dashboard/src/routes/app/settings/index.tsx +29 -0
  41. package/templates/dashboard/src/routes/app/users.tsx +50 -0
  42. package/templates/dashboard/src/routes/index.tsx +40 -0
  43. package/templates/dashboard/src/routes/login.tsx +79 -0
  44. package/templates/dashboard/src/routes/signup.tsx +78 -0
  45. package/templates/dashboard/tsconfig.json +16 -0
  46. package/lib/index.js.map +0 -1
  47. /package/templates/{default → app}/.mcp.json +0 -0
  48. /package/templates/{default → app}/CLAUDE.md +0 -0
  49. /package/templates/{default → app}/index.html +0 -0
  50. /package/templates/{default → app}/public/favicon.svg +0 -0
  51. /package/templates/{default → app}/src/entry-client.ts +0 -0
  52. /package/templates/{default → app}/src/features/posts.ts +0 -0
  53. /package/templates/{default → app}/src/global.css +0 -0
  54. /package/templates/{default → app}/src/routes/(admin)/dashboard.tsx +0 -0
  55. /package/templates/{default → app}/src/routes/_error.tsx +0 -0
  56. /package/templates/{default → app}/src/routes/_loading.tsx +0 -0
  57. /package/templates/{default → app}/src/routes/about.tsx +0 -0
  58. /package/templates/{default → app}/src/routes/api/health.ts +0 -0
  59. /package/templates/{default → app}/src/routes/api/posts.ts +0 -0
  60. /package/templates/{default → app}/src/routes/counter.tsx +0 -0
  61. /package/templates/{default → app}/src/routes/index.tsx +0 -0
  62. /package/templates/{default → app}/src/routes/posts/index.tsx +0 -0
  63. /package/templates/{default → app}/src/routes/posts/new.tsx +0 -0
  64. /package/templates/{default → app}/src/stores/app.ts +0 -0
  65. /package/templates/{default → app}/tsconfig.json +0 -0
@@ -0,0 +1,451 @@
1
+ /* ─── Reset ──────────────────────────────────────────────────────────────── */
2
+
3
+ *,
4
+ *::before,
5
+ *::after {
6
+ box-sizing: border-box;
7
+ margin: 0;
8
+ padding: 0;
9
+ }
10
+
11
+ :root {
12
+ --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
13
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
14
+
15
+ --c-bg: #0a0a0b;
16
+ --c-surface: #141416;
17
+ --c-surface-hover: #1c1c20;
18
+ --c-border: #27272a;
19
+ --c-border-subtle: #1e1e22;
20
+ --c-text: #fafafa;
21
+ --c-text-secondary: #a1a1aa;
22
+ --c-text-muted: #71717a;
23
+ --c-accent: #6d5cff;
24
+ --c-accent-hover: #7d6fff;
25
+ --c-accent-subtle: rgba(109, 92, 255, 0.12);
26
+ --c-success: #22c55e;
27
+ --c-warning: #eab308;
28
+ --c-danger: #ef4444;
29
+
30
+ --space-xs: 0.25rem;
31
+ --space-sm: 0.5rem;
32
+ --space-md: 1rem;
33
+ --space-lg: 1.5rem;
34
+ --space-xl: 2rem;
35
+ --space-2xl: 3rem;
36
+ --space-3xl: 4rem;
37
+
38
+ --radius-sm: 6px;
39
+ --radius-md: 10px;
40
+ --radius-lg: 16px;
41
+
42
+ --sidebar-w: 240px;
43
+ }
44
+
45
+ [data-theme='light'] {
46
+ --c-bg: #ffffff;
47
+ --c-surface: #f4f4f5;
48
+ --c-surface-hover: #e4e4e7;
49
+ --c-border: #d4d4d8;
50
+ --c-border-subtle: #e4e4e7;
51
+ --c-text: #18181b;
52
+ --c-text-secondary: #3f3f46;
53
+ --c-text-muted: #71717a;
54
+ --c-accent: #5b4fd6;
55
+ --c-accent-hover: #4a3fb8;
56
+ --c-accent-subtle: rgba(91, 79, 214, 0.1);
57
+ }
58
+
59
+ html,
60
+ body {
61
+ font-family: var(--font-sans);
62
+ background: var(--c-bg);
63
+ color: var(--c-text);
64
+ -webkit-font-smoothing: antialiased;
65
+ line-height: 1.55;
66
+ }
67
+
68
+ body {
69
+ min-height: 100vh;
70
+ }
71
+
72
+ a {
73
+ color: var(--c-accent);
74
+ text-decoration: none;
75
+ }
76
+
77
+ a:hover {
78
+ color: var(--c-accent-hover);
79
+ }
80
+
81
+ button {
82
+ font: inherit;
83
+ background: transparent;
84
+ border: 0;
85
+ color: inherit;
86
+ cursor: pointer;
87
+ }
88
+
89
+ input,
90
+ textarea,
91
+ select {
92
+ font: inherit;
93
+ width: 100%;
94
+ background: var(--c-bg);
95
+ color: var(--c-text);
96
+ border: 1px solid var(--c-border);
97
+ border-radius: var(--radius-md);
98
+ padding: var(--space-sm) var(--space-md);
99
+ transition: border-color 150ms ease, box-shadow 150ms ease;
100
+ }
101
+
102
+ input:focus,
103
+ textarea:focus,
104
+ select:focus {
105
+ outline: 0;
106
+ border-color: var(--c-accent);
107
+ box-shadow: 0 0 0 3px var(--c-accent-subtle);
108
+ }
109
+
110
+ label {
111
+ display: block;
112
+ margin-bottom: var(--space-xs);
113
+ font-size: 0.875rem;
114
+ color: var(--c-text-secondary);
115
+ }
116
+
117
+ /* ─── Buttons ────────────────────────────────────────────────────────────── */
118
+
119
+ .btn {
120
+ display: inline-flex;
121
+ align-items: center;
122
+ gap: var(--space-xs);
123
+ padding: var(--space-sm) var(--space-md);
124
+ border-radius: var(--radius-md);
125
+ font-weight: 500;
126
+ border: 1px solid transparent;
127
+ transition: background 150ms ease, border-color 150ms ease;
128
+ }
129
+
130
+ .btn-primary {
131
+ background: var(--c-accent);
132
+ color: #fff;
133
+ }
134
+
135
+ .btn-primary:hover {
136
+ background: var(--c-accent-hover);
137
+ color: #fff;
138
+ }
139
+
140
+ .btn-secondary {
141
+ border-color: var(--c-border);
142
+ }
143
+
144
+ .btn-secondary:hover {
145
+ background: var(--c-surface);
146
+ }
147
+
148
+ /* ─── Marketing landing ──────────────────────────────────────────────────── */
149
+
150
+ .marketing-header {
151
+ border-bottom: 1px solid var(--c-border);
152
+ padding: var(--space-md) var(--space-xl);
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: space-between;
156
+ }
157
+
158
+ .marketing-logo {
159
+ font-weight: 700;
160
+ font-size: 1.125rem;
161
+ letter-spacing: -0.02em;
162
+ }
163
+
164
+ .marketing-nav {
165
+ display: flex;
166
+ gap: var(--space-md);
167
+ align-items: center;
168
+ }
169
+
170
+ .hero {
171
+ text-align: center;
172
+ padding: var(--space-3xl) var(--space-xl);
173
+ max-width: 720px;
174
+ margin: 0 auto;
175
+ }
176
+
177
+ .hero h1 {
178
+ font-size: 3rem;
179
+ line-height: 1.1;
180
+ letter-spacing: -0.03em;
181
+ font-weight: 800;
182
+ margin-bottom: var(--space-md);
183
+ }
184
+
185
+ .hero p {
186
+ color: var(--c-text-secondary);
187
+ font-size: 1.125rem;
188
+ margin-bottom: var(--space-xl);
189
+ }
190
+
191
+ .hero-actions {
192
+ display: flex;
193
+ gap: var(--space-md);
194
+ justify-content: center;
195
+ }
196
+
197
+ /* ─── Auth screens ───────────────────────────────────────────────────────── */
198
+
199
+ .auth-shell {
200
+ min-height: 100vh;
201
+ display: flex;
202
+ align-items: center;
203
+ justify-content: center;
204
+ padding: var(--space-xl);
205
+ }
206
+
207
+ .auth-card {
208
+ width: 100%;
209
+ max-width: 380px;
210
+ background: var(--c-surface);
211
+ border: 1px solid var(--c-border);
212
+ border-radius: var(--radius-lg);
213
+ padding: var(--space-xl);
214
+ }
215
+
216
+ .auth-card h1 {
217
+ font-size: 1.5rem;
218
+ margin-bottom: var(--space-lg);
219
+ letter-spacing: -0.02em;
220
+ }
221
+
222
+ .field {
223
+ margin-bottom: var(--space-md);
224
+ }
225
+
226
+ .error {
227
+ color: var(--c-danger);
228
+ font-size: 0.875rem;
229
+ margin-top: var(--space-xs);
230
+ }
231
+
232
+ /* ─── App shell (auth-gated) ─────────────────────────────────────────────── */
233
+
234
+ .app-shell {
235
+ display: grid;
236
+ grid-template-columns: var(--sidebar-w) 1fr;
237
+ min-height: 100vh;
238
+ }
239
+
240
+ .app-sidebar {
241
+ background: var(--c-surface);
242
+ border-right: 1px solid var(--c-border);
243
+ padding: var(--space-lg) var(--space-md);
244
+ display: flex;
245
+ flex-direction: column;
246
+ gap: var(--space-xs);
247
+ }
248
+
249
+ .app-sidebar .brand {
250
+ font-weight: 700;
251
+ letter-spacing: -0.02em;
252
+ margin-bottom: var(--space-lg);
253
+ padding: var(--space-xs) var(--space-sm);
254
+ }
255
+
256
+ .app-sidebar a {
257
+ display: block;
258
+ padding: var(--space-sm) var(--space-md);
259
+ border-radius: var(--radius-sm);
260
+ color: var(--c-text-secondary);
261
+ font-size: 0.9375rem;
262
+ transition: background 150ms ease, color 150ms ease;
263
+ }
264
+
265
+ .app-sidebar a:hover {
266
+ color: var(--c-text);
267
+ background: var(--c-surface-hover);
268
+ }
269
+
270
+ .app-sidebar a.nav-active {
271
+ color: var(--c-text);
272
+ background: var(--c-accent-subtle);
273
+ }
274
+
275
+ .app-sidebar-footer {
276
+ margin-top: auto;
277
+ padding: var(--space-sm) var(--space-md);
278
+ font-size: 0.8125rem;
279
+ color: var(--c-text-muted);
280
+ }
281
+
282
+ .app-content {
283
+ padding: var(--space-xl) var(--space-2xl);
284
+ overflow: auto;
285
+ }
286
+
287
+ .app-page-header {
288
+ display: flex;
289
+ justify-content: space-between;
290
+ align-items: end;
291
+ margin-bottom: var(--space-xl);
292
+ padding-bottom: var(--space-md);
293
+ border-bottom: 1px solid var(--c-border);
294
+ }
295
+
296
+ .app-page-header h1 {
297
+ font-size: 1.5rem;
298
+ letter-spacing: -0.02em;
299
+ }
300
+
301
+ /* ─── Stat cards ─────────────────────────────────────────────────────────── */
302
+
303
+ .stats-grid {
304
+ display: grid;
305
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
306
+ gap: var(--space-md);
307
+ margin-bottom: var(--space-2xl);
308
+ }
309
+
310
+ .stat-card {
311
+ background: var(--c-surface);
312
+ border: 1px solid var(--c-border);
313
+ border-radius: var(--radius-md);
314
+ padding: var(--space-lg);
315
+ }
316
+
317
+ .stat-card .label {
318
+ font-size: 0.8125rem;
319
+ color: var(--c-text-muted);
320
+ text-transform: uppercase;
321
+ letter-spacing: 0.04em;
322
+ margin-bottom: var(--space-xs);
323
+ }
324
+
325
+ .stat-card .value {
326
+ font-size: 1.875rem;
327
+ font-weight: 700;
328
+ letter-spacing: -0.02em;
329
+ }
330
+
331
+ .stat-card .delta {
332
+ font-size: 0.875rem;
333
+ color: var(--c-success);
334
+ }
335
+
336
+ /* ─── Tables ─────────────────────────────────────────────────────────────── */
337
+
338
+ .data-table {
339
+ width: 100%;
340
+ border-collapse: collapse;
341
+ background: var(--c-surface);
342
+ border: 1px solid var(--c-border);
343
+ border-radius: var(--radius-md);
344
+ overflow: hidden;
345
+ }
346
+
347
+ .data-table th,
348
+ .data-table td {
349
+ text-align: left;
350
+ padding: var(--space-sm) var(--space-md);
351
+ border-bottom: 1px solid var(--c-border-subtle);
352
+ }
353
+
354
+ .data-table th {
355
+ font-size: 0.8125rem;
356
+ text-transform: uppercase;
357
+ letter-spacing: 0.04em;
358
+ color: var(--c-text-muted);
359
+ background: var(--c-bg);
360
+ }
361
+
362
+ .data-table tr:last-child td {
363
+ border-bottom: 0;
364
+ }
365
+
366
+ .data-table tr:hover td {
367
+ background: var(--c-surface-hover);
368
+ }
369
+
370
+ /* ─── Status pills ───────────────────────────────────────────────────────── */
371
+
372
+ .pill {
373
+ display: inline-flex;
374
+ align-items: center;
375
+ font-size: 0.75rem;
376
+ font-weight: 500;
377
+ padding: 0.125rem 0.5rem;
378
+ border-radius: 9999px;
379
+ background: var(--c-surface-hover);
380
+ border: 1px solid var(--c-border);
381
+ }
382
+
383
+ .pill.paid {
384
+ background: rgba(34, 197, 94, 0.12);
385
+ border-color: rgba(34, 197, 94, 0.3);
386
+ color: var(--c-success);
387
+ }
388
+
389
+ .pill.draft {
390
+ background: var(--c-surface-hover);
391
+ color: var(--c-text-secondary);
392
+ }
393
+
394
+ .pill.pending {
395
+ background: rgba(234, 179, 8, 0.12);
396
+ border-color: rgba(234, 179, 8, 0.3);
397
+ color: var(--c-warning);
398
+ }
399
+
400
+ /* ─── Invoice detail ─────────────────────────────────────────────────────── */
401
+
402
+ .invoice-detail {
403
+ display: grid;
404
+ grid-template-columns: 2fr 1fr;
405
+ gap: var(--space-2xl);
406
+ }
407
+
408
+ .invoice-actions {
409
+ background: var(--c-surface);
410
+ border: 1px solid var(--c-border);
411
+ border-radius: var(--radius-md);
412
+ padding: var(--space-lg);
413
+ height: fit-content;
414
+ display: flex;
415
+ flex-direction: column;
416
+ gap: var(--space-sm);
417
+ }
418
+
419
+ .invoice-preview {
420
+ background: #fff;
421
+ color: #111;
422
+ border-radius: var(--radius-md);
423
+ padding: var(--space-2xl);
424
+ font-family: var(--font-sans);
425
+ }
426
+
427
+ .invoice-preview h2 {
428
+ font-size: 1.5rem;
429
+ letter-spacing: -0.02em;
430
+ margin-bottom: var(--space-md);
431
+ }
432
+
433
+ .invoice-preview table {
434
+ width: 100%;
435
+ margin-top: var(--space-lg);
436
+ border-collapse: collapse;
437
+ }
438
+
439
+ .invoice-preview th,
440
+ .invoice-preview td {
441
+ text-align: left;
442
+ padding: var(--space-sm);
443
+ border-bottom: 1px solid #e5e7eb;
444
+ }
445
+
446
+ .invoice-preview .total {
447
+ text-align: right;
448
+ font-weight: 700;
449
+ font-size: 1.25rem;
450
+ margin-top: var(--space-md);
451
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * In-memory auth implementation. Stores users + sessions in module state
3
+ * on the server — every dev-server restart wipes it.
4
+ *
5
+ * The exported surface (`signIn` / `signUp` / `getSession` / `signOut` +
6
+ * `SessionInfo`) is the contract route guards consume; routes never import
7
+ * an underlying provider directly. To replace this file with a real
8
+ * implementation, run:
9
+ *
10
+ * bunx create-pyreon-app --template dashboard --integrations supabase
11
+ *
12
+ * (or pick Supabase in the interactive prompt) — the scaffolder overwrites
13
+ * this file with a Supabase-backed implementation that keeps the same
14
+ * function signatures, so routes don't need to change.
15
+ */
16
+
17
+ interface User {
18
+ id: string
19
+ email: string
20
+ password: string // plain text — STUB ONLY; real impl uses argon2id
21
+ createdAt: Date
22
+ }
23
+
24
+ interface Session {
25
+ id: string
26
+ userId: string
27
+ email: string
28
+ expiresAt: Date
29
+ }
30
+
31
+ const users = new Map<string, User>()
32
+ const sessions = new Map<string, Session>()
33
+
34
+ let nextUserId = 1
35
+ function newUserId(): string {
36
+ return `u_${nextUserId++}`
37
+ }
38
+
39
+ function newSessionId(): string {
40
+ // Real impl uses crypto.randomUUID + signed cookies
41
+ return `s_${Math.random().toString(36).slice(2)}_${Date.now()}`
42
+ }
43
+
44
+ // Seed a demo user so the login form has something to log in with
45
+ users.set("demo@example.com", {
46
+ id: newUserId(),
47
+ email: "demo@example.com",
48
+ password: "demo1234",
49
+ createdAt: new Date(),
50
+ })
51
+
52
+ export interface SessionInfo {
53
+ userId: string
54
+ email: string
55
+ }
56
+
57
+ export function signUp(email: string, password: string): { sessionId: string } | { error: string } {
58
+ if (users.has(email)) return { error: "Email already registered" }
59
+ if (password.length < 8) return { error: "Password must be at least 8 characters" }
60
+
61
+ const user: User = {
62
+ id: newUserId(),
63
+ email,
64
+ password,
65
+ createdAt: new Date(),
66
+ }
67
+ users.set(email, user)
68
+
69
+ const sid = newSessionId()
70
+ sessions.set(sid, {
71
+ id: sid,
72
+ userId: user.id,
73
+ email,
74
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
75
+ })
76
+ return { sessionId: sid }
77
+ }
78
+
79
+ export function signIn(email: string, password: string): { sessionId: string } | { error: string } {
80
+ const user = users.get(email)
81
+ if (!user || user.password !== password) return { error: "Invalid email or password" }
82
+
83
+ const sid = newSessionId()
84
+ sessions.set(sid, {
85
+ id: sid,
86
+ userId: user.id,
87
+ email: user.email,
88
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
89
+ })
90
+ return { sessionId: sid }
91
+ }
92
+
93
+ export async function getSession(sessionId: string | undefined): Promise<SessionInfo | null> {
94
+ if (!sessionId) return null
95
+ const s = sessions.get(sessionId)
96
+ if (!s) return null
97
+ if (s.expiresAt < new Date()) {
98
+ sessions.delete(sessionId)
99
+ return null
100
+ }
101
+ return { userId: s.userId, email: s.email }
102
+ }
103
+
104
+ export async function signOut(sessionId: string): Promise<void> {
105
+ sessions.delete(sessionId)
106
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * In-memory data layer. Holds users + invoices in module state on the
3
+ * server — every dev-server restart resets the data.
4
+ *
5
+ * The exported types (`User`, `Invoice`) and functions (`listUsers`,
6
+ * `listInvoices`, `invoiceById`, `invoiceTotal`) are the contract routes
7
+ * consume. Run `create-pyreon-app --integrations supabase` to overwrite
8
+ * this file with a Supabase-backed implementation that returns the same
9
+ * row shapes — routes don't need to change.
10
+ */
11
+
12
+ export interface User {
13
+ id: string
14
+ email: string
15
+ name: string
16
+ role: "admin" | "member"
17
+ createdAt: Date
18
+ }
19
+
20
+ export interface InvoiceItem {
21
+ description: string
22
+ qty: number
23
+ unitPrice: number
24
+ }
25
+
26
+ export interface Invoice {
27
+ id: string
28
+ number: string
29
+ customer: { name: string; email: string; address: string }
30
+ items: InvoiceItem[]
31
+ status: "draft" | "pending" | "paid"
32
+ issuedAt: Date
33
+ }
34
+
35
+ const users: User[] = [
36
+ {
37
+ id: "u_1",
38
+ email: "demo@example.com",
39
+ name: "Demo User",
40
+ role: "admin",
41
+ createdAt: new Date("2026-01-04"),
42
+ },
43
+ {
44
+ id: "u_2",
45
+ email: "alice@example.com",
46
+ name: "Alice Jensen",
47
+ role: "member",
48
+ createdAt: new Date("2026-02-11"),
49
+ },
50
+ {
51
+ id: "u_3",
52
+ email: "bob@example.com",
53
+ name: "Bob Patel",
54
+ role: "member",
55
+ createdAt: new Date("2026-03-19"),
56
+ },
57
+ ]
58
+
59
+ const invoices: Invoice[] = [
60
+ {
61
+ id: "inv_1001",
62
+ number: "INV-1001",
63
+ customer: {
64
+ name: "Acme Corp",
65
+ email: "ap@acme.com",
66
+ address: "100 Main St, Springfield",
67
+ },
68
+ items: [
69
+ { description: "Pro plan — annual", qty: 1, unitPrice: 1188 },
70
+ { description: "Setup assistance", qty: 2, unitPrice: 250 },
71
+ ],
72
+ status: "paid",
73
+ issuedAt: new Date("2026-04-01"),
74
+ },
75
+ {
76
+ id: "inv_1002",
77
+ number: "INV-1002",
78
+ customer: {
79
+ name: "Globex Industries",
80
+ email: "billing@globex.io",
81
+ address: "55 Tech Plaza, Capital City",
82
+ },
83
+ items: [{ description: "Team plan — quarterly", qty: 1, unitPrice: 297 }],
84
+ status: "pending",
85
+ issuedAt: new Date("2026-04-15"),
86
+ },
87
+ {
88
+ id: "inv_1003",
89
+ number: "INV-1003",
90
+ customer: {
91
+ name: "Initech LLC",
92
+ email: "finance@initech.com",
93
+ address: "9 TPS Way, Houston",
94
+ },
95
+ items: [
96
+ { description: "Enterprise plan — monthly", qty: 1, unitPrice: 599 },
97
+ { description: "Priority support", qty: 1, unitPrice: 199 },
98
+ ],
99
+ status: "draft",
100
+ issuedAt: new Date("2026-04-22"),
101
+ },
102
+ ]
103
+
104
+ export async function listUsers(): Promise<User[]> {
105
+ return [...users]
106
+ }
107
+
108
+ export async function listInvoices(): Promise<Invoice[]> {
109
+ return [...invoices]
110
+ }
111
+
112
+ export async function invoiceById(id: string): Promise<Invoice | undefined> {
113
+ return invoices.find((i) => i.id === id)
114
+ }
115
+
116
+ export function invoiceTotal(inv: Invoice): number {
117
+ return inv.items.reduce((sum, i) => sum + i.qty * i.unitPrice, 0)
118
+ }
@@ -0,0 +1,28 @@
1
+ import { RouterView } from "@pyreon/router"
2
+ import { Link } from "@pyreon/zero/link"
3
+ import { ThemeToggle } from "@pyreon/zero/theme"
4
+
5
+ /**
6
+ * Public marketing layout. Wraps `/`, `/login`, `/signup` — anything outside
7
+ * `/app/*`. The auth-gated `app/_layout.tsx` provides its own sidebar shell.
8
+ */
9
+ export function layout() {
10
+ return <RouterView />
11
+ }
12
+
13
+ export function MarketingHeader() {
14
+ return (
15
+ <header class="marketing-header">
16
+ <Link href="/" class="marketing-logo">
17
+ Dashboard
18
+ </Link>
19
+ <nav class="marketing-nav">
20
+ <Link href="/login">Sign in</Link>
21
+ <Link href="/signup" class="btn btn-primary">
22
+ Get started
23
+ </Link>
24
+ <ThemeToggle />
25
+ </nav>
26
+ </header>
27
+ )
28
+ }
@@ -0,0 +1,15 @@
1
+ import { signOut } from "../../lib/auth"
2
+
3
+ export async function GET(request: Request) {
4
+ const cookie = request.headers.get("cookie") ?? ""
5
+ const sid = /(?:^|;\s*)sid=([^;]+)/.exec(cookie)?.[1]
6
+ if (sid) await signOut(sid)
7
+
8
+ return new Response(null, {
9
+ status: 302,
10
+ headers: {
11
+ "set-cookie": "sid=; path=/; max-age=0",
12
+ location: "/",
13
+ },
14
+ })
15
+ }