boltdocs 1.4.1 → 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 (34) 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/{chunk-D7YBQG6H.mjs → chunk-FMQ4HRKZ.mjs} +311 -133
  4. package/dist/client/index.css +54 -5
  5. package/dist/client/index.d.mts +3 -3
  6. package/dist/client/index.d.ts +3 -3
  7. package/dist/client/index.js +624 -475
  8. package/dist/client/index.mjs +2 -4
  9. package/dist/client/ssr.css +54 -5
  10. package/dist/client/ssr.d.mts +1 -1
  11. package/dist/client/ssr.d.ts +1 -1
  12. package/dist/client/ssr.js +544 -395
  13. package/dist/client/ssr.mjs +1 -1
  14. package/dist/{config-BD5ZHz15.d.mts → config-DkZg5aCf.d.mts} +2 -0
  15. package/dist/{config-BD5ZHz15.d.ts → config-DkZg5aCf.d.ts} +2 -0
  16. package/dist/node/index.d.mts +2 -2
  17. package/dist/node/index.d.ts +2 -2
  18. package/dist/node/index.js +5 -1
  19. package/dist/node/index.mjs +5 -1
  20. package/dist/{types-CvrzTbEX.d.mts → types-DGIo1VKD.d.mts} +2 -0
  21. package/dist/{types-CvrzTbEX.d.ts → types-DGIo1VKD.d.ts} +2 -0
  22. package/package.json +1 -1
  23. package/src/client/app/index.tsx +2 -12
  24. package/src/client/app/preload.tsx +3 -1
  25. package/src/client/theme/components/CodeBlock/CodeBlock.tsx +0 -11
  26. package/src/client/theme/styles/markdown.css +1 -5
  27. package/src/client/theme/ui/Link/Link.tsx +156 -18
  28. package/src/client/theme/ui/Link/LinkPreview.tsx +64 -0
  29. package/src/client/theme/ui/Link/link-preview.css +64 -0
  30. package/src/client/types.ts +2 -0
  31. package/src/node/config.ts +2 -0
  32. package/src/node/routes/parser.ts +14 -1
  33. package/dist/CodeBlock-QYIKJMEB.mjs +0 -7
  34. 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.
@@ -514,7 +514,11 @@ function parseDocFile(file, docsDir, basePath, config) {
514
514
  headings.push({ level, text: escapeHtml(text), id });
515
515
  }
516
516
  const sanitizedTitle = data.title ? escapeHtml(data.title) : inferredTitle;
517
- 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
+ }
518
522
  const sanitizedBadge = data.badge ? escapeHtml(data.badge) : void 0;
519
523
  return {
520
524
  route: {
@@ -96,7 +96,11 @@ function parseDocFile(file, docsDir, basePath, config) {
96
96
  headings.push({ level, text: escapeHtml(text), id });
97
97
  }
98
98
  const sanitizedTitle = data.title ? escapeHtml(data.title) : inferredTitle;
99
- 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
+ }
100
104
  const sanitizedBadge = data.badge ? escapeHtml(data.badge) : void 0;
101
105
  return {
102
106
  route: {
@@ -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.1",
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
  );
@@ -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
- };