boltdocs 1.4.1 → 1.6.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 (43) hide show
  1. package/dist/{PackageManagerTabs-XW3AVXVX.mjs → PackageManagerTabs-NVT7G625.mjs} +1 -1
  2. package/dist/{SearchDialog-O3V36MXA.css → SearchDialog-3QICRMWF.css} +145 -5
  3. package/dist/{SearchDialog-FBNGKRPK.mjs → SearchDialog-J3KNRGNO.mjs} +1 -1
  4. package/dist/{chunk-S5G55FBI.mjs → chunk-7SFUJWTB.mjs} +4 -4
  5. package/dist/{chunk-D7YBQG6H.mjs → chunk-HSPDIRTW.mjs} +312 -134
  6. package/dist/client/index.css +145 -5
  7. package/dist/client/index.d.mts +22 -5
  8. package/dist/client/index.d.ts +22 -5
  9. package/dist/client/index.js +725 -459
  10. package/dist/client/index.mjs +182 -61
  11. package/dist/client/ssr.css +145 -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 +5 -1
  21. package/dist/node/index.mjs +5 -1
  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/index.ts +2 -0
  28. package/src/client/theme/components/CodeBlock/CodeBlock.tsx +0 -11
  29. package/src/client/theme/components/mdx/FileTree.tsx +229 -0
  30. package/src/client/theme/components/mdx/Tabs.tsx +1 -4
  31. package/src/client/theme/components/mdx/index.ts +3 -0
  32. package/src/client/theme/components/mdx/mdx-components.css +109 -0
  33. package/src/client/theme/icons/pnpm.tsx +5 -5
  34. package/src/client/theme/styles/markdown.css +1 -5
  35. package/src/client/theme/ui/Link/Link.tsx +156 -18
  36. package/src/client/theme/ui/Link/LinkPreview.tsx +64 -0
  37. package/src/client/theme/ui/Link/link-preview.css +64 -0
  38. package/src/client/types.ts +2 -0
  39. package/src/node/config.ts +2 -0
  40. package/src/node/routes/parser.ts +14 -1
  41. package/dist/CodeBlock-QYIKJMEB.mjs +0 -7
  42. package/dist/chunk-KS5B3O6W.mjs +0 -43
  43. package/src/client/theme/icons/yarn.tsx +0 -16
@@ -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
  );
@@ -0,0 +1,64 @@
1
+ import { useEffect, useState, useRef } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import "./link-preview.css";
4
+
5
+ interface LinkPreviewProps {
6
+ isVisible: boolean;
7
+ title: string;
8
+ summary?: string;
9
+ x: number;
10
+ y: number;
11
+ }
12
+
13
+ export function LinkPreview({
14
+ isVisible,
15
+ title,
16
+ summary,
17
+ x,
18
+ y,
19
+ }: LinkPreviewProps) {
20
+ const [mounted, setMounted] = useState(false);
21
+ const ref = useRef<HTMLDivElement>(null);
22
+ const [position, setPosition] = useState({ top: 0, left: 0 });
23
+
24
+ useEffect(() => {
25
+ setMounted(true);
26
+ }, []);
27
+
28
+ useEffect(() => {
29
+ if (isVisible && ref.current) {
30
+ const rect = ref.current.getBoundingClientRect();
31
+ const padding = 15;
32
+
33
+ let top = y + padding;
34
+ let left = x + padding;
35
+
36
+ // Keep within viewport
37
+ if (left + rect.width > window.innerWidth) {
38
+ left = x - rect.width - padding;
39
+ }
40
+ if (top + rect.height > window.innerHeight) {
41
+ top = y - rect.height - padding;
42
+ }
43
+
44
+ setPosition({ top, left });
45
+ }
46
+ }, [isVisible, x, y]);
47
+
48
+ if (!mounted) return null;
49
+
50
+ return createPortal(
51
+ <div
52
+ ref={ref}
53
+ className={`boltdocs-link-preview ${isVisible ? "is-visible" : ""}`}
54
+ style={{
55
+ top: position.top,
56
+ left: position.left,
57
+ }}
58
+ >
59
+ <span className="boltdocs-link-preview-title">{title}</span>
60
+ {summary && <p className="boltdocs-link-preview-summary">{summary}</p>}
61
+ </div>,
62
+ document.body,
63
+ );
64
+ }
@@ -0,0 +1,64 @@
1
+ .boltdocs-link-preview {
2
+ position: fixed;
3
+ z-index: 1000;
4
+ width: 320px;
5
+ padding: 1rem;
6
+ background-color: var(--ld-navbar-bg);
7
+ backdrop-filter: blur(var(--ld-navbar-blur));
8
+ -webkit-backdrop-filter: blur(var(--ld-navbar-blur));
9
+ border: 1px solid var(--ld-border-subtle);
10
+ border-radius: var(--ld-radius-md);
11
+ box-shadow:
12
+ 0 10px 25px -5px rgba(0, 0, 0, 0.1),
13
+ 0 8px 10px -6px rgba(0, 0, 0, 0.1);
14
+ pointer-events: none;
15
+ opacity: 0;
16
+ transform: translateY(10px) scale(0.95);
17
+ transition:
18
+ opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
19
+ transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
20
+ font-family: var(--ld-font-sans);
21
+ }
22
+
23
+ .boltdocs-link-preview.is-visible {
24
+ opacity: 1;
25
+ transform: translateY(0) scale(1);
26
+ }
27
+
28
+ .boltdocs-link-preview-title {
29
+ display: block;
30
+ font-weight: 600;
31
+ font-size: 0.95rem;
32
+ color: var(--ld-text-main);
33
+ margin-bottom: 0.5rem;
34
+ line-height: 1.4;
35
+ }
36
+
37
+ .boltdocs-link-preview-summary {
38
+ display: block;
39
+ font-size: 0.85rem;
40
+ color: var(--ld-text-muted);
41
+ line-height: 1.5;
42
+ display: -webkit-box;
43
+ -webkit-line-clamp: 4;
44
+ line-clamp: 4;
45
+ -webkit-box-orient: vertical;
46
+ overflow: hidden;
47
+ }
48
+
49
+ /* Dark mode adjustments */
50
+ [data-theme="dark"] .boltdocs-link-preview {
51
+ background-color: var(--ld-navbar-bg);
52
+ border-color: var(--ld-border-subtle);
53
+ box-shadow:
54
+ 0 20px 25px -5px rgba(0, 0, 0, 0.3),
55
+ 0 10px 10px -5px rgba(0, 0, 0, 0.2);
56
+ }
57
+
58
+ [data-theme="dark"] .boltdocs-link-preview-title {
59
+ color: #f8fafc;
60
+ }
61
+
62
+ [data-theme="dark"] .boltdocs-link-preview-summary {
63
+ color: #94a3b8;
64
+ }
@@ -23,6 +23,8 @@ export interface ComponentRoute {
23
23
  groupPosition?: number;
24
24
  /** Extracted markdown headings for search indexing */
25
25
  headings?: { level: number; text: string; id: string }[];
26
+ /** The page summary or description */
27
+ description?: string;
26
28
  /** The locale this route belongs to, if i18n is configured */
27
29
  locale?: string;
28
30
  /** The version this route belongs to, if versioning is configured */
@@ -53,6 +53,8 @@ export interface BoltdocsThemeConfig {
53
53
  githubRepo?: string;
54
54
  /** Whether to show the 'Powered by LiteDocs' badge in the sidebar (default: true) */
55
55
  poweredBy?: boolean;
56
+ /** Whether to show a preview tooltip on internal links hover (default: true) */
57
+ linkPreview?: boolean;
56
58
  /** Granular layout customization props */
57
59
  layoutProps?: {
58
60
  navbar?: any;
@@ -122,9 +122,22 @@ export function parseDocFile(
122
122
  }
123
123
 
124
124
  const sanitizedTitle = data.title ? escapeHtml(data.title) : inferredTitle;
125
- const sanitizedDescription = data.description
125
+ let sanitizedDescription = data.description
126
126
  ? escapeHtml(data.description)
127
127
  : "";
128
+
129
+ // If no description is provided, extract a summary from the content
130
+ if (!sanitizedDescription && content) {
131
+ const summary = content
132
+ .replace(/^#+.*$/gm, "") // Remove headers
133
+ .replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1") // Simplify links
134
+ .replace(/[_*`]/g, "") // Remove formatting
135
+ .replace(/\n+/g, " ") // Normalize whitespace
136
+ .trim()
137
+ .slice(0, 160);
138
+ sanitizedDescription = escapeHtml(summary);
139
+ }
140
+
128
141
  const sanitizedBadge = data.badge ? escapeHtml(data.badge) : undefined;
129
142
 
130
143
  return {
@@ -1,7 +0,0 @@
1
- import {
2
- CodeBlock
3
- } from "./chunk-KS5B3O6W.mjs";
4
- import "./chunk-FMTOYQLO.mjs";
5
- export {
6
- CodeBlock
7
- };
@@ -1,43 +0,0 @@
1
- import {
2
- copyToClipboard
3
- } from "./chunk-FMTOYQLO.mjs";
4
-
5
- // src/client/theme/components/CodeBlock/CodeBlock.tsx
6
- import React, { useState, useRef, useCallback } from "react";
7
- import { Copy, Check } from "lucide-react";
8
- import { jsx, jsxs } from "react/jsx-runtime";
9
- function CodeBlock({ children, ...props }) {
10
- const [copied, setCopied] = useState(false);
11
- const preRef = useRef(null);
12
- let language = "";
13
- if (React.isValidElement(children)) {
14
- const childProps = children.props;
15
- language = childProps?.["data-language"] || "";
16
- if (!language && childProps?.className) {
17
- const match = childProps.className.match(/language-(\w+)/);
18
- if (match) language = match[1];
19
- }
20
- }
21
- const handleCopy = useCallback(async () => {
22
- const code = preRef.current?.textContent || "";
23
- copyToClipboard(code);
24
- setCopied(true);
25
- setTimeout(() => setCopied(false), 2e3);
26
- }, []);
27
- return /* @__PURE__ */ jsxs("div", { className: "code-block-wrapper", children: [
28
- /* @__PURE__ */ jsx(
29
- "button",
30
- {
31
- className: `code-block-copy ${copied ? "copied" : ""}`,
32
- onClick: handleCopy,
33
- "aria-label": "Copy code",
34
- children: copied ? /* @__PURE__ */ jsx(Check, { size: 16 }) : /* @__PURE__ */ jsx(Copy, { size: 16 })
35
- }
36
- ),
37
- /* @__PURE__ */ jsx("pre", { ref: preRef, ...props, children })
38
- ] });
39
- }
40
-
41
- export {
42
- CodeBlock
43
- };
@@ -1,16 +0,0 @@
1
- import type { SVGProps } from "react";
2
-
3
- const Yarn = (props: SVGProps<SVGSVGElement>) => (
4
- <svg {...props} viewBox="0 0 256 256">
5
- <path
6
- fill="#2C8EBB"
7
- d="M128 0C57.307 0 0 57.307 0 128s57.307 128 128 128 128-57.307 128-128S198.693 0 128 0zm0 234.667C69.195 234.667 21.333 186.805 21.333 128S69.195 21.333 128 21.333 234.667 69.195 234.667 128 186.805 234.667 128 234.667z"
8
- />
9
- <path
10
- fill="#2C8EBB"
11
- d="M173.045 74.053c-4.632-4.632-12.144-4.632-16.776 0L128 102.323l-28.269-28.27c-4.632-4.632-12.144-4.632-16.776 0-4.632 4.632-4.632 12.144 0 16.776L111.224 119.1l-28.269 28.269c-4.632 4.632-4.632 12.144 0 16.776 2.316 2.316 5.352 3.474 8.388 3.474s6.072-1.158 8.388-3.474L128 135.877l28.269 28.268c2.316 2.316 5.352 3.474 8.388 3.474s6.072-1.158 8.388-3.474c4.632-4.632 4.632-12.144 0-16.776L144.776 119.1l28.269-28.271 c4.632-4.632 4.632-12.144 0-16.776z"
12
- />
13
- </svg>
14
- );
15
-
16
- export { Yarn };