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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jamdesk",
3
- "version": "1.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
- useEffect(() => {
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
- useEffect(() => {
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 or pressing Escape
206
- useEffect(() => {
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
- if (isTabsDropdownOpen) {
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
- useEffect(() => {
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
- <h4 className="text-sm font-semibold text-[var(--color-text-tertiary)] mb-3 flex items-center gap-2">
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
- </h4>
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 addFormats from 'ajv-formats';
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
- /* Move hamburger menu left on mobile */
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-left: 0 !important;
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
- /* Move hamburger menu left on mobile */
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-left: 0 !important;
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