jamdesk 1.1.7 → 1.1.9
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 +5 -2
- package/dist/lib/deps.js +3 -3
- 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/tech-words.js +5 -5
- package/dist/lib/tech-words.js.map +1 -1
- package/package.json +13 -10
- package/vendored/app/[[...slug]]/page.tsx +50 -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 +26 -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 -1
- package/vendored/lib/url-safety.ts +122 -0
- package/vendored/next.config.js +8 -0
- package/vendored/schema/docs-schema.json +16 -3
- package/vendored/scripts/copy-files.cjs +60 -54
- package/vendored/scripts/validate-links.cjs +1 -1
- package/vendored/shared/path-security.ts +17 -1
|
@@ -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 = 'interactive',
|
|
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
|
}
|
|
@@ -279,7 +279,7 @@ export function CodePanel({
|
|
|
279
279
|
{enableFullscreen && (
|
|
280
280
|
<button
|
|
281
281
|
onClick={() => setIsModalOpen(true)}
|
|
282
|
-
className="p-1.5 rounded-md transition-colors flex-shrink-0"
|
|
282
|
+
className="p-1.5 rounded-md transition-colors flex-shrink-0 cursor-pointer"
|
|
283
283
|
style={{ color: codePanelColors.textMuted }}
|
|
284
284
|
onMouseEnter={(e) => {
|
|
285
285
|
e.currentTarget.style.backgroundColor = codePanelColors.tabHoverBg;
|
|
@@ -298,7 +298,7 @@ export function CodePanel({
|
|
|
298
298
|
{/* Copy Button */}
|
|
299
299
|
<button
|
|
300
300
|
onClick={handleCopy}
|
|
301
|
-
className={`p-1.5 rounded-md transition-colors flex-shrink-0 ${enableFullscreen ? 'ml-1' : ''}`}
|
|
301
|
+
className={`p-1.5 rounded-md transition-colors flex-shrink-0 cursor-pointer ${enableFullscreen ? 'ml-1' : ''}`}
|
|
302
302
|
style={{ color: codePanelColors.textMuted }}
|
|
303
303
|
onMouseEnter={(e) => {
|
|
304
304
|
e.currentTarget.style.backgroundColor = codePanelColors.tabHoverBg;
|
|
@@ -348,7 +348,7 @@ export function CodePanel({
|
|
|
348
348
|
<button
|
|
349
349
|
key={index}
|
|
350
350
|
onClick={() => handleTabClick(index)}
|
|
351
|
-
className="px-1 py-1 font-medium transition-colors whitespace-nowrap flex items-center"
|
|
351
|
+
className="px-1 py-1 font-medium transition-colors whitespace-nowrap flex items-center cursor-pointer"
|
|
352
352
|
style={{
|
|
353
353
|
color: isActive ? activeColor : codePanelColors.textMuted,
|
|
354
354
|
cursor: 'pointer',
|
|
@@ -407,7 +407,7 @@ export function CodePanel({
|
|
|
407
407
|
{enableFullscreen && (
|
|
408
408
|
<button
|
|
409
409
|
onClick={() => setIsModalOpen(true)}
|
|
410
|
-
className="p-1.5 rounded-md transition-colors flex-shrink-0 ml-auto"
|
|
410
|
+
className="p-1.5 rounded-md transition-colors flex-shrink-0 ml-auto cursor-pointer"
|
|
411
411
|
style={{ color: codePanelColors.textMuted }}
|
|
412
412
|
onMouseEnter={(e) => {
|
|
413
413
|
e.currentTarget.style.backgroundColor = codePanelColors.tabHoverBg;
|
|
@@ -426,7 +426,7 @@ export function CodePanel({
|
|
|
426
426
|
{/* Fixed copy button */}
|
|
427
427
|
<button
|
|
428
428
|
onClick={handleCopy}
|
|
429
|
-
className={`p-1.5 rounded-md transition-colors flex-shrink-0 ${enableFullscreen ? 'ml-1' : 'ml-auto'}`}
|
|
429
|
+
className={`p-1.5 rounded-md transition-colors flex-shrink-0 cursor-pointer ${enableFullscreen ? 'ml-1' : 'ml-auto'}`}
|
|
430
430
|
style={{ color: codePanelColors.textMuted }}
|
|
431
431
|
onMouseEnter={(e) => {
|
|
432
432
|
e.currentTarget.style.backgroundColor = codePanelColors.tabHoverBg;
|
|
@@ -198,7 +198,7 @@ export function CodePanelModal({ isOpen, onClose, tabs, title, initialTabIndex =
|
|
|
198
198
|
<button
|
|
199
199
|
key={index}
|
|
200
200
|
onClick={() => setActiveTab(index)}
|
|
201
|
-
className="px-3 py-1.5 rounded-md text-sm font-medium transition-colors"
|
|
201
|
+
className="px-3 py-1.5 rounded-md text-sm font-medium transition-colors cursor-pointer"
|
|
202
202
|
style={{
|
|
203
203
|
backgroundColor: activeTab === index ? codePanelColors.tabActiveBg : 'transparent',
|
|
204
204
|
color: activeTab === index ? codePanelColors.text : codePanelColors.textMuted,
|
|
@@ -226,7 +226,7 @@ export function CodePanelModal({ isOpen, onClose, tabs, title, initialTabIndex =
|
|
|
226
226
|
{/* Copy Button */}
|
|
227
227
|
<button
|
|
228
228
|
onClick={handleCopy}
|
|
229
|
-
className="p-2 rounded-md transition-colors"
|
|
229
|
+
className="p-2 rounded-md transition-colors cursor-pointer"
|
|
230
230
|
style={{ color: codePanelColors.textMuted }}
|
|
231
231
|
onMouseEnter={(e) => {
|
|
232
232
|
e.currentTarget.style.backgroundColor = codePanelColors.tabHoverBg;
|
|
@@ -249,7 +249,7 @@ export function CodePanelModal({ isOpen, onClose, tabs, title, initialTabIndex =
|
|
|
249
249
|
{/* Close Button */}
|
|
250
250
|
<button
|
|
251
251
|
onClick={handleClose}
|
|
252
|
-
className="p-2 rounded-md transition-colors"
|
|
252
|
+
className="p-2 rounded-md transition-colors cursor-pointer"
|
|
253
253
|
style={{ color: codePanelColors.textMuted }}
|
|
254
254
|
onMouseEnter={(e) => {
|
|
255
255
|
e.currentTarget.style.backgroundColor = codePanelColors.tabHoverBg;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DevOnlyNotice
|
|
5
|
+
*
|
|
6
|
+
* A toast-like popup shown when users click features that are only available
|
|
7
|
+
* in production (e.g., API playground, AI chat). Appears briefly, then
|
|
8
|
+
* auto-dismisses. Used in local dev / CLI mode where the backend
|
|
9
|
+
* infrastructure (proxy endpoints, Upstash Vector, etc.) isn't available.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
13
|
+
|
|
14
|
+
/** Feature names for production-only features. */
|
|
15
|
+
export const DEV_FEATURE = {
|
|
16
|
+
AI_CHAT: 'AI Chat',
|
|
17
|
+
API_PLAYGROUND: 'API Playground',
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
interface DevOnlyNoticeProps {
|
|
21
|
+
feature: string;
|
|
22
|
+
onDismiss: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function DevOnlyNotice({ feature, onDismiss }: DevOnlyNoticeProps) {
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const timer = setTimeout(onDismiss, 4000);
|
|
28
|
+
return () => clearTimeout(timer);
|
|
29
|
+
}, [onDismiss]);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
role="alert"
|
|
34
|
+
style={{
|
|
35
|
+
position: 'fixed',
|
|
36
|
+
bottom: '1.5rem',
|
|
37
|
+
left: '50%',
|
|
38
|
+
transform: 'translateX(-50%)',
|
|
39
|
+
zIndex: 1000010,
|
|
40
|
+
background: 'var(--color-bg-primary, #fff)',
|
|
41
|
+
border: '1px solid var(--color-border, #e2e8f0)',
|
|
42
|
+
borderRadius: '8px',
|
|
43
|
+
padding: '0.75rem 1.25rem',
|
|
44
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
45
|
+
fontSize: '0.8125rem',
|
|
46
|
+
color: 'var(--color-text-primary, #1e293b)',
|
|
47
|
+
maxWidth: '400px',
|
|
48
|
+
textAlign: 'center',
|
|
49
|
+
animation: 'devNoticeIn 0.2s ease-out',
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<div style={{ fontWeight: 600, marginBottom: '0.25rem' }}>
|
|
53
|
+
{feature} is available in production
|
|
54
|
+
</div>
|
|
55
|
+
<div style={{ color: 'var(--color-text-muted, #64748b)', fontSize: '0.75rem' }}>
|
|
56
|
+
Deploy your docs to see this feature live.
|
|
57
|
+
</div>
|
|
58
|
+
<style>{`@keyframes devNoticeIn { from { opacity: 0; transform: translateX(-50%) translateY(8px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }`}</style>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Hook that returns a trigger function and the notice element.
|
|
65
|
+
* Call `showNotice()` to display the toast for a given feature.
|
|
66
|
+
*/
|
|
67
|
+
export function useDevOnlyNotice() {
|
|
68
|
+
const [activeFeature, setActiveFeature] = useState<string | null>(null);
|
|
69
|
+
const dismiss = useCallback(() => setActiveFeature(null), []);
|
|
70
|
+
|
|
71
|
+
const showNotice = setActiveFeature;
|
|
72
|
+
|
|
73
|
+
const notice = activeFeature ? (
|
|
74
|
+
<DevOnlyNotice feature={activeFeature} onDismiss={dismiss} />
|
|
75
|
+
) : null;
|
|
76
|
+
|
|
77
|
+
return { showNotice, notice };
|
|
78
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
|
|
4
|
+
import { useDevOnlyNotice, DEV_FEATURE } from '@/components/ui/DevOnlyNotice';
|
|
4
5
|
|
|
5
6
|
const MIN_WIDTH = 350;
|
|
6
7
|
const MAX_WIDTH = 500;
|
|
@@ -61,8 +62,9 @@ export function ChatPanelProvider({ children, chatEnabled, chatEndpoint, starter
|
|
|
61
62
|
|
|
62
63
|
// Keyboard shortcuts: Cmd+I / Ctrl+I to toggle, Escape to close
|
|
63
64
|
// Uses capture phase for Escape so it fires before SearchModal's bubble-phase handler
|
|
65
|
+
const isDevMode = process.env.NODE_ENV !== 'production';
|
|
64
66
|
useEffect(() => {
|
|
65
|
-
if (!chatEnabled) return;
|
|
67
|
+
if (!chatEnabled || isDevMode) return;
|
|
66
68
|
|
|
67
69
|
const handler = (e: KeyboardEvent) => {
|
|
68
70
|
// Cmd+I / Ctrl+I — toggle chat
|
|
@@ -81,7 +83,7 @@ export function ChatPanelProvider({ children, chatEnabled, chatEndpoint, starter
|
|
|
81
83
|
};
|
|
82
84
|
document.addEventListener('keydown', handler, true); // capture phase
|
|
83
85
|
return () => document.removeEventListener('keydown', handler, true);
|
|
84
|
-
}, [chatEnabled, isChatOpen]);
|
|
86
|
+
}, [chatEnabled, isChatOpen, isDevMode]);
|
|
85
87
|
|
|
86
88
|
return (
|
|
87
89
|
<ChatPanelContext.Provider value={{ isChatOpen, setIsChatOpen, chatWidth, setChatWidth, chatEnabled, chatEndpoint, starterQuestions }}>
|
|
@@ -99,3 +101,20 @@ export function useChatPanel(): ChatPanelContextValue {
|
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
export { MIN_WIDTH, MAX_WIDTH, DEFAULT_WIDTH };
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns a chat-open handler that shows DevOnlyNotice in dev mode
|
|
107
|
+
* instead of opening the panel. Used by Header and Sidebar.
|
|
108
|
+
*/
|
|
109
|
+
export function useChatOpenHandler() {
|
|
110
|
+
const { chatEnabled, setIsChatOpen } = useChatPanel();
|
|
111
|
+
const { showNotice, notice } = useDevOnlyNotice();
|
|
112
|
+
const isDevMode = process.env.NODE_ENV !== 'production';
|
|
113
|
+
|
|
114
|
+
const onChatOpen = chatEnabled ? () => {
|
|
115
|
+
if (isDevMode) { showNotice(DEV_FEATURE.AI_CHAT); return; }
|
|
116
|
+
setIsChatOpen(true);
|
|
117
|
+
} : undefined;
|
|
118
|
+
|
|
119
|
+
return { onChatOpen, devNotice: notice };
|
|
120
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SSR-safe media query hook using useSyncExternalStore.
|
|
7
|
+
*
|
|
8
|
+
* Returns true when the viewport matches the given CSS media query string,
|
|
9
|
+
* false otherwise. Returns false on the server (getServerSnapshot).
|
|
10
|
+
*/
|
|
11
|
+
export function useMediaQuery(query: string): boolean {
|
|
12
|
+
const subscribe = useCallback((callback: () => void) => {
|
|
13
|
+
if (typeof window === 'undefined') return () => {};
|
|
14
|
+
const mql = window.matchMedia(query);
|
|
15
|
+
mql.addEventListener('change', callback);
|
|
16
|
+
return () => mql.removeEventListener('change', callback);
|
|
17
|
+
}, [query]);
|
|
18
|
+
|
|
19
|
+
const getSnapshot = useCallback(() => {
|
|
20
|
+
if (typeof window === 'undefined') return false;
|
|
21
|
+
return window.matchMedia(query).matches;
|
|
22
|
+
}, [query]);
|
|
23
|
+
|
|
24
|
+
const getServerSnapshot = useCallback(() => false, []);
|
|
25
|
+
|
|
26
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
27
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { OpenApiEndpointData, ParsedParameter, ParsedRequestBody } from './openapi/types';
|
|
2
|
+
import type { ExtractedParam } from './remark-extract-param-fields';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build an OpenApiEndpointData from extracted ParamField data and api: frontmatter.
|
|
6
|
+
*
|
|
7
|
+
* Bridges `api:` pages (MDX-defined endpoints) into the same data structure
|
|
8
|
+
* that the playground consumes for `openapi:` pages.
|
|
9
|
+
*
|
|
10
|
+
* Known limitation: only top-level <ParamField> components are supported.
|
|
11
|
+
* Nested ParamFields (inside <Expandable> etc.) are extracted as flat fields.
|
|
12
|
+
*/
|
|
13
|
+
export function buildEndpointFromMdx(
|
|
14
|
+
method: string,
|
|
15
|
+
path: string,
|
|
16
|
+
params: ExtractedParam[],
|
|
17
|
+
serverUrl?: string,
|
|
18
|
+
): OpenApiEndpointData {
|
|
19
|
+
const bodyParams = params.filter(p => p.in === 'body');
|
|
20
|
+
const nonBodyParams = params.filter(p => p.in !== 'body');
|
|
21
|
+
|
|
22
|
+
const parameters: ParsedParameter[] = nonBodyParams.map(p => ({
|
|
23
|
+
name: p.name,
|
|
24
|
+
in: p.in as 'path' | 'query' | 'header' | 'cookie',
|
|
25
|
+
required: p.required,
|
|
26
|
+
schema: {
|
|
27
|
+
type: p.type,
|
|
28
|
+
...(p.default !== undefined ? { default: p.default } : {}),
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
let requestBody: ParsedRequestBody | undefined;
|
|
33
|
+
if (bodyParams.length > 0) {
|
|
34
|
+
const properties: Record<string, { type?: string }> = {};
|
|
35
|
+
const required: string[] = [];
|
|
36
|
+
for (const p of bodyParams) {
|
|
37
|
+
properties[p.name] = {
|
|
38
|
+
type: p.type,
|
|
39
|
+
...(p.default !== undefined ? { default: p.default } : {}),
|
|
40
|
+
};
|
|
41
|
+
if (p.required) required.push(p.name);
|
|
42
|
+
}
|
|
43
|
+
requestBody = {
|
|
44
|
+
required: required.length > 0,
|
|
45
|
+
content: {
|
|
46
|
+
'application/json': {
|
|
47
|
+
schema: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties,
|
|
50
|
+
...(required.length > 0 ? { required } : {}),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
method: method as OpenApiEndpointData['method'],
|
|
59
|
+
path,
|
|
60
|
+
parameters,
|
|
61
|
+
requestBody,
|
|
62
|
+
responses: {},
|
|
63
|
+
servers: serverUrl ? [{ url: serverUrl }] : [],
|
|
64
|
+
security: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -62,7 +62,7 @@ export async function collectMdxFiles(
|
|
|
62
62
|
* @returns Array of asset file paths relative to projectDir
|
|
63
63
|
*/
|
|
64
64
|
export async function collectAssetFiles(projectDir: string): Promise<string[]> {
|
|
65
|
-
const files = await glob('**/*.{png,jpg,jpeg,gif,svg,webp,ico}', {
|
|
65
|
+
const files = await glob('**/*.{png,jpg,jpeg,gif,svg,webp,ico,mp4,webm}', {
|
|
66
66
|
cwd: projectDir,
|
|
67
67
|
ignore: ['node_modules/**', '.git/**'],
|
|
68
68
|
});
|