@pyreon/create-zero 0.13.1 → 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.
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 +1296 -159
  4. package/package.json +5 -2
  5. package/templates/{default → app}/CLAUDE.md +5 -5
  6. package/templates/{default → app}/src/routes/_layout.tsx +5 -2
  7. package/templates/{default → app}/src/routes/counter.tsx +17 -15
  8. package/templates/blog/.mcp.json +8 -0
  9. package/templates/blog/CLAUDE.md +59 -0
  10. package/templates/blog/index.html +18 -0
  11. package/templates/blog/public/favicon.svg +4 -0
  12. package/templates/blog/src/content/posts/static-vs-ssr.tsx +54 -0
  13. package/templates/blog/src/content/posts/welcome.tsx +70 -0
  14. package/templates/blog/src/content/posts/why-signals.tsx +57 -0
  15. package/templates/blog/src/entry-client.ts +5 -0
  16. package/templates/blog/src/global.css +292 -0
  17. package/templates/blog/src/lib/posts.ts +45 -0
  18. package/templates/blog/src/routes/_layout.tsx +40 -0
  19. package/templates/blog/src/routes/about.tsx +28 -0
  20. package/templates/blog/src/routes/api/rss.ts +55 -0
  21. package/templates/blog/src/routes/blog/[slug].tsx +67 -0
  22. package/templates/blog/src/routes/blog/index.tsx +43 -0
  23. package/templates/blog/src/routes/index.tsx +52 -0
  24. package/templates/blog/tsconfig.json +16 -0
  25. package/templates/dashboard/.mcp.json +8 -0
  26. package/templates/dashboard/CLAUDE.md +50 -0
  27. package/templates/dashboard/index.html +16 -0
  28. package/templates/dashboard/public/favicon.svg +4 -0
  29. package/templates/dashboard/src/entry-client.ts +5 -0
  30. package/templates/dashboard/src/global.css +451 -0
  31. package/templates/dashboard/src/lib/auth.ts +106 -0
  32. package/templates/dashboard/src/lib/db.ts +118 -0
  33. package/templates/dashboard/src/routes/_layout.tsx +28 -0
  34. package/templates/dashboard/src/routes/api/signout.ts +15 -0
  35. package/templates/dashboard/src/routes/app/_layout.tsx +76 -0
  36. package/templates/dashboard/src/routes/app/dashboard.tsx +92 -0
  37. package/templates/dashboard/src/routes/app/invoices/[id].tsx +197 -0
  38. package/templates/dashboard/src/routes/app/invoices/index.tsx +61 -0
  39. package/templates/dashboard/src/routes/app/settings/account.tsx +31 -0
  40. package/templates/dashboard/src/routes/app/settings/billing.tsx +28 -0
  41. package/templates/dashboard/src/routes/app/settings/index.tsx +29 -0
  42. package/templates/dashboard/src/routes/app/users.tsx +50 -0
  43. package/templates/dashboard/src/routes/index.tsx +40 -0
  44. package/templates/dashboard/src/routes/login.tsx +79 -0
  45. package/templates/dashboard/src/routes/signup.tsx +78 -0
  46. package/templates/dashboard/tsconfig.json +16 -0
  47. package/lib/index.js.map +0 -1
  48. /package/templates/{default → app}/.mcp.json +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/index.tsx +0 -0
  61. /package/templates/{default → app}/src/routes/posts/[id].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,292 @@
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', 'Fira Code', ui-monospace, monospace;
14
+
15
+ --c-bg: #0a0a0b;
16
+ --c-surface: #141416;
17
+ --c-border: #27272a;
18
+ --c-text: #fafafa;
19
+ --c-text-secondary: #a1a1aa;
20
+ --c-text-muted: #71717a;
21
+ --c-accent: #6d5cff;
22
+ --c-accent-hover: #7d6fff;
23
+
24
+ --measure: 68ch;
25
+ --space-xs: 0.25rem;
26
+ --space-sm: 0.5rem;
27
+ --space-md: 1rem;
28
+ --space-lg: 1.5rem;
29
+ --space-xl: 2rem;
30
+ --space-2xl: 3rem;
31
+ --space-3xl: 5rem;
32
+
33
+ --radius-sm: 6px;
34
+ --radius-md: 10px;
35
+ }
36
+
37
+ [data-theme='light'] {
38
+ --c-bg: #ffffff;
39
+ --c-surface: #f4f4f5;
40
+ --c-border: #d4d4d8;
41
+ --c-text: #18181b;
42
+ --c-text-secondary: #3f3f46;
43
+ --c-text-muted: #71717a;
44
+ --c-accent: #5b4fd6;
45
+ --c-accent-hover: #4a3fb8;
46
+ }
47
+
48
+ html {
49
+ font-family: var(--font-sans);
50
+ background: var(--c-bg);
51
+ color: var(--c-text);
52
+ -webkit-font-smoothing: antialiased;
53
+ -moz-osx-font-smoothing: grayscale;
54
+ line-height: 1.6;
55
+ }
56
+
57
+ body {
58
+ min-height: 100vh;
59
+ display: flex;
60
+ flex-direction: column;
61
+ }
62
+
63
+ #app {
64
+ display: flex;
65
+ flex-direction: column;
66
+ flex: 1;
67
+ }
68
+
69
+ /* ─── Layout ─────────────────────────────────────────────────────────────── */
70
+
71
+ .site-header {
72
+ border-bottom: 1px solid var(--c-border);
73
+ padding: var(--space-md) var(--space-lg);
74
+ }
75
+
76
+ .site-header-inner {
77
+ max-width: 960px;
78
+ margin: 0 auto;
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: space-between;
82
+ gap: var(--space-lg);
83
+ }
84
+
85
+ .site-logo {
86
+ font-weight: 700;
87
+ font-size: 1.125rem;
88
+ letter-spacing: -0.02em;
89
+ color: var(--c-text);
90
+ text-decoration: none;
91
+ }
92
+
93
+ .site-nav {
94
+ display: flex;
95
+ gap: var(--space-lg);
96
+ align-items: center;
97
+ }
98
+
99
+ .site-nav a {
100
+ color: var(--c-text-secondary);
101
+ text-decoration: none;
102
+ font-size: 0.9375rem;
103
+ transition: color 150ms ease;
104
+ }
105
+
106
+ .site-nav a:hover,
107
+ .site-nav a.nav-active {
108
+ color: var(--c-text);
109
+ }
110
+
111
+ .site-main {
112
+ flex: 1;
113
+ padding: var(--space-2xl) var(--space-lg);
114
+ }
115
+
116
+ .site-main-inner {
117
+ max-width: var(--measure);
118
+ margin: 0 auto;
119
+ }
120
+
121
+ .site-footer {
122
+ border-top: 1px solid var(--c-border);
123
+ padding: var(--space-lg);
124
+ color: var(--c-text-muted);
125
+ font-size: 0.875rem;
126
+ text-align: center;
127
+ }
128
+
129
+ /* ─── Typography ─────────────────────────────────────────────────────────── */
130
+
131
+ h1,
132
+ h2,
133
+ h3,
134
+ h4 {
135
+ letter-spacing: -0.02em;
136
+ line-height: 1.2;
137
+ margin-bottom: var(--space-md);
138
+ }
139
+
140
+ h1 {
141
+ font-size: 2.5rem;
142
+ font-weight: 800;
143
+ }
144
+
145
+ h2 {
146
+ font-size: 1.75rem;
147
+ font-weight: 700;
148
+ margin-top: var(--space-2xl);
149
+ }
150
+
151
+ h3 {
152
+ font-size: 1.25rem;
153
+ font-weight: 600;
154
+ margin-top: var(--space-xl);
155
+ }
156
+
157
+ p {
158
+ margin-bottom: var(--space-md);
159
+ color: var(--c-text-secondary);
160
+ }
161
+
162
+ a {
163
+ color: var(--c-accent);
164
+ }
165
+
166
+ a:hover {
167
+ color: var(--c-accent-hover);
168
+ }
169
+
170
+ code {
171
+ font-family: var(--font-mono);
172
+ font-size: 0.875em;
173
+ background: var(--c-surface);
174
+ border: 1px solid var(--c-border);
175
+ border-radius: var(--radius-sm);
176
+ padding: 0.125em 0.375em;
177
+ }
178
+
179
+ pre {
180
+ font-family: var(--font-mono);
181
+ font-size: 0.875rem;
182
+ background: var(--c-surface);
183
+ border: 1px solid var(--c-border);
184
+ border-radius: var(--radius-md);
185
+ padding: var(--space-md);
186
+ margin-bottom: var(--space-lg);
187
+ overflow-x: auto;
188
+ }
189
+
190
+ pre code {
191
+ background: transparent;
192
+ border: 0;
193
+ padding: 0;
194
+ }
195
+
196
+ ul,
197
+ ol {
198
+ margin-bottom: var(--space-md);
199
+ padding-left: 1.5rem;
200
+ color: var(--c-text-secondary);
201
+ }
202
+
203
+ li {
204
+ margin-bottom: var(--space-xs);
205
+ }
206
+
207
+ blockquote {
208
+ border-left: 3px solid var(--c-accent);
209
+ padding-left: var(--space-md);
210
+ margin: var(--space-lg) 0;
211
+ color: var(--c-text-secondary);
212
+ font-style: italic;
213
+ }
214
+
215
+ img {
216
+ max-width: 100%;
217
+ border-radius: var(--radius-md);
218
+ margin: var(--space-lg) 0;
219
+ }
220
+
221
+ hr {
222
+ border: 0;
223
+ border-top: 1px solid var(--c-border);
224
+ margin: var(--space-2xl) 0;
225
+ }
226
+
227
+ /* ─── Post lists ─────────────────────────────────────────────────────────── */
228
+
229
+ .post-list {
230
+ list-style: none;
231
+ padding: 0;
232
+ margin: 0;
233
+ }
234
+
235
+ .post-list li {
236
+ margin-bottom: var(--space-xl);
237
+ padding-bottom: var(--space-xl);
238
+ border-bottom: 1px solid var(--c-border);
239
+ }
240
+
241
+ .post-list li:last-child {
242
+ border-bottom: 0;
243
+ }
244
+
245
+ .post-title {
246
+ font-size: 1.5rem;
247
+ font-weight: 700;
248
+ margin-bottom: var(--space-xs);
249
+ }
250
+
251
+ .post-title a {
252
+ color: var(--c-text);
253
+ text-decoration: none;
254
+ }
255
+
256
+ .post-title a:hover {
257
+ color: var(--c-accent);
258
+ }
259
+
260
+ .post-meta {
261
+ color: var(--c-text-muted);
262
+ font-size: 0.875rem;
263
+ margin-bottom: var(--space-sm);
264
+ }
265
+
266
+ .post-summary {
267
+ color: var(--c-text-secondary);
268
+ }
269
+
270
+ .tags {
271
+ display: flex;
272
+ gap: var(--space-xs);
273
+ flex-wrap: wrap;
274
+ margin-top: var(--space-sm);
275
+ }
276
+
277
+ .tag {
278
+ font-size: 0.75rem;
279
+ background: var(--c-surface);
280
+ border: 1px solid var(--c-border);
281
+ border-radius: var(--radius-sm);
282
+ padding: 0.125rem 0.5rem;
283
+ color: var(--c-text-secondary);
284
+ }
285
+
286
+ /* ─── Post detail ────────────────────────────────────────────────────────── */
287
+
288
+ .post-header {
289
+ margin-bottom: var(--space-2xl);
290
+ padding-bottom: var(--space-xl);
291
+ border-bottom: 1px solid var(--c-border);
292
+ }
@@ -0,0 +1,45 @@
1
+ import type { ComponentFn } from "@pyreon/core"
2
+
3
+ export interface PostMeta {
4
+ title: string
5
+ date: string
6
+ description: string
7
+ tags?: string[]
8
+ }
9
+
10
+ export interface Post extends PostMeta {
11
+ slug: string
12
+ Component: ComponentFn<unknown>
13
+ }
14
+
15
+ interface PostModule {
16
+ meta: PostMeta
17
+ default: ComponentFn<unknown>
18
+ }
19
+
20
+ /**
21
+ * Enumerate all `.tsx` posts in `src/content/posts/` at build time. Eagerly
22
+ * loads modules so SSG renders see frontmatter immediately. The slug is the
23
+ * filename without extension (`welcome.tsx` → `welcome`).
24
+ */
25
+ const modules = import.meta.glob<PostModule>("../content/posts/*.tsx", {
26
+ eager: true,
27
+ })
28
+
29
+ export const posts: Post[] = Object.entries(modules)
30
+ .map(([path, mod]) => {
31
+ const slug = path
32
+ .split("/")
33
+ .pop()!
34
+ .replace(/\.tsx$/, "")
35
+ return { ...mod.meta, slug, Component: mod.default }
36
+ })
37
+ .sort((a, b) => (a.date < b.date ? 1 : -1))
38
+
39
+ export function postBySlug(slug: string): Post | undefined {
40
+ return posts.find((p) => p.slug === slug)
41
+ }
42
+
43
+ export function postSlugs(): string[] {
44
+ return posts.map((p) => p.slug)
45
+ }
@@ -0,0 +1,40 @@
1
+ import { RouterView } from "@pyreon/router"
2
+ import { Link } from "@pyreon/zero/link"
3
+ import { ThemeToggle } from "@pyreon/zero/theme"
4
+
5
+ export function layout() {
6
+ return (
7
+ <>
8
+ <header class="site-header">
9
+ <div class="site-header-inner">
10
+ <Link href="/" class="site-logo">
11
+ Blog
12
+ </Link>
13
+ <nav class="site-nav">
14
+ <Link href="/" prefetch="hover" exactActiveClass="nav-active">
15
+ Home
16
+ </Link>
17
+ <Link href="/blog" prefetch="hover" activeClass="nav-active">
18
+ All posts
19
+ </Link>
20
+ <Link href="/about" prefetch="hover" exactActiveClass="nav-active">
21
+ About
22
+ </Link>
23
+ <a href="/api/rss" title="RSS feed">
24
+ RSS
25
+ </a>
26
+ <ThemeToggle />
27
+ </nav>
28
+ </div>
29
+ </header>
30
+
31
+ <main class="site-main">
32
+ <div class="site-main-inner">
33
+ <RouterView />
34
+ </div>
35
+ </main>
36
+
37
+ <footer class="site-footer">Built with Pyreon Zero.</footer>
38
+ </>
39
+ )
40
+ }
@@ -0,0 +1,28 @@
1
+ import { useHead } from "@pyreon/head"
2
+
3
+ export const meta = {
4
+ title: "About",
5
+ description: "About this blog.",
6
+ }
7
+
8
+ export default function About() {
9
+ useHead({
10
+ title: meta.title,
11
+ meta: [{ name: "description", content: meta.description }],
12
+ })
13
+
14
+ return (
15
+ <>
16
+ <h1>About</h1>
17
+ <p>
18
+ This is a Pyreon Zero blog. Edit <code>src/routes/about.tsx</code> to replace this
19
+ with your bio.
20
+ </p>
21
+ <p>
22
+ The blog renders statically — every page on this site is plain HTML on disk after{" "}
23
+ <code>bun run build</code>. Posts live in <code>src/content/posts/</code>; drop a new
24
+ TSX file there and it auto-appears in the listings and RSS feed.
25
+ </p>
26
+ </>
27
+ )
28
+ }
@@ -0,0 +1,55 @@
1
+ import { posts } from "../../lib/posts"
2
+
3
+ /**
4
+ * Site origin used in feed URLs. Set this to your deployed domain — RSS readers
5
+ * resolve relative links against it. The placeholder works locally and at any
6
+ * preview URL but should be updated before publishing.
7
+ */
8
+ const SITE_ORIGIN = "https://example.com"
9
+ const SITE_TITLE = "Blog"
10
+ const SITE_DESCRIPTION = "A Pyreon Zero blog."
11
+
12
+ function escape(s: string): string {
13
+ return s
14
+ .replace(/&/g, "&amp;")
15
+ .replace(/</g, "&lt;")
16
+ .replace(/>/g, "&gt;")
17
+ .replace(/"/g, "&quot;")
18
+ .replace(/'/g, "&apos;")
19
+ }
20
+
21
+ function rfc822(date: string): string {
22
+ // Accepts YYYY-MM-DD; outputs RFC 822 used by RSS readers
23
+ return new Date(`${date}T00:00:00Z`).toUTCString()
24
+ }
25
+
26
+ export function GET() {
27
+ const items = posts
28
+ .map((p) => {
29
+ const url = `${SITE_ORIGIN}/blog/${p.slug}`
30
+ return ` <item>
31
+ <title>${escape(p.title)}</title>
32
+ <link>${url}</link>
33
+ <guid>${url}</guid>
34
+ <pubDate>${rfc822(p.date)}</pubDate>
35
+ <description>${escape(p.description)}</description>
36
+ </item>`
37
+ })
38
+ .join("\n")
39
+
40
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
41
+ <rss version="2.0">
42
+ <channel>
43
+ <title>${escape(SITE_TITLE)}</title>
44
+ <link>${SITE_ORIGIN}</link>
45
+ <description>${escape(SITE_DESCRIPTION)}</description>
46
+ <language>en-us</language>
47
+ ${items}
48
+ </channel>
49
+ </rss>
50
+ `
51
+
52
+ return new Response(xml, {
53
+ headers: { "content-type": "application/rss+xml; charset=utf-8" },
54
+ })
55
+ }
@@ -0,0 +1,67 @@
1
+ import { useHead } from "@pyreon/head"
2
+ import { Link } from "@pyreon/zero/link"
3
+ import { useRoute } from "@pyreon/router"
4
+ import { postBySlug, postSlugs } from "../../lib/posts"
5
+
6
+ /**
7
+ * Tells the SSG plugin which slugs to pre-render. Without this, the dynamic
8
+ * route would only be reachable client-side at build time.
9
+ */
10
+ export const ssgPaths = () => postSlugs().map((slug) => `/blog/${slug}`)
11
+
12
+ export default function PostPage() {
13
+ // useRoute() returns an accessor — call it to read the resolved route.
14
+ const route = useRoute()
15
+ const slug = route().params.slug
16
+ const post = postBySlug(slug)
17
+
18
+ if (!post) {
19
+ return (
20
+ <>
21
+ <h1>Post not found</h1>
22
+ <p>
23
+ No post with slug <code>{slug}</code>.
24
+ </p>
25
+ <p>
26
+ <Link href="/blog">← Back to all posts</Link>
27
+ </p>
28
+ </>
29
+ )
30
+ }
31
+
32
+ useHead({
33
+ title: post.title,
34
+ meta: [
35
+ { name: "description", content: post.description },
36
+ { property: "og:title", content: post.title },
37
+ { property: "og:description", content: post.description },
38
+ { property: "og:type", content: "article" },
39
+ ],
40
+ })
41
+
42
+ const Body = post.Component
43
+
44
+ return (
45
+ <article>
46
+ <header class="post-header">
47
+ <h1>{post.title}</h1>
48
+ <div class="post-meta">{post.date}</div>
49
+ {post.tags && post.tags.length > 0 ? (
50
+ <div class="tags">
51
+ {post.tags.map((t) => (
52
+ <span class="tag">#{t}</span>
53
+ ))}
54
+ </div>
55
+ ) : null}
56
+ </header>
57
+
58
+ <Body />
59
+
60
+ <hr />
61
+
62
+ <p>
63
+ <Link href="/blog">← Back to all posts</Link>
64
+ </p>
65
+ </article>
66
+ )
67
+ }
@@ -0,0 +1,43 @@
1
+ import { useHead } from "@pyreon/head"
2
+ import { Link } from "@pyreon/zero/link"
3
+ import { posts } from "../../lib/posts"
4
+
5
+ export const meta = {
6
+ title: "All posts",
7
+ description: "Every post on this blog, newest first.",
8
+ }
9
+
10
+ export default function BlogIndex() {
11
+ useHead({
12
+ title: meta.title,
13
+ meta: [{ name: "description", content: meta.description }],
14
+ })
15
+
16
+ return (
17
+ <>
18
+ <h1>All posts</h1>
19
+ <p>{posts.length} posts in total.</p>
20
+
21
+ <ul class="post-list">
22
+ {posts.map((post) => (
23
+ <li>
24
+ <h2 class="post-title">
25
+ <Link href={`/blog/${post.slug}`} prefetch="hover">
26
+ {post.title}
27
+ </Link>
28
+ </h2>
29
+ <div class="post-meta">{post.date}</div>
30
+ <p class="post-summary">{post.description}</p>
31
+ {post.tags && post.tags.length > 0 ? (
32
+ <div class="tags">
33
+ {post.tags.map((t) => (
34
+ <span class="tag">#{t}</span>
35
+ ))}
36
+ </div>
37
+ ) : null}
38
+ </li>
39
+ ))}
40
+ </ul>
41
+ </>
42
+ )
43
+ }
@@ -0,0 +1,52 @@
1
+ import { useHead } from "@pyreon/head"
2
+ import { Link } from "@pyreon/zero/link"
3
+ import { posts } from "../lib/posts"
4
+
5
+ export const meta = {
6
+ title: "Blog",
7
+ description: "A statically-rendered Pyreon Zero blog.",
8
+ }
9
+
10
+ export default function Home() {
11
+ useHead({
12
+ title: meta.title,
13
+ meta: [{ name: "description", content: meta.description }],
14
+ })
15
+
16
+ // Show the 5 most recent posts on the homepage
17
+ const recent = posts.slice(0, 5)
18
+
19
+ return (
20
+ <>
21
+ <h1>Blog</h1>
22
+ <p>Recent writing, statically rendered. Subscribe via <a href="/api/rss">RSS</a>.</p>
23
+
24
+ <ul class="post-list">
25
+ {recent.map((post) => (
26
+ <li>
27
+ <h2 class="post-title">
28
+ <Link href={`/blog/${post.slug}`} prefetch="hover">
29
+ {post.title}
30
+ </Link>
31
+ </h2>
32
+ <div class="post-meta">{post.date}</div>
33
+ <p class="post-summary">{post.description}</p>
34
+ {post.tags && post.tags.length > 0 ? (
35
+ <div class="tags">
36
+ {post.tags.map((t) => (
37
+ <span class="tag">#{t}</span>
38
+ ))}
39
+ </div>
40
+ ) : null}
41
+ </li>
42
+ ))}
43
+ </ul>
44
+
45
+ {posts.length > recent.length ? (
46
+ <p>
47
+ <Link href="/blog">View all {posts.length} posts →</Link>
48
+ </p>
49
+ ) : null}
50
+ </>
51
+ )
52
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "preserve",
7
+ "jsxImportSource": "@pyreon/core",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "esModuleInterop": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "types": ["bun", "vite/client"]
14
+ },
15
+ "include": ["src", "env.d.ts"]
16
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "pyreon": {
4
+ "command": "bunx",
5
+ "args": ["@pyreon/mcp"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,50 @@
1
+ # Dashboard (Pyreon Zero)
2
+
3
+ A SaaS-shape application starter. Marketing site at `/`, auth-gated app under `/app/*`, with a built-in invoice export demo using `@pyreon/document-primitives` — the same component tree renders in the browser AND exports to PDF/email.
4
+
5
+ ## Reactivity (Pyreon, not React)
6
+
7
+ - `signal()` not `useState`; `computed()` not `useMemo`; `effect()` not `useEffect`.
8
+ - Write signals via `signal.set(value)` or `signal.update(fn)`.
9
+ - In JSX, signals auto-call: `{count}` (compiler inserts `()`).
10
+
11
+ ## JSX
12
+
13
+ - `class=` not `className`; `for=` not `htmlFor`; camelCase events.
14
+
15
+ ## Auth + DB
16
+
17
+ `src/lib/auth.ts` and `src/lib/db.ts` hold the auth + data layer. They
18
+ ship with **in-memory implementations** so the dashboard runs out of the
19
+ box on a fresh clone — every dev-server restart wipes the data.
20
+
21
+ To wire a real backend, run:
22
+
23
+ ```bash
24
+ bunx create-pyreon-app --template dashboard --integrations supabase,email
25
+ ```
26
+
27
+ The scaffolder overwrites `auth.ts` + `db.ts` with Supabase-backed
28
+ implementations and writes `src/lib/email.ts` + `src/emails/welcome.tsx`
29
+ for Resend. The exported function signatures stay identical, so no route
30
+ files need to change. On an existing project, you can run the same
31
+ scaffolder flags or hand-edit the two files — the surface is small.
32
+
33
+ ## Routes
34
+
35
+ - `/` — marketing landing
36
+ - `/login`, `/signup` — auth forms
37
+ - `/app/dashboard` — overview cards
38
+ - `/app/users` — table view of users
39
+ - `/app/invoices` — invoice list
40
+ - `/app/invoices/:id` — invoice detail with **"Export to PDF" / "Send email"** buttons (the headline demo)
41
+ - `/app/settings/*` — account / profile / billing settings
42
+
43
+ ## Commands
44
+
45
+ ```bash
46
+ bun run dev # dev server
47
+ bun run build # production build
48
+ bun run preview # serve build
49
+ bun run doctor # check for React patterns
50
+ ```