@beatzball/create-litro 0.1.3 → 0.2.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 (94) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/dist/recipes/fullstack/template/app.ts +11 -8
  3. package/dist/recipes/fullstack/template/pages/blog/[slug].ts +4 -5
  4. package/dist/recipes/fullstack/template/pages/index.ts +4 -5
  5. package/dist/recipes/starlight/recipe.config.d.ts +4 -0
  6. package/dist/recipes/starlight/recipe.config.d.ts.map +1 -0
  7. package/dist/recipes/starlight/recipe.config.js +9 -0
  8. package/dist/recipes/starlight/recipe.config.js.map +1 -0
  9. package/dist/recipes/starlight/recipe.config.ts +11 -0
  10. package/dist/recipes/starlight/template/_data/metadata.js +10 -0
  11. package/dist/recipes/starlight/template/app.ts +18 -0
  12. package/dist/recipes/starlight/template/content/blog/.11tydata.json +1 -0
  13. package/dist/recipes/starlight/template/content/blog/release-notes.md +44 -0
  14. package/dist/recipes/starlight/template/content/blog/welcome.md +44 -0
  15. package/dist/recipes/starlight/template/content/docs/.11tydata.json +1 -0
  16. package/dist/recipes/starlight/template/content/docs/configuration.md +77 -0
  17. package/dist/recipes/starlight/template/content/docs/getting-started.md +53 -0
  18. package/dist/recipes/starlight/template/content/docs/guides-deploying.md +79 -0
  19. package/dist/recipes/starlight/template/content/docs/guides-first-page.md +64 -0
  20. package/dist/recipes/starlight/template/content/docs/installation.md +54 -0
  21. package/dist/recipes/starlight/template/litro.recipe.json +7 -0
  22. package/dist/recipes/starlight/template/nitro.config.ts +57 -0
  23. package/dist/recipes/starlight/template/package.json +26 -0
  24. package/dist/recipes/starlight/template/pages/blog/[slug].ts +125 -0
  25. package/dist/recipes/starlight/template/pages/blog/index.ts +114 -0
  26. package/dist/recipes/starlight/template/pages/blog/tags/[tag].ts +110 -0
  27. package/dist/recipes/starlight/template/pages/docs/[slug].ts +147 -0
  28. package/dist/recipes/starlight/template/pages/index.ts +135 -0
  29. package/dist/recipes/starlight/template/public/styles/starlight.css +215 -0
  30. package/dist/recipes/starlight/template/server/middleware/vite-dev.ts +29 -0
  31. package/dist/recipes/starlight/template/server/routes/[...].ts +57 -0
  32. package/dist/recipes/starlight/template/server/starlight.config.js +29 -0
  33. package/dist/recipes/starlight/template/src/components/sl-aside.ts +91 -0
  34. package/dist/recipes/starlight/template/src/components/sl-badge.ts +76 -0
  35. package/dist/recipes/starlight/template/src/components/sl-card-grid.ts +34 -0
  36. package/dist/recipes/starlight/template/src/components/sl-card.ts +91 -0
  37. package/dist/recipes/starlight/template/src/components/sl-tab-item.ts +35 -0
  38. package/dist/recipes/starlight/template/src/components/sl-tabs.ts +108 -0
  39. package/dist/recipes/starlight/template/src/components/starlight-header.ts +152 -0
  40. package/dist/recipes/starlight/template/src/components/starlight-page.ts +168 -0
  41. package/dist/recipes/starlight/template/src/components/starlight-sidebar.ts +125 -0
  42. package/dist/recipes/starlight/template/src/components/starlight-toc.ts +133 -0
  43. package/dist/recipes/starlight/template/src/date-utils.ts +20 -0
  44. package/dist/recipes/starlight/template/src/extract-headings.ts +68 -0
  45. package/dist/recipes/starlight/template/src/route-meta.ts +16 -0
  46. package/dist/recipes/starlight/template/tsconfig.json +14 -0
  47. package/dist/recipes/starlight/template/vite.config.ts +19 -0
  48. package/dist/src/scaffold.test.js +134 -0
  49. package/dist/src/scaffold.test.js.map +1 -1
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +1 -1
  52. package/recipes/fullstack/template/app.ts +11 -8
  53. package/recipes/fullstack/template/pages/blog/[slug].ts +4 -5
  54. package/recipes/fullstack/template/pages/index.ts +4 -5
  55. package/recipes/starlight/recipe.config.ts +11 -0
  56. package/recipes/starlight/template/_data/metadata.js +10 -0
  57. package/recipes/starlight/template/app.ts +18 -0
  58. package/recipes/starlight/template/content/blog/.11tydata.json +1 -0
  59. package/recipes/starlight/template/content/blog/release-notes.md +44 -0
  60. package/recipes/starlight/template/content/blog/welcome.md +44 -0
  61. package/recipes/starlight/template/content/docs/.11tydata.json +1 -0
  62. package/recipes/starlight/template/content/docs/configuration.md +77 -0
  63. package/recipes/starlight/template/content/docs/getting-started.md +53 -0
  64. package/recipes/starlight/template/content/docs/guides-deploying.md +79 -0
  65. package/recipes/starlight/template/content/docs/guides-first-page.md +64 -0
  66. package/recipes/starlight/template/content/docs/installation.md +54 -0
  67. package/recipes/starlight/template/litro.recipe.json +7 -0
  68. package/recipes/starlight/template/nitro.config.ts +57 -0
  69. package/recipes/starlight/template/package.json +26 -0
  70. package/recipes/starlight/template/pages/blog/[slug].ts +125 -0
  71. package/recipes/starlight/template/pages/blog/index.ts +114 -0
  72. package/recipes/starlight/template/pages/blog/tags/[tag].ts +110 -0
  73. package/recipes/starlight/template/pages/docs/[slug].ts +147 -0
  74. package/recipes/starlight/template/pages/index.ts +135 -0
  75. package/recipes/starlight/template/public/styles/starlight.css +215 -0
  76. package/recipes/starlight/template/server/middleware/vite-dev.ts +29 -0
  77. package/recipes/starlight/template/server/routes/[...].ts +57 -0
  78. package/recipes/starlight/template/server/starlight.config.js +29 -0
  79. package/recipes/starlight/template/src/components/sl-aside.ts +91 -0
  80. package/recipes/starlight/template/src/components/sl-badge.ts +76 -0
  81. package/recipes/starlight/template/src/components/sl-card-grid.ts +34 -0
  82. package/recipes/starlight/template/src/components/sl-card.ts +91 -0
  83. package/recipes/starlight/template/src/components/sl-tab-item.ts +35 -0
  84. package/recipes/starlight/template/src/components/sl-tabs.ts +108 -0
  85. package/recipes/starlight/template/src/components/starlight-header.ts +152 -0
  86. package/recipes/starlight/template/src/components/starlight-page.ts +168 -0
  87. package/recipes/starlight/template/src/components/starlight-sidebar.ts +125 -0
  88. package/recipes/starlight/template/src/components/starlight-toc.ts +133 -0
  89. package/recipes/starlight/template/src/date-utils.ts +20 -0
  90. package/recipes/starlight/template/src/extract-headings.ts +68 -0
  91. package/recipes/starlight/template/src/route-meta.ts +16 -0
  92. package/recipes/starlight/template/tsconfig.json +14 -0
  93. package/recipes/starlight/template/vite.config.ts +19 -0
  94. package/src/scaffold.test.ts +148 -0
@@ -0,0 +1,110 @@
1
+ import { html } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import { LitroPage } from '@beatzball/litro/runtime';
4
+ import { definePageData } from '@beatzball/litro';
5
+ import type { Post } from 'litro:content';
6
+ import { getPosts } from 'litro:content';
7
+ import { siteConfig } from '../../../server/starlight.config.js';
8
+ import { starlightHead } from '../../../src/route-meta.js';
9
+ import { formatDate, isoDate } from '../../../src/date-utils.js';
10
+
11
+ // Register components used in render()
12
+ import '../../../src/components/starlight-header.js';
13
+
14
+ export interface TagPageData {
15
+ tag: string;
16
+ posts: Post[];
17
+ siteTitle: string;
18
+ nav: typeof siteConfig.nav;
19
+ }
20
+
21
+ export const pageData = definePageData(async (event) => {
22
+ const tag = event.context.params?.tag ?? '';
23
+ const all = await getPosts({ tag });
24
+ // Filter to only blog posts (docs might have unexpected tags)
25
+ const posts = all.filter(p => p.url.startsWith('/content/blog/'));
26
+ return {
27
+ tag,
28
+ posts,
29
+ siteTitle: siteConfig.title,
30
+ nav: siteConfig.nav,
31
+ } satisfies TagPageData;
32
+ });
33
+
34
+ export async function generateRoutes(): Promise<string[]> {
35
+ const all = await getPosts();
36
+ const blogPosts = all.filter(p => p.url.startsWith('/content/blog/'));
37
+ const tags = [...new Set(blogPosts.flatMap(p => p.tags))].sort();
38
+ return tags.map(tag => `/blog/tags/${tag}`);
39
+ }
40
+
41
+ export const routeMeta = {
42
+ head: starlightHead,
43
+ title: 'Tags — {{projectName}}',
44
+ };
45
+
46
+ @customElement('page-blog-tags-tag')
47
+ export class TagPage extends LitroPage {
48
+ override render() {
49
+ const data = this.serverData as TagPageData | null;
50
+ const { tag = '', posts = [], siteTitle = '{{projectName}}', nav = [] } = data ?? {};
51
+
52
+ return html`
53
+ <div style="min-height:100vh;display:flex;flex-direction:column;">
54
+ <starlight-header
55
+ siteTitle="${siteTitle}"
56
+ .nav="${nav}"
57
+ currentPath="/blog/tags/${tag}"
58
+ ></starlight-header>
59
+ <main style="
60
+ flex:1;
61
+ max-width:56rem;
62
+ margin:0 auto;
63
+ padding:var(--sl-content-pad-y,2rem) var(--sl-content-pad-x,1.5rem);
64
+ width:100%;
65
+ ">
66
+ <h1 style="font-size:var(--sl-text-4xl);font-weight:700;margin:0 0 2rem;">
67
+ Posts tagged: <span style="color:var(--sl-color-accent);">#${tag}</span>
68
+ </h1>
69
+
70
+ ${posts.length === 0 ? html`
71
+ <p style="color:var(--sl-color-gray-4);">No posts found for this tag.</p>
72
+ ` : html`
73
+ <ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:1.5rem;">
74
+ ${posts.map(post => {
75
+ const blogSlug = post.url.slice('/content/blog/'.length);
76
+ return html`
77
+ <li style="border-bottom:1px solid var(--sl-color-border);padding-bottom:1.5rem;">
78
+ <a href="/blog/${blogSlug}" style="
79
+ display:block;
80
+ font-size:var(--sl-text-xl);
81
+ font-weight:600;
82
+ color:var(--sl-color-text);
83
+ text-decoration:none;
84
+ margin-bottom:0.3rem;
85
+ ">${post.title}</a>
86
+ <time
87
+ datetime="${isoDate(post.date)}"
88
+ style="font-size:var(--sl-text-sm);color:var(--sl-color-gray-4);"
89
+ >${formatDate(post.date)}</time>
90
+ ${post.description ? html`
91
+ <p style="margin:0.4rem 0 0;color:var(--sl-color-gray-5);">${post.description}</p>
92
+ ` : ''}
93
+ </li>
94
+ `;
95
+ })}
96
+ </ul>
97
+ `}
98
+
99
+ <p style="margin-top:2rem;">
100
+ <a href="/blog" style="font-size:var(--sl-text-sm);color:var(--sl-color-accent);text-decoration:none;">
101
+ ← All Posts
102
+ </a>
103
+ </p>
104
+ </main>
105
+ </div>
106
+ `;
107
+ }
108
+ }
109
+
110
+ export default TagPage;
@@ -0,0 +1,147 @@
1
+ import { html } from 'lit';
2
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
3
+ import { customElement } from 'lit/decorators.js';
4
+ import { LitroPage } from '@beatzball/litro/runtime';
5
+ import { definePageData } from '@beatzball/litro';
6
+ import { createError } from 'h3';
7
+ import type { Post } from 'litro:content';
8
+ import { getPosts } from 'litro:content';
9
+ import { siteConfig } from '../../server/starlight.config.js';
10
+ import { extractHeadings, addHeadingIds } from '../../src/extract-headings.js';
11
+ import { starlightHead } from '../../src/route-meta.js';
12
+
13
+ // Register components used in render()
14
+ import '../../src/components/starlight-page.js';
15
+
16
+ export interface DocPageData {
17
+ doc: Post;
18
+ body: string;
19
+ toc: Array<{ depth: number; text: string; slug: string }>;
20
+ sidebar: typeof siteConfig.sidebar;
21
+ siteTitle: string;
22
+ currentSlug: string;
23
+ prevDoc: { label: string; href: string } | null;
24
+ nextDoc: { label: string; href: string } | null;
25
+ nav: typeof siteConfig.nav;
26
+ editUrl: string | null;
27
+ }
28
+
29
+ function computePrevNext(
30
+ sidebar: typeof siteConfig.sidebar,
31
+ currentSlug: string,
32
+ ): { prevDoc: DocPageData['prevDoc']; nextDoc: DocPageData['nextDoc'] } {
33
+ const flat = sidebar.flatMap(g => g.items);
34
+ const idx = flat.findIndex(item => item.slug === currentSlug);
35
+ return {
36
+ prevDoc: idx > 0
37
+ ? { label: flat[idx - 1].label, href: `/docs/${flat[idx - 1].slug}` }
38
+ : null,
39
+ nextDoc: idx < flat.length - 1
40
+ ? { label: flat[idx + 1].label, href: `/docs/${flat[idx + 1].slug}` }
41
+ : null,
42
+ };
43
+ }
44
+
45
+ export const pageData = definePageData(async (event) => {
46
+ const slug = event.context.params?.slug ?? '';
47
+
48
+ // Content URLs are /content/docs/<slug> (contentDir = 'content', so
49
+ // dirname is project root, and paths include the 'content/' prefix in the URL).
50
+ const posts = await getPosts();
51
+ const doc = posts.find(p => p.url === `/content/docs/${slug}`);
52
+
53
+ if (!doc) {
54
+ throw createError({ statusCode: 404, message: `Doc not found: ${slug}` });
55
+ }
56
+
57
+ const toc = extractHeadings(doc.rawBody);
58
+ const body = addHeadingIds(doc.body);
59
+ const { prevDoc, nextDoc } = computePrevNext(siteConfig.sidebar, slug);
60
+ const editUrl = siteConfig.editUrlBase
61
+ ? `${siteConfig.editUrlBase}/content/docs/${slug}.md`
62
+ : null;
63
+
64
+ return {
65
+ doc,
66
+ body,
67
+ toc,
68
+ sidebar: siteConfig.sidebar,
69
+ siteTitle: siteConfig.title,
70
+ currentSlug: slug,
71
+ prevDoc,
72
+ nextDoc,
73
+ nav: siteConfig.nav,
74
+ editUrl,
75
+ } satisfies DocPageData;
76
+ });
77
+
78
+ export async function generateRoutes(): Promise<string[]> {
79
+ const posts = await getPosts();
80
+ return posts
81
+ .filter(p => p.url.startsWith('/content/docs/'))
82
+ .map(p => '/docs' + p.url.slice('/content/docs'.length));
83
+ }
84
+
85
+ export const routeMeta = {
86
+ head: starlightHead,
87
+ title: 'Docs — {{projectName}}',
88
+ };
89
+
90
+ @customElement('page-docs-slug')
91
+ export class DocPage extends LitroPage {
92
+ override render() {
93
+ const data = this.serverData as DocPageData | null;
94
+ if (!data?.doc) return html`<p>Loading&hellip;</p>`;
95
+
96
+ return html`
97
+ <starlight-page
98
+ siteTitle="${data.siteTitle}"
99
+ pageTitle="${data.doc.title}"
100
+ .nav="${data.nav}"
101
+ .sidebar="${data.sidebar}"
102
+ .toc="${data.toc}"
103
+ currentSlug="${data.currentSlug}"
104
+ currentPath="/docs/${data.currentSlug}"
105
+ >
106
+ <div slot="content">
107
+ <!-- unsafeHTML renders the Markdown-generated HTML directly.
108
+ The content/docs directory is trusted-author-only; do not place
109
+ user-submitted or untrusted content here without sanitizing. -->
110
+ ${unsafeHTML(data.body)}
111
+
112
+ ${data.prevDoc || data.nextDoc ? html`
113
+ <nav style="
114
+ display:flex;
115
+ justify-content:space-between;
116
+ padding-top:2rem;
117
+ margin-top:2rem;
118
+ border-top:1px solid var(--sl-color-border);
119
+ font-size:var(--sl-text-sm);
120
+ " aria-label="Previous and next pages">
121
+ ${data.prevDoc ? html`
122
+ <a href="${data.prevDoc.href}" style="color:var(--sl-color-accent);text-decoration:none;">
123
+ ← ${data.prevDoc.label}
124
+ </a>
125
+ ` : html`<span></span>`}
126
+ ${data.nextDoc ? html`
127
+ <a href="${data.nextDoc.href}" style="color:var(--sl-color-accent);text-decoration:none;">
128
+ ${data.nextDoc.label} →
129
+ </a>
130
+ ` : ''}
131
+ </nav>
132
+ ` : ''}
133
+
134
+ ${data.editUrl ? html`
135
+ <p style="margin-top:1.5rem;font-size:var(--sl-text-xs);color:var(--sl-color-gray-4);">
136
+ <a href="${data.editUrl}" style="color:var(--sl-color-accent);" target="_blank" rel="noopener">
137
+ Edit this page
138
+ </a>
139
+ </p>
140
+ ` : ''}
141
+ </div>
142
+ </starlight-page>
143
+ `;
144
+ }
145
+ }
146
+
147
+ export default DocPage;
@@ -0,0 +1,135 @@
1
+ import { html } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import { LitroPage } from '@beatzball/litro/runtime';
4
+ import { definePageData } from '@beatzball/litro';
5
+ import { getGlobalData } from 'litro:content';
6
+ import { siteConfig } from '../server/starlight.config.js';
7
+ import { starlightHead } from '../src/route-meta.js';
8
+
9
+ // Register components used in render()
10
+ import '../src/components/starlight-header.js';
11
+ import '../src/components/sl-card.js';
12
+ import '../src/components/sl-card-grid.js';
13
+
14
+ export interface SplashData {
15
+ siteTitle: string;
16
+ description: string;
17
+ nav: Array<{ label: string; href: string }>;
18
+ features: Array<{ title: string; description: string; icon?: string }>;
19
+ }
20
+
21
+ export const pageData = definePageData(async (_event) => {
22
+ const metadata = await getGlobalData();
23
+ return {
24
+ siteTitle: String(metadata.title ?? siteConfig.title),
25
+ description: String(metadata.description ?? siteConfig.description),
26
+ nav: siteConfig.nav,
27
+ features: [
28
+ {
29
+ icon: '📄',
30
+ title: 'Docs',
31
+ description: 'Structured documentation with sidebar, TOC, and prev/next navigation.',
32
+ },
33
+ {
34
+ icon: '✍️',
35
+ title: 'Blog',
36
+ description: 'Write posts in Markdown. Tags, dates, and listing pages auto-generated.',
37
+ },
38
+ {
39
+ icon: '🎨',
40
+ title: 'Theming',
41
+ description: 'Light and dark mode via CSS custom properties. Zero JavaScript required.',
42
+ },
43
+ {
44
+ icon: '⚡',
45
+ title: 'Static',
46
+ description: 'Pre-rendered to plain HTML. Deploy to any CDN with no server required.',
47
+ },
48
+ ],
49
+ } satisfies SplashData;
50
+ });
51
+
52
+ export const routeMeta = {
53
+ head: starlightHead,
54
+ title: '{{projectName}}',
55
+ };
56
+
57
+ @customElement('page-home')
58
+ export class SplashPage extends LitroPage {
59
+ override render() {
60
+ const data = this.serverData as SplashData | null;
61
+ const { siteTitle = '{{projectName}}', description = '', nav = [], features = [] } = data ?? {};
62
+
63
+ return html`
64
+ <div style="min-height:100vh;display:flex;flex-direction:column;">
65
+ <starlight-header
66
+ siteTitle="${siteTitle}"
67
+ .nav="${nav}"
68
+ currentPath="/"
69
+ ></starlight-header>
70
+ <main style="
71
+ flex:1;
72
+ max-width:56rem;
73
+ margin:0 auto;
74
+ padding:4rem 1.5rem 3rem;
75
+ width:100%;
76
+ ">
77
+ <section style="text-align:center;margin-bottom:4rem;">
78
+ <h1 style="
79
+ font-size:clamp(2rem,5vw,3.5rem);
80
+ font-weight:800;
81
+ color:var(--sl-color-text);
82
+ margin:0 0 1rem;
83
+ line-height:1.1;
84
+ ">${siteTitle}</h1>
85
+ ${description ? html`
86
+ <p style="
87
+ font-size:var(--sl-text-xl);
88
+ color:var(--sl-color-gray-4);
89
+ max-width:36rem;
90
+ margin:0 auto 2.5rem;
91
+ line-height:1.6;
92
+ ">${description}</p>
93
+ ` : ''}
94
+ <div style="display:flex;gap:1rem;justify-content:center;flex-wrap:wrap;">
95
+ <a href="/docs/getting-started" style="
96
+ display:inline-block;
97
+ padding:0.6rem 1.5rem;
98
+ background:var(--sl-color-accent);
99
+ color:var(--sl-color-text-invert,#fff);
100
+ border-radius:var(--sl-border-radius);
101
+ font-weight:600;
102
+ text-decoration:none;
103
+ font-size:var(--sl-text-base);
104
+ ">Get Started</a>
105
+ <a href="/blog" style="
106
+ display:inline-block;
107
+ padding:0.6rem 1.5rem;
108
+ border:1px solid var(--sl-color-border);
109
+ color:var(--sl-color-text);
110
+ border-radius:var(--sl-border-radius);
111
+ font-weight:600;
112
+ text-decoration:none;
113
+ font-size:var(--sl-text-base);
114
+ ">Blog</a>
115
+ </div>
116
+ </section>
117
+
118
+ <section>
119
+ <sl-card-grid>
120
+ ${features.map(f => html`
121
+ <sl-card
122
+ icon="${f.icon ?? ''}"
123
+ title="${f.title}"
124
+ description="${f.description}"
125
+ ></sl-card>
126
+ `)}
127
+ </sl-card-grid>
128
+ </section>
129
+ </main>
130
+ </div>
131
+ `;
132
+ }
133
+ }
134
+
135
+ export default SplashPage;
@@ -0,0 +1,215 @@
1
+ /* Starlight design tokens — light mode defaults */
2
+ :root {
3
+ --sl-color-white: #fff;
4
+ --sl-color-black: #23262f;
5
+ --sl-color-gray-1: #f6f6f6;
6
+ --sl-color-gray-2: #e8e8e8;
7
+ --sl-color-gray-3: #bdbdbd;
8
+ --sl-color-gray-4: #757575;
9
+ --sl-color-gray-5: #4b4b4b;
10
+ --sl-color-gray-6: #2a2a2a;
11
+
12
+ --sl-color-accent: #7c3aed;
13
+ --sl-color-accent-high: #5b21b6;
14
+ --sl-color-accent-low: #ede9fe;
15
+
16
+ --sl-color-note: #1d4ed8;
17
+ --sl-color-tip: #15803d;
18
+ --sl-color-caution: #b45309;
19
+ --sl-color-danger: #b91c1c;
20
+
21
+ --sl-color-text: var(--sl-color-black);
22
+ --sl-color-text-accent: var(--sl-color-accent);
23
+ --sl-color-text-invert: var(--sl-color-white);
24
+ --sl-color-bg: var(--sl-color-white);
25
+ --sl-color-bg-sidebar: var(--sl-color-gray-1);
26
+ --sl-color-bg-nav: var(--sl-color-white);
27
+ --sl-color-bg-inline-code: var(--sl-color-gray-2);
28
+ --sl-color-border: var(--sl-color-gray-2);
29
+
30
+ --sl-font-sans: ui-sans-serif, system-ui, -apple-system, sans-serif;
31
+ --sl-font-mono: ui-monospace, "Cascadia Code", "Fira Code", monospace;
32
+
33
+ --sl-text-xs: 0.75rem;
34
+ --sl-text-sm: 0.875rem;
35
+ --sl-text-base: 1rem;
36
+ --sl-text-lg: 1.125rem;
37
+ --sl-text-xl: 1.25rem;
38
+ --sl-text-2xl: 1.5rem;
39
+ --sl-text-3xl: 1.875rem;
40
+ --sl-text-4xl: 2.25rem;
41
+
42
+ --sl-nav-height: 3.5rem;
43
+ --sl-sidebar-width: 16rem;
44
+ --sl-toc-width: 14rem;
45
+ --sl-content-width: 48rem;
46
+ --sl-content-pad-x: 1.5rem;
47
+ --sl-content-pad-y: 2rem;
48
+
49
+ --sl-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
50
+ --sl-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
51
+
52
+ --sl-border-radius: 0.375rem;
53
+ --sl-border-radius-sm: 0.25rem;
54
+ }
55
+
56
+ /* Dark mode overrides */
57
+ [data-theme='dark'] {
58
+ --sl-color-text: #f0f0f0;
59
+ --sl-color-bg: #17181c;
60
+ --sl-color-bg-sidebar: #1e1f24;
61
+ --sl-color-bg-nav: #17181c;
62
+ --sl-color-bg-inline-code: #2a2b30;
63
+ --sl-color-border: #2e2f35;
64
+
65
+ --sl-color-gray-1: #2a2b30;
66
+ --sl-color-gray-2: #35363d;
67
+ --sl-color-gray-3: #5a5b63;
68
+ --sl-color-gray-4: #8a8b94;
69
+ --sl-color-gray-5: #c0c1cb;
70
+ --sl-color-gray-6: #e8e9f0;
71
+
72
+ --sl-color-accent: #a78bfa;
73
+ --sl-color-accent-high: #c4b5fd;
74
+ --sl-color-accent-low: #2e1b54;
75
+
76
+ --sl-color-note: #60a5fa;
77
+ --sl-color-tip: #4ade80;
78
+ --sl-color-caution: #fbbf24;
79
+ --sl-color-danger: #f87171;
80
+ }
81
+
82
+ /* Base reset */
83
+ *, *::before, *::after {
84
+ box-sizing: border-box;
85
+ }
86
+
87
+ html {
88
+ font-family: var(--sl-font-sans);
89
+ font-size: var(--sl-text-base);
90
+ color: var(--sl-color-text);
91
+ background-color: var(--sl-color-bg);
92
+ -webkit-font-smoothing: antialiased;
93
+ scroll-behavior: smooth;
94
+ }
95
+
96
+ body {
97
+ margin: 0;
98
+ min-height: 100vh;
99
+ }
100
+
101
+ /* Typography reset for content areas */
102
+ h1, h2, h3, h4, h5, h6 {
103
+ margin-top: 1.5em;
104
+ margin-bottom: 0.5em;
105
+ font-weight: 600;
106
+ line-height: 1.25;
107
+ color: var(--sl-color-text);
108
+ }
109
+
110
+ h1 { font-size: var(--sl-text-4xl); }
111
+ h2 { font-size: var(--sl-text-2xl); border-bottom: 1px solid var(--sl-color-border); padding-bottom: 0.25em; }
112
+ h3 { font-size: var(--sl-text-xl); }
113
+ h4 { font-size: var(--sl-text-lg); }
114
+
115
+ p {
116
+ margin-top: 0;
117
+ margin-bottom: 1rem;
118
+ line-height: 1.7;
119
+ }
120
+
121
+ a {
122
+ color: var(--sl-color-text-accent);
123
+ text-decoration: none;
124
+ }
125
+
126
+ a:hover {
127
+ text-decoration: underline;
128
+ }
129
+
130
+ code {
131
+ font-family: var(--sl-font-mono);
132
+ font-size: 0.875em;
133
+ background-color: var(--sl-color-bg-inline-code);
134
+ border: 1px solid var(--sl-color-border);
135
+ border-radius: var(--sl-border-radius-sm);
136
+ padding: 0.15em 0.4em;
137
+ }
138
+
139
+ pre {
140
+ background-color: var(--sl-color-gray-6);
141
+ color: var(--sl-color-gray-1);
142
+ border-radius: var(--sl-border-radius);
143
+ padding: 1rem 1.25rem;
144
+ overflow-x: auto;
145
+ margin: 1.5rem 0;
146
+ font-size: var(--sl-text-sm);
147
+ line-height: 1.6;
148
+ }
149
+
150
+ [data-theme='dark'] pre {
151
+ background-color: #0d0e11;
152
+ color: #e2e4e9;
153
+ }
154
+
155
+ pre code {
156
+ background: none;
157
+ border: none;
158
+ padding: 0;
159
+ font-size: inherit;
160
+ }
161
+
162
+ table {
163
+ width: 100%;
164
+ border-collapse: collapse;
165
+ margin: 1.5rem 0;
166
+ font-size: var(--sl-text-sm);
167
+ }
168
+
169
+ th, td {
170
+ border: 1px solid var(--sl-color-border);
171
+ padding: 0.5rem 0.75rem;
172
+ text-align: left;
173
+ }
174
+
175
+ th {
176
+ background-color: var(--sl-color-gray-1);
177
+ font-weight: 600;
178
+ }
179
+
180
+ [data-theme='dark'] th {
181
+ background-color: var(--sl-color-gray-1);
182
+ }
183
+
184
+ ul, ol {
185
+ padding-left: 1.5rem;
186
+ margin: 0 0 1rem;
187
+ }
188
+
189
+ li {
190
+ margin-bottom: 0.25rem;
191
+ line-height: 1.7;
192
+ }
193
+
194
+ blockquote {
195
+ margin: 1.5rem 0;
196
+ padding: 0.75rem 1rem;
197
+ border-left: 4px solid var(--sl-color-accent);
198
+ background-color: var(--sl-color-accent-low);
199
+ border-radius: 0 var(--sl-border-radius) var(--sl-border-radius) 0;
200
+ }
201
+
202
+ [data-theme='dark'] blockquote {
203
+ background-color: var(--sl-color-accent-low);
204
+ }
205
+
206
+ hr {
207
+ border: none;
208
+ border-top: 1px solid var(--sl-color-border);
209
+ margin: 2rem 0;
210
+ }
211
+
212
+ img {
213
+ max-width: 100%;
214
+ height: auto;
215
+ }
@@ -0,0 +1,29 @@
1
+ import { defineEventHandler, fromNodeMiddleware } from 'h3';
2
+
3
+ let viteHandlerPromise: Promise<ReturnType<typeof fromNodeMiddleware>> | null = null;
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ if (!process.dev || !process.env.NITRO_DEV_WORKER_ID) return;
7
+
8
+ if (!viteHandlerPromise) {
9
+ const httpServer = (event.node.req.socket as import('node:net').Socket & {
10
+ server?: import('node:http').Server;
11
+ }).server;
12
+
13
+ viteHandlerPromise = import('vite')
14
+ .then(({ createServer }) =>
15
+ createServer({
16
+ server: {
17
+ middlewareMode: true,
18
+ hmr: httpServer ? { server: httpServer } : true,
19
+ },
20
+ appType: 'custom',
21
+ root: process.cwd(),
22
+ }),
23
+ )
24
+ .then((server) => fromNodeMiddleware(server.middlewares));
25
+ }
26
+
27
+ const viteHandler = await viteHandlerPromise;
28
+ return viteHandler(event);
29
+ });
@@ -0,0 +1,57 @@
1
+ import { defineEventHandler, setResponseHeader, getRequestURL } from 'h3';
2
+ import { createPageHandler } from '@beatzball/litro/runtime/create-page-handler.js';
3
+ import type { LitroRoute } from '@beatzball/litro';
4
+ import { routes, pageModules } from '#litro/page-manifest';
5
+
6
+ function matchRoute(
7
+ pathname: string,
8
+ ): { route: LitroRoute; params: Record<string, string> } | undefined {
9
+ for (const route of routes) {
10
+ if (route.isCatchAll) return { route, params: {} };
11
+
12
+ if (!route.isDynamic) {
13
+ if (pathname === route.path) return { route, params: {} };
14
+ continue;
15
+ }
16
+
17
+ const regexStr =
18
+ '^' +
19
+ route.path
20
+ .replace(/:([^/]+)\(\.\*\)\*/g, '(?<$1>.+)')
21
+ .replace(/:([^/?]+)\?/g, '(?<$1>[^/]*)?')
22
+ .replace(/:([^/]+)/g, '(?<$1>[^/]+)') +
23
+ '$';
24
+
25
+ try {
26
+ const match = pathname.match(new RegExp(regexStr));
27
+ if (match) return { route, params: (match.groups ?? {}) as Record<string, string> };
28
+ } catch {
29
+ // malformed pattern — skip
30
+ }
31
+ }
32
+ return undefined;
33
+ }
34
+
35
+ export default defineEventHandler(async (event) => {
36
+ const pathname = getRequestURL(event).pathname;
37
+ const result = matchRoute(pathname);
38
+
39
+ if (!result) {
40
+ setResponseHeader(event, 'content-type', 'text/html; charset=utf-8');
41
+ return `<!DOCTYPE html>
42
+ <html lang="en"><head><meta charset="UTF-8" /><title>404</title></head>
43
+ <body><h1>404 — Not Found</h1><p>No page matched <code>${pathname}</code>.</p></body>
44
+ </html>`;
45
+ }
46
+
47
+ const { route: matched, params } = result;
48
+ event.context.params = { ...event.context.params, ...params };
49
+
50
+ const mod = pageModules[matched.filePath];
51
+ const handler = createPageHandler({
52
+ route: matched,
53
+ routeMeta: (mod?.routeMeta as { title?: string; head?: string } | undefined),
54
+ pageModule: mod,
55
+ });
56
+ return handler(event);
57
+ });