@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,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,73 @@
1
+ import type { GetStaticPaths } from "@pyreon/zero/server"
2
+ import { useHead } from "@pyreon/head"
3
+ import { Link } from "@pyreon/zero/link"
4
+ import { useRoute } from "@pyreon/router"
5
+ import { postBySlug, postSlugs } from "../../lib/posts"
6
+
7
+ /**
8
+ * Enumerate the dynamic `:slug` values at build time. The SSG plugin expands
9
+ * `/blog/:slug` × this list into one prerendered HTML file per post
10
+ * (`dist/blog/<slug>/index.html`). Without this export the dynamic route
11
+ * is silently skipped during SSG auto-detect — only the static `/blog`
12
+ * index would be prerendered, and `pyreon doctor --check-ssg` warns
13
+ * about it.
14
+ */
15
+ export const getStaticPaths: GetStaticPaths<{ slug: string }> = () =>
16
+ postSlugs().map((slug) => ({ params: { slug } }))
17
+
18
+ export default function PostPage() {
19
+ // useRoute() returns an accessor — call it to read the resolved route.
20
+ const route = useRoute()
21
+ const slug = route().params.slug
22
+ const post = postBySlug(slug)
23
+
24
+ if (!post) {
25
+ return (
26
+ <>
27
+ <h1>Post not found</h1>
28
+ <p>
29
+ No post with slug <code>{slug}</code>.
30
+ </p>
31
+ <p>
32
+ <Link href="/blog">← Back to all posts</Link>
33
+ </p>
34
+ </>
35
+ )
36
+ }
37
+
38
+ useHead({
39
+ title: post.title,
40
+ meta: [
41
+ { name: "description", content: post.description },
42
+ { property: "og:title", content: post.title },
43
+ { property: "og:description", content: post.description },
44
+ { property: "og:type", content: "article" },
45
+ ],
46
+ })
47
+
48
+ const Body = post.Component
49
+
50
+ return (
51
+ <article>
52
+ <header class="post-header">
53
+ <h1>{post.title}</h1>
54
+ <div class="post-meta">{post.date}</div>
55
+ {post.tags && post.tags.length > 0 ? (
56
+ <div class="tags">
57
+ {post.tags.map((t) => (
58
+ <span class="tag">#{t}</span>
59
+ ))}
60
+ </div>
61
+ ) : null}
62
+ </header>
63
+
64
+ <Body />
65
+
66
+ <hr />
67
+
68
+ <p>
69
+ <Link href="/blog">← Back to all posts</Link>
70
+ </p>
71
+ </article>
72
+ )
73
+ }
@@ -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
+ ```
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#0a0a0b" />
7
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8
+ <script>(()=> {try{var t=localStorage.getItem("dashboard-theme");var r=t==="light"?"light":t==="dark"?"dark":window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light";document.documentElement.dataset.theme=r}catch(e){}})()</script>
9
+ <!--pyreon-head-->
10
+ </head>
11
+ <body>
12
+ <div id="app"><!--pyreon-app--></div>
13
+ <!--pyreon-scripts-->
14
+ <script type="module" src="/src/entry-client.ts"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="6" fill="#6d5cff"/>
3
+ <path d="M8 22l5-12 5 8 6-4" stroke="#fafafa" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
4
+ </svg>
@@ -0,0 +1,5 @@
1
+ import "./global.css"
2
+ import { routes } from "virtual:zero/routes"
3
+ import { startClient } from "@pyreon/zero/client"
4
+
5
+ startClient({ routes })