boltdocs 1.4.0 → 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.
Files changed (39) hide show
  1. package/dist/{SearchDialog-FBNGKRPK.mjs → SearchDialog-5ISK64QY.mjs} +1 -1
  2. package/dist/{SearchDialog-O3V36MXA.css → SearchDialog-CEVPEMT3.css} +54 -5
  3. package/dist/{cache-GQHF6BXI.mjs → cache-KNL5B4EE.mjs} +1 -1
  4. package/dist/{chunk-CYBWLFOG.mjs → chunk-FFBNU6IJ.mjs} +2 -1
  5. package/dist/{chunk-D7YBQG6H.mjs → chunk-FMQ4HRKZ.mjs} +311 -133
  6. package/dist/client/index.css +54 -5
  7. package/dist/client/index.d.mts +3 -3
  8. package/dist/client/index.d.ts +3 -3
  9. package/dist/client/index.js +624 -475
  10. package/dist/client/index.mjs +2 -4
  11. package/dist/client/ssr.css +54 -5
  12. package/dist/client/ssr.d.mts +1 -1
  13. package/dist/client/ssr.d.ts +1 -1
  14. package/dist/client/ssr.js +544 -395
  15. package/dist/client/ssr.mjs +1 -1
  16. package/dist/{config-BD5ZHz15.d.mts → config-DkZg5aCf.d.mts} +2 -0
  17. package/dist/{config-BD5ZHz15.d.ts → config-DkZg5aCf.d.ts} +2 -0
  18. package/dist/node/index.d.mts +2 -2
  19. package/dist/node/index.d.ts +2 -2
  20. package/dist/node/index.js +24 -17
  21. package/dist/node/index.mjs +25 -19
  22. package/dist/{types-CvrzTbEX.d.mts → types-DGIo1VKD.d.mts} +2 -0
  23. package/dist/{types-CvrzTbEX.d.ts → types-DGIo1VKD.d.ts} +2 -0
  24. package/package.json +1 -1
  25. package/src/client/app/index.tsx +2 -12
  26. package/src/client/app/preload.tsx +3 -1
  27. package/src/client/theme/components/CodeBlock/CodeBlock.tsx +0 -11
  28. package/src/client/theme/styles/markdown.css +1 -5
  29. package/src/client/theme/ui/Link/Link.tsx +156 -18
  30. package/src/client/theme/ui/Link/LinkPreview.tsx +64 -0
  31. package/src/client/theme/ui/Link/link-preview.css +64 -0
  32. package/src/client/types.ts +2 -0
  33. package/src/node/config.ts +15 -6
  34. package/src/node/mdx.ts +11 -4
  35. package/src/node/routes/parser.ts +24 -2
  36. package/src/node/ssg/index.ts +1 -10
  37. package/src/node/utils.ts +4 -1
  38. package/dist/CodeBlock-QYIKJMEB.mjs +0 -7
  39. package/dist/chunk-KS5B3O6W.mjs +0 -43
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  AppShell
3
- } from "../chunk-D7YBQG6H.mjs";
3
+ } from "../chunk-FMQ4HRKZ.mjs";
4
4
  import "../chunk-FMTOYQLO.mjs";
5
5
 
6
6
  // src/client/ssr.tsx
@@ -54,6 +54,8 @@ interface BoltdocsThemeConfig {
54
54
  githubRepo?: string;
55
55
  /** Whether to show the 'Powered by LiteDocs' badge in the sidebar (default: true) */
56
56
  poweredBy?: boolean;
57
+ /** Whether to show a preview tooltip on internal links hover (default: true) */
58
+ linkPreview?: boolean;
57
59
  /** Granular layout customization props */
58
60
  layoutProps?: {
59
61
  navbar?: any;
@@ -54,6 +54,8 @@ interface BoltdocsThemeConfig {
54
54
  githubRepo?: string;
55
55
  /** Whether to show the 'Powered by LiteDocs' badge in the sidebar (default: true) */
56
56
  poweredBy?: boolean;
57
+ /** Whether to show a preview tooltip on internal links hover (default: true) */
58
+ linkPreview?: boolean;
57
59
  /** Granular layout customization props */
58
60
  layoutProps?: {
59
61
  navbar?: any;
@@ -1,6 +1,6 @@
1
1
  import { Plugin } from 'vite';
2
- import { B as BoltdocsConfig } from '../config-BD5ZHz15.mjs';
3
- export { a as BoltdocsThemeConfig } from '../config-BD5ZHz15.mjs';
2
+ import { B as BoltdocsConfig } from '../config-DkZg5aCf.mjs';
3
+ export { a as BoltdocsThemeConfig } from '../config-DkZg5aCf.mjs';
4
4
 
5
5
  /**
6
6
  * Configuration options specifically for the Boltdocs Vite plugin.
@@ -1,6 +1,6 @@
1
1
  import { Plugin } from 'vite';
2
- import { B as BoltdocsConfig } from '../config-BD5ZHz15.js';
3
- export { a as BoltdocsThemeConfig } from '../config-BD5ZHz15.js';
2
+ import { B as BoltdocsConfig } from '../config-DkZg5aCf.js';
3
+ export { a as BoltdocsThemeConfig } from '../config-DkZg5aCf.js';
4
4
 
5
5
  /**
6
6
  * Configuration options specifically for the Boltdocs Vite plugin.
@@ -64,7 +64,8 @@ function escapeXml(str) {
64
64
  }
65
65
  function fileToRoutePath(relativePath) {
66
66
  let cleanedPath = relativePath.split("/").map(stripNumberPrefix).join("/");
67
- let routePath = cleanedPath.replace(/\.mdx?$/, "");
67
+ let routePath = cleanedPath.replace(/\/$/, "");
68
+ routePath = routePath.replace(/\.mdx?$/, "");
68
69
  if (routePath === "index" || routePath.endsWith("/index")) {
69
70
  routePath = routePath.replace(/index$/, "");
70
71
  }
@@ -478,7 +479,12 @@ function parseDocFile(file, docsDir, basePath, config) {
478
479
  }
479
480
  }
480
481
  const cleanRelativePath = parts.join("/");
481
- const cleanRoutePath = fileToRoutePath(cleanRelativePath || "index.md");
482
+ let cleanRoutePath;
483
+ if (data.permalink) {
484
+ cleanRoutePath = data.permalink.startsWith("/") ? data.permalink : `/${data.permalink}`;
485
+ } else {
486
+ cleanRoutePath = fileToRoutePath(cleanRelativePath || "index.md");
487
+ }
482
488
  let finalPath = basePath;
483
489
  if (version) {
484
490
  finalPath += "/" + version;
@@ -508,7 +514,11 @@ function parseDocFile(file, docsDir, basePath, config) {
508
514
  headings.push({ level, text: escapeHtml(text), id });
509
515
  }
510
516
  const sanitizedTitle = data.title ? escapeHtml(data.title) : inferredTitle;
511
- const sanitizedDescription = data.description ? escapeHtml(data.description) : "";
517
+ let sanitizedDescription = data.description ? escapeHtml(data.description) : "";
518
+ if (!sanitizedDescription && content) {
519
+ const summary = content.replace(/^#+.*$/gm, "").replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1").replace(/[_*`]/g, "").replace(/\n+/g, " ").trim().slice(0, 160);
520
+ sanitizedDescription = escapeHtml(summary);
521
+ }
512
522
  const sanitizedBadge = data.badge ? escapeHtml(data.badge) : void 0;
513
523
  return {
514
524
  route: {
@@ -677,8 +687,8 @@ var CONFIG_FILES = [
677
687
  "boltdocs.config.mjs",
678
688
  "boltdocs.config.ts"
679
689
  ];
680
- async function resolveConfig(docsDir) {
681
- const projectRoot = process.cwd();
690
+ async function resolveConfig(docsDir, root = process.cwd()) {
691
+ const projectRoot = root;
682
692
  const defaults = {
683
693
  docsDir: import_path3.default.resolve(docsDir),
684
694
  themeConfig: {
@@ -694,7 +704,8 @@ async function resolveConfig(docsDir) {
694
704
  const configPath = import_path3.default.resolve(projectRoot, filename);
695
705
  if (import_fs3.default.existsSync(configPath)) {
696
706
  try {
697
- const fileUrl = (0, import_url.pathToFileURL)(configPath).href + "?t=" + Date.now();
707
+ const isTest = process.env.NODE_ENV === "test" || global.__vitest_worker__;
708
+ const fileUrl = (0, import_url.pathToFileURL)(configPath).href + (isTest ? "" : "?t=" + Date.now());
698
709
  const mod = await import(fileUrl);
699
710
  const userConfig = mod.default || mod;
700
711
  const userThemeConfig = userConfig.themeConfig || userConfig;
@@ -806,12 +817,6 @@ async function generateStaticPages(options) {
806
817
  return;
807
818
  }
808
819
  const template = import_fs4.default.readFileSync(templatePath, "utf-8");
809
- let homePageComp;
810
- if (config?._homePagePath) {
811
- try {
812
- } catch (e) {
813
- }
814
- }
815
820
  await Promise.all(
816
821
  routes.map(async (route) => {
817
822
  const pageTitle = `${route.title} | ${siteTitle}`;
@@ -825,7 +830,8 @@ async function generateStaticPages(options) {
825
830
  routes,
826
831
  config: config || {},
827
832
  modules: fakeModules,
828
- homePage: homePageComp
833
+ homePage: void 0
834
+ // No custom home page for now
829
835
  });
830
836
  const html = replaceMetaTags(template, {
831
837
  title: escapeHtml(pageTitle),
@@ -1070,10 +1076,10 @@ var import_crypto2 = __toESM(require("crypto"));
1070
1076
  init_cache();
1071
1077
  var mdxCache = new TransformCache("mdx");
1072
1078
  var mdxCacheLoaded = false;
1073
- function boltdocsMdxPlugin(config) {
1079
+ function boltdocsMdxPlugin(config, compiler = import_rollup.default) {
1074
1080
  const extraRemarkPlugins = config?.plugins?.flatMap((p) => p.remarkPlugins || []) || [];
1075
1081
  const extraRehypePlugins = config?.plugins?.flatMap((p) => p.rehypePlugins || []) || [];
1076
- const baseMdxPlugin = (0, import_rollup.default)({
1082
+ const baseMdxPlugin = compiler({
1077
1083
  remarkPlugins: [import_remark_gfm.default, import_remark_frontmatter.default, ...extraRemarkPlugins],
1078
1084
  rehypePlugins: [
1079
1085
  import_rehype_slug.default,
@@ -1089,6 +1095,9 @@ function boltdocsMdxPlugin(config) {
1089
1095
  jsxRuntime: "automatic",
1090
1096
  providerImportSource: "@mdx-js/react"
1091
1097
  });
1098
+ if (baseMdxPlugin.isMock) {
1099
+ console.log("MDX PLUGIN IS MOCKED");
1100
+ }
1092
1101
  return {
1093
1102
  ...baseMdxPlugin,
1094
1103
  name: "vite-plugin-boltdocs-mdx",
@@ -1123,8 +1132,6 @@ function boltdocsMdxPlugin(config) {
1123
1132
  );
1124
1133
  if (result && typeof result === "object" && result.code) {
1125
1134
  mdxCache.set(cacheKey, result.code);
1126
- } else if (typeof result === "string") {
1127
- mdxCache.set(cacheKey, result);
1128
1135
  }
1129
1136
  return result;
1130
1137
  },
@@ -10,7 +10,7 @@ import {
10
10
  normalizePath,
11
11
  parseFrontmatter,
12
12
  stripNumberPrefix
13
- } from "../chunk-CYBWLFOG.mjs";
13
+ } from "../chunk-FFBNU6IJ.mjs";
14
14
 
15
15
  // src/node/plugin/index.ts
16
16
  import { loadEnv } from "vite";
@@ -61,7 +61,12 @@ function parseDocFile(file, docsDir, basePath, config) {
61
61
  }
62
62
  }
63
63
  const cleanRelativePath = parts.join("/");
64
- const cleanRoutePath = fileToRoutePath(cleanRelativePath || "index.md");
64
+ let cleanRoutePath;
65
+ if (data.permalink) {
66
+ cleanRoutePath = data.permalink.startsWith("/") ? data.permalink : `/${data.permalink}`;
67
+ } else {
68
+ cleanRoutePath = fileToRoutePath(cleanRelativePath || "index.md");
69
+ }
65
70
  let finalPath = basePath;
66
71
  if (version) {
67
72
  finalPath += "/" + version;
@@ -91,7 +96,11 @@ function parseDocFile(file, docsDir, basePath, config) {
91
96
  headings.push({ level, text: escapeHtml(text), id });
92
97
  }
93
98
  const sanitizedTitle = data.title ? escapeHtml(data.title) : inferredTitle;
94
- const sanitizedDescription = data.description ? escapeHtml(data.description) : "";
99
+ let sanitizedDescription = data.description ? escapeHtml(data.description) : "";
100
+ if (!sanitizedDescription && content) {
101
+ const summary = content.replace(/^#+.*$/gm, "").replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1").replace(/[_*`]/g, "").replace(/\n+/g, " ").trim().slice(0, 160);
102
+ sanitizedDescription = escapeHtml(summary);
103
+ }
95
104
  const sanitizedBadge = data.badge ? escapeHtml(data.badge) : void 0;
96
105
  return {
97
106
  route: {
@@ -260,8 +269,8 @@ var CONFIG_FILES = [
260
269
  "boltdocs.config.mjs",
261
270
  "boltdocs.config.ts"
262
271
  ];
263
- async function resolveConfig(docsDir) {
264
- const projectRoot = process.cwd();
272
+ async function resolveConfig(docsDir, root = process.cwd()) {
273
+ const projectRoot = root;
265
274
  const defaults = {
266
275
  docsDir: path2.resolve(docsDir),
267
276
  themeConfig: {
@@ -277,7 +286,8 @@ async function resolveConfig(docsDir) {
277
286
  const configPath = path2.resolve(projectRoot, filename);
278
287
  if (fs.existsSync(configPath)) {
279
288
  try {
280
- const fileUrl = pathToFileURL(configPath).href + "?t=" + Date.now();
289
+ const isTest = process.env.NODE_ENV === "test" || global.__vitest_worker__;
290
+ const fileUrl = pathToFileURL(configPath).href + (isTest ? "" : "?t=" + Date.now());
281
291
  const mod = await import(fileUrl);
282
292
  const userConfig = mod.default || mod;
283
293
  const userThemeConfig = userConfig.themeConfig || userConfig;
@@ -385,12 +395,6 @@ async function generateStaticPages(options) {
385
395
  return;
386
396
  }
387
397
  const template = fs2.readFileSync(templatePath, "utf-8");
388
- let homePageComp;
389
- if (config?._homePagePath) {
390
- try {
391
- } catch (e) {
392
- }
393
- }
394
398
  await Promise.all(
395
399
  routes.map(async (route) => {
396
400
  const pageTitle = `${route.title} | ${siteTitle}`;
@@ -404,7 +408,8 @@ async function generateStaticPages(options) {
404
408
  routes,
405
409
  config: config || {},
406
410
  modules: fakeModules,
407
- homePage: homePageComp
411
+ homePage: void 0
412
+ // No custom home page for now
408
413
  });
409
414
  const html = replaceMetaTags(template, {
410
415
  title: escapeHtml(pageTitle),
@@ -430,7 +435,7 @@ async function generateStaticPages(options) {
430
435
  console.log(
431
436
  `[boltdocs] Generated ${routes.length} static pages + sitemap.xml`
432
437
  );
433
- const { flushCache } = await import("../cache-GQHF6BXI.mjs");
438
+ const { flushCache } = await import("../cache-KNL5B4EE.mjs");
434
439
  await flushCache();
435
440
  }
436
441
 
@@ -612,7 +617,7 @@ function boltdocsPlugin(options = {}, passedConfig) {
612
617
  if (!isBuild) return;
613
618
  const outDir = viteConfig?.build?.outDir ? path4.resolve(viteConfig.root, viteConfig.build.outDir) : path4.resolve(process.cwd(), "dist");
614
619
  await generateStaticPages({ docsDir, outDir, config });
615
- const { flushCache } = await import("../cache-GQHF6BXI.mjs");
620
+ const { flushCache } = await import("../cache-KNL5B4EE.mjs");
616
621
  await flushCache();
617
622
  }
618
623
  },
@@ -646,10 +651,10 @@ import rehypePrettyCode from "rehype-pretty-code";
646
651
  import crypto from "crypto";
647
652
  var mdxCache = new TransformCache("mdx");
648
653
  var mdxCacheLoaded = false;
649
- function boltdocsMdxPlugin(config) {
654
+ function boltdocsMdxPlugin(config, compiler = mdxPlugin) {
650
655
  const extraRemarkPlugins = config?.plugins?.flatMap((p) => p.remarkPlugins || []) || [];
651
656
  const extraRehypePlugins = config?.plugins?.flatMap((p) => p.rehypePlugins || []) || [];
652
- const baseMdxPlugin = mdxPlugin({
657
+ const baseMdxPlugin = compiler({
653
658
  remarkPlugins: [remarkGfm, remarkFrontmatter, ...extraRemarkPlugins],
654
659
  rehypePlugins: [
655
660
  rehypeSlug,
@@ -665,6 +670,9 @@ function boltdocsMdxPlugin(config) {
665
670
  jsxRuntime: "automatic",
666
671
  providerImportSource: "@mdx-js/react"
667
672
  });
673
+ if (baseMdxPlugin.isMock) {
674
+ console.log("MDX PLUGIN IS MOCKED");
675
+ }
668
676
  return {
669
677
  ...baseMdxPlugin,
670
678
  name: "vite-plugin-boltdocs-mdx",
@@ -699,8 +707,6 @@ function boltdocsMdxPlugin(config) {
699
707
  );
700
708
  if (result && typeof result === "object" && result.code) {
701
709
  mdxCache.set(cacheKey, result.code);
702
- } else if (typeof result === "string") {
703
- mdxCache.set(cacheKey, result);
704
710
  }
705
711
  return result;
706
712
  },
@@ -27,6 +27,8 @@ interface ComponentRoute {
27
27
  text: string;
28
28
  id: string;
29
29
  }[];
30
+ /** The page summary or description */
31
+ description?: string;
30
32
  /** The locale this route belongs to, if i18n is configured */
31
33
  locale?: string;
32
34
  /** The version this route belongs to, if versioning is configured */
@@ -27,6 +27,8 @@ interface ComponentRoute {
27
27
  text: string;
28
28
  id: string;
29
29
  }[];
30
+ /** The page summary or description */
31
+ description?: string;
30
32
  /** The locale this route belongs to, if i18n is configured */
31
33
  locale?: string;
32
34
  /** The version this route belongs to, if versioning is configured */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "boltdocs",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "A lightweight documentation generator for React projects.",
5
5
  "main": "dist/node/index.js",
6
6
  "module": "dist/node/index.mjs",
@@ -27,11 +27,7 @@ export function useConfig() {
27
27
  return useContext(ConfigContext);
28
28
  }
29
29
 
30
- const CodeBlock = lazy(() =>
31
- import("../theme/components/CodeBlock").then((m) => ({
32
- default: m.CodeBlock,
33
- })),
34
- );
30
+ import { CodeBlock } from "../theme/components/CodeBlock";
35
31
  const Video = lazy(() =>
36
32
  import("../theme/components/Video").then((m) => ({ default: m.Video })),
37
33
  );
@@ -77,13 +73,7 @@ const mdxComponents = {
77
73
  h4: (props: any) => <Heading level={4} {...props} />,
78
74
  h5: (props: any) => <Heading level={5} {...props} />,
79
75
  h6: (props: any) => <Heading level={6} {...props} />,
80
- pre: (props: any) => {
81
- return (
82
- <Suspense fallback={<div className="code-block-skeleton" />}>
83
- <CodeBlock {...props}>{props.children}</CodeBlock>
84
- </Suspense>
85
- );
86
- },
76
+ pre: (props: any) => <CodeBlock {...props}>{props.children}</CodeBlock>,
87
77
  video: (props: any) => (
88
78
  <Suspense fallback={<div className="video-skeleton" />}>
89
79
  <Video {...props} />
@@ -3,10 +3,12 @@ import { ComponentRoute } from "../types";
3
3
 
4
4
  interface PreloadContextType {
5
5
  preload: (path: string) => void;
6
+ routes: ComponentRoute[];
6
7
  }
7
8
 
8
9
  const PreloadContext = createContext<PreloadContextType>({
9
10
  preload: () => {},
11
+ routes: [],
10
12
  });
11
13
 
12
14
  export function usePreload() {
@@ -49,7 +51,7 @@ export function PreloadProvider({
49
51
  );
50
52
 
51
53
  return (
52
- <PreloadContext.Provider value={{ preload }}>
54
+ <PreloadContext.Provider value={{ preload, routes }}>
53
55
  {children}
54
56
  </PreloadContext.Provider>
55
57
  );
@@ -16,17 +16,6 @@ export function CodeBlock({ children, ...props }: CodeBlockProps) {
16
16
  const [copied, setCopied] = useState(false);
17
17
  const preRef = useRef<HTMLPreElement>(null);
18
18
 
19
- // Extract language from the child <code> element's data-language or className
20
- let language = "";
21
- if (React.isValidElement(children)) {
22
- const childProps = children.props as any;
23
- language = childProps?.["data-language"] || "";
24
- if (!language && childProps?.className) {
25
- const match = childProps.className.match(/language-(\w+)/);
26
- if (match) language = match[1];
27
- }
28
- }
29
-
30
19
  const handleCopy = useCallback(async () => {
31
20
  const code = preRef.current?.textContent || "";
32
21
  copyToClipboard(code);
@@ -218,7 +218,7 @@
218
218
  right: 0.75rem;
219
219
  z-index: 50;
220
220
  padding: 0.4rem;
221
- background-color: rgba(20, 20, 30, 0.8);
221
+ background-color: var(--ld-surface);
222
222
  backdrop-filter: blur(8px);
223
223
  -webkit-backdrop-filter: blur(8px);
224
224
  border: 1px solid var(--ld-border-subtle);
@@ -227,14 +227,10 @@
227
227
  cursor: pointer;
228
228
  transition: all 0.2s ease;
229
229
  opacity: 0;
230
- visibility: hidden;
231
- pointer-events: none;
232
230
  }
233
231
 
234
232
  .code-block-wrapper:hover .code-block-copy {
235
233
  opacity: 1;
236
- visibility: visible;
237
- pointer-events: auto;
238
234
  }
239
235
 
240
236
  .code-block-copy:hover {
@@ -97,25 +97,44 @@ function useLocalizedTo(to: RouterLinkProps["to"]) {
97
97
  return finalPath === basePath ? basePath : finalPath;
98
98
  }
99
99
 
100
+ import { LinkPreview } from "./LinkPreview";
101
+
100
102
  export interface LinkProps extends Omit<RouterLinkProps, "prefetch"> {
101
103
  /** Should prefetch the page on hover? Options: 'hover' | 'none'. Default 'hover' */
102
104
  boltdocsPrefetch?: "hover" | "none";
105
+ /** Should show a preview tooltip on hover? Default true */
106
+ boltdocsPreview?: boolean;
103
107
  }
104
108
 
105
109
  export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
106
110
  (props, ref) => {
107
111
  const {
108
112
  boltdocsPrefetch = "hover",
113
+ boltdocsPreview = true,
109
114
  onMouseEnter,
115
+ onMouseLeave,
110
116
  onFocus,
117
+ onBlur,
111
118
  onClick,
112
119
  to,
113
120
  ...rest
114
121
  } = props;
115
122
  const localizedTo = useLocalizedTo(to);
116
- const { preload } = usePreload();
123
+ const { preload, routes } = usePreload();
124
+ const config = useConfig();
117
125
  const navigate = useNavigate();
118
126
 
127
+ const shouldShowPreview =
128
+ boltdocsPreview && config?.themeConfig?.linkPreview !== false;
129
+
130
+ const [preview, setPreview] = React.useState<{
131
+ visible: boolean;
132
+ x: number;
133
+ y: number;
134
+ title: string;
135
+ summary?: string;
136
+ }>({ visible: false, x: 0, y: 0, title: "" });
137
+
119
138
  const handleMouseEnter = (e: React.MouseEvent<HTMLAnchorElement>) => {
120
139
  onMouseEnter?.(e);
121
140
  if (
@@ -125,6 +144,37 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
125
144
  ) {
126
145
  preload(localizedTo);
127
146
  }
147
+
148
+ if (
149
+ shouldShowPreview &&
150
+ typeof localizedTo === "string" &&
151
+ localizedTo.startsWith("/")
152
+ ) {
153
+ const cleanPath = localizedTo.split("#")[0].split("?")[0];
154
+ const route = routes.find(
155
+ (r) => r.path === cleanPath || (cleanPath === "/" && r.path === ""),
156
+ );
157
+ if (route) {
158
+ setPreview({
159
+ visible: true,
160
+ x: e.clientX,
161
+ y: e.clientY,
162
+ title: route.title,
163
+ summary: route.description,
164
+ });
165
+ }
166
+ }
167
+ };
168
+
169
+ const handleMouseMove = (e: React.MouseEvent<HTMLAnchorElement>) => {
170
+ if (preview.visible) {
171
+ setPreview((prev) => ({ ...prev, x: e.clientX, y: e.clientY }));
172
+ }
173
+ };
174
+
175
+ const handleMouseLeave = (e: React.MouseEvent<HTMLAnchorElement>) => {
176
+ onMouseLeave?.(e);
177
+ setPreview((prev) => ({ ...prev, visible: false }));
128
178
  };
129
179
 
130
180
  const handleFocus = (e: React.FocusEvent<HTMLAnchorElement>) => {
@@ -138,9 +188,15 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
138
188
  }
139
189
  };
140
190
 
191
+ const handleBlur = (e: React.FocusEvent<HTMLAnchorElement>) => {
192
+ onBlur?.(e);
193
+ setPreview((prev) => ({ ...prev, visible: false }));
194
+ };
195
+
141
196
  const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
142
197
  // Allow user onClick to handle defaults or custom logic
143
198
  onClick?.(e);
199
+ setPreview((prev) => ({ ...prev, visible: false }));
144
200
 
145
201
  // If default prevented or not a simple left click, don't handle
146
202
  if (
@@ -164,14 +220,28 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
164
220
  };
165
221
 
166
222
  return (
167
- <RouterLink
168
- ref={ref}
169
- to={localizedTo}
170
- onMouseEnter={handleMouseEnter}
171
- onFocus={handleFocus}
172
- onClick={handleClick}
173
- {...rest}
174
- />
223
+ <>
224
+ <RouterLink
225
+ ref={ref}
226
+ to={localizedTo}
227
+ onMouseEnter={handleMouseEnter}
228
+ onMouseMove={handleMouseMove}
229
+ onMouseLeave={handleMouseLeave}
230
+ onFocus={handleFocus}
231
+ onBlur={handleBlur}
232
+ onClick={handleClick}
233
+ {...rest}
234
+ />
235
+ {shouldShowPreview && (
236
+ <LinkPreview
237
+ isVisible={preview.visible}
238
+ title={preview.title}
239
+ summary={preview.summary}
240
+ x={preview.x}
241
+ y={preview.y}
242
+ />
243
+ )}
244
+ </>
175
245
  );
176
246
  },
177
247
  );
@@ -180,23 +250,40 @@ Link.displayName = "Link";
180
250
  export interface NavLinkProps extends Omit<RouterNavLinkProps, "prefetch"> {
181
251
  /** Should prefetch the page on hover? Options: 'hover' | 'none'. Default 'hover' */
182
252
  boltdocsPrefetch?: "hover" | "none";
253
+ /** Should show a preview tooltip on hover? Default true */
254
+ boltdocsPreview?: boolean;
183
255
  }
184
256
 
185
257
  export const NavLink = React.forwardRef<HTMLAnchorElement, NavLinkProps>(
186
258
  (props, ref) => {
187
259
  const {
188
260
  boltdocsPrefetch = "hover",
261
+ boltdocsPreview = true,
189
262
  onMouseEnter,
263
+ onMouseLeave,
190
264
  onFocus,
265
+ onBlur,
191
266
  onClick,
192
267
  to,
193
268
  ...rest
194
269
  } = props;
195
270
 
196
271
  const localizedTo = useLocalizedTo(to);
197
- const { preload } = usePreload();
272
+ const { preload, routes } = usePreload();
273
+ const config = useConfig();
198
274
  const navigate = useNavigate();
199
275
 
276
+ const shouldShowPreview =
277
+ boltdocsPreview && config?.themeConfig?.linkPreview !== false;
278
+
279
+ const [preview, setPreview] = React.useState<{
280
+ visible: boolean;
281
+ x: number;
282
+ y: number;
283
+ title: string;
284
+ summary?: string;
285
+ }>({ visible: false, x: 0, y: 0, title: "" });
286
+
200
287
  const handleMouseEnter = (e: React.MouseEvent<HTMLAnchorElement>) => {
201
288
  onMouseEnter?.(e);
202
289
  if (
@@ -206,6 +293,37 @@ export const NavLink = React.forwardRef<HTMLAnchorElement, NavLinkProps>(
206
293
  ) {
207
294
  preload(localizedTo);
208
295
  }
296
+
297
+ if (
298
+ shouldShowPreview &&
299
+ typeof localizedTo === "string" &&
300
+ localizedTo.startsWith("/")
301
+ ) {
302
+ const cleanPath = localizedTo.split("#")[0].split("?")[0];
303
+ const route = routes.find(
304
+ (r) => r.path === cleanPath || (cleanPath === "/" && r.path === ""),
305
+ );
306
+ if (route) {
307
+ setPreview({
308
+ visible: true,
309
+ x: e.clientX,
310
+ y: e.clientY,
311
+ title: route.title,
312
+ summary: route.description,
313
+ });
314
+ }
315
+ }
316
+ };
317
+
318
+ const handleMouseMove = (e: React.MouseEvent<HTMLAnchorElement>) => {
319
+ if (preview.visible) {
320
+ setPreview((prev) => ({ ...prev, x: e.clientX, y: e.clientY }));
321
+ }
322
+ };
323
+
324
+ const handleMouseLeave = (e: React.MouseEvent<HTMLAnchorElement>) => {
325
+ onMouseLeave?.(e);
326
+ setPreview((prev) => ({ ...prev, visible: false }));
209
327
  };
210
328
 
211
329
  const handleFocus = (e: React.FocusEvent<HTMLAnchorElement>) => {
@@ -219,8 +337,14 @@ export const NavLink = React.forwardRef<HTMLAnchorElement, NavLinkProps>(
219
337
  }
220
338
  };
221
339
 
340
+ const handleBlur = (e: React.FocusEvent<HTMLAnchorElement>) => {
341
+ onBlur?.(e);
342
+ setPreview((prev) => ({ ...prev, visible: false }));
343
+ };
344
+
222
345
  const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
223
346
  onClick?.(e);
347
+ setPreview((prev) => ({ ...prev, visible: false }));
224
348
  if (
225
349
  e.defaultPrevented ||
226
350
  e.button !== 0 ||
@@ -240,14 +364,28 @@ export const NavLink = React.forwardRef<HTMLAnchorElement, NavLinkProps>(
240
364
  };
241
365
 
242
366
  return (
243
- <RouterNavLink
244
- ref={ref}
245
- to={localizedTo}
246
- onMouseEnter={handleMouseEnter}
247
- onFocus={handleFocus}
248
- onClick={handleClick}
249
- {...rest}
250
- />
367
+ <>
368
+ <RouterNavLink
369
+ ref={ref}
370
+ to={localizedTo}
371
+ onMouseEnter={handleMouseEnter}
372
+ onMouseMove={handleMouseMove}
373
+ onMouseLeave={handleMouseLeave}
374
+ onFocus={handleFocus}
375
+ onBlur={handleBlur}
376
+ onClick={handleClick}
377
+ {...rest}
378
+ />
379
+ {shouldShowPreview && (
380
+ <LinkPreview
381
+ isVisible={preview.visible}
382
+ title={preview.title}
383
+ summary={preview.summary}
384
+ x={preview.x}
385
+ y={preview.y}
386
+ />
387
+ )}
388
+ </>
251
389
  );
252
390
  },
253
391
  );