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.
- package/dist/{SearchDialog-FBNGKRPK.mjs → SearchDialog-5ISK64QY.mjs} +1 -1
- package/dist/{SearchDialog-O3V36MXA.css → SearchDialog-CEVPEMT3.css} +54 -5
- package/dist/{chunk-D7YBQG6H.mjs → chunk-FMQ4HRKZ.mjs} +311 -133
- package/dist/client/index.css +54 -5
- package/dist/client/index.d.mts +3 -3
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.js +624 -475
- package/dist/client/index.mjs +2 -4
- package/dist/client/ssr.css +54 -5
- package/dist/client/ssr.d.mts +1 -1
- package/dist/client/ssr.d.ts +1 -1
- package/dist/client/ssr.js +544 -395
- package/dist/client/ssr.mjs +1 -1
- package/dist/{config-BD5ZHz15.d.mts → config-DkZg5aCf.d.mts} +2 -0
- package/dist/{config-BD5ZHz15.d.ts → config-DkZg5aCf.d.ts} +2 -0
- package/dist/node/index.d.mts +2 -2
- package/dist/node/index.d.ts +2 -2
- package/dist/node/index.js +5 -1
- package/dist/node/index.mjs +5 -1
- package/dist/{types-CvrzTbEX.d.mts → types-DGIo1VKD.d.mts} +2 -0
- package/dist/{types-CvrzTbEX.d.ts → types-DGIo1VKD.d.ts} +2 -0
- package/package.json +1 -1
- package/src/client/app/index.tsx +2 -12
- package/src/client/app/preload.tsx +3 -1
- package/src/client/theme/components/CodeBlock/CodeBlock.tsx +0 -11
- package/src/client/theme/styles/markdown.css +1 -5
- package/src/client/theme/ui/Link/Link.tsx +156 -18
- package/src/client/theme/ui/Link/LinkPreview.tsx +64 -0
- package/src/client/theme/ui/Link/link-preview.css +64 -0
- package/src/client/types.ts +2 -0
- package/src/node/config.ts +2 -0
- package/src/node/routes/parser.ts +14 -1
- package/dist/CodeBlock-QYIKJMEB.mjs +0 -7
- package/dist/chunk-KS5B3O6W.mjs +0 -43
package/dist/client/ssr.mjs
CHANGED
|
@@ -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;
|
package/dist/node/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
|
-
import { B as BoltdocsConfig } from '../config-
|
|
3
|
-
export { a as BoltdocsThemeConfig } from '../config-
|
|
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.
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
|
-
import { B as BoltdocsConfig } from '../config-
|
|
3
|
-
export { a as BoltdocsThemeConfig } from '../config-
|
|
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.
|
package/dist/node/index.js
CHANGED
|
@@ -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
|
-
|
|
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: {
|
package/dist/node/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
package/src/client/app/index.tsx
CHANGED
|
@@ -27,11 +27,7 @@ export function useConfig() {
|
|
|
27
27
|
return useContext(ConfigContext);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
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:
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
+
}
|
package/src/client/types.ts
CHANGED
|
@@ -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 */
|
package/src/node/config.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|