jamdesk 1.1.6 → 1.1.8
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/README.md +46 -1
- package/dist/__tests__/unit/docs-json-writer.test.js +59 -1
- package/dist/__tests__/unit/docs-json-writer.test.js.map +1 -1
- package/dist/__tests__/unit/spellcheck-fix.test.d.ts +2 -0
- package/dist/__tests__/unit/spellcheck-fix.test.d.ts.map +1 -0
- package/dist/__tests__/unit/spellcheck-fix.test.js +82 -0
- package/dist/__tests__/unit/spellcheck-fix.test.js.map +1 -0
- package/dist/__tests__/unit/spellcheck-utils.test.d.ts +2 -0
- package/dist/__tests__/unit/spellcheck-utils.test.d.ts.map +1 -0
- package/dist/__tests__/unit/spellcheck-utils.test.js +234 -0
- package/dist/__tests__/unit/spellcheck-utils.test.js.map +1 -0
- package/dist/__tests__/unit/tech-words.test.d.ts +2 -0
- package/dist/__tests__/unit/tech-words.test.d.ts.map +1 -0
- package/dist/__tests__/unit/tech-words.test.js +31 -0
- package/dist/__tests__/unit/tech-words.test.js.map +1 -0
- package/dist/commands/spellcheck.d.ts +13 -0
- package/dist/commands/spellcheck.d.ts.map +1 -0
- package/dist/commands/spellcheck.js +144 -0
- package/dist/commands/spellcheck.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/deps.js +2 -2
- package/dist/lib/docs-json-writer.d.ts +6 -0
- package/dist/lib/docs-json-writer.d.ts.map +1 -1
- package/dist/lib/docs-json-writer.js +29 -0
- package/dist/lib/docs-json-writer.js.map +1 -1
- package/dist/lib/openapi/types.d.ts +11 -6
- package/dist/lib/openapi/types.d.ts.map +1 -1
- package/dist/lib/path-security.d.ts +3 -0
- package/dist/lib/path-security.d.ts.map +1 -1
- package/dist/lib/path-security.js +14 -1
- package/dist/lib/path-security.js.map +1 -1
- package/dist/lib/spellcheck-fix.d.ts +37 -0
- package/dist/lib/spellcheck-fix.d.ts.map +1 -0
- package/dist/lib/spellcheck-fix.js +292 -0
- package/dist/lib/spellcheck-fix.js.map +1 -0
- package/dist/lib/spellcheck-utils.d.ts +36 -0
- package/dist/lib/spellcheck-utils.d.ts.map +1 -0
- package/dist/lib/spellcheck-utils.js +138 -0
- package/dist/lib/spellcheck-utils.js.map +1 -0
- package/dist/lib/tech-words.d.ts +9 -0
- package/dist/lib/tech-words.d.ts.map +1 -0
- package/dist/lib/tech-words.js +118 -0
- package/dist/lib/tech-words.js.map +1 -0
- package/package.json +15 -10
- package/vendored/app/[[...slug]]/page.tsx +48 -13
- package/vendored/app/api/assets/[...path]/route.ts +2 -0
- package/vendored/components/layout/LayoutWrapper.tsx +3 -4
- package/vendored/components/mdx/ApiEndpoint.tsx +13 -2
- package/vendored/components/mdx/MDXComponents.tsx +16 -0
- package/vendored/components/mdx/OpenApiEndpoint.tsx +76 -36
- package/vendored/components/mdx/Tabs.tsx +1 -1
- package/vendored/components/mdx/Video.tsx +82 -0
- package/vendored/components/navigation/Header.tsx +3 -3
- package/vendored/components/navigation/Sidebar.tsx +3 -3
- package/vendored/components/ui/CodePanel.tsx +5 -5
- package/vendored/components/ui/CodePanelModal.tsx +3 -3
- package/vendored/components/ui/DevOnlyNotice.tsx +78 -0
- package/vendored/hooks/useChatPanel.tsx +21 -2
- package/vendored/hooks/useMediaQuery.ts +27 -0
- package/vendored/lib/build-endpoint-from-mdx.ts +66 -0
- package/vendored/lib/isr-build-executor.ts +1 -1
- package/vendored/lib/middleware-helpers.ts +6 -1
- package/vendored/lib/openapi/code-examples.ts +479 -99
- package/vendored/lib/openapi/index.ts +9 -1
- package/vendored/lib/openapi/types.ts +29 -5
- package/vendored/lib/preprocess-mdx.ts +103 -36
- package/vendored/lib/process-mdx-with-exports.ts +22 -14
- package/vendored/lib/remark-extract-param-fields.ts +134 -0
- package/vendored/lib/shiki-client.ts +12 -0
- package/vendored/lib/static-artifacts.ts +2 -0
- package/vendored/lib/static-file-route.ts +1 -1
- package/vendored/lib/url-safety.ts +122 -0
- package/vendored/next.config.js +7 -0
- package/vendored/schema/docs-schema.json +35 -4
- package/vendored/scripts/copy-files.cjs +60 -54
- package/vendored/scripts/validate-links.cjs +1 -1
- package/vendored/shared/path-security.ts +17 -1
package/package.json
CHANGED
|
@@ -1,36 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jamdesk",
|
|
7
7
|
"documentation",
|
|
8
8
|
"documentation-generator",
|
|
9
9
|
"documentation-platform",
|
|
10
|
+
"documentation-tool",
|
|
10
11
|
"docs",
|
|
11
12
|
"docs-as-code",
|
|
13
|
+
"docs-generator",
|
|
12
14
|
"mdx",
|
|
13
15
|
"markdown",
|
|
14
16
|
"cli",
|
|
15
|
-
"dev-server",
|
|
16
|
-
"hot-reload",
|
|
17
|
-
"turbopack",
|
|
18
17
|
"developer-tools",
|
|
19
18
|
"api-documentation",
|
|
20
19
|
"openapi",
|
|
21
20
|
"api-reference",
|
|
22
|
-
"static-site",
|
|
21
|
+
"static-site-generator",
|
|
23
22
|
"nextjs",
|
|
23
|
+
"react",
|
|
24
|
+
"typescript",
|
|
24
25
|
"technical-writing",
|
|
25
26
|
"mintlify",
|
|
26
27
|
"mintlify-alternative",
|
|
28
|
+
"docusaurus-alternative",
|
|
29
|
+
"gitbook-alternative",
|
|
27
30
|
"documentation-hosting",
|
|
28
31
|
"documentation-site",
|
|
29
32
|
"developer-documentation",
|
|
30
33
|
"api-docs",
|
|
31
|
-
"react-components",
|
|
32
34
|
"docs-site-generator",
|
|
33
|
-
"ai-search"
|
|
35
|
+
"ai-search",
|
|
36
|
+
"llms-txt"
|
|
34
37
|
],
|
|
35
38
|
"homepage": "https://www.jamdesk.com",
|
|
36
39
|
"bugs": {
|
|
@@ -44,7 +47,7 @@
|
|
|
44
47
|
"license": "Apache-2.0",
|
|
45
48
|
"author": {
|
|
46
49
|
"name": "Jamdesk",
|
|
47
|
-
"email": "
|
|
50
|
+
"email": "support@jamdesk.com",
|
|
48
51
|
"url": "https://www.jamdesk.com"
|
|
49
52
|
},
|
|
50
53
|
"type": "module",
|
|
@@ -97,12 +100,14 @@
|
|
|
97
100
|
"ajv-formats": "^3.0.1",
|
|
98
101
|
"chalk": "^5.3.0",
|
|
99
102
|
"commander": "^14.0.3",
|
|
103
|
+
"dictionary-en": "^4.0.0",
|
|
100
104
|
"fastest-levenshtein": "^1.0.16",
|
|
101
105
|
"fs-extra": "^11.2.0",
|
|
102
106
|
"glob": "^13.0.6",
|
|
103
107
|
"gray-matter": "^4.0.3",
|
|
104
108
|
"ignore": "^7.0.5",
|
|
105
109
|
"json5": "^2.2.3",
|
|
110
|
+
"nspell": "^2.1.5",
|
|
106
111
|
"open": "^11.0.0",
|
|
107
112
|
"ora": "^9.3.0",
|
|
108
113
|
"tar": "^7.5.9"
|
|
@@ -111,8 +116,8 @@
|
|
|
111
116
|
"@mdx-js/mdx": "^3.1.1",
|
|
112
117
|
"@types/fs-extra": "^11.0.0",
|
|
113
118
|
"@types/node": "^25.5.0",
|
|
114
|
-
"typescript": "^
|
|
115
|
-
"vitest": "^4.1.
|
|
119
|
+
"typescript": "^6.0.2",
|
|
120
|
+
"vitest": "^4.1.2"
|
|
116
121
|
},
|
|
117
122
|
"engines": {
|
|
118
123
|
"node": ">=20.0.0"
|
|
@@ -42,6 +42,7 @@ import { getLatexRemarkPlugins, getLatexRehypePlugins } from '@/lib/latex-config
|
|
|
42
42
|
import { getTypographyRemarkPlugins } from '@/lib/typography-config';
|
|
43
43
|
import { recmaCompoundComponents } from '@/lib/recma-compound-components';
|
|
44
44
|
import { extractInlineComponents } from '@/lib/process-mdx-with-exports';
|
|
45
|
+
import { buildEndpointFromMdx } from '@/lib/build-endpoint-from-mdx';
|
|
45
46
|
import { mdxSecurityOptions } from '@/lib/mdx-security-options';
|
|
46
47
|
import fs from 'fs';
|
|
47
48
|
import path from 'path';
|
|
@@ -67,7 +68,7 @@ import {
|
|
|
67
68
|
generateCodeExamples,
|
|
68
69
|
formatOpenApiWarning,
|
|
69
70
|
type OpenApiEndpointData,
|
|
70
|
-
type
|
|
71
|
+
type CodeExample,
|
|
71
72
|
type AuthMethod,
|
|
72
73
|
} from '@/lib/openapi';
|
|
73
74
|
import { ApiEndpoint } from '@/components/mdx/ApiEndpoint';
|
|
@@ -134,6 +135,7 @@ interface FrontmatterData {
|
|
|
134
135
|
description?: string;
|
|
135
136
|
api?: string;
|
|
136
137
|
openapi?: string;
|
|
138
|
+
playground?: string;
|
|
137
139
|
mode?: string;
|
|
138
140
|
hideFooter?: boolean;
|
|
139
141
|
rss?: boolean;
|
|
@@ -505,7 +507,7 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
505
507
|
|
|
506
508
|
// Extract and compile inline component exports from MDX
|
|
507
509
|
// Only pass MDXComponents (server-compatible) to inline extraction
|
|
508
|
-
const { inlineComponents } = await extractInlineComponents(content, MDXComponents);
|
|
510
|
+
const { inlineComponents, paramFields } = await extractInlineComponents(content, MDXComponents);
|
|
509
511
|
|
|
510
512
|
// Check for component name collisions and warn
|
|
511
513
|
const overriddenComponents = Object.keys(inlineComponents).filter(
|
|
@@ -560,7 +562,7 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
560
562
|
|
|
561
563
|
// Parse OpenAPI endpoint data if openapi frontmatter is present
|
|
562
564
|
let openApiEndpointData: OpenApiEndpointData | null = null;
|
|
563
|
-
let openApiCodeExamples:
|
|
565
|
+
let openApiCodeExamples: CodeExample[] | null = null;
|
|
564
566
|
|
|
565
567
|
// OpenAPI spec parsing - supports both static and ISR modes
|
|
566
568
|
if (data.openapi && typeof data.openapi === 'string') {
|
|
@@ -591,7 +593,8 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
591
593
|
|
|
592
594
|
// Generate code examples
|
|
593
595
|
const authMethod = config.api?.mdx?.auth?.method as AuthMethod | undefined;
|
|
594
|
-
|
|
596
|
+
const languages = config.api?.examples?.languages;
|
|
597
|
+
openApiCodeExamples = generateCodeExamples(openApiEndpointData, { authMethod, languages });
|
|
595
598
|
} catch (err) {
|
|
596
599
|
// Log formatted warning to console (appears in CLI and build logs)
|
|
597
600
|
// Check if it's an OpenAPI validation error with our format
|
|
@@ -607,16 +610,12 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
607
610
|
// Parse MDX api field for pages with api: frontmatter (but not openapi:)
|
|
608
611
|
let mdxApiMethod: HttpMethod | null = null;
|
|
609
612
|
let mdxApiPath: string | null = null;
|
|
610
|
-
let mdxApiBaseUrl: string | undefined;
|
|
611
613
|
|
|
612
614
|
if (data.api && typeof data.api === 'string' && !data.openapi) {
|
|
613
615
|
const parsed = parseMdxApiField(data.api);
|
|
614
616
|
if (parsed) {
|
|
615
617
|
mdxApiMethod = parsed.method;
|
|
616
618
|
mdxApiPath = parsed.path;
|
|
617
|
-
// Get server URL from config, with fallback
|
|
618
|
-
const serverConfig = config.api?.mdx?.server;
|
|
619
|
-
mdxApiBaseUrl = Array.isArray(serverConfig) ? serverConfig[0] : serverConfig;
|
|
620
619
|
}
|
|
621
620
|
}
|
|
622
621
|
|
|
@@ -644,6 +643,23 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
644
643
|
// Prose class for MDX content styling (defined in base.css)
|
|
645
644
|
const proseClasses = 'prose max-w-none';
|
|
646
645
|
|
|
646
|
+
// Playground configuration — covers both openapi: and api: pages
|
|
647
|
+
const hasApiEndpoint = openApiEndpointData || (mdxApiMethod && mdxApiPath);
|
|
648
|
+
const playgroundDisplay = hasApiEndpoint
|
|
649
|
+
? ((data.playground as 'interactive' | 'simple' | 'none' | undefined)
|
|
650
|
+
|| config.api?.playground?.display || 'none') as 'interactive' | 'simple' | 'none'
|
|
651
|
+
: 'none';
|
|
652
|
+
const mdxServerConfig = config.api?.mdx?.server;
|
|
653
|
+
const fallbackServerUrl = Array.isArray(mdxServerConfig) ? mdxServerConfig[0] : mdxServerConfig;
|
|
654
|
+
const proxyEnabled = config.api?.playground?.proxy
|
|
655
|
+
?? (playgroundDisplay === 'interactive');
|
|
656
|
+
|
|
657
|
+
// Build endpoint data for api: pages (for playground)
|
|
658
|
+
let mdxEndpointData: OpenApiEndpointData | undefined;
|
|
659
|
+
if (!openApiEndpointData && mdxApiMethod && mdxApiPath && playgroundDisplay !== 'none') {
|
|
660
|
+
mdxEndpointData = buildEndpointFromMdx(mdxApiMethod, mdxApiPath, paramFields, fallbackServerUrl);
|
|
661
|
+
}
|
|
662
|
+
|
|
647
663
|
// For API pages, wrap the entire content area with ApiPageWrapper
|
|
648
664
|
// so code panels can be positioned as siblings at the page level
|
|
649
665
|
if (isApiPage) {
|
|
@@ -676,11 +692,24 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
676
692
|
<div className={proseClasses}>
|
|
677
693
|
{/* MDX API endpoint badge (for pages with api: frontmatter) */}
|
|
678
694
|
{mdxApiMethod && mdxApiPath && (
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
695
|
+
mdxEndpointData && playgroundDisplay !== 'none' ? (
|
|
696
|
+
<OpenApiEndpoint
|
|
697
|
+
endpoint={mdxEndpointData}
|
|
698
|
+
playgroundOnly
|
|
699
|
+
playgroundDisplay={playgroundDisplay}
|
|
700
|
+
authMethod={config.api?.mdx?.auth?.method as AuthMethod | undefined}
|
|
701
|
+
authHeaderName={config.api?.mdx?.auth?.name}
|
|
702
|
+
serverUrl={fallbackServerUrl}
|
|
703
|
+
proxyEnabled={proxyEnabled}
|
|
704
|
+
languages={config.api?.examples?.languages}
|
|
705
|
+
/>
|
|
706
|
+
) : (
|
|
707
|
+
<ApiEndpoint
|
|
708
|
+
method={mdxApiMethod}
|
|
709
|
+
path={mdxApiPath}
|
|
710
|
+
baseUrl={fallbackServerUrl}
|
|
711
|
+
/>
|
|
712
|
+
)
|
|
684
713
|
)}
|
|
685
714
|
|
|
686
715
|
{/* OpenAPI endpoint documentation (auto-generated from spec) */}
|
|
@@ -688,6 +717,12 @@ export default async function DocPage({ params }: PageProps) {
|
|
|
688
717
|
<OpenApiEndpoint
|
|
689
718
|
endpoint={openApiEndpointData}
|
|
690
719
|
codeExamples={openApiCodeExamples || undefined}
|
|
720
|
+
playgroundDisplay={playgroundDisplay}
|
|
721
|
+
authMethod={config.api?.mdx?.auth?.method as AuthMethod | undefined}
|
|
722
|
+
authHeaderName={config.api?.mdx?.auth?.name}
|
|
723
|
+
serverUrl={fallbackServerUrl}
|
|
724
|
+
proxyEnabled={proxyEnabled}
|
|
725
|
+
languages={config.api?.examples?.languages}
|
|
691
726
|
/>
|
|
692
727
|
)}
|
|
693
728
|
|
|
@@ -18,6 +18,8 @@ const CACHE_DURATIONS: Record<string, number> = {
|
|
|
18
18
|
'image/svg+xml': 86400, // 1 day (may change)
|
|
19
19
|
'image/x-icon': 31536000,
|
|
20
20
|
'application/pdf': 86400,
|
|
21
|
+
'video/mp4': 31536000, // 1 year (busted by ?v= query param on new builds)
|
|
22
|
+
'video/webm': 31536000,
|
|
21
23
|
default: 3600, // 1 hour
|
|
22
24
|
};
|
|
23
25
|
|
|
@@ -37,10 +37,9 @@ function useMediaQuery(query: string): boolean {
|
|
|
37
37
|
|
|
38
38
|
export function LayoutWrapper({ config, children }: LayoutWrapperProps) {
|
|
39
39
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
40
|
-
// Chat is
|
|
41
|
-
//
|
|
42
|
-
const chatEnabled =
|
|
43
|
-
&& (config.chat?.enabled !== false);
|
|
40
|
+
// Chat button is shown in both dev and production so the layout matches.
|
|
41
|
+
// In dev mode, clicking it shows a "production only" notice instead of opening the panel.
|
|
42
|
+
const chatEnabled = config.chat?.enabled !== false;
|
|
44
43
|
|
|
45
44
|
// xl breakpoint (1280px) — chat renders inline on desktop, overlay on mobile
|
|
46
45
|
const isDesktop = useMediaQuery('(min-width: 1280px)');
|
|
@@ -6,6 +6,7 @@ interface ApiEndpointProps {
|
|
|
6
6
|
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'TRACE';
|
|
7
7
|
path: string;
|
|
8
8
|
baseUrl?: string;
|
|
9
|
+
onTryIt?: () => void;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
const methodColors: Record<string, { bg: string; text: string; border: string }> = {
|
|
@@ -51,7 +52,7 @@ const methodColors: Record<string, { bg: string; text: string; border: string }>
|
|
|
51
52
|
},
|
|
52
53
|
};
|
|
53
54
|
|
|
54
|
-
export function ApiEndpoint({ method, path, baseUrl = 'https://api.jamdesk.com/api' }: ApiEndpointProps) {
|
|
55
|
+
export function ApiEndpoint({ method, path, baseUrl = 'https://api.jamdesk.com/api', onTryIt }: ApiEndpointProps) {
|
|
55
56
|
const [copied, setCopied] = useState(false);
|
|
56
57
|
const colors = methodColors[method] || methodColors.GET;
|
|
57
58
|
const fullUrl = `${baseUrl}${path}`;
|
|
@@ -73,10 +74,20 @@ export function ApiEndpoint({ method, path, baseUrl = 'https://api.jamdesk.com/a
|
|
|
73
74
|
<span className="text-[var(--color-text-muted)]">{baseUrl}</span>
|
|
74
75
|
<span className="text-[var(--color-text-primary)]">{path}</span>
|
|
75
76
|
</span>
|
|
77
|
+
{/* Try it button */}
|
|
78
|
+
{onTryIt && (
|
|
79
|
+
<button
|
|
80
|
+
onClick={onTryIt}
|
|
81
|
+
className="px-2.5 py-1 text-xs font-medium rounded-md bg-gradient-to-r from-[var(--color-accent)] to-[var(--color-accent-hover,var(--color-accent))] text-white hover:opacity-90 transition-opacity cursor-pointer"
|
|
82
|
+
aria-label="Open API playground"
|
|
83
|
+
>
|
|
84
|
+
Try it
|
|
85
|
+
</button>
|
|
86
|
+
)}
|
|
76
87
|
{/* Copy button */}
|
|
77
88
|
<button
|
|
78
89
|
onClick={handleCopy}
|
|
79
|
-
className="p-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] rounded-md transition-colors"
|
|
90
|
+
className="p-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] rounded-md transition-colors cursor-pointer"
|
|
80
91
|
title="Copy endpoint URL"
|
|
81
92
|
aria-label="Copy endpoint URL"
|
|
82
93
|
>
|
|
@@ -30,6 +30,7 @@ import { Tree, TreeFolder, TreeFile } from './Tree';
|
|
|
30
30
|
import { Columns } from './Columns';
|
|
31
31
|
import { View, ViewProvider, ViewSelector, ViewWrapper } from './View';
|
|
32
32
|
import { YouTube } from './YouTube';
|
|
33
|
+
import { Video } from './Video';
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Extract language from a pre element for tab label
|
|
@@ -148,6 +149,14 @@ function HttpCodeBlock({ method, url }: { method: string; url: string }) {
|
|
|
148
149
|
);
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
/** Fallback for raw <img src="video.mp4"> in MDX (preprocessor handles markdown syntax) */
|
|
153
|
+
const VIDEO_EXTENSIONS_IMG = ['.mp4', '.webm'];
|
|
154
|
+
function isVideoUrl(src: string | undefined): boolean {
|
|
155
|
+
if (!src) return false;
|
|
156
|
+
const pathOnly = src.split('?')[0];
|
|
157
|
+
return VIDEO_EXTENSIONS_IMG.some(ext => pathOnly.toLowerCase().endsWith(ext));
|
|
158
|
+
}
|
|
159
|
+
|
|
151
160
|
export const MDXComponents = {
|
|
152
161
|
Card,
|
|
153
162
|
// Callout components
|
|
@@ -218,6 +227,8 @@ export const MDXComponents = {
|
|
|
218
227
|
ViewWrapper,
|
|
219
228
|
// Media embeds
|
|
220
229
|
YouTube,
|
|
230
|
+
// Video player for local video files
|
|
231
|
+
Video,
|
|
221
232
|
// Sized images from preprocess-mdx ( syntax).
|
|
222
233
|
// These are output as <SizedImage> JSX so they go through component mapping
|
|
223
234
|
// (raw <img> JSX in MDX bypasses the components provider).
|
|
@@ -253,6 +264,11 @@ export const MDXComponents = {
|
|
|
253
264
|
// Destructure and ignore any other props to prevent them from being passed to DOM
|
|
254
265
|
...rest
|
|
255
266
|
}: any) => {
|
|
267
|
+
// Check if this is a video file — render <Video> instead of <ZoomableImage>
|
|
268
|
+
if (isVideoUrl(src)) {
|
|
269
|
+
return <Video src={src} title={alt || undefined} />;
|
|
270
|
+
}
|
|
271
|
+
|
|
256
272
|
// Check for data-no-zoom attribute or noZoom prop (handle both camelCase and lowercase)
|
|
257
273
|
const disableZoom = dataNoZoom === 'true' || dataNoZoom === true ||
|
|
258
274
|
noZoom === true || noZoom === 'true' ||
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useDevOnlyNotice, DEV_FEATURE } from '../ui/DevOnlyNotice';
|
|
4
|
+
|
|
5
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
4
6
|
import { ApiEndpoint } from './ApiEndpoint';
|
|
5
7
|
import { ParamField } from './ParamField';
|
|
6
8
|
import { CodeGroup } from './CodeGroup';
|
|
@@ -14,9 +16,10 @@ import type {
|
|
|
14
16
|
OpenApiEndpointData,
|
|
15
17
|
ParsedParameter,
|
|
16
18
|
ParsedResponse,
|
|
17
|
-
|
|
19
|
+
CodeExample,
|
|
18
20
|
JsonSchema,
|
|
19
21
|
SecurityRequirement,
|
|
22
|
+
AuthMethod,
|
|
20
23
|
} from '@/lib/openapi/types';
|
|
21
24
|
|
|
22
25
|
// Preload Shiki highlighter on module load
|
|
@@ -28,9 +31,25 @@ interface OpenApiEndpointProps {
|
|
|
28
31
|
/** Endpoint data from OpenAPI spec */
|
|
29
32
|
endpoint: OpenApiEndpointData;
|
|
30
33
|
/** Generated code examples */
|
|
31
|
-
codeExamples?:
|
|
34
|
+
codeExamples?: CodeExample[];
|
|
32
35
|
/** Additional content to render after endpoint info */
|
|
33
36
|
children?: React.ReactNode;
|
|
37
|
+
/** Playground display mode */
|
|
38
|
+
playgroundDisplay?: 'interactive' | 'simple' | 'none';
|
|
39
|
+
/** Auth method for playground */
|
|
40
|
+
authMethod?: AuthMethod;
|
|
41
|
+
/** Custom auth header name */
|
|
42
|
+
authHeaderName?: string;
|
|
43
|
+
/** Fallback server URL when endpoint.servers is empty */
|
|
44
|
+
serverUrl?: string;
|
|
45
|
+
/** Whether CORS proxy is enabled */
|
|
46
|
+
proxyEnabled?: boolean;
|
|
47
|
+
/** Languages for code generation */
|
|
48
|
+
languages?: string[];
|
|
49
|
+
/** When true, render only the ApiEndpoint badge (with Try it) + PlaygroundModal.
|
|
50
|
+
* Skips all auto-generated param docs, response panels, and code examples.
|
|
51
|
+
* Used for api: pages where the MDX content provides the documentation. */
|
|
52
|
+
playgroundOnly?: boolean;
|
|
34
53
|
}
|
|
35
54
|
|
|
36
55
|
/**
|
|
@@ -834,35 +853,23 @@ function ResponseFieldItem({
|
|
|
834
853
|
/**
|
|
835
854
|
* Code examples section - renders in right panel using CodePanel
|
|
836
855
|
*/
|
|
837
|
-
function CodeExamplesSection({ examples }: { examples:
|
|
856
|
+
function CodeExamplesSection({ examples }: { examples: CodeExample[] }) {
|
|
838
857
|
const codeItems = useMemo(
|
|
839
|
-
() =>
|
|
840
|
-
|
|
841
|
-
{ code: examples.python, language: 'python' },
|
|
842
|
-
{ code: examples.javascript, language: 'javascript' },
|
|
843
|
-
],
|
|
844
|
-
[examples.curl, examples.python, examples.javascript]
|
|
858
|
+
() => examples.map(ex => ({ code: ex.code, language: ex.language })),
|
|
859
|
+
[examples]
|
|
845
860
|
);
|
|
846
861
|
|
|
847
862
|
const { results: highlightedResults, isLoading } = useShikiHighlightMultiple(codeItems);
|
|
848
863
|
|
|
849
|
-
//
|
|
864
|
+
// Shiki generates HTML from trusted code strings (not user input)
|
|
850
865
|
const tabs: CodePanelTab[] = useMemo(
|
|
851
|
-
() =>
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
content: <div dangerouslySetInnerHTML={{ __html: highlightedResults[1] || '' }} />,
|
|
859
|
-
},
|
|
860
|
-
{
|
|
861
|
-
label: 'JavaScript',
|
|
862
|
-
content: <div dangerouslySetInnerHTML={{ __html: highlightedResults[2] || '' }} />,
|
|
863
|
-
},
|
|
864
|
-
],
|
|
865
|
-
[highlightedResults]
|
|
866
|
+
() => examples.map((ex, i) => ({
|
|
867
|
+
label: ex.label,
|
|
868
|
+
content: highlightedResults[i]
|
|
869
|
+
? <div dangerouslySetInnerHTML={{ __html: highlightedResults[i] }} />
|
|
870
|
+
: null,
|
|
871
|
+
})),
|
|
872
|
+
[examples, highlightedResults]
|
|
866
873
|
);
|
|
867
874
|
|
|
868
875
|
// Don't render until highlighting is complete to prevent flash of unformatted content
|
|
@@ -880,13 +887,20 @@ function CodeExamplesSection({ examples }: { examples: CodeExamples }) {
|
|
|
880
887
|
export function OpenApiEndpoint({
|
|
881
888
|
endpoint,
|
|
882
889
|
codeExamples,
|
|
883
|
-
children
|
|
890
|
+
children,
|
|
891
|
+
playgroundDisplay = 'none',
|
|
892
|
+
authMethod,
|
|
893
|
+
authHeaderName,
|
|
894
|
+
serverUrl,
|
|
895
|
+
proxyEnabled,
|
|
896
|
+
languages,
|
|
897
|
+
playgroundOnly,
|
|
884
898
|
}: OpenApiEndpointProps) {
|
|
885
|
-
const {
|
|
886
|
-
method,
|
|
887
|
-
path,
|
|
888
|
-
summary,
|
|
889
|
-
description,
|
|
899
|
+
const {
|
|
900
|
+
method,
|
|
901
|
+
path,
|
|
902
|
+
summary,
|
|
903
|
+
description,
|
|
890
904
|
deprecated,
|
|
891
905
|
parameters,
|
|
892
906
|
requestBody,
|
|
@@ -895,15 +909,33 @@ export function OpenApiEndpoint({
|
|
|
895
909
|
externalDocs,
|
|
896
910
|
servers,
|
|
897
911
|
} = endpoint;
|
|
898
|
-
|
|
912
|
+
|
|
899
913
|
// Group parameters by location
|
|
900
914
|
const pathParams = parameters.filter(p => p.in === 'path');
|
|
901
915
|
const queryParams = parameters.filter(p => p.in === 'query');
|
|
902
916
|
const headerParams = parameters.filter(p => p.in === 'header');
|
|
903
917
|
const cookieParams = parameters.filter(p => p.in === 'cookie');
|
|
904
|
-
|
|
918
|
+
|
|
905
919
|
const baseUrl = servers[0]?.url;
|
|
906
|
-
|
|
920
|
+
|
|
921
|
+
// Playground state
|
|
922
|
+
const showPlayground = playgroundDisplay !== 'none';
|
|
923
|
+
const { showNotice: showDevNotice, notice: devNotice } = useDevOnlyNotice();
|
|
924
|
+
|
|
925
|
+
if (playgroundOnly) {
|
|
926
|
+
return (
|
|
927
|
+
<div className="openapi-endpoint">
|
|
928
|
+
<ApiEndpoint
|
|
929
|
+
method={method}
|
|
930
|
+
path={path}
|
|
931
|
+
baseUrl={baseUrl}
|
|
932
|
+
onTryIt={showPlayground ? () => { showDevNotice(DEV_FEATURE.API_PLAYGROUND); } : undefined}
|
|
933
|
+
/>
|
|
934
|
+
{devNotice}
|
|
935
|
+
</div>
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
907
939
|
return (
|
|
908
940
|
<div className="openapi-endpoint">
|
|
909
941
|
{/* Deprecation Warning */}
|
|
@@ -917,7 +949,12 @@ export function OpenApiEndpoint({
|
|
|
917
949
|
)}
|
|
918
950
|
|
|
919
951
|
{/* Endpoint Badge */}
|
|
920
|
-
<ApiEndpoint
|
|
952
|
+
<ApiEndpoint
|
|
953
|
+
method={method}
|
|
954
|
+
path={path}
|
|
955
|
+
baseUrl={baseUrl}
|
|
956
|
+
onTryIt={showPlayground ? () => { showDevNotice(DEV_FEATURE.API_PLAYGROUND); } : undefined}
|
|
957
|
+
/>
|
|
921
958
|
|
|
922
959
|
{/* External Docs */}
|
|
923
960
|
{externalDocs && (
|
|
@@ -961,9 +998,12 @@ export function OpenApiEndpoint({
|
|
|
961
998
|
|
|
962
999
|
{/* Responses - field documentation in main content */}
|
|
963
1000
|
<ResponseSection responses={responses} />
|
|
1001
|
+
{devNotice}
|
|
964
1002
|
|
|
965
1003
|
{/* Additional Content */}
|
|
966
1004
|
{children}
|
|
1005
|
+
|
|
1006
|
+
{/* Playground Modal (lazy-loaded) */}
|
|
967
1007
|
</div>
|
|
968
1008
|
);
|
|
969
1009
|
}
|
|
@@ -115,7 +115,7 @@ export const Tabs = memo(function Tabs({ children, borderBottom }: TabsProps) {
|
|
|
115
115
|
<button
|
|
116
116
|
key={index}
|
|
117
117
|
onClick={() => handleTabClick(index)}
|
|
118
|
-
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
|
|
118
|
+
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors whitespace-nowrap cursor-pointer ${
|
|
119
119
|
isActive
|
|
120
120
|
? 'border-[var(--color-accent)] text-[var(--color-accent-text)]'
|
|
121
121
|
: 'border-transparent text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] hover:border-[var(--color-border)]'
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
/** Shared with preprocess-mdx.ts — keep in sync or extract to shared module */
|
|
6
|
+
export const VIDEO_EXTENSIONS = ['.mp4', '.webm'];
|
|
7
|
+
|
|
8
|
+
interface VideoProps {
|
|
9
|
+
src: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
/** Default: true */
|
|
12
|
+
controls?: boolean;
|
|
13
|
+
/** Browsers block unmuted autoplay — pair with muted */
|
|
14
|
+
autoPlay?: boolean;
|
|
15
|
+
muted?: boolean;
|
|
16
|
+
loop?: boolean;
|
|
17
|
+
width?: string;
|
|
18
|
+
height?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* HTML5 video player for embedded video files.
|
|
23
|
+
*
|
|
24
|
+
* Markdown  is converted to <Video> JSX by the
|
|
25
|
+
* preprocessor (transformMarkdownVideos). The img handler in MDXComponents
|
|
26
|
+
* has a fallback isVideoUrl check for raw <img src="video.mp4"> in MDX.
|
|
27
|
+
*
|
|
28
|
+
* muted is set via useRef/useEffect — React doesn't reflect it as a DOM
|
|
29
|
+
* attribute (https://github.com/facebook/react/issues/6544).
|
|
30
|
+
*/
|
|
31
|
+
export function Video({
|
|
32
|
+
src,
|
|
33
|
+
title,
|
|
34
|
+
controls = true,
|
|
35
|
+
autoPlay,
|
|
36
|
+
muted,
|
|
37
|
+
loop,
|
|
38
|
+
width,
|
|
39
|
+
height,
|
|
40
|
+
}: VideoProps): React.ReactElement | null {
|
|
41
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (videoRef.current) {
|
|
45
|
+
if (muted) {
|
|
46
|
+
videoRef.current.setAttribute('muted', '');
|
|
47
|
+
} else {
|
|
48
|
+
videoRef.current.removeAttribute('muted');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, [muted]);
|
|
52
|
+
|
|
53
|
+
if (!src) {
|
|
54
|
+
console.warn('Video component requires a "src" prop');
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Append #t=0.1 to force browsers to decode and display a preview frame.
|
|
59
|
+
// Without it, Chrome/Safari often show a black rectangle with preload="metadata".
|
|
60
|
+
const srcWithPreview = src.includes('#') ? src : `${src}#t=0.1`;
|
|
61
|
+
|
|
62
|
+
// No <div> wrapper — markdown syntax can place this inside a <p>, and
|
|
63
|
+
// <p><div> is invalid HTML (causes hydration errors).
|
|
64
|
+
return (
|
|
65
|
+
<video
|
|
66
|
+
ref={videoRef}
|
|
67
|
+
data-testid="video-player"
|
|
68
|
+
src={srcWithPreview}
|
|
69
|
+
controls={controls}
|
|
70
|
+
autoPlay={autoPlay}
|
|
71
|
+
loop={loop}
|
|
72
|
+
width={width}
|
|
73
|
+
height={height}
|
|
74
|
+
preload="metadata"
|
|
75
|
+
playsInline
|
|
76
|
+
aria-label={title}
|
|
77
|
+
className="my-6 rounded-xl w-full border border-theme-border"
|
|
78
|
+
>
|
|
79
|
+
Your browser does not support the video tag.
|
|
80
|
+
</video>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -16,7 +16,7 @@ import { LanguageSelector } from './LanguageSelector';
|
|
|
16
16
|
import { resolveNavigation } from '@/lib/navigation-resolver';
|
|
17
17
|
import { getIconClass } from '@/lib/icon-utils';
|
|
18
18
|
import { useLinkPrefix } from '@/lib/link-prefix-context';
|
|
19
|
-
import {
|
|
19
|
+
import { useChatOpenHandler } from '@/hooks/useChatPanel';
|
|
20
20
|
|
|
21
21
|
interface HeaderProps {
|
|
22
22
|
config: DocsConfig;
|
|
@@ -36,8 +36,7 @@ const MOBILE_ICON_BUTTON = 'lg:hidden p-2 text-[var(--color-text-muted)] hover:t
|
|
|
36
36
|
const COMPACT_ICON_BUTTON = 'hidden md:flex p-2 hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] rounded-lg transition-colors cursor-pointer';
|
|
37
37
|
|
|
38
38
|
export function Header({ config, layout = 'header-logo', tabsPosition: tabsPositionProp, isSidebarOpen, onToggleSidebar }: HeaderProps) {
|
|
39
|
-
const {
|
|
40
|
-
const onChatOpen = chatContextEnabled ? () => setIsChatOpen(true) : undefined;
|
|
39
|
+
const { onChatOpen, devNotice } = useChatOpenHandler();
|
|
41
40
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
42
41
|
const [logoError, setLogoError] = useState(false);
|
|
43
42
|
const [isDark, setIsDark] = useState(false);
|
|
@@ -542,6 +541,7 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
542
541
|
onClose={() => setIsSearchOpen(false)}
|
|
543
542
|
popularPages={config.search?.popularPages}
|
|
544
543
|
/>
|
|
544
|
+
{devNotice}
|
|
545
545
|
</>
|
|
546
546
|
);
|
|
547
547
|
}
|
|
@@ -30,7 +30,7 @@ import { ThemeToggle, ThemeToggleCycle } from '@/components/theme/ThemeToggle';
|
|
|
30
30
|
import { getIconClass } from '@/lib/icon-utils';
|
|
31
31
|
import { getTabsFromConfig } from '@/lib/navigation-utils';
|
|
32
32
|
import { useLinkPrefix } from '@/lib/link-prefix-context';
|
|
33
|
-
import {
|
|
33
|
+
import { useChatOpenHandler } from '@/hooks/useChatPanel';
|
|
34
34
|
|
|
35
35
|
interface SidebarProps {
|
|
36
36
|
config: DocsConfig;
|
|
@@ -150,8 +150,7 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
150
150
|
const linkPrefix = useLinkPrefix();
|
|
151
151
|
|
|
152
152
|
// Chat panel state — used for Pulsar sidebar AI button
|
|
153
|
-
const {
|
|
154
|
-
const onChatOpen = chatEnabled ? () => setIsChatOpen(true) : undefined;
|
|
153
|
+
const { onChatOpen, devNotice } = useChatOpenHandler();
|
|
155
154
|
|
|
156
155
|
// Optimistic navigation: immediately highlight clicked link while page loads.
|
|
157
156
|
// Strips hash fragments so anchor links (e.g. /page#section) match pathname.
|
|
@@ -750,6 +749,7 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
750
749
|
|
|
751
750
|
{/* Search Modal - shown when search is opened from sidebar */}
|
|
752
751
|
<SearchModal isOpen={isSearchOpen} onClose={() => setIsSearchOpen(false)} onNavigate={handleNavigate} />
|
|
752
|
+
{devNotice}
|
|
753
753
|
</>
|
|
754
754
|
);
|
|
755
755
|
}
|