boltdocs 1.3.0 → 1.3.2
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/dist/{cache-EHR7SXRU.mjs → cache-GQHF6BXI.mjs} +1 -1
- package/dist/{chunk-GSYECEZY.mjs → chunk-CYBWLFOG.mjs} +5 -1
- package/dist/node/index.js +36 -20
- package/dist/node/index.mjs +34 -22
- package/package.json +1 -1
- package/src/client/app/index.tsx +344 -344
- package/src/client/app/preload.tsx +56 -56
- package/src/client/index.ts +40 -40
- package/src/client/ssr.tsx +51 -51
- package/src/client/theme/components/CodeBlock/CodeBlock.tsx +76 -76
- package/src/client/theme/components/CodeBlock/index.ts +1 -1
- package/src/client/theme/components/PackageManagerTabs/PackageManagerTabs.tsx +154 -154
- package/src/client/theme/components/PackageManagerTabs/index.ts +1 -1
- package/src/client/theme/components/PackageManagerTabs/pkg-tabs.css +64 -64
- package/src/client/theme/components/Playground/Playground.tsx +124 -124
- package/src/client/theme/components/Playground/index.ts +1 -1
- package/src/client/theme/components/Playground/playground.css +168 -168
- package/src/client/theme/components/Video/Video.tsx +84 -84
- package/src/client/theme/components/Video/index.ts +1 -1
- package/src/client/theme/components/Video/video.css +41 -41
- package/src/client/theme/components/mdx/Admonition.tsx +80 -80
- package/src/client/theme/components/mdx/Badge.tsx +31 -31
- package/src/client/theme/components/mdx/Button.tsx +50 -50
- package/src/client/theme/components/mdx/Card.tsx +80 -80
- package/src/client/theme/components/mdx/List.tsx +57 -57
- package/src/client/theme/components/mdx/Tabs.tsx +94 -94
- package/src/client/theme/components/mdx/index.ts +18 -18
- package/src/client/theme/components/mdx/mdx-components.css +424 -424
- package/src/client/theme/icons/bun.tsx +62 -62
- package/src/client/theme/icons/deno.tsx +20 -20
- package/src/client/theme/icons/discord.tsx +12 -12
- package/src/client/theme/icons/github.tsx +15 -15
- package/src/client/theme/icons/npm.tsx +13 -13
- package/src/client/theme/icons/pnpm.tsx +72 -72
- package/src/client/theme/icons/twitter.tsx +12 -12
- package/src/client/theme/styles/markdown.css +343 -343
- package/src/client/theme/styles/variables.css +162 -162
- package/src/client/theme/styles.css +37 -37
- package/src/client/theme/ui/BackgroundGradient/BackgroundGradient.tsx +10 -10
- package/src/client/theme/ui/BackgroundGradient/index.ts +1 -1
- package/src/client/theme/ui/Breadcrumbs/Breadcrumbs.tsx +68 -68
- package/src/client/theme/ui/Breadcrumbs/index.ts +1 -1
- package/src/client/theme/ui/Footer/footer.css +32 -32
- package/src/client/theme/ui/Head/Head.tsx +69 -69
- package/src/client/theme/ui/Head/index.ts +1 -1
- package/src/client/theme/ui/LanguageSwitcher/LanguageSwitcher.tsx +125 -125
- package/src/client/theme/ui/LanguageSwitcher/index.ts +1 -1
- package/src/client/theme/ui/LanguageSwitcher/language-switcher.css +98 -98
- package/src/client/theme/ui/Layout/Layout.tsx +202 -202
- package/src/client/theme/ui/Layout/base.css +76 -76
- package/src/client/theme/ui/Layout/index.ts +2 -2
- package/src/client/theme/ui/Layout/pagination.css +72 -72
- package/src/client/theme/ui/Layout/responsive.css +36 -36
- package/src/client/theme/ui/Link/Link.tsx +254 -254
- package/src/client/theme/ui/Link/index.ts +2 -2
- package/src/client/theme/ui/Loading/Loading.tsx +10 -10
- package/src/client/theme/ui/Loading/index.ts +1 -1
- package/src/client/theme/ui/Loading/loading.css +30 -30
- package/src/client/theme/ui/Navbar/GithubStars.tsx +27 -27
- package/src/client/theme/ui/Navbar/Navbar.tsx +145 -145
- package/src/client/theme/ui/Navbar/index.ts +2 -2
- package/src/client/theme/ui/Navbar/navbar.css +233 -233
- package/src/client/theme/ui/NotFound/NotFound.tsx +19 -19
- package/src/client/theme/ui/NotFound/index.ts +1 -1
- package/src/client/theme/ui/NotFound/not-found.css +64 -64
- package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +235 -235
- package/src/client/theme/ui/OnThisPage/index.ts +1 -1
- package/src/client/theme/ui/OnThisPage/toc.css +132 -132
- package/src/client/theme/ui/PoweredBy/PoweredBy.tsx +18 -18
- package/src/client/theme/ui/PoweredBy/index.ts +1 -1
- package/src/client/theme/ui/PoweredBy/powered-by.css +76 -76
- package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +199 -199
- package/src/client/theme/ui/SearchDialog/index.ts +1 -1
- package/src/client/theme/ui/SearchDialog/search.css +152 -152
- package/src/client/theme/ui/Sidebar/Sidebar.tsx +204 -204
- package/src/client/theme/ui/Sidebar/index.ts +1 -1
- package/src/client/theme/ui/Sidebar/sidebar.css +236 -236
- package/src/client/theme/ui/ThemeToggle/ThemeToggle.tsx +69 -69
- package/src/client/theme/ui/ThemeToggle/index.ts +1 -1
- package/src/client/theme/ui/VersionSwitcher/VersionSwitcher.tsx +136 -136
- package/src/client/theme/ui/VersionSwitcher/index.ts +1 -1
- package/src/client/types.ts +50 -50
- package/src/client/utils.ts +26 -26
- package/src/node/cache.ts +408 -408
- package/src/node/config.ts +192 -192
- package/src/node/index.ts +21 -21
- package/src/node/mdx.ts +120 -120
- package/src/node/plugin/entry.ts +58 -58
- package/src/node/plugin/html.ts +55 -55
- package/src/node/plugin/index.ts +193 -193
- package/src/node/plugin/types.ts +11 -11
- package/src/node/routes/cache.ts +28 -28
- package/src/node/routes/index.ts +167 -167
- package/src/node/routes/parser.ts +153 -127
- package/src/node/routes/sorter.ts +42 -42
- package/src/node/routes/types.ts +49 -49
- package/src/node/ssg/index.ts +114 -114
- package/src/node/ssg/meta.ts +33 -34
- package/src/node/ssg/options.ts +13 -13
- package/src/node/ssg/sitemap.ts +55 -54
- package/src/node/utils.ts +145 -134
- package/tsconfig.json +20 -20
- package/tsup.config.ts +22 -22
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
import React, { createContext, useContext, useCallback } from "react";
|
|
2
|
-
import { ComponentRoute } from "../types";
|
|
3
|
-
|
|
4
|
-
interface PreloadContextType {
|
|
5
|
-
preload: (path: string) => void;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const PreloadContext = createContext<PreloadContextType>({
|
|
9
|
-
preload: () => {},
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export function usePreload() {
|
|
13
|
-
return useContext(PreloadContext);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function PreloadProvider({
|
|
17
|
-
routes,
|
|
18
|
-
modules,
|
|
19
|
-
children,
|
|
20
|
-
}: {
|
|
21
|
-
routes: ComponentRoute[];
|
|
22
|
-
modules: Record<string, () => Promise<any>>;
|
|
23
|
-
children: React.ReactNode;
|
|
24
|
-
}) {
|
|
25
|
-
const preload = useCallback(
|
|
26
|
-
(path: string) => {
|
|
27
|
-
// Normalize path (remove hash and search)
|
|
28
|
-
const cleanPath = path.split("#")[0].split("?")[0];
|
|
29
|
-
|
|
30
|
-
// Support index routes matching "/"
|
|
31
|
-
const route = routes.find(
|
|
32
|
-
(r) => r.path === cleanPath || (cleanPath === "/" && r.path === ""),
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
if (route && route.filePath) {
|
|
36
|
-
const loaderKey = Object.keys(modules).find((k) =>
|
|
37
|
-
k.endsWith("/" + route.filePath),
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
if (loaderKey) {
|
|
41
|
-
// Trigger the dynamic import
|
|
42
|
-
modules[loaderKey]().catch((err: any) => {
|
|
43
|
-
console.error(`[boltdocs] Failed to preload route ${path}:`, err);
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
[routes, modules],
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<PreloadContext.Provider value={{ preload }}>
|
|
53
|
-
{children}
|
|
54
|
-
</PreloadContext.Provider>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
1
|
+
import React, { createContext, useContext, useCallback } from "react";
|
|
2
|
+
import { ComponentRoute } from "../types";
|
|
3
|
+
|
|
4
|
+
interface PreloadContextType {
|
|
5
|
+
preload: (path: string) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const PreloadContext = createContext<PreloadContextType>({
|
|
9
|
+
preload: () => {},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export function usePreload() {
|
|
13
|
+
return useContext(PreloadContext);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function PreloadProvider({
|
|
17
|
+
routes,
|
|
18
|
+
modules,
|
|
19
|
+
children,
|
|
20
|
+
}: {
|
|
21
|
+
routes: ComponentRoute[];
|
|
22
|
+
modules: Record<string, () => Promise<any>>;
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}) {
|
|
25
|
+
const preload = useCallback(
|
|
26
|
+
(path: string) => {
|
|
27
|
+
// Normalize path (remove hash and search)
|
|
28
|
+
const cleanPath = path.split("#")[0].split("?")[0];
|
|
29
|
+
|
|
30
|
+
// Support index routes matching "/"
|
|
31
|
+
const route = routes.find(
|
|
32
|
+
(r) => r.path === cleanPath || (cleanPath === "/" && r.path === ""),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (route && route.filePath) {
|
|
36
|
+
const loaderKey = Object.keys(modules).find((k) =>
|
|
37
|
+
k.endsWith("/" + route.filePath),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (loaderKey) {
|
|
41
|
+
// Trigger the dynamic import
|
|
42
|
+
modules[loaderKey]().catch((err: any) => {
|
|
43
|
+
console.error(`[boltdocs] Failed to preload route ${path}:`, err);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
[routes, modules],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<PreloadContext.Provider value={{ preload }}>
|
|
53
|
+
{children}
|
|
54
|
+
</PreloadContext.Provider>
|
|
55
|
+
);
|
|
56
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
export type { BoltdocsConfig, BoltdocsThemeConfig } from "../node/config";
|
|
2
|
-
export type { ComponentRoute, CreateBoltdocsAppOptions } from "./types";
|
|
3
|
-
export { createBoltdocsApp } from "./app";
|
|
4
|
-
export { ThemeLayout } from "./theme/ui/Layout";
|
|
5
|
-
export { Navbar } from "./theme/ui/Navbar";
|
|
6
|
-
export { Sidebar } from "./theme/ui/Sidebar";
|
|
7
|
-
export { OnThisPage } from "./theme/ui/OnThisPage";
|
|
8
|
-
export { Head } from "./theme/ui/Head";
|
|
9
|
-
export { Breadcrumbs } from "./theme/ui/Breadcrumbs";
|
|
10
|
-
export { BackgroundGradient } from "./theme/ui/BackgroundGradient";
|
|
11
|
-
export { Playground } from "./theme/components/Playground";
|
|
12
|
-
export { NotFound } from "./theme/ui/NotFound";
|
|
13
|
-
export { Loading } from "./theme/ui/Loading";
|
|
14
|
-
export { CodeBlock } from "./theme/components/CodeBlock";
|
|
15
|
-
export { Video } from "./theme/components/Video";
|
|
16
|
-
export {
|
|
17
|
-
Button,
|
|
18
|
-
Badge,
|
|
19
|
-
Card,
|
|
20
|
-
Cards,
|
|
21
|
-
Tabs,
|
|
22
|
-
Tab,
|
|
23
|
-
Admonition,
|
|
24
|
-
Note,
|
|
25
|
-
Tip,
|
|
26
|
-
Warning,
|
|
27
|
-
Danger,
|
|
28
|
-
InfoBox,
|
|
29
|
-
List,
|
|
30
|
-
} from "./theme/components/mdx";
|
|
31
|
-
export type {
|
|
32
|
-
ButtonProps,
|
|
33
|
-
BadgeProps,
|
|
34
|
-
CardProps,
|
|
35
|
-
CardsProps,
|
|
36
|
-
TabsProps,
|
|
37
|
-
TabProps,
|
|
38
|
-
AdmonitionProps,
|
|
39
|
-
ListProps,
|
|
40
|
-
} from "./theme/components/mdx";
|
|
1
|
+
export type { BoltdocsConfig, BoltdocsThemeConfig } from "../node/config";
|
|
2
|
+
export type { ComponentRoute, CreateBoltdocsAppOptions } from "./types";
|
|
3
|
+
export { createBoltdocsApp } from "./app";
|
|
4
|
+
export { ThemeLayout } from "./theme/ui/Layout";
|
|
5
|
+
export { Navbar } from "./theme/ui/Navbar";
|
|
6
|
+
export { Sidebar } from "./theme/ui/Sidebar";
|
|
7
|
+
export { OnThisPage } from "./theme/ui/OnThisPage";
|
|
8
|
+
export { Head } from "./theme/ui/Head";
|
|
9
|
+
export { Breadcrumbs } from "./theme/ui/Breadcrumbs";
|
|
10
|
+
export { BackgroundGradient } from "./theme/ui/BackgroundGradient";
|
|
11
|
+
export { Playground } from "./theme/components/Playground";
|
|
12
|
+
export { NotFound } from "./theme/ui/NotFound";
|
|
13
|
+
export { Loading } from "./theme/ui/Loading";
|
|
14
|
+
export { CodeBlock } from "./theme/components/CodeBlock";
|
|
15
|
+
export { Video } from "./theme/components/Video";
|
|
16
|
+
export {
|
|
17
|
+
Button,
|
|
18
|
+
Badge,
|
|
19
|
+
Card,
|
|
20
|
+
Cards,
|
|
21
|
+
Tabs,
|
|
22
|
+
Tab,
|
|
23
|
+
Admonition,
|
|
24
|
+
Note,
|
|
25
|
+
Tip,
|
|
26
|
+
Warning,
|
|
27
|
+
Danger,
|
|
28
|
+
InfoBox,
|
|
29
|
+
List,
|
|
30
|
+
} from "./theme/components/mdx";
|
|
31
|
+
export type {
|
|
32
|
+
ButtonProps,
|
|
33
|
+
BadgeProps,
|
|
34
|
+
CardProps,
|
|
35
|
+
CardsProps,
|
|
36
|
+
TabsProps,
|
|
37
|
+
TabProps,
|
|
38
|
+
AdmonitionProps,
|
|
39
|
+
ListProps,
|
|
40
|
+
} from "./theme/components/mdx";
|
package/src/client/ssr.tsx
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import ReactDOMServer from "react-dom/server";
|
|
3
|
-
import { StaticRouter } from "react-router-dom/server";
|
|
4
|
-
import { AppShell } from "./app";
|
|
5
|
-
import { ComponentRoute } from "./types";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Options for rendering the Boltdocs application on the server (SSG).
|
|
9
|
-
*/
|
|
10
|
-
export interface RenderOptions {
|
|
11
|
-
/** The URL path currently being rendered */
|
|
12
|
-
path: string;
|
|
13
|
-
/** Initial routes generated by the Vite plugin (`virtual:boltdocs-routes`) */
|
|
14
|
-
routes: ComponentRoute[];
|
|
15
|
-
/** Site configuration (`virtual:boltdocs-config`) */
|
|
16
|
-
config: any;
|
|
17
|
-
/** Optional custom React component to render when visiting the root path ('/') */
|
|
18
|
-
homePage?: React.ComponentType;
|
|
19
|
-
/** Preloaded modules (since SSR cannot use dynamic imports easily) */
|
|
20
|
-
modules: Record<string, any>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Renders the full React application into an HTML string for a specific route.
|
|
25
|
-
* This is called by the Node SSG script during the Vite build process.
|
|
26
|
-
*/
|
|
27
|
-
export async function render(options: RenderOptions): Promise<string> {
|
|
28
|
-
const { path, routes, config, modules, homePage } = options;
|
|
29
|
-
|
|
30
|
-
// For SSR, we must resolve modules synchronously. We create a mock 'loader'
|
|
31
|
-
// that instantly returns the module since the SSG script already loaded it.
|
|
32
|
-
const resolvedModules: Record<string, () => Promise<any>> = {};
|
|
33
|
-
for (const [key, mod] of Object.entries(modules)) {
|
|
34
|
-
resolvedModules[key] = () => Promise.resolve(mod);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const html = ReactDOMServer.renderToString(
|
|
38
|
-
<React.StrictMode>
|
|
39
|
-
<StaticRouter location={path}>
|
|
40
|
-
<AppShell
|
|
41
|
-
initialRoutes={routes}
|
|
42
|
-
initialConfig={config}
|
|
43
|
-
modules={resolvedModules}
|
|
44
|
-
homePage={homePage}
|
|
45
|
-
/>
|
|
46
|
-
</StaticRouter>
|
|
47
|
-
</React.StrictMode>,
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
return html;
|
|
51
|
-
}
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ReactDOMServer from "react-dom/server";
|
|
3
|
+
import { StaticRouter } from "react-router-dom/server";
|
|
4
|
+
import { AppShell } from "./app";
|
|
5
|
+
import { ComponentRoute } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for rendering the Boltdocs application on the server (SSG).
|
|
9
|
+
*/
|
|
10
|
+
export interface RenderOptions {
|
|
11
|
+
/** The URL path currently being rendered */
|
|
12
|
+
path: string;
|
|
13
|
+
/** Initial routes generated by the Vite plugin (`virtual:boltdocs-routes`) */
|
|
14
|
+
routes: ComponentRoute[];
|
|
15
|
+
/** Site configuration (`virtual:boltdocs-config`) */
|
|
16
|
+
config: any;
|
|
17
|
+
/** Optional custom React component to render when visiting the root path ('/') */
|
|
18
|
+
homePage?: React.ComponentType;
|
|
19
|
+
/** Preloaded modules (since SSR cannot use dynamic imports easily) */
|
|
20
|
+
modules: Record<string, any>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Renders the full React application into an HTML string for a specific route.
|
|
25
|
+
* This is called by the Node SSG script during the Vite build process.
|
|
26
|
+
*/
|
|
27
|
+
export async function render(options: RenderOptions): Promise<string> {
|
|
28
|
+
const { path, routes, config, modules, homePage } = options;
|
|
29
|
+
|
|
30
|
+
// For SSR, we must resolve modules synchronously. We create a mock 'loader'
|
|
31
|
+
// that instantly returns the module since the SSG script already loaded it.
|
|
32
|
+
const resolvedModules: Record<string, () => Promise<any>> = {};
|
|
33
|
+
for (const [key, mod] of Object.entries(modules)) {
|
|
34
|
+
resolvedModules[key] = () => Promise.resolve(mod);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const html = ReactDOMServer.renderToString(
|
|
38
|
+
<React.StrictMode>
|
|
39
|
+
<StaticRouter location={path}>
|
|
40
|
+
<AppShell
|
|
41
|
+
initialRoutes={routes}
|
|
42
|
+
initialConfig={config}
|
|
43
|
+
modules={resolvedModules}
|
|
44
|
+
homePage={homePage}
|
|
45
|
+
/>
|
|
46
|
+
</StaticRouter>
|
|
47
|
+
</React.StrictMode>,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return html;
|
|
51
|
+
}
|
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
import React, { useState, useRef, useCallback } from "react";
|
|
2
|
-
import { Copy, Check } from "lucide-react";
|
|
3
|
-
|
|
4
|
-
interface CodeBlockProps {
|
|
5
|
-
children?: React.ReactNode;
|
|
6
|
-
className?: string;
|
|
7
|
-
[key: string]: any;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* A specialized wrapper for code snippets compiled from MDX blocks.
|
|
12
|
-
* Provides syntax highlighting styling scaffolding and a "Copy to Clipboard" button.
|
|
13
|
-
*/
|
|
14
|
-
export function CodeBlock({ children, ...props }: CodeBlockProps) {
|
|
15
|
-
const [copied, setCopied] = useState(false);
|
|
16
|
-
const preRef = useRef<HTMLPreElement>(null);
|
|
17
|
-
|
|
18
|
-
// Extract language from the child <code> element's data-language or className
|
|
19
|
-
let language = "";
|
|
20
|
-
if (React.isValidElement(children)) {
|
|
21
|
-
const childProps = children.props as any;
|
|
22
|
-
language = childProps?.["data-language"] || "";
|
|
23
|
-
if (!language && childProps?.className) {
|
|
24
|
-
const match = childProps.className.match(/language-(\w+)/);
|
|
25
|
-
if (match) language = match[1];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const handleCopy = useCallback(async () => {
|
|
30
|
-
const code = preRef.current?.textContent || "";
|
|
31
|
-
try {
|
|
32
|
-
await navigator.clipboard.writeText(code);
|
|
33
|
-
setCopied(true);
|
|
34
|
-
setTimeout(() => setCopied(false), 2000);
|
|
35
|
-
} catch {
|
|
36
|
-
// Fallback
|
|
37
|
-
const textarea = document.createElement("textarea");
|
|
38
|
-
textarea.value = code;
|
|
39
|
-
textarea.style.position = "fixed";
|
|
40
|
-
textarea.style.opacity = "0";
|
|
41
|
-
document.body.appendChild(textarea);
|
|
42
|
-
textarea.select();
|
|
43
|
-
document.execCommand("copy");
|
|
44
|
-
document.body.removeChild(textarea);
|
|
45
|
-
setCopied(true);
|
|
46
|
-
setTimeout(() => setCopied(false), 2000);
|
|
47
|
-
}
|
|
48
|
-
}, []);
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<div className="code-block-wrapper">
|
|
52
|
-
<div className="code-block-header">
|
|
53
|
-
<span className="code-block-lang">{language || "code"}</span>
|
|
54
|
-
<button
|
|
55
|
-
className={`code-block-copy ${copied ? "copied" : ""}`}
|
|
56
|
-
onClick={handleCopy}
|
|
57
|
-
type="button"
|
|
58
|
-
aria-label="Copy code"
|
|
59
|
-
>
|
|
60
|
-
{copied ? (
|
|
61
|
-
<>
|
|
62
|
-
<Check size={20} />
|
|
63
|
-
</>
|
|
64
|
-
) : (
|
|
65
|
-
<>
|
|
66
|
-
<Copy size={20} />
|
|
67
|
-
</>
|
|
68
|
-
)}
|
|
69
|
-
</button>
|
|
70
|
-
</div>
|
|
71
|
-
<pre ref={preRef} {...props}>
|
|
72
|
-
{children}
|
|
73
|
-
</pre>
|
|
74
|
-
</div>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
1
|
+
import React, { useState, useRef, useCallback } from "react";
|
|
2
|
+
import { Copy, Check } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface CodeBlockProps {
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A specialized wrapper for code snippets compiled from MDX blocks.
|
|
12
|
+
* Provides syntax highlighting styling scaffolding and a "Copy to Clipboard" button.
|
|
13
|
+
*/
|
|
14
|
+
export function CodeBlock({ children, ...props }: CodeBlockProps) {
|
|
15
|
+
const [copied, setCopied] = useState(false);
|
|
16
|
+
const preRef = useRef<HTMLPreElement>(null);
|
|
17
|
+
|
|
18
|
+
// Extract language from the child <code> element's data-language or className
|
|
19
|
+
let language = "";
|
|
20
|
+
if (React.isValidElement(children)) {
|
|
21
|
+
const childProps = children.props as any;
|
|
22
|
+
language = childProps?.["data-language"] || "";
|
|
23
|
+
if (!language && childProps?.className) {
|
|
24
|
+
const match = childProps.className.match(/language-(\w+)/);
|
|
25
|
+
if (match) language = match[1];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const handleCopy = useCallback(async () => {
|
|
30
|
+
const code = preRef.current?.textContent || "";
|
|
31
|
+
try {
|
|
32
|
+
await navigator.clipboard.writeText(code);
|
|
33
|
+
setCopied(true);
|
|
34
|
+
setTimeout(() => setCopied(false), 2000);
|
|
35
|
+
} catch {
|
|
36
|
+
// Fallback
|
|
37
|
+
const textarea = document.createElement("textarea");
|
|
38
|
+
textarea.value = code;
|
|
39
|
+
textarea.style.position = "fixed";
|
|
40
|
+
textarea.style.opacity = "0";
|
|
41
|
+
document.body.appendChild(textarea);
|
|
42
|
+
textarea.select();
|
|
43
|
+
document.execCommand("copy");
|
|
44
|
+
document.body.removeChild(textarea);
|
|
45
|
+
setCopied(true);
|
|
46
|
+
setTimeout(() => setCopied(false), 2000);
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="code-block-wrapper">
|
|
52
|
+
<div className="code-block-header">
|
|
53
|
+
<span className="code-block-lang">{language || "code"}</span>
|
|
54
|
+
<button
|
|
55
|
+
className={`code-block-copy ${copied ? "copied" : ""}`}
|
|
56
|
+
onClick={handleCopy}
|
|
57
|
+
type="button"
|
|
58
|
+
aria-label="Copy code"
|
|
59
|
+
>
|
|
60
|
+
{copied ? (
|
|
61
|
+
<>
|
|
62
|
+
<Check size={20} />
|
|
63
|
+
</>
|
|
64
|
+
) : (
|
|
65
|
+
<>
|
|
66
|
+
<Copy size={20} />
|
|
67
|
+
</>
|
|
68
|
+
)}
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
<pre ref={preRef} {...props}>
|
|
72
|
+
{children}
|
|
73
|
+
</pre>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { CodeBlock } from "./CodeBlock";
|
|
1
|
+
export { CodeBlock } from "./CodeBlock";
|