@pyreon/create-zero 0.14.0 → 0.15.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.
- package/README.md +85 -22
- package/bin/create-pyreon-app.js +2 -0
- package/lib/index.js +1254 -191
- package/package.json +5 -2
- package/templates/{default → app}/src/routes/_layout.tsx +5 -2
- package/templates/blog/.mcp.json +8 -0
- package/templates/blog/CLAUDE.md +59 -0
- package/templates/blog/index.html +18 -0
- package/templates/blog/public/favicon.svg +4 -0
- package/templates/blog/src/content/posts/static-vs-ssr.tsx +54 -0
- package/templates/blog/src/content/posts/welcome.tsx +70 -0
- package/templates/blog/src/content/posts/why-signals.tsx +57 -0
- package/templates/blog/src/entry-client.ts +5 -0
- package/templates/blog/src/global.css +292 -0
- package/templates/blog/src/lib/posts.ts +45 -0
- package/templates/blog/src/routes/_layout.tsx +40 -0
- package/templates/blog/src/routes/about.tsx +28 -0
- package/templates/blog/src/routes/api/rss.ts +55 -0
- package/templates/blog/src/routes/blog/[slug].tsx +67 -0
- package/templates/blog/src/routes/blog/index.tsx +43 -0
- package/templates/blog/src/routes/index.tsx +52 -0
- package/templates/blog/tsconfig.json +16 -0
- package/templates/dashboard/.mcp.json +8 -0
- package/templates/dashboard/CLAUDE.md +50 -0
- package/templates/dashboard/index.html +16 -0
- package/templates/dashboard/public/favicon.svg +4 -0
- package/templates/dashboard/src/entry-client.ts +5 -0
- package/templates/dashboard/src/global.css +451 -0
- package/templates/dashboard/src/lib/auth.ts +106 -0
- package/templates/dashboard/src/lib/db.ts +118 -0
- package/templates/dashboard/src/routes/_layout.tsx +28 -0
- package/templates/dashboard/src/routes/api/signout.ts +15 -0
- package/templates/dashboard/src/routes/app/_layout.tsx +76 -0
- package/templates/dashboard/src/routes/app/dashboard.tsx +92 -0
- package/templates/dashboard/src/routes/app/invoices/[id].tsx +197 -0
- package/templates/dashboard/src/routes/app/invoices/index.tsx +61 -0
- package/templates/dashboard/src/routes/app/settings/account.tsx +31 -0
- package/templates/dashboard/src/routes/app/settings/billing.tsx +28 -0
- package/templates/dashboard/src/routes/app/settings/index.tsx +29 -0
- package/templates/dashboard/src/routes/app/users.tsx +50 -0
- package/templates/dashboard/src/routes/index.tsx +40 -0
- package/templates/dashboard/src/routes/login.tsx +79 -0
- package/templates/dashboard/src/routes/signup.tsx +78 -0
- package/templates/dashboard/tsconfig.json +16 -0
- package/lib/index.js.map +0 -1
- /package/templates/{default → app}/.mcp.json +0 -0
- /package/templates/{default → app}/CLAUDE.md +0 -0
- /package/templates/{default → app}/index.html +0 -0
- /package/templates/{default → app}/public/favicon.svg +0 -0
- /package/templates/{default → app}/src/entry-client.ts +0 -0
- /package/templates/{default → app}/src/features/posts.ts +0 -0
- /package/templates/{default → app}/src/global.css +0 -0
- /package/templates/{default → app}/src/routes/(admin)/dashboard.tsx +0 -0
- /package/templates/{default → app}/src/routes/_error.tsx +0 -0
- /package/templates/{default → app}/src/routes/_loading.tsx +0 -0
- /package/templates/{default → app}/src/routes/about.tsx +0 -0
- /package/templates/{default → app}/src/routes/api/health.ts +0 -0
- /package/templates/{default → app}/src/routes/api/posts.ts +0 -0
- /package/templates/{default → app}/src/routes/counter.tsx +0 -0
- /package/templates/{default → app}/src/routes/index.tsx +0 -0
- /package/templates/{default → app}/src/routes/posts/[id].tsx +0 -0
- /package/templates/{default → app}/src/routes/posts/index.tsx +0 -0
- /package/templates/{default → app}/src/routes/posts/new.tsx +0 -0
- /package/templates/{default → app}/src/stores/app.ts +0 -0
- /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
|
+
}
|