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
@@ -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;
@@ -139,11 +141,15 @@ export const CONFIG_FILES = [
139
141
  * Loads user's configuration file (e.g., `boltdocs.config.js` or `boltdocs.config.ts`) if it exists,
140
142
  * merges it with the default configuration, and returns the final `BoltdocsConfig`.
141
143
  *
142
- * @param docsDir - The fallback/default documentation directory
143
- * @returns A promise resolving to the final merged configuration object
144
+ * @param docsDir - The directory containing the documentation files
145
+ * @param root - The project root directory (defaults to process.cwd())
146
+ * @returns The merged configuration object
144
147
  */
145
- export async function resolveConfig(docsDir: string): Promise<BoltdocsConfig> {
146
- const projectRoot = process.cwd();
148
+ export async function resolveConfig(
149
+ docsDir: string,
150
+ root: string = process.cwd(),
151
+ ): Promise<BoltdocsConfig> {
152
+ const projectRoot = root;
147
153
 
148
154
  const defaults: BoltdocsConfig = {
149
155
  docsDir: path.resolve(docsDir),
@@ -162,8 +168,11 @@ export async function resolveConfig(docsDir: string): Promise<BoltdocsConfig> {
162
168
  const configPath = path.resolve(projectRoot, filename);
163
169
  if (fs.existsSync(configPath)) {
164
170
  try {
165
- // Add a timestamp query parameter to bust the ESM cache
166
- const fileUrl = pathToFileURL(configPath).href + "?t=" + Date.now();
171
+ // Add a timestamp query parameter to bust the ESM cache in dev
172
+ const isTest =
173
+ process.env.NODE_ENV === "test" || (global as any).__vitest_worker__;
174
+ const fileUrl =
175
+ pathToFileURL(configPath).href + (isTest ? "" : "?t=" + Date.now());
167
176
  const mod = await import(fileUrl);
168
177
  const userConfig = mod.default || mod;
169
178
 
package/src/node/mdx.ts CHANGED
@@ -24,15 +24,19 @@ let mdxCacheLoaded = false;
24
24
  * Also wraps the plugin with a persistent cache to avoid re-compiling unchanged MDX files.
25
25
  *
26
26
  * @param config - The Boltdocs configuration containing custom plugins
27
+ * @param compiler - The MDX compiler plugin (for testing)
27
28
  * @returns A Vite plugin configured for MDX parsing with caching
28
29
  */
29
- export function boltdocsMdxPlugin(config?: BoltdocsConfig): Plugin {
30
+ export function boltdocsMdxPlugin(
31
+ config?: BoltdocsConfig,
32
+ compiler = mdxPlugin,
33
+ ): Plugin {
30
34
  const extraRemarkPlugins =
31
35
  config?.plugins?.flatMap((p) => p.remarkPlugins || []) || [];
32
36
  const extraRehypePlugins =
33
37
  config?.plugins?.flatMap((p) => p.rehypePlugins || []) || [];
34
38
 
35
- const baseMdxPlugin = mdxPlugin({
39
+ const baseMdxPlugin = compiler({
36
40
  remarkPlugins: [remarkGfm, remarkFrontmatter, ...extraRemarkPlugins],
37
41
  rehypePlugins: [
38
42
  rehypeSlug,
@@ -49,6 +53,11 @@ export function boltdocsMdxPlugin(config?: BoltdocsConfig): Plugin {
49
53
  providerImportSource: "@mdx-js/react",
50
54
  }) as Plugin;
51
55
 
56
+ // @ts-ignore
57
+ if (baseMdxPlugin.isMock) {
58
+ console.log("MDX PLUGIN IS MOCKED");
59
+ }
60
+
52
61
  return {
53
62
  ...baseMdxPlugin,
54
63
  name: "vite-plugin-boltdocs-mdx",
@@ -90,8 +99,6 @@ export function boltdocsMdxPlugin(config?: BoltdocsConfig): Plugin {
90
99
 
91
100
  if (result && typeof result === "object" && result.code) {
92
101
  mdxCache.set(cacheKey, result.code);
93
- } else if (typeof result === "string") {
94
- mdxCache.set(cacheKey, result);
95
102
  }
96
103
 
97
104
  return result;
@@ -70,7 +70,16 @@ export function parseDocFile(
70
70
  }
71
71
 
72
72
  const cleanRelativePath = parts.join("/");
73
- const cleanRoutePath = fileToRoutePath(cleanRelativePath || "index.md");
73
+
74
+ let cleanRoutePath: string;
75
+ if (data.permalink) {
76
+ // If a permalink is specified, ensure it starts with a slash
77
+ cleanRoutePath = data.permalink.startsWith("/")
78
+ ? data.permalink
79
+ : `/${data.permalink}`;
80
+ } else {
81
+ cleanRoutePath = fileToRoutePath(cleanRelativePath || "index.md");
82
+ }
74
83
 
75
84
  let finalPath = basePath;
76
85
  if (version) {
@@ -113,9 +122,22 @@ export function parseDocFile(
113
122
  }
114
123
 
115
124
  const sanitizedTitle = data.title ? escapeHtml(data.title) : inferredTitle;
116
- const sanitizedDescription = data.description
125
+ let sanitizedDescription = data.description
117
126
  ? escapeHtml(data.description)
118
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
+
119
141
  const sanitizedBadge = data.badge ? escapeHtml(data.badge) : undefined;
120
142
 
121
143
  return {
@@ -49,15 +49,6 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
49
49
  }
50
50
  const template = fs.readFileSync(templatePath, "utf-8");
51
51
 
52
- // Load user's homePage if configured
53
- let homePageComp;
54
- if ((config as any)?._homePagePath) {
55
- try {
56
- // Simplistic: if there's a custom home page compiled, we'd need it available to SSR.
57
- // In a full framework this is complex, but for Boltdocs we assume it's bundled if needed.
58
- } catch (e) {}
59
- }
60
-
61
52
  // Generate an HTML file for each route concurrently
62
53
  await Promise.all(
63
54
  routes.map(async (route) => {
@@ -74,7 +65,7 @@ export async function generateStaticPages(options: SSGOptions): Promise<void> {
74
65
  routes: routes,
75
66
  config: config || {},
76
67
  modules: fakeModules,
77
- homePage: homePageComp,
68
+ homePage: undefined, // No custom home page for now
78
69
  });
79
70
 
80
71
  const html = replaceMetaTags(template, {
package/src/node/utils.ts CHANGED
@@ -113,7 +113,10 @@ export function fileToRoutePath(relativePath: string): string {
113
113
  // Strip number prefixes from every segment
114
114
  let cleanedPath = relativePath.split("/").map(stripNumberPrefix).join("/");
115
115
 
116
- let routePath = cleanedPath.replace(/\.mdx?$/, "");
116
+ // Remove trailing slash if present
117
+ let routePath = cleanedPath.replace(/\/$/, "");
118
+
119
+ routePath = routePath.replace(/\.mdx?$/, "");
117
120
 
118
121
  // Handle index files → directory root
119
122
  if (routePath === "index" || routePath.endsWith("/index")) {
@@ -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
- };