jamdesk 1.1.7 → 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 +5 -2
- package/dist/lib/deps.js +2 -2
- 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/package.json +13 -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/url-safety.ts +122 -0
- package/vendored/next.config.js +7 -0
- package/vendored/schema/docs-schema.json +17 -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
|
@@ -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
|
});
|
|
@@ -491,7 +491,7 @@ export function buildProjectHeaders(
|
|
|
491
491
|
/**
|
|
492
492
|
* Asset extensions that need to be routed to the assets API.
|
|
493
493
|
*/
|
|
494
|
-
const ASSET_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.pdf'];
|
|
494
|
+
const ASSET_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.pdf', '.mp4', '.webm'];
|
|
495
495
|
|
|
496
496
|
/**
|
|
497
497
|
* Project-specific files that need to be fetched from R2 via assets API.
|
|
@@ -522,6 +522,11 @@ export function isAssetRequest(pathname: string): boolean {
|
|
|
522
522
|
return true;
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
// Project videos under /_jd/videos/ are served from R2
|
|
526
|
+
if (pathname.startsWith(`${ASSET_PREFIX}/videos/`)) {
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
|
|
525
530
|
// Project-specific files from R2, or files with asset extensions
|
|
526
531
|
return (
|
|
527
532
|
PROJECT_SPECIFIC_FILES.includes(pathname) ||
|