create-neutron 0.1.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/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +201 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
- package/templates/app/_gitignore +3 -0
- package/templates/app/index.html +12 -0
- package/templates/app/neutron.config.ts +5 -0
- package/templates/app/package.json +24 -0
- package/templates/app/src/main.tsx +5 -0
- package/templates/app/src/routes/_layout.tsx +18 -0
- package/templates/app/src/routes/api/session/refresh.tsx +28 -0
- package/templates/app/src/routes/app/dashboard.tsx +39 -0
- package/templates/app/src/routes/app/settings.tsx +68 -0
- package/templates/app/src/routes/index.tsx +26 -0
- package/templates/app/src/routes/login.tsx +14 -0
- package/templates/app/src/routes/protected.tsx +53 -0
- package/templates/app/src/routes/users/[id].tsx +26 -0
- package/templates/app/tsconfig.json +14 -0
- package/templates/app/vite.config.ts +7 -0
- package/templates/basic/_gitignore +3 -0
- package/templates/basic/index.html +12 -0
- package/templates/basic/neutron.config.ts +5 -0
- package/templates/basic/package.json +24 -0
- package/templates/basic/src/main.tsx +5 -0
- package/templates/basic/src/routes/_layout.tsx +16 -0
- package/templates/basic/src/routes/index.tsx +19 -0
- package/templates/basic/src/routes/users/[id].tsx +26 -0
- package/templates/basic/tsconfig.json +14 -0
- package/templates/basic/vite.config.ts +7 -0
- package/templates/docs/_gitignore +3 -0
- package/templates/docs/index.html +12 -0
- package/templates/docs/neutron.config.ts +8 -0
- package/templates/docs/package.json +22 -0
- package/templates/docs/public/favicon.svg +1 -0
- package/templates/docs/src/components/Breadcrumbs.tsx +47 -0
- package/templates/docs/src/components/Callout.tsx +40 -0
- package/templates/docs/src/components/Card.tsx +31 -0
- package/templates/docs/src/components/CopyButton.tsx +35 -0
- package/templates/docs/src/components/Footer.tsx +72 -0
- package/templates/docs/src/components/Search.tsx +139 -0
- package/templates/docs/src/components/Sidebar.tsx +59 -0
- package/templates/docs/src/components/SidebarToggle.tsx +47 -0
- package/templates/docs/src/components/Steps.tsx +9 -0
- package/templates/docs/src/components/Tabs.tsx +35 -0
- package/templates/docs/src/components/ThemeToggle.tsx +45 -0
- package/templates/docs/src/components/Toc.tsx +36 -0
- package/templates/docs/src/components/TocTracker.tsx +35 -0
- package/templates/docs/src/content/config.ts +13 -0
- package/templates/docs/src/content/docs/getting-started/installation.mdx +18 -0
- package/templates/docs/src/content/docs/getting-started/quick-start.mdx +19 -0
- package/templates/docs/src/content/docs/index.mdx +13 -0
- package/templates/docs/src/lib/pagination.ts +39 -0
- package/templates/docs/src/lib/sidebar.ts +100 -0
- package/templates/docs/src/main.tsx +8 -0
- package/templates/docs/src/routes/_layout.tsx +27 -0
- package/templates/docs/src/routes/docs/[...slug].tsx +85 -0
- package/templates/docs/src/routes/docs/_layout.tsx +47 -0
- package/templates/docs/src/styles/code.css +188 -0
- package/templates/docs/src/styles/components.css +264 -0
- package/templates/docs/src/styles/docs.css +416 -0
- package/templates/docs/src/styles/prose.css +224 -0
- package/templates/docs/src/styles/search.css +225 -0
- package/templates/docs/tsconfig.json +19 -0
- package/templates/docs/vite.config.ts +6 -0
- package/templates/full/_gitignore +3 -0
- package/templates/full/index.html +12 -0
- package/templates/full/neutron.config.ts +5 -0
- package/templates/full/package.json +24 -0
- package/templates/full/src/components/Counter.tsx +13 -0
- package/templates/full/src/main.tsx +5 -0
- package/templates/full/src/routes/(marketing)/pricing.tsx +15 -0
- package/templates/full/src/routes/_layout.tsx +17 -0
- package/templates/full/src/routes/app/dashboard.tsx +28 -0
- package/templates/full/src/routes/app/settings.tsx +42 -0
- package/templates/full/src/routes/index.tsx +31 -0
- package/templates/full/src/routes/users/[id].tsx +26 -0
- package/templates/full/tsconfig.json +14 -0
- package/templates/full/vite.config.ts +7 -0
- package/templates/marketing/_gitignore +3 -0
- package/templates/marketing/index.html +12 -0
- package/templates/marketing/neutron.config.ts +5 -0
- package/templates/marketing/package.json +24 -0
- package/templates/marketing/src/components/Counter.tsx +14 -0
- package/templates/marketing/src/main.tsx +5 -0
- package/templates/marketing/src/routes/_layout.tsx +16 -0
- package/templates/marketing/src/routes/about.tsx +10 -0
- package/templates/marketing/src/routes/blog/[slug].tsx +34 -0
- package/templates/marketing/src/routes/blog/index.tsx +27 -0
- package/templates/marketing/src/routes/index.tsx +26 -0
- package/templates/marketing/src/routes/users/index.tsx +10 -0
- package/templates/marketing/tsconfig.json +14 -0
- package/templates/marketing/vite.config.ts +7 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState } from "preact/hooks";
|
|
2
|
+
|
|
3
|
+
export function SidebarToggle() {
|
|
4
|
+
const [open, setOpen] = useState(false);
|
|
5
|
+
|
|
6
|
+
function toggle() {
|
|
7
|
+
const next = !open;
|
|
8
|
+
setOpen(next);
|
|
9
|
+
const sidebar = document.querySelector(".docs-sidebar");
|
|
10
|
+
if (sidebar) {
|
|
11
|
+
sidebar.classList.toggle("is-open", next);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function close() {
|
|
16
|
+
setOpen(false);
|
|
17
|
+
const sidebar = document.querySelector(".docs-sidebar");
|
|
18
|
+
if (sidebar) {
|
|
19
|
+
sidebar.classList.remove("is-open");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<button
|
|
26
|
+
class="docs-mobile-toggle"
|
|
27
|
+
onClick={toggle}
|
|
28
|
+
aria-label={open ? "Close sidebar" : "Open sidebar"}
|
|
29
|
+
aria-expanded={open}
|
|
30
|
+
>
|
|
31
|
+
{open ? (
|
|
32
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
33
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
34
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
35
|
+
</svg>
|
|
36
|
+
) : (
|
|
37
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
38
|
+
<line x1="3" y1="12" x2="21" y2="12" />
|
|
39
|
+
<line x1="3" y1="6" x2="21" y2="6" />
|
|
40
|
+
<line x1="3" y1="18" x2="21" y2="18" />
|
|
41
|
+
</svg>
|
|
42
|
+
)}
|
|
43
|
+
</button>
|
|
44
|
+
{open && <div class="docs-sidebar-overlay" onClick={close} />}
|
|
45
|
+
</>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useState } from "preact/hooks";
|
|
2
|
+
import type { ComponentChildren } from "preact";
|
|
3
|
+
|
|
4
|
+
export function Tabs({ labels, children }: { labels: string[]; children: ComponentChildren }) {
|
|
5
|
+
const [active, setActive] = useState(0);
|
|
6
|
+
const panels = Array.isArray(children) ? children : [children];
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div class="tabs">
|
|
10
|
+
<div class="tabs-bar" role="tablist">
|
|
11
|
+
{labels.map((label, i) => (
|
|
12
|
+
<button
|
|
13
|
+
key={label}
|
|
14
|
+
role="tab"
|
|
15
|
+
aria-selected={i === active}
|
|
16
|
+
class="tabs-trigger"
|
|
17
|
+
onClick={() => setActive(i)}
|
|
18
|
+
>
|
|
19
|
+
{label}
|
|
20
|
+
</button>
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
{panels.map((panel, i) => (
|
|
24
|
+
<div
|
|
25
|
+
key={i}
|
|
26
|
+
role="tabpanel"
|
|
27
|
+
class="tabs-panel"
|
|
28
|
+
hidden={i !== active}
|
|
29
|
+
>
|
|
30
|
+
{panel}
|
|
31
|
+
</div>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useState, useEffect } from "preact/hooks";
|
|
2
|
+
|
|
3
|
+
export function ThemeToggle() {
|
|
4
|
+
const [theme, setTheme] = useState("dark");
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const saved = localStorage.getItem("docs-theme") || "dark";
|
|
8
|
+
setTheme(saved);
|
|
9
|
+
document.documentElement.setAttribute("data-theme", saved);
|
|
10
|
+
}, []);
|
|
11
|
+
|
|
12
|
+
function toggle() {
|
|
13
|
+
const next = theme === "dark" ? "light" : "dark";
|
|
14
|
+
setTheme(next);
|
|
15
|
+
localStorage.setItem("docs-theme", next);
|
|
16
|
+
document.documentElement.setAttribute("data-theme", next);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<button
|
|
21
|
+
class="theme-toggle"
|
|
22
|
+
onClick={toggle}
|
|
23
|
+
aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
|
|
24
|
+
title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
|
|
25
|
+
>
|
|
26
|
+
{theme === "dark" ? (
|
|
27
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
28
|
+
<circle cx="12" cy="12" r="5" />
|
|
29
|
+
<line x1="12" y1="1" x2="12" y2="3" />
|
|
30
|
+
<line x1="12" y1="21" x2="12" y2="23" />
|
|
31
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
32
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
33
|
+
<line x1="1" y1="12" x2="3" y2="12" />
|
|
34
|
+
<line x1="21" y1="12" x2="23" y2="12" />
|
|
35
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
36
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
37
|
+
</svg>
|
|
38
|
+
) : (
|
|
39
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
40
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
41
|
+
</svg>
|
|
42
|
+
)}
|
|
43
|
+
</button>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Island } from "@neutron-build/core/client";
|
|
2
|
+
import { TocTracker } from "./TocTracker";
|
|
3
|
+
|
|
4
|
+
export interface TocEntry {
|
|
5
|
+
id: string;
|
|
6
|
+
text: string;
|
|
7
|
+
level: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Toc({ entries }: { entries: TocEntry[] }) {
|
|
11
|
+
if (!entries || entries.length === 0) return null;
|
|
12
|
+
|
|
13
|
+
const filtered = entries.filter(
|
|
14
|
+
(entry) => entry.level === 2 || entry.level === 3
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
if (filtered.length === 0) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div class="docs-toc">
|
|
21
|
+
<p class="docs-toc-title">On this page</p>
|
|
22
|
+
<nav aria-label="Table of contents">
|
|
23
|
+
{filtered.map((entry) => (
|
|
24
|
+
<a
|
|
25
|
+
key={entry.id}
|
|
26
|
+
href={`#${entry.id}`}
|
|
27
|
+
class={`docs-toc-link toc-link${entry.level === 3 ? " docs-toc-link--h3" : ""}`}
|
|
28
|
+
>
|
|
29
|
+
{entry.text}
|
|
30
|
+
</a>
|
|
31
|
+
))}
|
|
32
|
+
</nav>
|
|
33
|
+
<Island component={TocTracker} client="idle" id="toc-tracker" />
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useEffect } from "preact/hooks";
|
|
2
|
+
|
|
3
|
+
export function TocTracker() {
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
const headings = document.querySelectorAll(".prose h2[id], .prose h3[id]");
|
|
6
|
+
if (!headings.length) return;
|
|
7
|
+
|
|
8
|
+
const tocLinks = document.querySelectorAll(".toc-link");
|
|
9
|
+
if (!tocLinks.length) return;
|
|
10
|
+
|
|
11
|
+
function clearActive() {
|
|
12
|
+
tocLinks.forEach((link) => link.removeAttribute("aria-current"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const observer = new IntersectionObserver(
|
|
16
|
+
(entries) => {
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (entry.isIntersecting) {
|
|
19
|
+
clearActive();
|
|
20
|
+
const id = entry.target.getAttribute("id");
|
|
21
|
+
const link = document.querySelector(`.toc-link[href="#${id}"]`);
|
|
22
|
+
if (link) link.setAttribute("aria-current", "true");
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{ rootMargin: "-80px 0px -70% 0px", threshold: 0 },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
headings.forEach((h) => observer.observe(h));
|
|
31
|
+
return () => observer.disconnect();
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineCollection, z } from "@neutron-build/core/content";
|
|
2
|
+
|
|
3
|
+
export const collections = {
|
|
4
|
+
docs: defineCollection({
|
|
5
|
+
schema: z.object({
|
|
6
|
+
title: z.string(),
|
|
7
|
+
description: z.string().optional(),
|
|
8
|
+
sidebar_label: z.string().optional(),
|
|
9
|
+
order: z.number().optional(),
|
|
10
|
+
draft: z.boolean().default(false),
|
|
11
|
+
}),
|
|
12
|
+
}),
|
|
13
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Quick Start
|
|
3
|
+
description: Your first steps
|
|
4
|
+
order: 2
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Quick Start
|
|
8
|
+
|
|
9
|
+
Follow these steps to get started:
|
|
10
|
+
|
|
11
|
+
1. Create a new project
|
|
12
|
+
2. Install dependencies
|
|
13
|
+
3. Start the dev server
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm run dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Your site is now running at `http://localhost:3000`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Welcome
|
|
3
|
+
description: Getting started with __PROJECT_NAME__
|
|
4
|
+
order: 0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Welcome to __PROJECT_NAME__
|
|
8
|
+
|
|
9
|
+
This is your documentation site built with [Neutron](https://neutron.build).
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
Get up and running in minutes.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface PaginationLink {
|
|
2
|
+
title: string;
|
|
3
|
+
slug: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface EntryInput {
|
|
7
|
+
slug: string;
|
|
8
|
+
data: {
|
|
9
|
+
title: string;
|
|
10
|
+
order?: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getPagination(
|
|
15
|
+
entries: EntryInput[],
|
|
16
|
+
currentSlug: string,
|
|
17
|
+
): { prev?: PaginationLink; next?: PaginationLink } {
|
|
18
|
+
const sorted = [...entries].sort(
|
|
19
|
+
(a, b) => (a.data.order ?? 999) - (b.data.order ?? 999) || a.slug.localeCompare(b.slug),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const index = sorted.findIndex((e) => e.slug === currentSlug);
|
|
23
|
+
|
|
24
|
+
if (index === -1) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const prev =
|
|
29
|
+
index > 0
|
|
30
|
+
? { title: sorted[index - 1]!.data.title, slug: sorted[index - 1]!.slug }
|
|
31
|
+
: undefined;
|
|
32
|
+
|
|
33
|
+
const next =
|
|
34
|
+
index < sorted.length - 1
|
|
35
|
+
? { title: sorted[index + 1]!.data.title, slug: sorted[index + 1]!.slug }
|
|
36
|
+
: undefined;
|
|
37
|
+
|
|
38
|
+
return { prev, next };
|
|
39
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export interface SidebarItem {
|
|
2
|
+
title: string;
|
|
3
|
+
slug: string;
|
|
4
|
+
order: number;
|
|
5
|
+
active?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SidebarSection {
|
|
9
|
+
title: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
order: number;
|
|
12
|
+
items: SidebarItem[];
|
|
13
|
+
children: SidebarSection[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface EntryInput {
|
|
17
|
+
slug: string;
|
|
18
|
+
data: {
|
|
19
|
+
title: string;
|
|
20
|
+
sidebar_label?: string;
|
|
21
|
+
order?: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toTitle(segment: string): string {
|
|
26
|
+
return segment
|
|
27
|
+
.replace(/[-_]/g, " ")
|
|
28
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildSidebarTree(
|
|
32
|
+
entries: EntryInput[],
|
|
33
|
+
activeSlug?: string,
|
|
34
|
+
): SidebarSection[] {
|
|
35
|
+
const sectionMap = new Map<string, SidebarSection>();
|
|
36
|
+
const rootItems: SidebarItem[] = [];
|
|
37
|
+
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const parts = entry.slug.split("/");
|
|
40
|
+
|
|
41
|
+
if (parts.length === 1 || (parts.length === 2 && parts[1] === "index")) {
|
|
42
|
+
rootItems.push({
|
|
43
|
+
title: entry.data.sidebar_label || entry.data.title,
|
|
44
|
+
slug: entry.slug,
|
|
45
|
+
order: entry.data.order ?? 999,
|
|
46
|
+
active: activeSlug === entry.slug,
|
|
47
|
+
});
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sectionSlug = parts[0]!;
|
|
52
|
+
let section = sectionMap.get(sectionSlug);
|
|
53
|
+
|
|
54
|
+
if (!section) {
|
|
55
|
+
section = {
|
|
56
|
+
title: toTitle(sectionSlug),
|
|
57
|
+
slug: sectionSlug,
|
|
58
|
+
order: 999,
|
|
59
|
+
items: [],
|
|
60
|
+
children: [],
|
|
61
|
+
};
|
|
62
|
+
sectionMap.set(sectionSlug, section);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const isIndex = parts[parts.length - 1] === "index";
|
|
66
|
+
|
|
67
|
+
if (isIndex) {
|
|
68
|
+
section.title = entry.data.sidebar_label || entry.data.title;
|
|
69
|
+
section.order = entry.data.order ?? section.order;
|
|
70
|
+
} else {
|
|
71
|
+
section.items.push({
|
|
72
|
+
title: entry.data.sidebar_label || entry.data.title,
|
|
73
|
+
slug: entry.slug,
|
|
74
|
+
order: entry.data.order ?? 999,
|
|
75
|
+
active: activeSlug === entry.slug,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const section of sectionMap.values()) {
|
|
81
|
+
section.items.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const sections = Array.from(sectionMap.values());
|
|
85
|
+
sections.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
|
|
86
|
+
|
|
87
|
+
if (rootItems.length > 0) {
|
|
88
|
+
rootItems.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
|
|
89
|
+
const rootSection: SidebarSection = {
|
|
90
|
+
title: "",
|
|
91
|
+
slug: "",
|
|
92
|
+
order: -1,
|
|
93
|
+
items: rootItems,
|
|
94
|
+
children: [],
|
|
95
|
+
};
|
|
96
|
+
return [rootSection, ...sections];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return sections;
|
|
100
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { init, registerRoutes } from "@neutron-build/core/client";
|
|
2
|
+
import { routes } from "virtual:neutron/routes";
|
|
3
|
+
|
|
4
|
+
const saved = localStorage.getItem("docs-theme") || "dark";
|
|
5
|
+
document.documentElement.setAttribute("data-theme", saved);
|
|
6
|
+
|
|
7
|
+
registerRoutes(routes);
|
|
8
|
+
void init();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import "../styles/docs.css";
|
|
2
|
+
import { Island, ViewTransitions } from "@neutron-build/core/client";
|
|
3
|
+
import { ThemeToggle } from "../components/ThemeToggle";
|
|
4
|
+
|
|
5
|
+
export function head() {
|
|
6
|
+
return {
|
|
7
|
+
titleTemplate: "%s — __PROJECT_NAME__",
|
|
8
|
+
description: "__PROJECT_NAME__ Documentation",
|
|
9
|
+
htmlAttrs: { lang: "en", "data-theme": "dark" },
|
|
10
|
+
headScripts: [
|
|
11
|
+
{
|
|
12
|
+
content: `(function(){var s=localStorage.getItem("docs-theme")||"dark";document.documentElement.setAttribute("data-theme",s)})();`,
|
|
13
|
+
id: "theme-init",
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function RootLayout({ children }: { children?: unknown }) {
|
|
20
|
+
return (
|
|
21
|
+
<div class="docs-app">
|
|
22
|
+
<ViewTransitions />
|
|
23
|
+
<Island component={ThemeToggle} client="load" id="theme-toggle" />
|
|
24
|
+
{children}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { getCollection, getEntry } from "@neutron-build/core/content";
|
|
2
|
+
import { extractToc } from "@neutron-build/core";
|
|
3
|
+
import { calculateReadingTime } from "@neutron-build/core";
|
|
4
|
+
import { Toc } from "../../components/Toc";
|
|
5
|
+
import { Footer } from "../../components/Footer";
|
|
6
|
+
import { Breadcrumbs } from "../../components/Breadcrumbs";
|
|
7
|
+
import { getPagination } from "../../lib/pagination";
|
|
8
|
+
|
|
9
|
+
export const config = { mode: "static" };
|
|
10
|
+
|
|
11
|
+
export async function getStaticPaths() {
|
|
12
|
+
const docs = await getCollection("docs");
|
|
13
|
+
return {
|
|
14
|
+
paths: docs
|
|
15
|
+
.filter((d: any) => !d.data.draft)
|
|
16
|
+
.map((d: any) => ({ params: { "*": d.slug } })),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function loader({ params }: { params: Record<string, string> }) {
|
|
21
|
+
const slug = params["*"] || "index";
|
|
22
|
+
const entry = await getEntry("docs", slug);
|
|
23
|
+
if (!entry) throw new Response("Not found", { status: 404 });
|
|
24
|
+
|
|
25
|
+
const toc = extractToc(entry.html);
|
|
26
|
+
const readingTime = calculateReadingTime(entry.body);
|
|
27
|
+
|
|
28
|
+
const docs = await getCollection("docs");
|
|
29
|
+
const allEntries = docs
|
|
30
|
+
.filter((d: any) => !d.data.draft)
|
|
31
|
+
.map((d: any) => ({ slug: d.slug, data: d.data }));
|
|
32
|
+
const pagination = getPagination(allEntries, slug);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
title: entry.data.title,
|
|
36
|
+
description: entry.data.description,
|
|
37
|
+
html: entry.html,
|
|
38
|
+
slug,
|
|
39
|
+
toc,
|
|
40
|
+
readingTime,
|
|
41
|
+
pagination,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function head({ data }: { data: { title: string; description?: string } }) {
|
|
46
|
+
return {
|
|
47
|
+
title: data.title,
|
|
48
|
+
description: data.description,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default function DocPage({
|
|
53
|
+
data,
|
|
54
|
+
}: {
|
|
55
|
+
data: {
|
|
56
|
+
title: string;
|
|
57
|
+
html: string;
|
|
58
|
+
slug: string;
|
|
59
|
+
toc: { id: string; text: string; level: number }[];
|
|
60
|
+
readingTime?: { text: string; minutes: number };
|
|
61
|
+
pagination: {
|
|
62
|
+
prev?: { title: string; slug: string };
|
|
63
|
+
next?: { title: string; slug: string };
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
}) {
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<div class="docs-main">
|
|
70
|
+
<Breadcrumbs slug={data.slug} />
|
|
71
|
+
<article class="prose">
|
|
72
|
+
<h1>{data.title}</h1>
|
|
73
|
+
{data.readingTime && (
|
|
74
|
+
<p class="reading-time">{data.readingTime.text}</p>
|
|
75
|
+
)}
|
|
76
|
+
<div dangerouslySetInnerHTML={{ __html: data.html }} />
|
|
77
|
+
</article>
|
|
78
|
+
<Footer prev={data.pagination.prev} next={data.pagination.next} />
|
|
79
|
+
</div>
|
|
80
|
+
<div class="docs-toc-wrapper">
|
|
81
|
+
<Toc entries={data.toc} />
|
|
82
|
+
</div>
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getCollection } from "@neutron-build/core/content";
|
|
2
|
+
import { buildSidebarTree } from "../../lib/sidebar";
|
|
3
|
+
import { Sidebar } from "../../components/Sidebar";
|
|
4
|
+
import { Island } from "@neutron-build/core/client";
|
|
5
|
+
import { SidebarToggle } from "../../components/SidebarToggle";
|
|
6
|
+
import { Search } from "../../components/Search";
|
|
7
|
+
|
|
8
|
+
export const config = { mode: "static" };
|
|
9
|
+
|
|
10
|
+
export async function loader({ request }: { request: Request }) {
|
|
11
|
+
const docs = await getCollection("docs");
|
|
12
|
+
const entries = docs
|
|
13
|
+
.filter((d: any) => !d.data.draft)
|
|
14
|
+
.map((d: any) => ({ slug: d.slug, data: d.data }));
|
|
15
|
+
|
|
16
|
+
const url = new URL(request.url);
|
|
17
|
+
const activeSlug = url.pathname.replace(/^\/docs\/?/, "").replace(/\/$/, "") || "index";
|
|
18
|
+
|
|
19
|
+
const tree = buildSidebarTree(entries, activeSlug);
|
|
20
|
+
return { entries, tree };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function DocsLayout({
|
|
24
|
+
data,
|
|
25
|
+
children,
|
|
26
|
+
}: {
|
|
27
|
+
data: { entries: any[]; tree: any[] };
|
|
28
|
+
children?: unknown;
|
|
29
|
+
}) {
|
|
30
|
+
return (
|
|
31
|
+
<div class="docs-layout">
|
|
32
|
+
<Island component={SidebarToggle} client="load" id="sidebar-toggle" />
|
|
33
|
+
<aside class="docs-sidebar">
|
|
34
|
+
<div class="docs-sidebar-header">
|
|
35
|
+
<a href="/docs" class="docs-sidebar-logo">__PROJECT_NAME__</a>
|
|
36
|
+
<Island component={Search} client="idle" id="docs-search" props={{ entries: data.entries }} />
|
|
37
|
+
</div>
|
|
38
|
+
<nav class="docs-sidebar-nav">
|
|
39
|
+
<Sidebar tree={data.tree} />
|
|
40
|
+
</nav>
|
|
41
|
+
</aside>
|
|
42
|
+
<div class="docs-content">
|
|
43
|
+
{children}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|