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,7 @@
1
+ import { defineConfig } from "vite";
2
+ import preact from "@preact/preset-vite";
3
+ import { neutronPlugin } from "@neutron-build/core/vite";
4
+
5
+ export default defineConfig({
6
+ plugins: [preact(), neutronPlugin()],
7
+ });
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ .neutron
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>__PROJECT_NAME__</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from "@neutron-build/core";
2
+
3
+ export default defineConfig({
4
+ runtime: "__RUNTIME__",
5
+ });
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "__PACKAGE_NAME__",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "neutron dev",
8
+ "build": "neutron build",
9
+ "build:vercel": "neutron build --preset vercel",
10
+ "start": "neutron start",
11
+ "preview": "neutron preview",
12
+ "release:check": "neutron release-check --preset vercel"
13
+ },
14
+ "dependencies": {
15
+ "@neutron-build/core": "__NEUTRON_VERSION__",
16
+ "@neutron-build/cli": "__NEUTRON_CLI_VERSION__",
17
+ "preact": "^10.25.4"
18
+ },
19
+ "devDependencies": {
20
+ "@preact/preset-vite": "^2.9.4",
21
+ "typescript": "^5.7.2",
22
+ "vite": "^6.0.7"
23
+ }
24
+ }
@@ -0,0 +1,5 @@
1
+ import { init, registerRoutes } from "@neutron-build/core/client";
2
+ import { routes } from "virtual:neutron/routes";
3
+
4
+ registerRoutes(routes);
5
+ void init();
@@ -0,0 +1,16 @@
1
+ export default function Layout(props: { children?: unknown }) {
2
+ return (
3
+ <div style="max-width: 900px; margin: 0 auto; padding: 2rem; font-family: system-ui, sans-serif;">
4
+ <header style="margin-bottom: 1.5rem;">
5
+ <h1 style="margin: 0;">__PROJECT_NAME__</h1>
6
+ <p style="color: #666;">Neutron starter template</p>
7
+ </header>
8
+ <nav style="margin-bottom: 1.5rem;">
9
+ <a href="/">Home</a>
10
+ {" | "}
11
+ <a href="/users/1">User 1</a>
12
+ </nav>
13
+ <main>{props.children}</main>
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,19 @@
1
+ export const config = { mode: "static" };
2
+
3
+ export async function loader() {
4
+ return {
5
+ title: "__PROJECT_NAME__",
6
+ generatedAt: new Date().toISOString(),
7
+ };
8
+ }
9
+
10
+ export default function Home(props: { data?: { title: string; generatedAt: string } }) {
11
+ return (
12
+ <section>
13
+ <h2>{props.data?.title}</h2>
14
+ <p>
15
+ Static route generated at <strong>{props.data?.generatedAt}</strong>.
16
+ </p>
17
+ </section>
18
+ );
19
+ }
@@ -0,0 +1,26 @@
1
+ import type { LoaderArgs } from "@neutron-build/core";
2
+
3
+ export const config = { mode: "app", cache: { maxAge: 30 } };
4
+
5
+ export async function loader({ params }: LoaderArgs) {
6
+ const id = params.id || "1";
7
+ return {
8
+ user: {
9
+ id,
10
+ name: `User ${id}`,
11
+ email: `user${id}@example.com`,
12
+ },
13
+ };
14
+ }
15
+
16
+ export default function UserRoute(props: {
17
+ data?: { user: { id: string; name: string; email: string } };
18
+ }) {
19
+ return (
20
+ <section>
21
+ <h2>{props.data?.user.name}</h2>
22
+ <p>ID: {props.data?.user.id}</p>
23
+ <p>Email: {props.data?.user.email}</p>
24
+ </section>
25
+ );
26
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM"],
7
+ "jsx": "react-jsx",
8
+ "jsxImportSource": "preact",
9
+ "strict": true,
10
+ "skipLibCheck": true,
11
+ "types": ["vite/client"]
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vite";
2
+ import preact from "@preact/preset-vite";
3
+ import { neutronPlugin } from "@neutron-build/core/vite";
4
+
5
+ export default defineConfig({
6
+ plugins: [preact(), neutronPlugin()],
7
+ });
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ .neutron
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>__PROJECT_NAME__</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "@neutron-build/core";
2
+
3
+ export default defineConfig({
4
+ runtime: "preact",
5
+ markdown: {
6
+ syntaxHighlight: { theme: "github-dark" },
7
+ },
8
+ });
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "__PACKAGE_NAME__",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "neutron-ts dev",
8
+ "build": "neutron-ts build",
9
+ "start": "neutron-ts start",
10
+ "preview": "neutron-ts preview"
11
+ },
12
+ "dependencies": {
13
+ "@neutron-build/core": "__NEUTRON_VERSION__",
14
+ "@neutron-build/cli": "__NEUTRON_CLI_VERSION__",
15
+ "preact": "^10.25.4"
16
+ },
17
+ "devDependencies": {
18
+ "@preact/preset-vite": "^2.9.4",
19
+ "typescript": "^5.7.2",
20
+ "vite": "^6.0.7"
21
+ }
22
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="none" stroke="#818cf8" stroke-width="2"/><circle cx="16" cy="16" r="4" fill="#818cf8"/></svg>
@@ -0,0 +1,47 @@
1
+ import { Link } from "@neutron-build/core/client";
2
+
3
+ function toTitle(segment: string): string {
4
+ return segment
5
+ .replace(/[-_]/g, " ")
6
+ .replace(/\b\w/g, (c) => c.toUpperCase());
7
+ }
8
+
9
+ export function Breadcrumbs({ slug }: { slug: string }) {
10
+ const parts = slug.split("/").filter(Boolean);
11
+ if (parts.length === 0 || (parts.length === 1 && parts[0] === "index")) {
12
+ return null;
13
+ }
14
+
15
+ const crumbs: { label: string; href: string }[] = [
16
+ { label: "Docs", href: "/docs" },
17
+ ];
18
+
19
+ let path = "";
20
+ for (let i = 0; i < parts.length; i++) {
21
+ path += `/${parts[i]}`;
22
+ const isLast = i === parts.length - 1;
23
+ if (parts[i] !== "index") {
24
+ crumbs.push({
25
+ label: toTitle(parts[i]!),
26
+ href: isLast ? "" : `/docs${path}`,
27
+ });
28
+ }
29
+ }
30
+
31
+ return (
32
+ <nav class="docs-breadcrumbs" aria-label="Breadcrumb">
33
+ {crumbs.map((crumb, i) => (
34
+ <>
35
+ {i > 0 && <span class="docs-breadcrumbs-sep">/</span>}
36
+ {crumb.href ? (
37
+ <Link to={crumb.href}>{crumb.label}</Link>
38
+ ) : (
39
+ <span class="docs-breadcrumbs-current" aria-current="page">
40
+ {crumb.label}
41
+ </span>
42
+ )}
43
+ </>
44
+ ))}
45
+ </nav>
46
+ );
47
+ }
@@ -0,0 +1,40 @@
1
+ import type { ComponentChildren } from "preact";
2
+
3
+ export type CalloutType = "note" | "warning" | "tip" | "danger";
4
+
5
+ export interface CalloutProps {
6
+ type: CalloutType;
7
+ title?: string;
8
+ children: ComponentChildren;
9
+ }
10
+
11
+ const icons: Record<CalloutType, string> = {
12
+ note: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
13
+ warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
14
+ tip: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 014 12.7V17H8v-2.3A7 7 0 0112 2z"/></svg>`,
15
+ danger: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>`,
16
+ };
17
+
18
+ const defaultTitles: Record<CalloutType, string> = {
19
+ note: "Note",
20
+ warning: "Warning",
21
+ tip: "Tip",
22
+ danger: "Danger",
23
+ };
24
+
25
+ export default function Callout(props: CalloutProps) {
26
+ const title = props.title ?? defaultTitles[props.type];
27
+
28
+ return (
29
+ <aside class={`callout callout-${props.type}`}>
30
+ <span
31
+ class="callout-icon"
32
+ dangerouslySetInnerHTML={{ __html: icons[props.type] }}
33
+ />
34
+ <div class="callout-content">
35
+ {title && <p class="callout-title">{title}</p>}
36
+ {props.children}
37
+ </div>
38
+ </aside>
39
+ );
40
+ }
@@ -0,0 +1,31 @@
1
+ import { Link } from "@neutron-build/core/client";
2
+ import type { ComponentChildren } from "preact";
3
+
4
+ export interface CardProps {
5
+ title: string;
6
+ description?: string;
7
+ href?: string;
8
+ icon?: ComponentChildren;
9
+ }
10
+
11
+ export default function Card(props: CardProps) {
12
+ const content = (
13
+ <>
14
+ {props.icon && <div class="card-icon">{props.icon}</div>}
15
+ <span class="card-title">{props.title}</span>
16
+ {props.description && (
17
+ <span class="card-description">{props.description}</span>
18
+ )}
19
+ </>
20
+ );
21
+
22
+ if (props.href) {
23
+ return (
24
+ <Link to={props.href} class="card card--link">
25
+ {content}
26
+ </Link>
27
+ );
28
+ }
29
+
30
+ return <div class="card">{content}</div>;
31
+ }
@@ -0,0 +1,35 @@
1
+ import { useState } from "preact/hooks";
2
+
3
+ export function CopyButton({ code }: { code: string }) {
4
+ const [copied, setCopied] = useState(false);
5
+
6
+ async function handleCopy() {
7
+ try {
8
+ await navigator.clipboard.writeText(code);
9
+ setCopied(true);
10
+ setTimeout(() => setCopied(false), 2000);
11
+ } catch {
12
+ /* clipboard unavailable */
13
+ }
14
+ }
15
+
16
+ return (
17
+ <button
18
+ class="code-block-copy"
19
+ onClick={handleCopy}
20
+ aria-label={copied ? "Copied" : "Copy code"}
21
+ data-copied={copied ? "true" : undefined}
22
+ >
23
+ {copied ? (
24
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
25
+ <polyline points="20 6 9 17 4 12" />
26
+ </svg>
27
+ ) : (
28
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
29
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
30
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
31
+ </svg>
32
+ )}
33
+ </button>
34
+ );
35
+ }
@@ -0,0 +1,72 @@
1
+ import { Link } from "@neutron-build/core/client";
2
+
3
+ interface PaginationLink {
4
+ title: string;
5
+ slug: string;
6
+ }
7
+
8
+ export function Footer({
9
+ prev,
10
+ next,
11
+ }: {
12
+ prev?: PaginationLink;
13
+ next?: PaginationLink;
14
+ }) {
15
+ if (!prev && !next) return null;
16
+
17
+ return (
18
+ <footer class="docs-footer">
19
+ {prev ? (
20
+ <Link to={`/docs/${prev.slug}`} class="docs-footer-link">
21
+ <span class="docs-footer-label">
22
+ <svg
23
+ width="14"
24
+ height="14"
25
+ viewBox="0 0 24 24"
26
+ fill="none"
27
+ stroke="currentColor"
28
+ stroke-width="2"
29
+ stroke-linecap="round"
30
+ stroke-linejoin="round"
31
+ style="display:inline;vertical-align:middle;margin-right:0.25rem"
32
+ >
33
+ <line x1="19" y1="12" x2="5" y2="12" />
34
+ <polyline points="12 19 5 12 12 5" />
35
+ </svg>
36
+ Previous
37
+ </span>
38
+ <span class="docs-footer-title">{prev.title}</span>
39
+ </Link>
40
+ ) : (
41
+ <span />
42
+ )}
43
+ {next ? (
44
+ <Link
45
+ to={`/docs/${next.slug}`}
46
+ class="docs-footer-link docs-footer-link--next"
47
+ >
48
+ <span class="docs-footer-label">
49
+ Next
50
+ <svg
51
+ width="14"
52
+ height="14"
53
+ viewBox="0 0 24 24"
54
+ fill="none"
55
+ stroke="currentColor"
56
+ stroke-width="2"
57
+ stroke-linecap="round"
58
+ stroke-linejoin="round"
59
+ style="display:inline;vertical-align:middle;margin-left:0.25rem"
60
+ >
61
+ <line x1="5" y1="12" x2="19" y2="12" />
62
+ <polyline points="12 5 19 12 12 19" />
63
+ </svg>
64
+ </span>
65
+ <span class="docs-footer-title">{next.title}</span>
66
+ </Link>
67
+ ) : (
68
+ <span />
69
+ )}
70
+ </footer>
71
+ );
72
+ }
@@ -0,0 +1,139 @@
1
+ import { useState, useEffect, useRef } from "preact/hooks";
2
+
3
+ interface SearchResult {
4
+ slug: string;
5
+ title: string;
6
+ path: string;
7
+ }
8
+
9
+ export function Search({ entries }: { entries?: { slug: string; data: { title: string; description?: string } }[] }) {
10
+ const [open, setOpen] = useState(false);
11
+ const [query, setQuery] = useState("");
12
+ const [results, setResults] = useState<SearchResult[]>([]);
13
+ const [active, setActive] = useState(0);
14
+ const modalRef = useRef<HTMLDivElement>(null);
15
+ const inputRef = useRef<HTMLInputElement>(null);
16
+
17
+ useEffect(() => {
18
+ function onKeyDown(e: KeyboardEvent) {
19
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
20
+ e.preventDefault();
21
+ setOpen(true);
22
+ }
23
+ }
24
+ document.addEventListener("keydown", onKeyDown);
25
+ return () => document.removeEventListener("keydown", onKeyDown);
26
+ }, []);
27
+
28
+ useEffect(() => {
29
+ if (open) {
30
+ requestAnimationFrame(() => inputRef.current?.focus());
31
+ } else {
32
+ setQuery("");
33
+ setResults([]);
34
+ setActive(0);
35
+ }
36
+ }, [open]);
37
+
38
+ function search(q: string) {
39
+ setQuery(q);
40
+ setActive(0);
41
+ if (!q.trim() || !entries) {
42
+ setResults([]);
43
+ return;
44
+ }
45
+ const lower = q.toLowerCase();
46
+ const matched = entries
47
+ .filter((e) => {
48
+ const title = e.data.title.toLowerCase();
49
+ const desc = (e.data.description || "").toLowerCase();
50
+ return title.includes(lower) || desc.includes(lower) || e.slug.includes(lower);
51
+ })
52
+ .slice(0, 10)
53
+ .map((e) => ({
54
+ slug: e.slug,
55
+ title: e.data.title,
56
+ path: e.slug.replace(/\//g, " > "),
57
+ }));
58
+ setResults(matched);
59
+ }
60
+
61
+ function navigate(slug: string) {
62
+ setOpen(false);
63
+ window.location.href = `/docs/${slug}`;
64
+ }
65
+
66
+ function onKeyDown(e: KeyboardEvent) {
67
+ if (e.key === "ArrowDown") {
68
+ e.preventDefault();
69
+ setActive((i) => Math.min(i + 1, results.length - 1));
70
+ } else if (e.key === "ArrowUp") {
71
+ e.preventDefault();
72
+ setActive((i) => Math.max(i - 1, 0));
73
+ } else if (e.key === "Enter" && results[active]) {
74
+ navigate(results[active].slug);
75
+ } else if (e.key === "Escape") {
76
+ setOpen(false);
77
+ }
78
+ }
79
+
80
+ return (
81
+ <>
82
+ <button class="search-trigger" onClick={() => setOpen(true)} aria-label="Search docs">
83
+ <svg class="search-trigger-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
84
+ <circle cx="11" cy="11" r="8" />
85
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
86
+ </svg>
87
+ <span class="search-trigger-text">Search</span>
88
+ <span class="search-trigger-kbd">&#8984;K</span>
89
+ </button>
90
+ <div class={`search-overlay${open ? " is-open" : ""}`} onClick={(e) => { if (e.target === e.currentTarget) setOpen(false); }}>
91
+ <div class="search-modal" ref={modalRef}>
92
+ <div class="search-input-wrap">
93
+ <svg class="search-input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
94
+ <circle cx="11" cy="11" r="8" />
95
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
96
+ </svg>
97
+ <input
98
+ ref={inputRef}
99
+ type="text"
100
+ class="search-input"
101
+ placeholder="Search documentation..."
102
+ value={query}
103
+ onInput={(e) => search((e.target as HTMLInputElement).value)}
104
+ onKeyDown={onKeyDown}
105
+ />
106
+ <kbd class="search-input-kbd">Esc</kbd>
107
+ </div>
108
+ {results.length > 0 && (
109
+ <div class="search-results" role="listbox">
110
+ {results.map((r, i) => (
111
+ <div
112
+ key={r.slug}
113
+ role="option"
114
+ aria-selected={i === active}
115
+ class="search-result"
116
+ onClick={() => navigate(r.slug)}
117
+ onMouseEnter={() => setActive(i)}
118
+ >
119
+ <span class="search-result-title">{r.title}</span>
120
+ <span class="search-result-path">{r.path}</span>
121
+ </div>
122
+ ))}
123
+ </div>
124
+ )}
125
+ {query && results.length === 0 && (
126
+ <div class="search-no-results">No results for "{query}"</div>
127
+ )}
128
+ <div class="search-footer">
129
+ <div class="search-footer-keys">
130
+ <span class="search-footer-key"><kbd>&#8593;&#8595;</kbd> Navigate</span>
131
+ <span class="search-footer-key"><kbd>&#9166;</kbd> Open</span>
132
+ <span class="search-footer-key"><kbd>Esc</kbd> Close</span>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </>
138
+ );
139
+ }
@@ -0,0 +1,59 @@
1
+ import { Link } from "@neutron-build/core/client";
2
+ import type { SidebarSection } from "../lib/sidebar";
3
+
4
+ function SidebarLink(props: {
5
+ slug: string;
6
+ title: string;
7
+ active?: boolean;
8
+ }) {
9
+ return (
10
+ <Link
11
+ to={`/docs/${props.slug}`}
12
+ class="docs-sidebar-link"
13
+ aria-current={props.active ? "page" : undefined}
14
+ >
15
+ {props.title}
16
+ </Link>
17
+ );
18
+ }
19
+
20
+ export function Sidebar({ tree }: { tree: SidebarSection[] }) {
21
+ return (
22
+ <>
23
+ {tree.map((section) => {
24
+ if (!section.title) {
25
+ return section.items.map((item) => (
26
+ <SidebarLink
27
+ key={item.slug}
28
+ slug={item.slug}
29
+ title={item.title}
30
+ active={item.active}
31
+ />
32
+ ));
33
+ }
34
+
35
+ const hasActive = section.items.some((item) => item.active);
36
+
37
+ return (
38
+ <details
39
+ key={section.slug}
40
+ class="docs-sidebar-section"
41
+ open={hasActive || undefined}
42
+ >
43
+ <summary>{section.title}</summary>
44
+ <div class="docs-sidebar-section-items">
45
+ {section.items.map((item) => (
46
+ <SidebarLink
47
+ key={item.slug}
48
+ slug={item.slug}
49
+ title={item.title}
50
+ active={item.active}
51
+ />
52
+ ))}
53
+ </div>
54
+ </details>
55
+ );
56
+ })}
57
+ </>
58
+ );
59
+ }