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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +201 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +45 -0
  8. package/templates/app/_gitignore +3 -0
  9. package/templates/app/index.html +12 -0
  10. package/templates/app/neutron.config.ts +5 -0
  11. package/templates/app/package.json +24 -0
  12. package/templates/app/src/main.tsx +5 -0
  13. package/templates/app/src/routes/_layout.tsx +18 -0
  14. package/templates/app/src/routes/api/session/refresh.tsx +28 -0
  15. package/templates/app/src/routes/app/dashboard.tsx +39 -0
  16. package/templates/app/src/routes/app/settings.tsx +68 -0
  17. package/templates/app/src/routes/index.tsx +26 -0
  18. package/templates/app/src/routes/login.tsx +14 -0
  19. package/templates/app/src/routes/protected.tsx +53 -0
  20. package/templates/app/src/routes/users/[id].tsx +26 -0
  21. package/templates/app/tsconfig.json +14 -0
  22. package/templates/app/vite.config.ts +7 -0
  23. package/templates/basic/_gitignore +3 -0
  24. package/templates/basic/index.html +12 -0
  25. package/templates/basic/neutron.config.ts +5 -0
  26. package/templates/basic/package.json +24 -0
  27. package/templates/basic/src/main.tsx +5 -0
  28. package/templates/basic/src/routes/_layout.tsx +16 -0
  29. package/templates/basic/src/routes/index.tsx +19 -0
  30. package/templates/basic/src/routes/users/[id].tsx +26 -0
  31. package/templates/basic/tsconfig.json +14 -0
  32. package/templates/basic/vite.config.ts +7 -0
  33. package/templates/docs/_gitignore +3 -0
  34. package/templates/docs/index.html +12 -0
  35. package/templates/docs/neutron.config.ts +8 -0
  36. package/templates/docs/package.json +22 -0
  37. package/templates/docs/public/favicon.svg +1 -0
  38. package/templates/docs/src/components/Breadcrumbs.tsx +47 -0
  39. package/templates/docs/src/components/Callout.tsx +40 -0
  40. package/templates/docs/src/components/Card.tsx +31 -0
  41. package/templates/docs/src/components/CopyButton.tsx +35 -0
  42. package/templates/docs/src/components/Footer.tsx +72 -0
  43. package/templates/docs/src/components/Search.tsx +139 -0
  44. package/templates/docs/src/components/Sidebar.tsx +59 -0
  45. package/templates/docs/src/components/SidebarToggle.tsx +47 -0
  46. package/templates/docs/src/components/Steps.tsx +9 -0
  47. package/templates/docs/src/components/Tabs.tsx +35 -0
  48. package/templates/docs/src/components/ThemeToggle.tsx +45 -0
  49. package/templates/docs/src/components/Toc.tsx +36 -0
  50. package/templates/docs/src/components/TocTracker.tsx +35 -0
  51. package/templates/docs/src/content/config.ts +13 -0
  52. package/templates/docs/src/content/docs/getting-started/installation.mdx +18 -0
  53. package/templates/docs/src/content/docs/getting-started/quick-start.mdx +19 -0
  54. package/templates/docs/src/content/docs/index.mdx +13 -0
  55. package/templates/docs/src/lib/pagination.ts +39 -0
  56. package/templates/docs/src/lib/sidebar.ts +100 -0
  57. package/templates/docs/src/main.tsx +8 -0
  58. package/templates/docs/src/routes/_layout.tsx +27 -0
  59. package/templates/docs/src/routes/docs/[...slug].tsx +85 -0
  60. package/templates/docs/src/routes/docs/_layout.tsx +47 -0
  61. package/templates/docs/src/styles/code.css +188 -0
  62. package/templates/docs/src/styles/components.css +264 -0
  63. package/templates/docs/src/styles/docs.css +416 -0
  64. package/templates/docs/src/styles/prose.css +224 -0
  65. package/templates/docs/src/styles/search.css +225 -0
  66. package/templates/docs/tsconfig.json +19 -0
  67. package/templates/docs/vite.config.ts +6 -0
  68. package/templates/full/_gitignore +3 -0
  69. package/templates/full/index.html +12 -0
  70. package/templates/full/neutron.config.ts +5 -0
  71. package/templates/full/package.json +24 -0
  72. package/templates/full/src/components/Counter.tsx +13 -0
  73. package/templates/full/src/main.tsx +5 -0
  74. package/templates/full/src/routes/(marketing)/pricing.tsx +15 -0
  75. package/templates/full/src/routes/_layout.tsx +17 -0
  76. package/templates/full/src/routes/app/dashboard.tsx +28 -0
  77. package/templates/full/src/routes/app/settings.tsx +42 -0
  78. package/templates/full/src/routes/index.tsx +31 -0
  79. package/templates/full/src/routes/users/[id].tsx +26 -0
  80. package/templates/full/tsconfig.json +14 -0
  81. package/templates/full/vite.config.ts +7 -0
  82. package/templates/marketing/_gitignore +3 -0
  83. package/templates/marketing/index.html +12 -0
  84. package/templates/marketing/neutron.config.ts +5 -0
  85. package/templates/marketing/package.json +24 -0
  86. package/templates/marketing/src/components/Counter.tsx +14 -0
  87. package/templates/marketing/src/main.tsx +5 -0
  88. package/templates/marketing/src/routes/_layout.tsx +16 -0
  89. package/templates/marketing/src/routes/about.tsx +10 -0
  90. package/templates/marketing/src/routes/blog/[slug].tsx +34 -0
  91. package/templates/marketing/src/routes/blog/index.tsx +27 -0
  92. package/templates/marketing/src/routes/index.tsx +26 -0
  93. package/templates/marketing/src/routes/users/index.tsx +10 -0
  94. package/templates/marketing/tsconfig.json +14 -0
  95. 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,9 @@
1
+ import type { ComponentChildren } from "preact";
2
+
3
+ export interface StepsProps {
4
+ children: ComponentChildren;
5
+ }
6
+
7
+ export default function Steps(props: StepsProps) {
8
+ return <ol class="steps">{props.children}</ol>;
9
+ }
@@ -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,18 @@
1
+ ---
2
+ title: Installation
3
+ description: How to install the project
4
+ order: 1
5
+ ---
6
+
7
+ # Installation
8
+
9
+ Install via npm:
10
+
11
+ ```bash
12
+ npm install __PACKAGE_NAME__
13
+ ```
14
+
15
+ ## Requirements
16
+
17
+ - Node.js 20+
18
+ - npm, pnpm, or yarn
@@ -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
+ }