@marlinjai/clearify 1.5.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 +81 -0
- package/bin/clearify.js +2 -0
- package/dist/node/chunk-5TD7NQIW.js +25 -0
- package/dist/node/chunk-B2Q23JW3.js +55 -0
- package/dist/node/chunk-CQ4MNGBE.js +301 -0
- package/dist/node/chunk-GFD54GNO.js +223 -0
- package/dist/node/chunk-IBK35HZR.js +194 -0
- package/dist/node/chunk-L24ILRSX.js +125 -0
- package/dist/node/chunk-NXQNNLGC.js +395 -0
- package/dist/node/chunk-PRTER35L.js +48 -0
- package/dist/node/chunk-SCZZB7OE.js +9 -0
- package/dist/node/chunk-V7LLYIRO.js +8 -0
- package/dist/node/chunk-WT5W333R.js +136 -0
- package/dist/node/cli/index.d.ts +2 -0
- package/dist/node/cli/index.js +41 -0
- package/dist/node/core/config.d.ts +9 -0
- package/dist/node/core/config.js +16 -0
- package/dist/node/core/mermaid-renderer.d.ts +22 -0
- package/dist/node/core/mermaid-renderer.js +125 -0
- package/dist/node/core/mermaid-utils.d.ts +3 -0
- package/dist/node/core/mermaid-utils.js +6 -0
- package/dist/node/core/navigation.d.ts +2 -0
- package/dist/node/core/navigation.js +13 -0
- package/dist/node/core/openapi-parser.d.ts +14 -0
- package/dist/node/core/openapi-parser.js +6 -0
- package/dist/node/core/remark-mermaid.d.ts +10 -0
- package/dist/node/core/remark-mermaid.js +11 -0
- package/dist/node/core/search.d.ts +31 -0
- package/dist/node/core/search.js +6 -0
- package/dist/node/node/build.d.ts +3 -0
- package/dist/node/node/build.js +14 -0
- package/dist/node/node/check.d.ts +3 -0
- package/dist/node/node/check.js +10 -0
- package/dist/node/node/index.d.ts +11 -0
- package/dist/node/node/index.js +108 -0
- package/dist/node/node/init.d.ts +6 -0
- package/dist/node/node/init.js +6 -0
- package/dist/node/presets/nestjs.d.ts +15 -0
- package/dist/node/presets/nestjs.js +98 -0
- package/dist/node/types/index.d.ts +79 -0
- package/dist/node/types/index.js +6 -0
- package/dist/node/vite-plugin/index.d.ts +13 -0
- package/dist/node/vite-plugin/index.js +11 -0
- package/package.json +94 -0
- package/src/client/App.tsx +101 -0
- package/src/client/Page.tsx +15 -0
- package/src/client/entry-server.tsx +79 -0
- package/src/client/index.html +18 -0
- package/src/client/main.tsx +11 -0
- package/src/theme/CodeBlock.tsx +103 -0
- package/src/theme/Content.tsx +32 -0
- package/src/theme/Footer.tsx +53 -0
- package/src/theme/Head.tsx +80 -0
- package/src/theme/HeadContext.tsx +32 -0
- package/src/theme/Header.tsx +177 -0
- package/src/theme/Layout.tsx +44 -0
- package/src/theme/MDXComponents.tsx +40 -0
- package/src/theme/NotFound.tsx +246 -0
- package/src/theme/Search.tsx +359 -0
- package/src/theme/Sidebar.tsx +325 -0
- package/src/theme/TableOfContents.tsx +153 -0
- package/src/theme/ThemeProvider.tsx +77 -0
- package/src/theme/components/Accordion.tsx +109 -0
- package/src/theme/components/Badge.tsx +72 -0
- package/src/theme/components/Breadcrumbs.tsx +88 -0
- package/src/theme/components/Callout.tsx +115 -0
- package/src/theme/components/Card.tsx +103 -0
- package/src/theme/components/CodeGroup.tsx +79 -0
- package/src/theme/components/Columns.tsx +42 -0
- package/src/theme/components/Frame.tsx +55 -0
- package/src/theme/components/Mermaid.tsx +99 -0
- package/src/theme/components/MermaidStatic.tsx +32 -0
- package/src/theme/components/OpenAPI.tsx +160 -0
- package/src/theme/components/OpenAPIPage.tsx +16 -0
- package/src/theme/components/Steps.tsx +76 -0
- package/src/theme/components/Tabs.tsx +75 -0
- package/src/theme/components/Tooltip.tsx +108 -0
- package/src/theme/components/index.ts +14 -0
- package/src/theme/styles/globals.css +363 -0
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@marlinjai/clearify",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "An open-source documentation site generator. Turn markdown into beautiful docs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clearify": "./bin/clearify.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/node/types/index.d.ts",
|
|
12
|
+
"import": "./dist/node/types/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./config": {
|
|
15
|
+
"types": "./dist/node/core/config.d.ts",
|
|
16
|
+
"import": "./dist/node/core/config.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin",
|
|
21
|
+
"dist",
|
|
22
|
+
"src/client",
|
|
23
|
+
"src/theme"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev:self": "node bin/clearify.js dev"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@mdx-js/react": "^3.1.1",
|
|
31
|
+
"@mdx-js/rollup": "^3.1.1",
|
|
32
|
+
"@scalar/api-reference-react": "^0.8.52",
|
|
33
|
+
"@shikijs/rehype": "^3.22.0",
|
|
34
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
35
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
36
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
37
|
+
"autoprefixer": "^10.4.24",
|
|
38
|
+
"cac": "^6.7.14",
|
|
39
|
+
"clsx": "^2.1.1",
|
|
40
|
+
"flexsearch": "^0.8.212",
|
|
41
|
+
"globby": "^16.1.0",
|
|
42
|
+
"gray-matter": "^4.0.3",
|
|
43
|
+
"mermaid": "^11.0.0",
|
|
44
|
+
"postcss": "^8.5.6",
|
|
45
|
+
"react-router-dom": "^7.13.0",
|
|
46
|
+
"remark-frontmatter": "^5.0.0",
|
|
47
|
+
"remark-gfm": "^4.0.1",
|
|
48
|
+
"remark-mdx-frontmatter": "^5.2.0",
|
|
49
|
+
"shiki": "^3.22.0",
|
|
50
|
+
"tailwindcss": "^4.1.18",
|
|
51
|
+
"unist-util-visit": "^5.0.0",
|
|
52
|
+
"vite": "^6.1.0",
|
|
53
|
+
"zod": "^4.3.6"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"puppeteer": ">=22.0.0",
|
|
57
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
58
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependenciesMeta": {
|
|
61
|
+
"puppeteer": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/node": "^25.2.2",
|
|
67
|
+
"@types/react": "^18.3.18",
|
|
68
|
+
"@types/react-dom": "^18.3.5",
|
|
69
|
+
"puppeteer": "^24.37.2",
|
|
70
|
+
"tsup": "^8.3.6",
|
|
71
|
+
"typescript": "^5.7.3"
|
|
72
|
+
},
|
|
73
|
+
"license": "MIT",
|
|
74
|
+
"author": "MarlinJai",
|
|
75
|
+
"repository": {
|
|
76
|
+
"type": "git",
|
|
77
|
+
"url": "https://github.com/marlinjai/clearify.git"
|
|
78
|
+
},
|
|
79
|
+
"homepage": "https://github.com/marlinjai/clearify",
|
|
80
|
+
"bugs": {
|
|
81
|
+
"url": "https://github.com/marlinjai/clearify/issues"
|
|
82
|
+
},
|
|
83
|
+
"keywords": [
|
|
84
|
+
"documentation",
|
|
85
|
+
"docs",
|
|
86
|
+
"markdown",
|
|
87
|
+
"mdx",
|
|
88
|
+
"static-site-generator",
|
|
89
|
+
"vite",
|
|
90
|
+
"react",
|
|
91
|
+
"mermaid",
|
|
92
|
+
"openapi"
|
|
93
|
+
]
|
|
94
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React, { useEffect, useState, useRef } from 'react';
|
|
2
|
+
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
|
3
|
+
import { MDXProvider } from '@mdx-js/react';
|
|
4
|
+
// @ts-expect-error virtual module
|
|
5
|
+
import routes from 'virtual:clearify/routes';
|
|
6
|
+
// @ts-expect-error virtual module
|
|
7
|
+
import config from 'virtual:clearify/config';
|
|
8
|
+
// @ts-expect-error virtual module
|
|
9
|
+
import sections from 'virtual:clearify/navigation';
|
|
10
|
+
import { Layout } from '../theme/Layout.js';
|
|
11
|
+
import { Head } from '../theme/Head.js';
|
|
12
|
+
import { NotFound } from '../theme/NotFound.js';
|
|
13
|
+
import { mdxComponents } from '../theme/MDXComponents.js';
|
|
14
|
+
import '../theme/styles/globals.css';
|
|
15
|
+
|
|
16
|
+
interface RouteEntry {
|
|
17
|
+
path: string;
|
|
18
|
+
component: () => Promise<any>;
|
|
19
|
+
frontmatter: { title?: string; description?: string };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function PageWrapper({ loader, fallbackFrontmatter }: { loader: () => Promise<any>; fallbackFrontmatter: any }) {
|
|
23
|
+
const location = useLocation();
|
|
24
|
+
const [page, setPage] = useState<{ Component: React.ComponentType; fm: any } | null>(null);
|
|
25
|
+
const isInitialRender = useRef(true);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
let cancelled = false;
|
|
29
|
+
|
|
30
|
+
// On SSR-hydrated pages, the initial route's content is already visible.
|
|
31
|
+
// Still load the module so subsequent SPA navigations work, but don't
|
|
32
|
+
// flash "Loading..." by clearing state only on navigation changes.
|
|
33
|
+
if (isInitialRender.current) {
|
|
34
|
+
isInitialRender.current = false;
|
|
35
|
+
// Load eagerly but don't clear existing DOM content
|
|
36
|
+
loader().then((mod) => {
|
|
37
|
+
if (cancelled) return;
|
|
38
|
+
setPage({
|
|
39
|
+
Component: mod.default,
|
|
40
|
+
fm: mod.frontmatter ?? fallbackFrontmatter,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
return () => { cancelled = true; };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setPage(null);
|
|
47
|
+
loader().then((mod) => {
|
|
48
|
+
if (cancelled) return;
|
|
49
|
+
setPage({
|
|
50
|
+
Component: mod.default,
|
|
51
|
+
fm: mod.frontmatter ?? fallbackFrontmatter,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
return () => { cancelled = true; };
|
|
55
|
+
}, [location.pathname]);
|
|
56
|
+
|
|
57
|
+
if (!page) {
|
|
58
|
+
// If we're hydrating an SSR page, don't show loading — the HTML is already there.
|
|
59
|
+
// The browser will display the server-rendered content until React hydrates.
|
|
60
|
+
const isSSR = typeof document !== 'undefined' && document.getElementById('root')?.dataset.clearifySsr;
|
|
61
|
+
if (isSSR && isInitialRender.current) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return <div style={{ padding: '2rem', color: 'var(--clearify-text-secondary)' }}>Loading...</div>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { Component, fm } = page;
|
|
68
|
+
return (
|
|
69
|
+
<article>
|
|
70
|
+
<Head title={fm?.title} description={fm?.description} url={location.pathname} />
|
|
71
|
+
<Component />
|
|
72
|
+
</article>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function AppRoutes() {
|
|
77
|
+
return (
|
|
78
|
+
<Layout config={config} sections={sections}>
|
|
79
|
+
<Routes>
|
|
80
|
+
{(routes as RouteEntry[]).map((route) => (
|
|
81
|
+
<Route
|
|
82
|
+
key={route.path}
|
|
83
|
+
path={route.path}
|
|
84
|
+
element={<PageWrapper loader={route.component} fallbackFrontmatter={route.frontmatter} />}
|
|
85
|
+
/>
|
|
86
|
+
))}
|
|
87
|
+
<Route path="*" element={<NotFound />} />
|
|
88
|
+
</Routes>
|
|
89
|
+
</Layout>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function App() {
|
|
94
|
+
return (
|
|
95
|
+
<BrowserRouter>
|
|
96
|
+
<MDXProvider components={mdxComponents}>
|
|
97
|
+
<AppRoutes />
|
|
98
|
+
</MDXProvider>
|
|
99
|
+
</BrowserRouter>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface PageProps {
|
|
4
|
+
content: React.ComponentType;
|
|
5
|
+
frontmatter: { title?: string; description?: string };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Page({ content: Content, frontmatter }: PageProps) {
|
|
9
|
+
return (
|
|
10
|
+
<article>
|
|
11
|
+
{frontmatter.title && <h1>{frontmatter.title}</h1>}
|
|
12
|
+
<Content />
|
|
13
|
+
</article>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderToString } from 'react-dom/server';
|
|
3
|
+
import { MemoryRouter, Routes, Route } from 'react-router';
|
|
4
|
+
import { MDXProvider } from '@mdx-js/react';
|
|
5
|
+
// @ts-expect-error virtual module
|
|
6
|
+
import routes from 'virtual:clearify/routes';
|
|
7
|
+
// @ts-expect-error virtual module
|
|
8
|
+
import config from 'virtual:clearify/config';
|
|
9
|
+
// @ts-expect-error virtual module
|
|
10
|
+
import sections from 'virtual:clearify/navigation';
|
|
11
|
+
import { Layout } from '../theme/Layout.js';
|
|
12
|
+
import { Head } from '../theme/Head.js';
|
|
13
|
+
import { mdxComponents } from '../theme/MDXComponents.js';
|
|
14
|
+
import { HeadProvider, type HeadData } from '../theme/HeadContext.js';
|
|
15
|
+
import '../theme/styles/globals.css';
|
|
16
|
+
|
|
17
|
+
interface RouteEntry {
|
|
18
|
+
path: string;
|
|
19
|
+
component: () => Promise<any>;
|
|
20
|
+
frontmatter: { title?: string; description?: string };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function render(url: string): Promise<{ html: string; head: HeadData }> {
|
|
24
|
+
// Find the matching route and pre-load its module
|
|
25
|
+
const allRoutes = routes as RouteEntry[];
|
|
26
|
+
// Prefer exact match over catch-all
|
|
27
|
+
const matchedRoute =
|
|
28
|
+
allRoutes.find((r) => r.path === url) ??
|
|
29
|
+
allRoutes.find((r) => {
|
|
30
|
+
if (!r.path.endsWith('/*')) return false;
|
|
31
|
+
const base = r.path.slice(0, -2);
|
|
32
|
+
return url === base || url.startsWith(base + '/');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let Component: React.ComponentType = () => null;
|
|
36
|
+
let fm: { title?: string; description?: string } = {};
|
|
37
|
+
|
|
38
|
+
if (matchedRoute) {
|
|
39
|
+
const mod = await matchedRoute.component();
|
|
40
|
+
Component = mod.default;
|
|
41
|
+
fm = mod.frontmatter ?? matchedRoute.frontmatter;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Collect head data during render
|
|
45
|
+
let headData: HeadData = {
|
|
46
|
+
title: config.name,
|
|
47
|
+
description: `${config.name} documentation`,
|
|
48
|
+
url,
|
|
49
|
+
siteName: config.name,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const onCollect = (data: HeadData) => {
|
|
53
|
+
headData = data;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const html = renderToString(
|
|
57
|
+
<HeadProvider onCollect={onCollect}>
|
|
58
|
+
<MemoryRouter initialEntries={[url]}>
|
|
59
|
+
<MDXProvider components={mdxComponents}>
|
|
60
|
+
<Layout config={config} sections={sections}>
|
|
61
|
+
<Routes>
|
|
62
|
+
<Route
|
|
63
|
+
path={matchedRoute?.path ?? url}
|
|
64
|
+
element={
|
|
65
|
+
<article>
|
|
66
|
+
<Head title={fm.title} description={fm.description} url={url} />
|
|
67
|
+
<Component />
|
|
68
|
+
</article>
|
|
69
|
+
}
|
|
70
|
+
/>
|
|
71
|
+
</Routes>
|
|
72
|
+
</Layout>
|
|
73
|
+
</MDXProvider>
|
|
74
|
+
</MemoryRouter>
|
|
75
|
+
</HeadProvider>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return { html, head: headData };
|
|
79
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
<meta name="generator" content="Clearify" />
|
|
7
|
+
<!--clearify-head-->
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400;1,500&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
11
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📖</text></svg>" />
|
|
12
|
+
<title>Documentation</title>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div id="root"><!--clearify-outlet--></div>
|
|
16
|
+
<script type="module" src="./main.tsx"></script>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
3
|
+
import { App } from './App.js';
|
|
4
|
+
|
|
5
|
+
const container = document.getElementById('root')!;
|
|
6
|
+
|
|
7
|
+
if (container.dataset.clearifySsr) {
|
|
8
|
+
hydrateRoot(container, <App />);
|
|
9
|
+
} else {
|
|
10
|
+
createRoot(container).render(<App />);
|
|
11
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export function CodeBlock({ children, className, ...props }: React.HTMLAttributes<HTMLPreElement>) {
|
|
4
|
+
const [copied, setCopied] = useState(false);
|
|
5
|
+
const preRef = useRef<HTMLPreElement>(null);
|
|
6
|
+
|
|
7
|
+
const lang = className
|
|
8
|
+
?.split(' ')
|
|
9
|
+
.find((c) => c.startsWith('language-'))
|
|
10
|
+
?.replace('language-', '') ?? '';
|
|
11
|
+
|
|
12
|
+
const handleCopy = async () => {
|
|
13
|
+
const text = preRef.current?.textContent ?? '';
|
|
14
|
+
await navigator.clipboard.writeText(text);
|
|
15
|
+
setCopied(true);
|
|
16
|
+
setTimeout(() => setCopied(false), 2000);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div style={{ position: 'relative', marginBottom: '1.25rem' }} className="clearify-codeblock">
|
|
21
|
+
{lang && (
|
|
22
|
+
<div
|
|
23
|
+
style={{
|
|
24
|
+
position: 'absolute',
|
|
25
|
+
top: '0.625rem',
|
|
26
|
+
right: '3.5rem',
|
|
27
|
+
fontSize: '0.6875rem',
|
|
28
|
+
color: 'var(--clearify-text-tertiary)',
|
|
29
|
+
textTransform: 'uppercase',
|
|
30
|
+
letterSpacing: '0.05em',
|
|
31
|
+
userSelect: 'none',
|
|
32
|
+
fontFamily: 'var(--font-mono)',
|
|
33
|
+
fontWeight: 500,
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{lang}
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
<button
|
|
40
|
+
onClick={handleCopy}
|
|
41
|
+
style={{
|
|
42
|
+
position: 'absolute',
|
|
43
|
+
top: '0.5rem',
|
|
44
|
+
right: '0.5rem',
|
|
45
|
+
background: 'var(--clearify-bg)',
|
|
46
|
+
border: '1px solid var(--clearify-border)',
|
|
47
|
+
borderRadius: 'var(--clearify-radius-sm)',
|
|
48
|
+
padding: '0.25rem 0.625rem',
|
|
49
|
+
cursor: 'pointer',
|
|
50
|
+
fontSize: '0.6875rem',
|
|
51
|
+
fontWeight: 500,
|
|
52
|
+
color: copied ? 'var(--clearify-primary)' : 'var(--clearify-text-tertiary)',
|
|
53
|
+
transition: 'all 0.15s ease',
|
|
54
|
+
fontFamily: 'var(--font-sans)',
|
|
55
|
+
}}
|
|
56
|
+
className="clearify-copy-btn"
|
|
57
|
+
aria-label="Copy code"
|
|
58
|
+
>
|
|
59
|
+
{copied ? (
|
|
60
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
|
61
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
62
|
+
<polyline points="20 6 9 17 4 12" />
|
|
63
|
+
</svg>
|
|
64
|
+
Copied
|
|
65
|
+
</span>
|
|
66
|
+
) : (
|
|
67
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
|
68
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
69
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
70
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
71
|
+
</svg>
|
|
72
|
+
Copy
|
|
73
|
+
</span>
|
|
74
|
+
)}
|
|
75
|
+
</button>
|
|
76
|
+
<pre
|
|
77
|
+
ref={preRef}
|
|
78
|
+
className={className}
|
|
79
|
+
style={{
|
|
80
|
+
backgroundColor: 'var(--clearify-bg-secondary)',
|
|
81
|
+
border: '1px solid var(--clearify-border)',
|
|
82
|
+
borderRadius: 'var(--clearify-radius)',
|
|
83
|
+
padding: '1rem 1.25rem',
|
|
84
|
+
overflow: 'auto',
|
|
85
|
+
fontSize: '0.8125rem',
|
|
86
|
+
lineHeight: 1.75,
|
|
87
|
+
fontFamily: 'var(--font-mono)',
|
|
88
|
+
}}
|
|
89
|
+
{...props}
|
|
90
|
+
>
|
|
91
|
+
{children}
|
|
92
|
+
</pre>
|
|
93
|
+
|
|
94
|
+
<style>{`
|
|
95
|
+
.clearify-copy-btn:hover {
|
|
96
|
+
border-color: var(--clearify-border-strong) !important;
|
|
97
|
+
color: var(--clearify-text) !important;
|
|
98
|
+
background: var(--clearify-bg-secondary) !important;
|
|
99
|
+
}
|
|
100
|
+
`}</style>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Breadcrumbs } from './components/Breadcrumbs.js';
|
|
3
|
+
|
|
4
|
+
interface ContentProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Content({ children }: ContentProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className="clearify-prose"
|
|
12
|
+
style={{
|
|
13
|
+
flex: 1,
|
|
14
|
+
minWidth: 0,
|
|
15
|
+
padding: '2.5rem 3rem',
|
|
16
|
+
maxWidth: 'var(--clearify-content-max)',
|
|
17
|
+
animation: 'clearify-fade-in 0.3s ease-out',
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
<Breadcrumbs />
|
|
21
|
+
{children}
|
|
22
|
+
|
|
23
|
+
<style>{`
|
|
24
|
+
@media (max-width: 768px) {
|
|
25
|
+
.clearify-prose {
|
|
26
|
+
padding: 1.5rem 1.25rem !important;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
`}</style>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface FooterProps {
|
|
4
|
+
name: string;
|
|
5
|
+
links?: { github?: string; [key: string]: string | undefined };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Footer({ name, links }: FooterProps) {
|
|
9
|
+
return (
|
|
10
|
+
<footer
|
|
11
|
+
style={{
|
|
12
|
+
borderTop: '1px solid var(--clearify-border)',
|
|
13
|
+
padding: '2rem 2.5rem',
|
|
14
|
+
fontSize: '0.8125rem',
|
|
15
|
+
color: 'var(--clearify-text-tertiary)',
|
|
16
|
+
display: 'flex',
|
|
17
|
+
justifyContent: 'space-between',
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
flexWrap: 'wrap',
|
|
20
|
+
gap: '1rem',
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
<div style={{ letterSpacing: '-0.01em' }}>
|
|
24
|
+
© {new Date().getFullYear()} {name}.{' '}
|
|
25
|
+
<span style={{ opacity: 0.6 }}>Built with Clearify.</span>
|
|
26
|
+
</div>
|
|
27
|
+
{links && (
|
|
28
|
+
<div style={{ display: 'flex', gap: '1.25rem' }}>
|
|
29
|
+
{Object.entries(links).map(([key, url]) =>
|
|
30
|
+
url ? (
|
|
31
|
+
<a
|
|
32
|
+
key={key}
|
|
33
|
+
href={url}
|
|
34
|
+
target="_blank"
|
|
35
|
+
rel="noopener noreferrer"
|
|
36
|
+
style={{
|
|
37
|
+
color: 'var(--clearify-text-tertiary)',
|
|
38
|
+
textDecoration: 'none',
|
|
39
|
+
transition: 'color 0.15s',
|
|
40
|
+
fontSize: '0.8125rem',
|
|
41
|
+
}}
|
|
42
|
+
onMouseEnter={(e) => (e.currentTarget.style.color = 'var(--clearify-text-secondary)')}
|
|
43
|
+
onMouseLeave={(e) => (e.currentTarget.style.color = 'var(--clearify-text-tertiary)')}
|
|
44
|
+
>
|
|
45
|
+
{key.charAt(0).toUpperCase() + key.slice(1)}
|
|
46
|
+
</a>
|
|
47
|
+
) : null
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
</footer>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
// @ts-expect-error virtual module
|
|
3
|
+
import config from 'virtual:clearify/config';
|
|
4
|
+
import { useHeadContext } from './HeadContext.js';
|
|
5
|
+
|
|
6
|
+
interface HeadProps {
|
|
7
|
+
title?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Head({ title, description, url }: HeadProps) {
|
|
13
|
+
const headCtx = useHeadContext();
|
|
14
|
+
|
|
15
|
+
// SSR mode: push data to collector, no DOM access
|
|
16
|
+
if (headCtx) {
|
|
17
|
+
const pageTitle = title ? `${title} | ${config.name}` : config.name;
|
|
18
|
+
const desc = description ?? `${config.name} documentation`;
|
|
19
|
+
headCtx.collect({
|
|
20
|
+
title: pageTitle,
|
|
21
|
+
description: desc,
|
|
22
|
+
url: url ?? '',
|
|
23
|
+
siteName: config.name,
|
|
24
|
+
});
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Client mode: useEffect DOM mutations (existing behavior)
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const pageTitle = title ? `${title} | ${config.name}` : config.name;
|
|
31
|
+
document.title = pageTitle;
|
|
32
|
+
|
|
33
|
+
const desc = description ?? `${config.name} documentation`;
|
|
34
|
+
|
|
35
|
+
// Update meta description
|
|
36
|
+
let metaDesc = document.querySelector('meta[name="description"]');
|
|
37
|
+
if (!metaDesc) {
|
|
38
|
+
metaDesc = document.createElement('meta');
|
|
39
|
+
metaDesc.setAttribute('name', 'description');
|
|
40
|
+
document.head.appendChild(metaDesc);
|
|
41
|
+
}
|
|
42
|
+
metaDesc.setAttribute('content', desc);
|
|
43
|
+
|
|
44
|
+
// Update OG tags
|
|
45
|
+
setMeta('og:title', pageTitle);
|
|
46
|
+
setMeta('og:description', desc);
|
|
47
|
+
setMeta('og:type', 'article');
|
|
48
|
+
setMeta('og:site_name', config.name);
|
|
49
|
+
|
|
50
|
+
// Update Twitter tags
|
|
51
|
+
setMeta('twitter:card', 'summary', 'name');
|
|
52
|
+
setMeta('twitter:title', pageTitle, 'name');
|
|
53
|
+
setMeta('twitter:description', desc, 'name');
|
|
54
|
+
|
|
55
|
+
// Update canonical link
|
|
56
|
+
if (url) {
|
|
57
|
+
const siteUrl = config.siteUrl?.replace(/\/$/, '') ?? '';
|
|
58
|
+
const canonical = siteUrl ? `${siteUrl}${url}` : url;
|
|
59
|
+
let link = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null;
|
|
60
|
+
if (!link) {
|
|
61
|
+
link = document.createElement('link');
|
|
62
|
+
link.setAttribute('rel', 'canonical');
|
|
63
|
+
document.head.appendChild(link);
|
|
64
|
+
}
|
|
65
|
+
link.setAttribute('href', canonical);
|
|
66
|
+
}
|
|
67
|
+
}, [title, description, url]);
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function setMeta(key: string, content: string, attr: 'property' | 'name' = 'property') {
|
|
73
|
+
let el = document.querySelector(`meta[${attr}="${key}"]`);
|
|
74
|
+
if (!el) {
|
|
75
|
+
el = document.createElement('meta');
|
|
76
|
+
el.setAttribute(attr, key);
|
|
77
|
+
document.head.appendChild(el);
|
|
78
|
+
}
|
|
79
|
+
el.setAttribute('content', content);
|
|
80
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface HeadData {
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
url: string;
|
|
7
|
+
siteName: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface HeadContextValue {
|
|
11
|
+
collect: (data: HeadData) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const HeadContext = createContext<HeadContextValue | null>(null);
|
|
15
|
+
|
|
16
|
+
export function useHeadContext() {
|
|
17
|
+
return useContext(HeadContext);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function HeadProvider({
|
|
21
|
+
children,
|
|
22
|
+
onCollect,
|
|
23
|
+
}: {
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
onCollect: (data: HeadData) => void;
|
|
26
|
+
}) {
|
|
27
|
+
return (
|
|
28
|
+
<HeadContext.Provider value={{ collect: onCollect }}>
|
|
29
|
+
{children}
|
|
30
|
+
</HeadContext.Provider>
|
|
31
|
+
);
|
|
32
|
+
}
|