jamdesk 1.1.1 → 1.1.2
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/package.json +1 -1
- package/vendored/app/api/og/route.tsx +5 -1
- package/vendored/components/AIActionsMenu.tsx +2 -10
- package/vendored/components/chat/ChatPanel.tsx +3 -3
- package/vendored/components/layout/LayoutWrapper.tsx +1 -1
- package/vendored/components/mdx/View.tsx +2 -12
- package/vendored/components/navigation/Header.tsx +9 -19
- package/vendored/components/navigation/LanguageSelector.tsx +2 -12
- package/vendored/components/navigation/TableOfContents.tsx +2 -2
- package/vendored/components/snippets/generated/CodeLink.tsx +25 -0
- package/vendored/components/snippets/generated/HeaderAPI.tsx +44 -0
- package/vendored/components/snippets/generated/PlansAvailable.tsx +53 -0
- package/vendored/components/snippets/generated/SnippetIntro.tsx +43 -0
- package/vendored/components/ui/CodePanel.tsx +2 -2
- package/vendored/hooks/useOnClickOutside.ts +28 -0
- package/vendored/lib/validate-config.ts +3 -1
- package/vendored/themes/jam/variables.css +10 -3
- package/vendored/themes/nebula/variables.css +10 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
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",
|
|
@@ -49,8 +49,10 @@ export async function GET(request: NextRequest) {
|
|
|
49
49
|
// (Satori doesn't support webp and can fail on remote URLs)
|
|
50
50
|
let logo = '';
|
|
51
51
|
if (logoUrl) {
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
52
54
|
try {
|
|
53
|
-
const res = await fetch(logoUrl);
|
|
55
|
+
const res = await fetch(logoUrl, { signal: controller.signal });
|
|
54
56
|
if (res.ok) {
|
|
55
57
|
const contentType = res.headers.get('content-type') || 'image/png';
|
|
56
58
|
const buf = await res.arrayBuffer();
|
|
@@ -58,6 +60,8 @@ export async function GET(request: NextRequest) {
|
|
|
58
60
|
}
|
|
59
61
|
} catch {
|
|
60
62
|
// Skip logo on fetch failure
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timeout);
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
4
|
+
import { useOnClickOutside } from '@/hooks/useOnClickOutside';
|
|
4
5
|
import { usePathname } from 'next/navigation';
|
|
5
6
|
import { useLinkPrefix } from '@/lib/link-prefix-context';
|
|
6
7
|
import { getIconClass } from '@/lib/icon-utils';
|
|
@@ -317,16 +318,7 @@ export function AIActionsMenu({ options, projectName }: AIActionsMenuProps) {
|
|
|
317
318
|
}, []);
|
|
318
319
|
|
|
319
320
|
// Close on click outside
|
|
320
|
-
|
|
321
|
-
if (!isOpen) return;
|
|
322
|
-
function handleMouseDown(e: MouseEvent) {
|
|
323
|
-
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
324
|
-
setIsOpen(false);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
document.addEventListener('mousedown', handleMouseDown);
|
|
328
|
-
return () => document.removeEventListener('mousedown', handleMouseDown);
|
|
329
|
-
}, [isOpen]);
|
|
321
|
+
useOnClickOutside(menuRef, () => setIsOpen(false), isOpen);
|
|
330
322
|
|
|
331
323
|
// Close on Escape
|
|
332
324
|
useEffect(() => {
|
|
@@ -76,9 +76,9 @@ export function ChatPanel({ isOpen, onClose, starterQuestions, chatEndpoint, mod
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
vv.addEventListener('resize', update);
|
|
80
|
-
vv.addEventListener('scroll', update);
|
|
81
|
-
window.addEventListener('scroll', update);
|
|
79
|
+
vv.addEventListener('resize', update, { passive: true });
|
|
80
|
+
vv.addEventListener('scroll', update, { passive: true });
|
|
81
|
+
window.addEventListener('scroll', update, { passive: true });
|
|
82
82
|
|
|
83
83
|
return () => {
|
|
84
84
|
vv.removeEventListener('resize', update);
|
|
@@ -64,7 +64,7 @@ export function LayoutWrapper({ config, children }: LayoutWrapperProps) {
|
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
scrollTarget.addEventListener('scroll', handleScroll);
|
|
67
|
+
scrollTarget.addEventListener('scroll', handleScroll, { passive: true });
|
|
68
68
|
handleScroll(); // Check initial position
|
|
69
69
|
|
|
70
70
|
return () => scrollTarget.removeEventListener('scroll', handleScroll);
|
|
@@ -10,6 +10,7 @@ import React, {
|
|
|
10
10
|
useRef,
|
|
11
11
|
type ReactNode,
|
|
12
12
|
} from 'react';
|
|
13
|
+
import { useOnClickOutside } from '@/hooks/useOnClickOutside';
|
|
13
14
|
import { Icon } from './Icon';
|
|
14
15
|
|
|
15
16
|
// ============================================================================
|
|
@@ -120,18 +121,7 @@ export function ViewSelector({ className = '' }: ViewSelectorProps) {
|
|
|
120
121
|
const currentIndex = views.findIndex((v) => v.title === selectedView);
|
|
121
122
|
|
|
122
123
|
// Handle click outside to close
|
|
123
|
-
|
|
124
|
-
if (!isOpen) return;
|
|
125
|
-
|
|
126
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
127
|
-
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
128
|
-
setIsOpen(false);
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
133
|
-
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
134
|
-
}, [isOpen]);
|
|
124
|
+
useOnClickOutside(containerRef, () => setIsOpen(false), isOpen);
|
|
135
125
|
|
|
136
126
|
// Focus the item when focusedIndex changes
|
|
137
127
|
useEffect(() => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
4
|
+
import { useOnClickOutside } from '@/hooks/useOnClickOutside';
|
|
4
5
|
import Link from 'next/link';
|
|
5
6
|
import { usePathname } from 'next/navigation';
|
|
6
7
|
// Icons use Font Awesome CSS classes for lightweight rendering
|
|
@@ -202,28 +203,17 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
202
203
|
return () => document.removeEventListener('keydown', down);
|
|
203
204
|
}, [showLogoInHeader]);
|
|
204
205
|
|
|
205
|
-
// Close tabs dropdown when clicking outside
|
|
206
|
-
|
|
207
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
208
|
-
if (tabsDropdownRef.current && !tabsDropdownRef.current.contains(e.target as Node)) {
|
|
209
|
-
setIsTabsDropdownOpen(false);
|
|
210
|
-
}
|
|
211
|
-
};
|
|
206
|
+
// Close tabs dropdown when clicking outside
|
|
207
|
+
useOnClickOutside(tabsDropdownRef, () => setIsTabsDropdownOpen(false), isTabsDropdownOpen);
|
|
212
208
|
|
|
209
|
+
// Close tabs dropdown on Escape
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (!isTabsDropdownOpen) return;
|
|
213
212
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
214
|
-
if (e.key === 'Escape')
|
|
215
|
-
setIsTabsDropdownOpen(false);
|
|
216
|
-
}
|
|
213
|
+
if (e.key === 'Escape') setIsTabsDropdownOpen(false);
|
|
217
214
|
};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
221
|
-
document.addEventListener('keydown', handleKeyDown);
|
|
222
|
-
return () => {
|
|
223
|
-
document.removeEventListener('mousedown', handleClickOutside);
|
|
224
|
-
document.removeEventListener('keydown', handleKeyDown);
|
|
225
|
-
};
|
|
226
|
-
}
|
|
215
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
216
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
227
217
|
}, [isTabsDropdownOpen]);
|
|
228
218
|
|
|
229
219
|
// Split tabs into visible and overflow
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import { useOnClickOutside } from '@/hooks/useOnClickOutside';
|
|
4
5
|
import { useRouter, usePathname } from 'next/navigation';
|
|
5
6
|
import type { ResolvedLanguage } from '@/lib/navigation-resolver';
|
|
6
7
|
import type { LanguageCode } from '@/lib/docs-types';
|
|
@@ -64,18 +65,7 @@ export function LanguageSelector({
|
|
|
64
65
|
}, []); // Only run once on mount
|
|
65
66
|
|
|
66
67
|
// Handle click outside to close
|
|
67
|
-
|
|
68
|
-
if (!isOpen) return;
|
|
69
|
-
|
|
70
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
71
|
-
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
72
|
-
setIsOpen(false);
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
77
|
-
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
78
|
-
}, [isOpen]);
|
|
68
|
+
useOnClickOutside(containerRef, () => setIsOpen(false), isOpen);
|
|
79
69
|
|
|
80
70
|
// Handle keyboard navigation
|
|
81
71
|
const handleKeyDown = useCallback(
|
|
@@ -362,10 +362,10 @@ export function TableOfContents({ content, className = '' }: TableOfContentsProp
|
|
|
362
362
|
|
|
363
363
|
return (
|
|
364
364
|
<nav className={className}>
|
|
365
|
-
<
|
|
365
|
+
<p className="text-sm font-semibold text-[var(--color-text-tertiary)] mb-3 flex items-center gap-2">
|
|
366
366
|
<i className={`${getIconClass('bars-sort')} h-3.5 w-3.5`} aria-hidden="true" />
|
|
367
367
|
On this page
|
|
368
|
-
</
|
|
368
|
+
</p>
|
|
369
369
|
<div className="relative">
|
|
370
370
|
{/* SVG background line (grey) — rendered inline for perfect alignment */}
|
|
371
371
|
{svgPath && (
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Auto-generated file - do not edit manually
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
// Import built-in MDX components that snippets can use
|
|
7
|
+
import { Note, Info, Warning, Tip, Check, Danger, Callout } from '@/components/mdx/Callouts';
|
|
8
|
+
import { Card } from '@/components/mdx/Card';
|
|
9
|
+
import { CardGroup } from '@/components/mdx/CardGroup';
|
|
10
|
+
import { ParamField } from '@/components/mdx/ParamField';
|
|
11
|
+
import { ResponseField } from '@/components/mdx/ResponseField';
|
|
12
|
+
import { Accordion, AccordionGroup } from '@/components/mdx/Accordion';
|
|
13
|
+
import { CodeGroup } from '@/components/mdx/CodeGroup';
|
|
14
|
+
import { Steps, Step } from '@/components/mdx/Steps';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const CodeLink = ({ title, href }: any) => {
|
|
18
|
+
return (
|
|
19
|
+
<Card title={`${title}`} href={`${href}`} horizontal icon="code">
|
|
20
|
+
{" "}
|
|
21
|
+
</Card>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default CodeLink;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Auto-generated file - do not edit manually
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
// Import built-in MDX components that snippets can use
|
|
7
|
+
import { Note, Info, Warning, Tip, Check, Danger, Callout } from '@/components/mdx/Callouts';
|
|
8
|
+
import { Card } from '@/components/mdx/Card';
|
|
9
|
+
import { CardGroup } from '@/components/mdx/CardGroup';
|
|
10
|
+
import { ParamField } from '@/components/mdx/ParamField';
|
|
11
|
+
import { ResponseField } from '@/components/mdx/ResponseField';
|
|
12
|
+
import { Accordion, AccordionGroup } from '@/components/mdx/Accordion';
|
|
13
|
+
import { CodeGroup } from '@/components/mdx/CodeGroup';
|
|
14
|
+
import { Steps, Step } from '@/components/mdx/Steps';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const HeaderAPI = ({ noProfileKey, profileKeyRequired }: any) => (
|
|
18
|
+
<>
|
|
19
|
+
<ParamField header="Authorization" type="string" required>
|
|
20
|
+
<a href="/apis/overview#authorization">API Key</a> of the Primary Profile.
|
|
21
|
+
<br />
|
|
22
|
+
<br />
|
|
23
|
+
Format: <code>Authorization: Bearer API_KEY</code>
|
|
24
|
+
</ParamField>
|
|
25
|
+
{!noProfileKey &&
|
|
26
|
+
(profileKeyRequired ? (
|
|
27
|
+
<ParamField header="Profile-Key" type="string" required>
|
|
28
|
+
<a href="/apis/overview#profile-key-format">Profile Key</a> of a User Profile.
|
|
29
|
+
<br />
|
|
30
|
+
<br />
|
|
31
|
+
Format: <code>Profile-Key: PROFILE_KEY</code>
|
|
32
|
+
</ParamField>
|
|
33
|
+
) : (
|
|
34
|
+
<ParamField header="Profile-Key" type="string">
|
|
35
|
+
<a href="/apis/overview#profile-key-format">Profile Key</a> of a User Profile.
|
|
36
|
+
<br />
|
|
37
|
+
<br />
|
|
38
|
+
Format: <code>Profile-Key: PROFILE_KEY</code>
|
|
39
|
+
</ParamField>
|
|
40
|
+
))}
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export default HeaderAPI;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Auto-generated file - do not edit manually
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
// Import built-in MDX components that snippets can use
|
|
7
|
+
import { Note, Info, Warning, Tip, Check, Danger, Callout } from '@/components/mdx/Callouts';
|
|
8
|
+
import { Card } from '@/components/mdx/Card';
|
|
9
|
+
import { CardGroup } from '@/components/mdx/CardGroup';
|
|
10
|
+
import { ParamField } from '@/components/mdx/ParamField';
|
|
11
|
+
import { ResponseField } from '@/components/mdx/ResponseField';
|
|
12
|
+
import { Accordion, AccordionGroup } from '@/components/mdx/Accordion';
|
|
13
|
+
import { CodeGroup } from '@/components/mdx/CodeGroup';
|
|
14
|
+
import { Steps, Step } from '@/components/mdx/Steps';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const PlansAvailable = ({ plans, maxPackRequired }: any) => {
|
|
18
|
+
let displayPlans = plans;
|
|
19
|
+
|
|
20
|
+
if (plans.length === 1) {
|
|
21
|
+
const lowerCasePlan = plans[0].toLowerCase();
|
|
22
|
+
if (lowerCasePlan === "basic") {
|
|
23
|
+
displayPlans = ["Basic", "Premium", "Business", "Enterprise"];
|
|
24
|
+
} else if (lowerCasePlan === "business") {
|
|
25
|
+
displayPlans = ["Business", "Enterprise"];
|
|
26
|
+
} else if (lowerCasePlan === "premium") {
|
|
27
|
+
displayPlans = ["Premium", "Business", "Enterprise"];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
|
|
33
|
+
<Note>
|
|
34
|
+
Available on {displayPlans.length === 1 ? "the " : ""}
|
|
35
|
+
{displayPlans.join(", ").replace(/\b\w/g, (l) => l.toUpperCase())}{" "}
|
|
36
|
+
{displayPlans.length > 1 ? "plans" : "plan"}.
|
|
37
|
+
|
|
38
|
+
{maxPackRequired && (
|
|
39
|
+
|
|
40
|
+
<a href="https://www.acme.com/docs/additional/maxpack"
|
|
41
|
+
className="flex items-center mt-2 cursor-pointer"
|
|
42
|
+
>
|
|
43
|
+
<span className="px-1.5 py-0.5 rounded text-sm" style={{backgroundColor: '#C264B6', color: 'white', fontSize: '12px'}}>
|
|
44
|
+
Max Pack required
|
|
45
|
+
</span>
|
|
46
|
+
</a>
|
|
47
|
+
)}
|
|
48
|
+
</Note>
|
|
49
|
+
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default PlansAvailable;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Auto-generated file - do not edit manually
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
// Import built-in MDX components that snippets can use
|
|
7
|
+
import { Note, Info, Warning, Tip, Check, Danger, Callout } from '@/components/mdx/Callouts';
|
|
8
|
+
import { Card } from '@/components/mdx/Card';
|
|
9
|
+
import { CardGroup } from '@/components/mdx/CardGroup';
|
|
10
|
+
import { ParamField } from '@/components/mdx/ParamField';
|
|
11
|
+
import { ResponseField } from '@/components/mdx/ResponseField';
|
|
12
|
+
import { Accordion, AccordionGroup } from '@/components/mdx/Accordion';
|
|
13
|
+
import { CodeGroup } from '@/components/mdx/CodeGroup';
|
|
14
|
+
import { Steps, Step } from '@/components/mdx/Steps';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
// Helper component for rendering plain MDX snippets
|
|
18
|
+
// Content is from project snippets (controlled source), not user input
|
|
19
|
+
const PlainMdxSnippet = ({ content }: { content: string }) => {
|
|
20
|
+
const formattedContent = content
|
|
21
|
+
.split('\n\n')
|
|
22
|
+
.map((paragraph) => {
|
|
23
|
+
let html = paragraph.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
24
|
+
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
25
|
+
html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
|
|
26
|
+
return html;
|
|
27
|
+
})
|
|
28
|
+
.filter(p => p.trim());
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="snippet-content">
|
|
32
|
+
{formattedContent.map((p, i) => (
|
|
33
|
+
<p key={i} dangerouslySetInnerHTML={{ __html: p }} />
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const SnippetIntro = () => {
|
|
40
|
+
return <PlainMdxSnippet content={"One of the core principles of software development is DRY (Don't Repeat\nYourself). This is a principle that apply to documentation as\nwell. If you find yourself repeating the same content in multiple places, you\nshould consider creating a custom snippet to keep your content in sync."} />;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default SnippetIntro;
|
|
@@ -178,8 +178,8 @@ export function CodePanel({
|
|
|
178
178
|
setTimeout(checkOverflow, 100);
|
|
179
179
|
});
|
|
180
180
|
const el = tabsRef.current;
|
|
181
|
-
el?.addEventListener('scroll', checkOverflow);
|
|
182
|
-
window.addEventListener('resize', checkOverflow);
|
|
181
|
+
el?.addEventListener('scroll', checkOverflow, { passive: true });
|
|
182
|
+
window.addEventListener('resize', checkOverflow, { passive: true });
|
|
183
183
|
return () => {
|
|
184
184
|
el?.removeEventListener('scroll', checkOverflow);
|
|
185
185
|
window.removeEventListener('resize', checkOverflow);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, type RefObject } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Calls `handler` when a mousedown occurs outside `ref`.
|
|
7
|
+
* Only active when `enabled` is true (defaults to true).
|
|
8
|
+
* Handler is stored in a ref — safe to pass inline functions.
|
|
9
|
+
*/
|
|
10
|
+
export function useOnClickOutside(
|
|
11
|
+
ref: RefObject<HTMLElement | null>,
|
|
12
|
+
handler: () => void,
|
|
13
|
+
enabled = true,
|
|
14
|
+
) {
|
|
15
|
+
const handlerRef = useRef(handler);
|
|
16
|
+
handlerRef.current = handler;
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!enabled) return;
|
|
20
|
+
function onMouseDown(e: MouseEvent) {
|
|
21
|
+
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
22
|
+
handlerRef.current();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
document.addEventListener('mousedown', onMouseDown);
|
|
26
|
+
return () => document.removeEventListener('mousedown', onMouseDown);
|
|
27
|
+
}, [ref, enabled]);
|
|
28
|
+
}
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import AjvModule, { ErrorObject } from 'ajv';
|
|
13
|
-
import
|
|
13
|
+
import addFormatsModule from 'ajv-formats';
|
|
14
|
+
// ESM compatibility - ajv-formats exports differently under NodeNext resolution
|
|
15
|
+
const addFormats = (addFormatsModule as any).default || addFormatsModule;
|
|
14
16
|
// ESM compatibility - Ajv exports differently in Node.js ESM
|
|
15
17
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
18
|
const Ajv = (AjvModule as any).default || AjvModule;
|
|
@@ -823,11 +823,18 @@ body[data-theme="jam"] .prose video {
|
|
|
823
823
|
}
|
|
824
824
|
}
|
|
825
825
|
|
|
826
|
-
/*
|
|
826
|
+
/* Align header and content on mobile — zero out inline margin, use consistent padding */
|
|
827
|
+
/* Selector uses [data-has-tabs] to only target the nav header, not <header> inside articles */
|
|
827
828
|
@media (max-width: 1023px) {
|
|
828
|
-
body[data-theme="jam"] header > div {
|
|
829
|
-
margin
|
|
829
|
+
body[data-theme="jam"] header[data-has-tabs] > div {
|
|
830
|
+
margin: 0 !important;
|
|
830
831
|
padding-left: 0.5rem;
|
|
832
|
+
padding-right: 0.5rem;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
body[data-theme="jam"] #main-content article {
|
|
836
|
+
padding-left: 0.5rem !important;
|
|
837
|
+
padding-right: 0.5rem !important;
|
|
831
838
|
}
|
|
832
839
|
}
|
|
833
840
|
|
|
@@ -261,11 +261,18 @@ body[data-theme="nebula"] .code-panels-sidebar {
|
|
|
261
261
|
padding-right: 1rem;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
/*
|
|
264
|
+
/* Align header and content on mobile — zero out inline margin, use consistent padding */
|
|
265
|
+
/* Selector uses [data-has-tabs] to only target the nav header, not <header> inside articles */
|
|
265
266
|
@media (max-width: 1023px) {
|
|
266
|
-
body[data-theme="nebula"] header > div {
|
|
267
|
-
margin
|
|
267
|
+
body[data-theme="nebula"] header[data-has-tabs] > div {
|
|
268
|
+
margin: 0 !important;
|
|
268
269
|
padding-left: 0.5rem;
|
|
270
|
+
padding-right: 0.5rem;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
body[data-theme="nebula"] #main-content article {
|
|
274
|
+
padding-left: 0.5rem !important;
|
|
275
|
+
padding-right: 0.5rem !important;
|
|
269
276
|
}
|
|
270
277
|
}
|
|
271
278
|
|