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,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,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,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,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,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">⌘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>↑↓</kbd> Navigate</span>
|
|
131
|
+
<span class="search-footer-key"><kbd>⏎</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
|
+
}
|