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.
Files changed (79) hide show
  1. package/README.md +46 -1
  2. package/dist/__tests__/unit/docs-json-writer.test.js +59 -1
  3. package/dist/__tests__/unit/docs-json-writer.test.js.map +1 -1
  4. package/dist/__tests__/unit/spellcheck-fix.test.d.ts +2 -0
  5. package/dist/__tests__/unit/spellcheck-fix.test.d.ts.map +1 -0
  6. package/dist/__tests__/unit/spellcheck-fix.test.js +82 -0
  7. package/dist/__tests__/unit/spellcheck-fix.test.js.map +1 -0
  8. package/dist/__tests__/unit/spellcheck-utils.test.d.ts +2 -0
  9. package/dist/__tests__/unit/spellcheck-utils.test.d.ts.map +1 -0
  10. package/dist/__tests__/unit/spellcheck-utils.test.js +234 -0
  11. package/dist/__tests__/unit/spellcheck-utils.test.js.map +1 -0
  12. package/dist/__tests__/unit/tech-words.test.d.ts +2 -0
  13. package/dist/__tests__/unit/tech-words.test.d.ts.map +1 -0
  14. package/dist/__tests__/unit/tech-words.test.js +31 -0
  15. package/dist/__tests__/unit/tech-words.test.js.map +1 -0
  16. package/dist/commands/spellcheck.d.ts +13 -0
  17. package/dist/commands/spellcheck.d.ts.map +1 -0
  18. package/dist/commands/spellcheck.js +144 -0
  19. package/dist/commands/spellcheck.js.map +1 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +28 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/lib/deps.js +2 -2
  24. package/dist/lib/docs-json-writer.d.ts +6 -0
  25. package/dist/lib/docs-json-writer.d.ts.map +1 -1
  26. package/dist/lib/docs-json-writer.js +29 -0
  27. package/dist/lib/docs-json-writer.js.map +1 -1
  28. package/dist/lib/openapi/types.d.ts +11 -6
  29. package/dist/lib/openapi/types.d.ts.map +1 -1
  30. package/dist/lib/path-security.d.ts +3 -0
  31. package/dist/lib/path-security.d.ts.map +1 -1
  32. package/dist/lib/path-security.js +14 -1
  33. package/dist/lib/path-security.js.map +1 -1
  34. package/dist/lib/spellcheck-fix.d.ts +37 -0
  35. package/dist/lib/spellcheck-fix.d.ts.map +1 -0
  36. package/dist/lib/spellcheck-fix.js +292 -0
  37. package/dist/lib/spellcheck-fix.js.map +1 -0
  38. package/dist/lib/spellcheck-utils.d.ts +36 -0
  39. package/dist/lib/spellcheck-utils.d.ts.map +1 -0
  40. package/dist/lib/spellcheck-utils.js +138 -0
  41. package/dist/lib/spellcheck-utils.js.map +1 -0
  42. package/dist/lib/tech-words.d.ts +9 -0
  43. package/dist/lib/tech-words.d.ts.map +1 -0
  44. package/dist/lib/tech-words.js +118 -0
  45. package/dist/lib/tech-words.js.map +1 -0
  46. package/package.json +15 -10
  47. package/vendored/app/[[...slug]]/page.tsx +48 -13
  48. package/vendored/app/api/assets/[...path]/route.ts +2 -0
  49. package/vendored/components/layout/LayoutWrapper.tsx +3 -4
  50. package/vendored/components/mdx/ApiEndpoint.tsx +13 -2
  51. package/vendored/components/mdx/MDXComponents.tsx +16 -0
  52. package/vendored/components/mdx/OpenApiEndpoint.tsx +76 -36
  53. package/vendored/components/mdx/Tabs.tsx +1 -1
  54. package/vendored/components/mdx/Video.tsx +82 -0
  55. package/vendored/components/navigation/Header.tsx +3 -3
  56. package/vendored/components/navigation/Sidebar.tsx +3 -3
  57. package/vendored/components/ui/CodePanel.tsx +5 -5
  58. package/vendored/components/ui/CodePanelModal.tsx +3 -3
  59. package/vendored/components/ui/DevOnlyNotice.tsx +78 -0
  60. package/vendored/hooks/useChatPanel.tsx +21 -2
  61. package/vendored/hooks/useMediaQuery.ts +27 -0
  62. package/vendored/lib/build-endpoint-from-mdx.ts +66 -0
  63. package/vendored/lib/isr-build-executor.ts +1 -1
  64. package/vendored/lib/middleware-helpers.ts +6 -1
  65. package/vendored/lib/openapi/code-examples.ts +479 -99
  66. package/vendored/lib/openapi/index.ts +9 -1
  67. package/vendored/lib/openapi/types.ts +29 -5
  68. package/vendored/lib/preprocess-mdx.ts +103 -36
  69. package/vendored/lib/process-mdx-with-exports.ts +22 -14
  70. package/vendored/lib/remark-extract-param-fields.ts +134 -0
  71. package/vendored/lib/shiki-client.ts +12 -0
  72. package/vendored/lib/static-artifacts.ts +2 -0
  73. package/vendored/lib/static-file-route.ts +1 -1
  74. package/vendored/lib/url-safety.ts +122 -0
  75. package/vendored/next.config.js +7 -0
  76. package/vendored/schema/docs-schema.json +35 -4
  77. package/vendored/scripts/copy-files.cjs +60 -54
  78. package/vendored/scripts/validate-links.cjs +1 -1
  79. 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.6",
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": "hello@jamdesk.com",
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": "^5.3.0",
115
- "vitest": "^4.1.0"
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 CodeExamples,
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: CodeExamples | null = null;
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
- openApiCodeExamples = generateCodeExamples(openApiEndpointData, { authMethod });
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
- <ApiEndpoint
680
- method={mdxApiMethod}
681
- path={mdxApiPath}
682
- baseUrl={mdxApiBaseUrl}
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 on by default in production; projects can opt out with chat.enabled: false.
41
- // Hidden in local dev the chat API route needs Upstash Vector + Anthropic API.
42
- const chatEnabled = process.env.NODE_ENV === 'production'
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 (![alt](url =WIDTHx) 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 { useState, useMemo } from 'react';
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
- CodeExamples,
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?: 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: CodeExamples }) {
856
+ function CodeExamplesSection({ examples }: { examples: CodeExample[] }) {
838
857
  const codeItems = useMemo(
839
- () => [
840
- { code: examples.curl, language: 'bash' },
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
- // Render full Shiki HTML for theme background support
864
+ // Shiki generates HTML from trusted code strings (not user input)
850
865
  const tabs: CodePanelTab[] = useMemo(
851
- () => [
852
- {
853
- label: 'cURL',
854
- content: <div dangerouslySetInnerHTML={{ __html: highlightedResults[0] || '' }} />,
855
- },
856
- {
857
- label: 'Python',
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 method={method} path={path} baseUrl={baseUrl} />
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 ![alt](/videos/demo.mp4) 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 { useChatPanel } from '@/hooks/useChatPanel';
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 { chatEnabled: chatContextEnabled, setIsChatOpen } = useChatPanel();
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 { useChatPanel } from '@/hooks/useChatPanel';
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 { chatEnabled, setIsChatOpen } = useChatPanel();
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
  }