@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.
- 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/{default → app}/src/routes/posts/[id].tsx +14 -0
- 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 +73 -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 +214 -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/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,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, "&")
|
|
15
|
+
.replace(/</g, "<")
|
|
16
|
+
.replace(/>/g, ">")
|
|
17
|
+
.replace(/"/g, """)
|
|
18
|
+
.replace(/'/g, "'")
|
|
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,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>
|