boltdocs 1.0.1 → 1.3.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 (64) hide show
  1. package/dist/{CodeBlock-37XMKCYY.mjs → CodeBlock-V3Z5EKGR.mjs} +0 -1
  2. package/dist/{PackageManagerTabs-4NWXLXQO.mjs → PackageManagerTabs-XEKI3L7P.mjs} +0 -2
  3. package/dist/{SearchDialog-FTOQZ763.mjs → SearchDialog-5EDRACEG.mjs} +1 -2
  4. package/dist/{SearchDialog-ZAZXYIFX.css → SearchDialog-X57WPTNN.css} +57 -129
  5. package/dist/{Video-I6QY4X7J.mjs → Video-KNTY5BNO.mjs} +0 -1
  6. package/dist/cache-EHR7SXRU.mjs +12 -0
  7. package/dist/chunk-GSYECEZY.mjs +381 -0
  8. package/dist/{chunk-ZFCOLEXN.mjs → chunk-NS7WHDYA.mjs} +234 -426
  9. package/dist/client/index.css +57 -129
  10. package/dist/client/index.d.mts +39 -8
  11. package/dist/client/index.d.ts +39 -8
  12. package/dist/client/index.js +557 -564
  13. package/dist/client/index.mjs +305 -18
  14. package/dist/client/ssr.css +57 -129
  15. package/dist/client/ssr.d.mts +1 -1
  16. package/dist/client/ssr.d.ts +1 -1
  17. package/dist/client/ssr.js +257 -558
  18. package/dist/client/ssr.mjs +1 -2
  19. package/dist/{config-D2XmHJYe.d.mts → config-BD5ZHz15.d.mts} +7 -0
  20. package/dist/{config-D2XmHJYe.d.ts → config-BD5ZHz15.d.ts} +7 -0
  21. package/dist/node/index.d.mts +2 -2
  22. package/dist/node/index.d.ts +2 -2
  23. package/dist/node/index.js +457 -118
  24. package/dist/node/index.mjs +144 -147
  25. package/dist/{index-CRQKWAeo.d.mts → types-CvrzTbEX.d.mts} +1 -28
  26. package/dist/{index-CRQKWAeo.d.ts → types-CvrzTbEX.d.ts} +1 -28
  27. package/package.json +2 -2
  28. package/src/client/app/index.tsx +32 -110
  29. package/src/client/app/preload.tsx +1 -1
  30. package/src/client/index.ts +1 -1
  31. package/src/client/ssr.tsx +2 -1
  32. package/src/client/theme/components/Playground/Playground.tsx +40 -2
  33. package/src/client/theme/components/mdx/mdx-components.css +39 -20
  34. package/src/client/theme/styles/markdown.css +4 -4
  35. package/src/client/theme/styles.css +0 -1
  36. package/src/client/theme/ui/Breadcrumbs/Breadcrumbs.tsx +1 -1
  37. package/src/client/theme/ui/LanguageSwitcher/LanguageSwitcher.tsx +1 -1
  38. package/src/client/theme/ui/Layout/Layout.tsx +3 -14
  39. package/src/client/theme/ui/Layout/responsive.css +0 -4
  40. package/src/client/theme/ui/Link/Link.tsx +52 -0
  41. package/src/client/theme/ui/Navbar/Navbar.tsx +1 -1
  42. package/src/client/theme/ui/NotFound/NotFound.tsx +0 -1
  43. package/src/client/theme/ui/OnThisPage/OnThisPage.tsx +45 -2
  44. package/src/client/theme/ui/SearchDialog/SearchDialog.tsx +1 -1
  45. package/src/client/theme/ui/Sidebar/Sidebar.tsx +44 -40
  46. package/src/client/theme/ui/Sidebar/sidebar.css +25 -58
  47. package/src/client/theme/ui/VersionSwitcher/VersionSwitcher.tsx +1 -1
  48. package/src/client/types.ts +50 -0
  49. package/src/node/cache.ts +360 -46
  50. package/src/node/config.ts +7 -0
  51. package/src/node/mdx.ts +83 -4
  52. package/src/node/plugin/index.ts +3 -0
  53. package/src/node/routes/cache.ts +5 -1
  54. package/src/node/routes/index.ts +17 -2
  55. package/src/node/ssg/index.ts +4 -0
  56. package/dist/Playground-OE2OE6B6.mjs +0 -7
  57. package/dist/chunk-PN4GCTYG.mjs +0 -67
  58. package/dist/chunk-X2TDGMTR.mjs +0 -64
  59. package/dist/chunk-X6BYQHVC.mjs +0 -12
  60. package/dist/node/cli/index.d.mts +0 -1
  61. package/dist/node/cli/index.d.ts +0 -1
  62. package/dist/node/cli/index.js +0 -199
  63. package/dist/node/cli/index.mjs +0 -154
  64. package/src/client/theme/styles/home.css +0 -60
@@ -11,6 +11,7 @@ import { ThemeLayout } from "../theme/ui/Layout";
11
11
  import { NotFound } from "../theme/ui/NotFound";
12
12
  import { Loading } from "../theme/ui/Loading";
13
13
  import { MDXProvider } from "@mdx-js/react";
14
+ import { ComponentRoute, CreateBoltdocsAppOptions } from "../types";
14
15
  import {
15
16
  createContext,
16
17
  useContext,
@@ -39,27 +40,6 @@ const PackageManagerTabs = lazy(() =>
39
40
  default: m.PackageManagerTabs,
40
41
  })),
41
42
  );
42
- const Playground = lazy(() =>
43
- import("../theme/components/Playground").then((m) => ({
44
- default: m.Playground,
45
- })),
46
- );
47
-
48
- import {
49
- Button,
50
- Badge,
51
- Card,
52
- Cards,
53
- Tabs,
54
- Tab,
55
- Admonition,
56
- Note,
57
- Tip,
58
- Warning,
59
- Danger,
60
- InfoBox,
61
- List,
62
- } from "../theme/components/mdx";
63
43
  declare global {
64
44
  interface ImportMeta {
65
45
  env: Record<string, any>;
@@ -114,75 +94,8 @@ const mdxComponents = {
114
94
  <PackageManagerTabs {...props} />
115
95
  </Suspense>
116
96
  ),
117
- Playground: (props: any) => (
118
- <Suspense fallback={<div className="playground-skeleton" />}>
119
- <Playground {...props} />
120
- </Suspense>
121
- ),
122
- Button,
123
- Badge,
124
- Card,
125
- Cards,
126
- Tabs,
127
- Tab,
128
- Admonition,
129
- Note,
130
- Tip,
131
- Warning,
132
- Danger,
133
- InfoBox,
134
- List,
135
97
  };
136
98
 
137
- /**
138
- * Metadata provided by the server for a specific route.
139
- * Maps closely to the `RouteMeta` type in the Node environment.
140
- */
141
- export interface ComponentRoute {
142
- /** The final URL path */
143
- path: string;
144
- /** The absolute filesystem path of the source file */
145
- componentPath: string;
146
- /** The page title */
147
- title: string;
148
- /** Explicit order in the sidebar */
149
- sidebarPosition?: number;
150
- /** The relative path from the docs directory */
151
- filePath: string;
152
- /** The group directory name */
153
- group?: string;
154
- /** The display title of the group */
155
- groupTitle?: string;
156
- /** Explicit order of the group in the sidebar */
157
- groupPosition?: number;
158
- /** Extracted markdown headings for search indexing */
159
- headings?: { level: number; text: string; id: string }[];
160
- /** The locale this route belongs to, if i18n is configured */
161
- locale?: string;
162
- /** The version this route belongs to, if versioning is configured */
163
- version?: string;
164
- }
165
-
166
- /**
167
- * Configuration options for initializing the Boltdocs client app.
168
- */
169
- export interface CreateBoltdocsAppOptions {
170
- /** CSS selector for the DOM element where the app should mount (e.g. '#root') */
171
- target: string;
172
- /** Initial routes generated by the Vite plugin (`virtual:boltdocs-routes`) */
173
- routes: ComponentRoute[];
174
- /** Site configuration (`virtual:boltdocs-config`) */
175
- config: any;
176
- /** Dynamic import mapping from `import.meta.glob` for the documentation pages */
177
- modules: Record<string, () => Promise<any>>;
178
- /** The `import.meta.hot` instance necessary for fast refresh/HMR updates */
179
- hot?: any;
180
- /** Optional custom React component to render when visiting the root path ('/') */
181
- homePage?: React.ComponentType;
182
- /** Optional custom MDX components provided by plugins */
183
- components?: Record<string, React.ComponentType<any>>;
184
- }
185
-
186
99
  export function AppShell({
187
100
  initialRoutes,
188
101
  initialConfig,
@@ -200,20 +113,9 @@ export function AppShell({
200
113
  }) {
201
114
  const [routesInfo, setRoutesInfo] = useState<ComponentRoute[]>(initialRoutes);
202
115
  const [config] = useState(initialConfig);
203
- const [resolvedRoutes, setResolvedRoutes] = useState<any[]>([]);
204
-
205
- // Subscribe to HMR events
206
- useEffect(() => {
207
- if (hot) {
208
- hot.on("boltdocs:routes-update", (newRoutes: ComponentRoute[]) => {
209
- setRoutesInfo(newRoutes);
210
- });
211
- }
212
- }, [hot]);
213
116
 
214
- // Resolve MDX components
215
- useEffect(() => {
216
- const mapped = routesInfo
117
+ const resolveRoutes = (infos: ComponentRoute[]) => {
118
+ return infos
217
119
  .filter(
218
120
  (route) => !(HomePage && (route.path === "/" || route.path === "")),
219
121
  )
@@ -232,8 +134,24 @@ export function AppShell({
232
134
  }),
233
135
  };
234
136
  });
137
+ };
138
+
139
+ const [resolvedRoutes, setResolvedRoutes] = useState<any[]>(() =>
140
+ resolveRoutes(initialRoutes),
141
+ );
142
+
143
+ // Subscribe to HMR events
144
+ useEffect(() => {
145
+ if (hot) {
146
+ hot.on("boltdocs:routes-update", (newRoutes: ComponentRoute[]) => {
147
+ setRoutesInfo(newRoutes);
148
+ });
149
+ }
150
+ }, [hot]);
235
151
 
236
- setResolvedRoutes(mapped);
152
+ // Sync resolved routes when info or modules change
153
+ useEffect(() => {
154
+ setResolvedRoutes(resolveRoutes(routesInfo));
237
155
  }, [routesInfo, modules]);
238
156
 
239
157
  return (
@@ -261,7 +179,10 @@ export function AppShell({
261
179
  )}
262
180
 
263
181
  {/* Documentation pages WITH sidebar + TOC layout */}
264
- <Route element={<DocsLayout config={config} routes={routesInfo} />}>
182
+ <Route
183
+ key="docs-layout"
184
+ element={<DocsLayout config={config} routes={routesInfo} />}
185
+ >
265
186
  {resolvedRoutes.map((route: any) => (
266
187
  <Route
267
188
  key={route.path}
@@ -303,6 +224,8 @@ function ScrollHandler() {
303
224
  const { pathname, hash } = useLocation();
304
225
 
305
226
  useLayoutEffect(() => {
227
+ // Only scroll if we are not in a pending transition state (if we were using useTransition)
228
+ // For now, we ensure the scroll happens.
306
229
  if (hash) {
307
230
  const id = hash.replace("#", "");
308
231
  const element = document.getElementById(id);
@@ -412,11 +335,10 @@ export function createBoltdocsApp(options: CreateBoltdocsAppOptions) {
412
335
  </React.StrictMode>
413
336
  );
414
337
 
415
- // In production (built app), the HTML is pre-rendered by SSG, so we hydrate.
416
- // In development, the root is empty, so we createRoot.
417
- if (import.meta.env.PROD && container.innerHTML.trim() !== "") {
418
- ReactDOM.hydrateRoot(container as HTMLElement, app);
419
- } else {
420
- ReactDOM.createRoot(container as HTMLElement).render(app);
421
- }
338
+ // SSG pre-renders a shell with mock components for SEO crawlers.
339
+ // We always use createRoot because the SSG output doesn't match the
340
+ // real client-side component tree (components are lazy/dynamic).
341
+ // Clear any SSG placeholder content before mounting.
342
+ container.innerHTML = "";
343
+ ReactDOM.createRoot(container as HTMLElement).render(app);
422
344
  }
@@ -1,5 +1,5 @@
1
1
  import React, { createContext, useContext, useCallback } from "react";
2
- import { ComponentRoute } from "./index";
2
+ import { ComponentRoute } from "../types";
3
3
 
4
4
  interface PreloadContextType {
5
5
  preload: (path: string) => void;
@@ -1,5 +1,5 @@
1
1
  export type { BoltdocsConfig, BoltdocsThemeConfig } from "../node/config";
2
- export type { ComponentRoute, CreateBoltdocsAppOptions } from "./app";
2
+ export type { ComponentRoute, CreateBoltdocsAppOptions } from "./types";
3
3
  export { createBoltdocsApp } from "./app";
4
4
  export { ThemeLayout } from "./theme/ui/Layout";
5
5
  export { Navbar } from "./theme/ui/Navbar";
@@ -1,7 +1,8 @@
1
1
  import React from "react";
2
2
  import ReactDOMServer from "react-dom/server";
3
3
  import { StaticRouter } from "react-router-dom/server";
4
- import { AppShell, ComponentRoute } from "./app";
4
+ import { AppShell } from "./app";
5
+ import { ComponentRoute } from "./types";
5
6
 
6
7
  /**
7
8
  * Options for rendering the Boltdocs application on the server (SSG).
@@ -7,17 +7,52 @@ interface PlaygroundProps {
7
7
  children?: string | React.ReactNode;
8
8
  scope?: Record<string, any>;
9
9
  readonly?: boolean;
10
+ noInline?: boolean;
11
+ }
12
+
13
+ /**
14
+ * Transforms code that uses `export default` into a format compatible
15
+ * with react-live's `noInline` mode by stripping the export and
16
+ * appending a `render(<ComponentName />)` call.
17
+ */
18
+ function prepareCode(raw: string): { code: string; noInline: boolean } {
19
+ const trimmed = raw.trim();
20
+
21
+ // Match: export default function Name(...)
22
+ const fnMatch = trimmed.match(/export\s+default\s+function\s+(\w+)/);
23
+ if (fnMatch) {
24
+ const name = fnMatch[1];
25
+ const code =
26
+ trimmed.replace(/export\s+default\s+/, "") + `\n\nrender(<${name} />);`;
27
+ return { code, noInline: true };
28
+ }
29
+
30
+ // Match: export default ComponentName (at the end)
31
+ const varMatch = trimmed.match(/export\s+default\s+(\w+)\s*;?\s*$/);
32
+ if (varMatch) {
33
+ const name = varMatch[1];
34
+ const code =
35
+ trimmed.replace(/export\s+default\s+\w+\s*;?\s*$/, "") +
36
+ `\nrender(<${name} />);`;
37
+ return { code, noInline: true };
38
+ }
39
+
40
+ // No export default — use inline mode (simple JSX expression)
41
+ return { code: trimmed, noInline: false };
10
42
  }
11
43
 
12
44
  /**
13
45
  * A live React playground component.
14
46
  * Features a split layout with a live editor and a preview section.
47
+ *
48
+ * Supports `export default function App()` style code out of the box.
15
49
  */
16
50
  export function Playground({
17
51
  code,
18
52
  children,
19
53
  scope = {},
20
54
  readonly = false,
55
+ noInline: forceNoInline,
21
56
  }: PlaygroundProps) {
22
57
  // Extract code from either `code` prop or `children`
23
58
  let initialCode = code || "";
@@ -25,8 +60,11 @@ export function Playground({
25
60
  initialCode = children;
26
61
  }
27
62
 
63
+ const prepared = prepareCode(initialCode);
64
+ const useNoInline = forceNoInline ?? prepared.noInline;
65
+
28
66
  const [copied, setCopied] = useState(false);
29
- const [activeCode, setActiveCode] = useState(initialCode.trim());
67
+ const [activeCode, setActiveCode] = useState(prepared.code);
30
68
 
31
69
  const handleCopy = () => {
32
70
  navigator.clipboard.writeText(activeCode);
@@ -43,7 +81,7 @@ export function Playground({
43
81
  code={activeCode}
44
82
  scope={extendedScope}
45
83
  theme={undefined}
46
- noInline={false}
84
+ noInline={useNoInline}
47
85
  >
48
86
  <div className="playground-split-container">
49
87
  {/* Editor Side */}
@@ -6,54 +6,77 @@
6
6
 
7
7
  /* ─── Button ──────────────────────────────────────────────── */
8
8
  .ld-btn {
9
+ /* Total Reset */
10
+ all: unset;
11
+ box-sizing: border-box !important;
9
12
  display: inline-flex;
10
13
  align-items: center;
11
- gap: 0.4rem;
14
+ justify-content: center;
15
+ gap: 0.5rem;
16
+
17
+ /* Typography */
18
+ font-family: var(--ld-font-sans);
12
19
  font-weight: 600;
20
+ font-size: 0.9375rem;
21
+ line-height: normal; /* Essential for flex centering */
22
+ letter-spacing: -0.01em;
23
+ text-align: center;
24
+ text-decoration: none !important;
25
+ white-space: nowrap;
26
+
27
+ /* Appearance */
13
28
  border-radius: var(--ld-radius-md);
14
29
  cursor: pointer;
15
- transition: all 300ms cubic-bezier(0.16, 1, 0.3, 1);
16
- font-family: var(--ld-font-sans);
30
+ transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
17
31
  border: 1px solid transparent;
18
- line-height: 1;
19
- letter-spacing: -0.01em;
32
+ user-select: none;
33
+ position: relative;
20
34
  }
21
35
 
22
36
  .ld-btn:hover {
23
- transform: scale(1.05);
37
+ transform: translateY(-1px);
24
38
  }
25
39
 
26
40
  .ld-btn:active {
27
- transform: scale(0.95);
41
+ transform: translateY(0) scale(0.98);
28
42
  }
29
43
 
30
44
  /* sizes */
31
45
  .ld-btn--sm {
32
- padding: 0.45rem 1.1rem;
46
+ min-height: 2rem;
47
+ padding: 0 1rem;
33
48
  font-size: 0.8125rem;
34
49
  border-radius: var(--ld-radius-md);
35
50
  }
36
51
  .ld-btn--md {
37
- padding: 0.7rem 1.6rem;
52
+ min-height: 2.625rem;
53
+ padding: 0 1.6rem;
38
54
  font-size: 0.9375rem;
39
55
  }
40
56
  .ld-btn--lg {
41
- padding: 0.85rem 2.1rem;
57
+ min-height: 3.25rem;
58
+ padding: 0 2.2rem;
42
59
  font-size: 1.05rem;
43
60
  }
44
61
 
45
62
  /* variants */
46
63
  .ld-btn--primary {
47
- background-color: var(--ld-ui-btn-primary-bg, var(--ld-btn-primary-bg));
64
+ background-color: var(
65
+ --ld-ui-btn-primary-bg,
66
+ var(--ld-btn-primary-bg)
67
+ ) !important;
48
68
  color: var(--ld-ui-btn-primary-text, var(--ld-btn-primary-text)) !important;
69
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
49
70
  }
50
71
  .ld-btn--primary:hover {
51
- opacity: 0.92;
52
- color: var(--ld-ui-btn-primary-text, var(--ld-btn-primary-text)) !important;
72
+ filter: brightness(1.1);
53
73
  }
54
74
 
55
75
  .ld-btn--secondary {
56
- background-color: var(--ld-ui-btn-secondary-bg, var(--ld-btn-secondary-bg));
76
+ background-color: var(
77
+ --ld-ui-btn-secondary-bg,
78
+ var(--ld-btn-secondary-bg)
79
+ ) !important;
57
80
  color: var(
58
81
  --ld-ui-btn-secondary-text,
59
82
  var(--ld-btn-secondary-text)
@@ -63,14 +86,10 @@
63
86
  .ld-btn--secondary:hover {
64
87
  background-color: var(--ld-bg-mute);
65
88
  border-color: var(--ld-border-strong);
66
- color: var(
67
- --ld-ui-btn-secondary-text,
68
- var(--ld-btn-secondary-text)
69
- ) !important;
70
89
  }
71
90
 
72
91
  .ld-btn--outline {
73
- background: transparent;
92
+ background: transparent !important;
74
93
  color: var(--ld-text-main) !important;
75
94
  border-color: var(--ld-border-strong);
76
95
  }
@@ -80,7 +99,7 @@
80
99
  }
81
100
 
82
101
  .ld-btn--ghost {
83
- background: transparent;
102
+ background: transparent !important;
84
103
  color: var(--ld-text-muted) !important;
85
104
  }
86
105
  .ld-btn--ghost:hover {
@@ -265,12 +265,12 @@
265
265
  }
266
266
 
267
267
  .code-block-wrapper pre > code {
268
- padding: 1.25rem !important;
269
- display: block !important;
268
+ display: grid !important;
269
+ padding: 1.25rem 1rem !important;
270
270
  }
271
271
 
272
272
  .code-block-wrapper pre > code .line {
273
- padding: 0;
273
+ padding: 0 1.25rem;
274
274
  }
275
275
 
276
276
  /* Default pre (without wrapper) */
@@ -288,7 +288,7 @@
288
288
 
289
289
  .boltdocs-page pre > code {
290
290
  display: grid;
291
- padding: 1rem 0;
291
+ padding: 1rem 1rem;
292
292
  background-color: transparent;
293
293
  border: none;
294
294
  color: inherit;
@@ -27,7 +27,6 @@
27
27
  @import "./ui/Layout/pagination.css";
28
28
 
29
29
  /* Pages */
30
- @import "./styles/home.css";
31
30
  @import "./ui/NotFound/not-found.css";
32
31
  @import "./ui/Loading/loading.css";
33
32
 
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import { useLocation } from "react-router-dom";
3
3
  import { Link } from "../Link";
4
4
  import { Home, ChevronRight } from "lucide-react";
5
- import { ComponentRoute } from "../../../app";
5
+ import { ComponentRoute } from "../../../types";
6
6
  import { BoltdocsConfig } from "../../../../node/config";
7
7
 
8
8
  export interface BreadcrumbsProps {
@@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from "react";
2
2
  import { Globe, ChevronDown } from "lucide-react";
3
3
  import { useNavigate, useLocation } from "react-router-dom";
4
4
  import { BoltdocsI18nConfig } from "../../../../node/config";
5
- import { ComponentRoute } from "../../../app";
5
+ import { ComponentRoute } from "../../../types";
6
6
 
7
7
  function getBaseFilePath(
8
8
  filePath: string,
@@ -4,7 +4,7 @@ import { Link } from "../Link";
4
4
  import { ChevronLeft, ChevronRight, Menu } from "lucide-react";
5
5
  import { usePreload } from "../../../app/preload";
6
6
  import { BoltdocsConfig } from "../../../../node/config";
7
- import { ComponentRoute } from "../../../app";
7
+ import { ComponentRoute } from "../../../types";
8
8
  export { Navbar } from "../Navbar";
9
9
  export { Sidebar } from "../Sidebar";
10
10
  export { OnThisPage } from "../OnThisPage";
@@ -139,22 +139,11 @@ export function ThemeLayout({
139
139
  <Sidebar
140
140
  routes={filteredRoutes}
141
141
  config={config}
142
- onCollapse={() => setIsSidebarOpen(false)}
142
+ isCollapsed={!isSidebarOpen}
143
+ onToggle={() => setIsSidebarOpen(!isSidebarOpen)}
143
144
  />
144
145
  )}
145
146
 
146
- {/* Floating Expand Button when Sidebar is Collapsed */}
147
- {sidebar === undefined && (
148
- <button
149
- className="sidebar-toggle-floating"
150
- onClick={() => setIsSidebarOpen(true)}
151
- aria-label="Expand Sidebar"
152
- title="Expand Sidebar"
153
- >
154
- <Menu size={20} />
155
- </button>
156
- )}
157
-
158
147
  <main className="boltdocs-content">
159
148
  {breadcrumbs !== undefined ? (
160
149
  breadcrumbs
@@ -12,10 +12,6 @@
12
12
  display: none;
13
13
  }
14
14
 
15
- .sidebar-toggle-floating {
16
- display: none !important;
17
- }
18
-
19
15
  .boltdocs-content {
20
16
  padding: 1.5rem 1rem 3rem;
21
17
  }
@@ -5,6 +5,7 @@ import {
5
5
  LinkProps as RouterLinkProps,
6
6
  NavLinkProps as RouterNavLinkProps,
7
7
  useLocation,
8
+ useNavigate,
8
9
  } from "react-router-dom";
9
10
  import { usePreload } from "../../../app/preload";
10
11
  import { useConfig } from "../../../app";
@@ -107,11 +108,13 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
107
108
  boltdocsPrefetch = "hover",
108
109
  onMouseEnter,
109
110
  onFocus,
111
+ onClick,
110
112
  to,
111
113
  ...rest
112
114
  } = props;
113
115
  const localizedTo = useLocalizedTo(to);
114
116
  const { preload } = usePreload();
117
+ const navigate = useNavigate();
115
118
 
116
119
  const handleMouseEnter = (e: React.MouseEvent<HTMLAnchorElement>) => {
117
120
  onMouseEnter?.(e);
@@ -135,12 +138,38 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
135
138
  }
136
139
  };
137
140
 
141
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
142
+ // Allow user onClick to handle defaults or custom logic
143
+ onClick?.(e);
144
+
145
+ // If default prevented or not a simple left click, don't handle
146
+ if (
147
+ e.defaultPrevented ||
148
+ e.button !== 0 ||
149
+ e.metaKey ||
150
+ e.ctrlKey ||
151
+ e.shiftKey ||
152
+ e.altKey
153
+ ) {
154
+ return;
155
+ }
156
+
157
+ // Intercept navigation to wrap in startTransition
158
+ if (typeof localizedTo === "string" && !localizedTo.startsWith("http")) {
159
+ e.preventDefault();
160
+ React.startTransition(() => {
161
+ navigate(localizedTo);
162
+ });
163
+ }
164
+ };
165
+
138
166
  return (
139
167
  <RouterLink
140
168
  ref={ref}
141
169
  to={localizedTo}
142
170
  onMouseEnter={handleMouseEnter}
143
171
  onFocus={handleFocus}
172
+ onClick={handleClick}
144
173
  {...rest}
145
174
  />
146
175
  );
@@ -159,12 +188,14 @@ export const NavLink = React.forwardRef<HTMLAnchorElement, NavLinkProps>(
159
188
  boltdocsPrefetch = "hover",
160
189
  onMouseEnter,
161
190
  onFocus,
191
+ onClick,
162
192
  to,
163
193
  ...rest
164
194
  } = props;
165
195
 
166
196
  const localizedTo = useLocalizedTo(to);
167
197
  const { preload } = usePreload();
198
+ const navigate = useNavigate();
168
199
 
169
200
  const handleMouseEnter = (e: React.MouseEvent<HTMLAnchorElement>) => {
170
201
  onMouseEnter?.(e);
@@ -188,12 +219,33 @@ export const NavLink = React.forwardRef<HTMLAnchorElement, NavLinkProps>(
188
219
  }
189
220
  };
190
221
 
222
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
223
+ onClick?.(e);
224
+ if (
225
+ e.defaultPrevented ||
226
+ e.button !== 0 ||
227
+ e.metaKey ||
228
+ e.ctrlKey ||
229
+ e.shiftKey ||
230
+ e.altKey
231
+ ) {
232
+ return;
233
+ }
234
+ if (typeof localizedTo === "string" && !localizedTo.startsWith("http")) {
235
+ e.preventDefault();
236
+ React.startTransition(() => {
237
+ navigate(localizedTo);
238
+ });
239
+ }
240
+ };
241
+
191
242
  return (
192
243
  <RouterNavLink
193
244
  ref={ref}
194
245
  to={localizedTo}
195
246
  onMouseEnter={handleMouseEnter}
196
247
  onFocus={handleFocus}
248
+ onClick={handleClick}
197
249
  {...rest}
198
250
  />
199
251
  );
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
2
2
  import { Link } from "../Link";
3
3
  import { Book, ChevronDown } from "lucide-react";
4
4
  import { BoltdocsConfig } from "../../../../node/config";
5
- import { ComponentRoute } from "../../../app";
5
+ import { ComponentRoute } from "../../../types";
6
6
  import { LanguageSwitcher } from "../LanguageSwitcher";
7
7
  import { VersionSwitcher } from "../VersionSwitcher";
8
8
  import { ThemeToggle } from "../ThemeToggle";
@@ -1,4 +1,3 @@
1
- import React from "react";
2
1
  import { Link } from "../Link";
3
2
  import { ArrowLeft } from "lucide-react";
4
3